Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement preserveSvg flag for srcSet() #370

Merged
merged 2 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/general-usage/graphql-types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ the root query type like so:
srcSet(
sizes: [Int]
format: String
preserveSvg: Boolean
): String
isSvg: Boolean!

Expand Down
29 changes: 25 additions & 4 deletions grapple/types/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,24 @@ def get_rendition_field_kwargs() -> dict[str, graphene.Scalar]:
return kwargs


def get_src_set_field_kwargs() -> dict[str, graphene.Scalar]:
"""
Returns a list of kwargs for the srcSet field.
Extracted for convenience, to accommodate for the conditional logic needed for various Wagtail versions.
"""
kwargs = {
"sizes": graphene.List(graphene.Int),
"format": graphene.String(),
}
if WAGTAIL_VERSION > (5, 0):
kwargs["preserve_svg"] = graphene.Boolean(
description="Prevents raster image operations (e.g. `format-webp`, `bgcolor`, etc.) being applied to SVGs. "
"More info: https://docs.wagtail.org/en/stable/topics/images.html#svg-images"
)

return kwargs


def rendition_allowed(filter_specs: str) -> bool:
"""Checks a given rendition filter is allowed"""
allowed_filters = grapple_settings.ALLOWED_IMAGE_FILTERS
Expand Down Expand Up @@ -110,9 +128,7 @@ class ImageObjectType(DjangoObjectType):
collection = graphene.Field(lambda: CollectionObjectType, required=True)
tags = graphene.List(graphene.NonNull(lambda: TagObjectType), required=True)
rendition = graphene.Field(get_rendition_type, **get_rendition_field_kwargs())
src_set = graphene.String(
sizes=graphene.List(graphene.Int), format=graphene.String()
)
src_set = graphene.String(**get_src_set_field_kwargs())
if WAGTAIL_VERSION > (5, 0):
is_svg = graphene.Boolean(required=True)

Expand Down Expand Up @@ -181,6 +197,7 @@ def resolve_src_set(
info: GraphQLResolveInfo,
sizes: list[int],
format: str | None = None,
preserve_svg: bool = False,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on a conversation in the Wagtail Slack, I am very much inclined to make preserve_svg default to True here (and on the rendition)

ie. until Wagtail (well, Willlow) can actually rasterize SVGs, the only way to work with SVGs is to use preserve_svg in Grapple, and preserve-svg in regular templates, which adds so much extra effort.

What d'ya think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think it very much makes sense. I can't imagine a scenario where you would not want preserveSvg: true as a default, even if it were possible to rasterize them.

We need to make sure this doesn't break on Wagtail < 5 though; I remember the code was relying on the fact that, on old Wagtails, it would not be possible to change the default False value.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be fine. as preserve_svg filters out non-SVG filters -

if instance.is_svg() and preserve_svg:
# when dealing with SVGs, we want to limit the filter specs to those that are safe
filter_specs = to_svg_safe_spec(filter_specs)
if not filter_specs:
raise TypeError(
"No valid filter specs for SVG. "
"See https://docs.wagtail.org/en/stable/topics/images.html#svg-images for details."
)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but to_svg_safe_spec is imported only for Wagtail >= 5; just wanted to mention this, as there don't seem to be a CI matrix that includes older Wagtails:

if WAGTAIL_VERSION > (5, 0):
from wagtail.images.utils import to_svg_safe_spec


I can implement the change; do you feel it should be part of this PR or separate?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we run tox with tox-gh-actions in CI, so the matrix is in https://github.com/torchbox/wagtail-grapple/blob/main/tox.ini

let's follow up in a separate PR. We'd want to wrap if instance.is_svg() and preserve_svg: in an if WAGTAIL_VERSION > (5, 0) to be sure

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we run tox with tox-gh-actions in CI, so the matrix is in https://github.com/torchbox/wagtail-grapple/blob/main/tox.ini

@zerolab I think something is missing. I couldn't find a CI job that mentioned an old version of Wagtail so I made a branch to see if I can break it. AFAICT this should have failed CI: mgax@7b52a38

**kwargs,
) -> str:
"""
Expand All @@ -191,7 +208,11 @@ def resolve_src_set(
if instance.file.name is not None:
rendition_list = [
ImageObjectType.resolve_rendition(
instance, info, width=width, **format_kwarg
instance,
info,
width=width,
preserve_svg=preserve_svg,
**format_kwarg,
)
for width in sizes
if rendition_allowed(f"width-{width}{filter_suffix}")
Expand Down
44 changes: 44 additions & 0 deletions tests/test_image_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,11 @@ def test_schema_for_svg_related_fields_and_arguments(self):
field["name"]: field for field in results["data"]["__type"]["fields"]
}
rendition_args = {arg["name"]: arg for arg in mapping["rendition"]["args"]}
src_set_args = {arg["name"]: arg for arg in mapping["srcSet"]["args"]}

self.assertNotIn("isSvg", mapping)
self.assertNotIn("preserveSvg", rendition_args)
self.assertNotIn("preserveSvg", src_set_args)


if WAGTAIL_VERSION >= (5, 0):
Expand Down Expand Up @@ -262,6 +264,7 @@ def test_schema_for_svg_related_fields_and_arguments(self):
field["name"]: field for field in results["data"]["__type"]["fields"]
}
rendition_args = {arg["name"]: arg for arg in mapping["rendition"]["args"]}
src_set_args = {arg["name"]: arg for arg in mapping["srcSet"]["args"]}

self.assertIn("isSvg", mapping)
self.assertEqual(mapping["isSvg"]["type"]["kind"], "NON_NULL")
Expand All @@ -271,6 +274,12 @@ def test_schema_for_svg_related_fields_and_arguments(self):
"Prevents raster image operations (e.g. `format-webp`, `bgcolor`, etc.) being applied to SVGs. "
"More info: https://docs.wagtail.org/en/stable/topics/images.html#svg-images",
)
self.assertEqual(src_set_args["preserveSvg"]["type"]["name"], "Boolean")
self.assertEqual(
src_set_args["preserveSvg"]["description"],
"Prevents raster image operations (e.g. `format-webp`, `bgcolor`, etc.) being applied to SVGs. "
"More info: https://docs.wagtail.org/en/stable/topics/images.html#svg-images",
)

def test_svg_rendition(self):
query = """
Expand Down Expand Up @@ -310,6 +319,24 @@ def test_svg_rendition_with_raster_format_with_preserve_svg(self):
)
)

def test_svg_src_set_with_raster_format_with_preserve_svg(self):
query = """
query ($id: ID!) {
image(id: $id) {
srcSet(sizes: [100], format: "webp", preserveSvg: true)
}
}
"""

results = self.client.execute(
query, variables={"id": self.example_svg_image.id}
)
self.assertTrue(
results["data"]["image"]["srcSet"]
.split()[0]
.endswith("test.width-100.svg")
)

def test_svg_rendition_with_raster_format_without_preserve_svg(self):
query = """
query ($id: ID!) {
Expand All @@ -329,6 +356,23 @@ def test_svg_rendition_with_raster_format_without_preserve_svg(self):
"'SvgImage' object has no attribute 'save_as_webp'",
)

def test_svg_src_set_with_raster_format_without_preserve_svg(self):
query = """
query ($id: ID!) {
image(id: $id) {
srcSet(sizes: [100], format: "webp")
}
}
"""

results = self.client.execute(
query, variables={"id": self.example_svg_image.id}
)
self.assertEqual(
results["errors"][0]["message"],
"'SvgImage' object has no attribute 'save_as_webp'",
)

def test_svg_rendition_with_filters_passed_through_to_svg_safe_spec(self):
# bgcolor is not one of the allowed filters, so we should end with an empty filter spec
query = """
Expand Down