Add support for SVG when using jinja2 (#13016)

Fixes #12997
pull/13082/head
Vishesh Garg 2025-04-04 02:36:45 +05:30 zatwierdzone przez Matt Westcott
rodzic 68ac218c4f
commit d1fbb2e262
7 zmienionych plików z 158 dodań i 0 usunięć

Wyświetl plik

@ -14,6 +14,7 @@ Changelog
* Use `requests` to access oEmbed endpoints, for more robust SSL certificate handling (Matt Westcott)
* Ensure that bulk deletion views respect protected foreign keys (Sage Abdullah)
* Add minimum length validation for `RichTextBlock` and `RichTextField` (Alec Baron)
* Support `preserve-svg` in Jinja2 image tags (Vishesh Garg)
* Fix: Handle lazy translation strings as `preview_value` for `RichTextBlock` (Seb Corbin)
* Fix: Fix handling of newline-separated choices in form builder when using non-windows newline characters (Baptiste Mispelon)
* Fix: Ensure `WAGTAILADMIN_LOGIN_URL` is respected when logging out of the admin (Antoine Rodriguez, Ramon de Jezus)

Wyświetl plik

@ -887,6 +887,7 @@
* Gorlik
* manu
* Maciek Baron
* Vishesh Garg
## Translators

Wyświetl plik

@ -83,6 +83,12 @@ Or resize an image and retrieve the resized image object (rendition) for more be
<div class="wrapper" style="background-image: url({{ background.url }});"></div>
```
When working with SVG images, you can use `preserve_svg` in the filter string to prevent operations that would require rasterizing the SVG. When preserve_svg is set to True and the image is an SVG, operations that would require rasterization (like format conversion) will be automatically filtered out, ensuring SVGs remain as vector graphics. This is especially useful in loops processing both raster images and SVGs.
```html+jinja
{{ image(page.svg_image, "width-400|format-webp|preserve_svg") }}
```
See [](image_tag) for more information
### `srcset_image()`
@ -108,6 +114,12 @@ Or resize an image and retrieve the renditions for more bespoke use:
<div class="wrapper" style="background-image: image-set(url({{ bg.renditions[0].url }}) 1x, url({{ bg.renditions[1].url }}) 2x);"></div>
```
When working with SVG images, you can use `preserve_svg` in the filter string to prevent operations that would require rasterizing the SVG.
```html+jinja
{{ srcset_image(page.svg_image, "width-400|format-webp|preserve_svg") }}
```
### `picture()`
Resize or convert an image, rendering a `<picture>` tag including multiple `source` formats with `srcset` for multiple sizes, and a fallback `<img>` tag.
@ -152,6 +164,12 @@ Or resize an image and retrieve the renditions for more bespoke use:
<div class="wrapper" style="background-image: image-set(url({{ bg.formats['avif'][0].url }}) 1x type('image/avif'), url({{ bg.formats['avif'][1].url }}) 2x type('image/avif'), url({{ bg.formats['jpeg'][0].url }}) 1x type('image/jpeg'), url({{ bg.formats['jpeg'][1].url }}) 2x type('image/jpeg'));"></div>
```
For SVG images, you can use `preserve_svg` in the filter string to ensure they remain as vector graphics:
```html+jinja
{{ picture(page.header_image, "format-{avif,webp,jpeg}|width-{400,800}|preserve_svg", sizes="80vw") }}
```
### `|richtext`
Transform Wagtail's internal HTML representation, expanding internal references to pages and images.

Wyświetl plik

@ -23,6 +23,7 @@ depth: 1
* Use `requests` to access oEmbed endpoints, for more robust SSL certificate handling (Matt Westcott)
* Ensure that bulk deletion views respect protected foreign keys (Sage Abdullah)
* Add minimum length validation for `RichTextBlock` and `RichTextField` (Alec Baron)
* Support `preserve-svg` in Jinja2 image tags (Vishesh Garg)
### Bug fixes

Wyświetl plik

@ -4,6 +4,7 @@ from jinja2.ext import Extension
from .models import Filter, Picture, ResponsiveImage
from .shortcuts import get_rendition_or_not_found, get_renditions_or_not_found
from .templatetags.wagtailimages_tags import image_url
from .utils import to_svg_safe_spec
def image(image, filterspec, **attrs):
@ -16,6 +17,10 @@ def image(image, filterspec, **attrs):
"(given filter: {})".format(filterspec)
)
preserve_svg = "preserve-svg" in filterspec.split("|")
if preserve_svg and image.is_svg():
filterspec = to_svg_safe_spec(filterspec)
rendition = get_rendition_or_not_found(image, filterspec)
if attrs:
@ -34,6 +39,10 @@ def srcset_image(image, filterspec, **attrs):
"(given filter: {})".format(filterspec)
)
preserve_svg = "preserve-svg" in filterspec.split("|")
if preserve_svg and image.is_svg():
filterspec = to_svg_safe_spec(filterspec)
specs = Filter.expand_spec(filterspec)
renditions = get_renditions_or_not_found(image, specs)
@ -50,6 +59,10 @@ def picture(image, filterspec, **attrs):
"(given filter: {})".format(filterspec)
)
preserve_svg = "preserve-svg" in filterspec.split("|")
if preserve_svg and image.is_svg():
filterspec = to_svg_safe_spec(filterspec)
specs = Filter.expand_spec(filterspec)
renditions = get_renditions_or_not_found(image, specs)

Wyświetl plik

@ -0,0 +1,115 @@
from django.test import TestCase
from wagtail.images.models import Image
from wagtail.images.tests.utils import (
get_test_image_file,
get_test_image_file_svg,
)
from wagtail.test.utils import WagtailTestUtils
class TestJinja2SVGSupport(WagtailTestUtils, TestCase):
"""Test SVG support in Jinja2 templates with preserve-svg filter."""
def setUp(self):
# Create a real test engine
from django.template.loader import engines
self.engine = engines["jinja2"]
# Create a raster image
self.raster_image = Image.objects.create(
title="Test raster image",
file=get_test_image_file(),
)
# Create an SVG image
self.svg_image = Image.objects.create(
title="Test SVG image",
file=get_test_image_file_svg(),
)
def render(self, string, context=None):
if context is None:
context = {}
template = self.engine.from_string(string)
return template.render(context)
def test_image_with_raster_image(self):
"""Test that raster images work normally without preserve-svg."""
html = self.render(
'{{ image(img, "width-200|format-webp") }}', {"img": self.raster_image}
)
self.assertIn('width="200"', html)
self.assertIn(".webp", html) # Format conversion applied
def test_image_with_svg_without_preserve(self):
"""Test that without preserve-svg, SVGs get all operations (which would fail in production)."""
with self.assertRaises(AttributeError):
self.render(
'{{ image(img, "width-200|format-webp") }}', {"img": self.svg_image}
)
def test_image_with_svg_with_preserve(self):
"""Test that with preserve-svg filter, SVGs only get safe operations."""
html = self.render(
'{{ image(img, "width-200|format-webp|preserve-svg") }}',
{"img": self.svg_image},
)
# Check the SVG is preserved
self.assertIn(".svg", html)
self.assertNotIn(".webp", html)
def test_srcset_image_with_svg_preserve(self):
"""Test that preserve-svg works with srcset_image function."""
html = self.render(
'{{ srcset_image(img, "width-{200,400}|format-webp|preserve-svg", sizes="100vw") }}',
{"img": self.svg_image},
)
# Should preserve SVG format
self.assertIn(".svg", html)
self.assertNotIn(".webp", html)
def test_picture_with_svg_preserve(self):
"""Test that preserve-svg works with picture function."""
html = self.render(
'{{ picture(img, "format-{avif,webp,jpeg}|width-400|preserve-svg") }}',
{"img": self.svg_image},
)
# Should preserve SVG format
self.assertIn(".svg", html)
self.assertNotIn(".webp", html)
self.assertNotIn(".avif", html)
self.assertNotIn(".jpeg", html)
def test_preserve_svg_with_multiple_operations(self):
"""Test preserve-svg with multiple operations, some safe, some unsafe for SVGs."""
html = self.render(
'{{ image(img, "width-300|height-200|format-webp|fill-100x100|jpegquality-80|preserve-svg") }}',
{"img": self.svg_image},
)
# Should preserve SVG format
self.assertIn(".svg", html)
self.assertNotIn(".webp", html)
self.assertNotIn("jpegquality-80", html)
def test_preserve_svg_with_custom_attributes(self):
"""Test preserve-svg works with custom HTML attributes."""
html = self.render(
'{{ image(img, "width-200|format-webp|preserve-svg", class="my-image", alt="Custom alt") }}',
{"img": self.svg_image},
)
# Check custom attributes are present
self.assertIn('class="my-image"', html)
self.assertIn('alt="Custom alt"', html)
# SVG should be preserved
self.assertNotIn(".webp", html)
self.assertIn(".svg", html)

Wyświetl plik

@ -117,6 +117,7 @@ def to_svg_safe_spec(filter_specs):
"""
if isinstance(filter_specs, str):
filter_specs = filter_specs.split("|")
svg_preserving_specs = [
"max",
"min",
@ -126,9 +127,17 @@ def to_svg_safe_spec(filter_specs):
"fill",
"original",
]
# Keep only safe operations and remove preserve-svg
safe_specs = [
x
for x in filter_specs
if any(x.startswith(prefix) for prefix in svg_preserving_specs)
and x != "preserve-svg"
]
# If no safe operations remain, use 'original'
if not safe_specs:
return "original"
return "|".join(safe_specs)