Skip to content

Add a method to the @@images view to render a img tag with srcset #170

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

Merged
merged 28 commits into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b4b394c
fix: fix rendering unrenderable values
erral Mar 17, 2025
0e47b89
feat: add a method to render a img tag with srcset
erral Mar 17, 2025
1a2c0bb
add tests
erral Mar 17, 2025
bb7047c
show the srcset example in the image-test view
erral Mar 17, 2025
d46112b
changelog
erral Mar 17, 2025
4634b00
add link in the images test
erral Mar 17, 2025
2b69491
test file
erral Mar 17, 2025
f5133fa
lint
erral Mar 17, 2025
00166e8
Update plone/namedfile/test.pt
erral Mar 19, 2025
08fe009
Update plone/namedfile/tests/test_scaling.py
erral Mar 19, 2025
ac8b949
Update plone/namedfile/test.pt
erral Mar 21, 2025
681facd
Update plone/namedfile/test.pt
erral Mar 21, 2025
d913573
Merge branch 'main' into erral-img-srcset
jensens Apr 2, 2025
85ecca3
do not generate the scale itself, but use pre_scale to pre_create it,…
erral Apr 3, 2025
abee1ff
support passing custom CSS classes
erral Apr 3, 2025
11ce52d
Merge branch 'main' into erral-img-srcset
erral Apr 3, 2025
77ad029
Merge branch 'main' into erral-img-srcset
MrTango Apr 17, 2025
10a4ac3
documentation
erral May 27, 2025
90f7525
Merge branch 'main' into erral-img-srcset
erral May 27, 2025
2dc84ba
fix spelling
erral May 27, 2025
0fc3c04
Update plone/namedfile/usage.rst
erral May 27, 2025
de4c763
Update plone/namedfile/usage.rst
erral May 27, 2025
41be735
Apply suggestions from code review
erral May 27, 2025
0a6f4e0
point documentation of this package to Plone docs
erral May 27, 2025
60e3642
Update plone/namedfile/scaling.py
erral May 27, 2025
30108e7
make attribute generation consistent with the method
erral May 27, 2025
0850c09
self.title does not work
erral May 27, 2025
68c3e06
fix testing after the alt/title changes
erral May 27, 2025
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
2 changes: 2 additions & 0 deletions news/170.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add a srcset method to the @@images view
[erral]
65 changes: 65 additions & 0 deletions plone/namedfile/scaling.py
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,71 @@ def picture(
fieldname=fieldname,
).prettify()

def srcset(
Copy link
Member

Choose a reason for hiding this comment

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

I see the tag method having a srcset_attribute, I wonder why adding a new method rather than changing the tag one.

Copy link
Member Author

Choose a reason for hiding this comment

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

The point is that I didn't want to add a breaking change in the tag method, I want it to keep it working as it is and offer a way for the user who wants to render the srcset automatically, to get that.

I could do the changes to get that also in the tag method, but that would mean to change the semantics of it, so I discarded it.

self,
fieldname=None,
scale_in_src="huge",
sizes="",
alt=_marker,
css_class=None,
title=_marker,
**kwargs,
):
if fieldname is None:
try:
primary = IPrimaryFieldInfo(self.context, None)
except TypeError:
return
if primary is None:
return # 404
fieldname = primary.fieldname

original_width, original_height = self.getImageSize(fieldname)

storage = getMultiAdapter(
(self.context, functools.partial(self.modified, fieldname)),
IImageScaleStorage,
)

srcset_urls = []
for width, height in self.available_sizes.values():
if width <= original_width:
scale = storage.pre_scale(
fieldname=fieldname, width=width, height=height, mode="scale"
)
extension = scale["mimetype"].split("/")[-1].lower()
srcset_urls.append(
f'{self.context.absolute_url()}/@@images/{scale["uid"]}.{extension} {scale["width"]}w'
)
attributes = {}
if title is _marker:
attributes["title"] = self.context.Title()
elif title:
attributes["title"] = title
if alt is not _marker:
attributes["alt"] = alt

if css_class is not None:
attributes["class"] = css_class

attributes.update(**kwargs)

attributes["sizes"] = sizes

srcset_string = ", ".join(srcset_urls)
attributes["srcset"] = srcset_string

if scale_in_src not in self.available_sizes:
for key, (width, height) in self.available_sizes.items():
if width <= original_width:
scale_in_src = key
break

scale = self.scale(fieldname=fieldname, scale=scale_in_src)
attributes["src"] = scale.url

return _image_tag_from_values(*attributes.items())


class NavigationRootScaling(ImageScaling):
@lazy_property
Expand Down
24 changes: 24 additions & 0 deletions plone/namedfile/test.pt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
i18n:name=""
i18n:translate=""
>picture tags</a>,
<a href="#srcset"
i18n="name"
i18n:translate=""
>img tag with srcset</a>,
<a href="#stored"
i18n:name=""
i18n:translate=""
Expand Down Expand Up @@ -198,6 +202,26 @@
</p>
</section>

<section class="section"
id="srcset"
>
<h2 i18n:translate="">img with srcset attributes</h2>
<p i18n:translate="">
srcset allows the browser to select the correct image, depending on the space the image has on a page.
</p>
<p i18n:translate="msg_images_test_srcset">
To do so, the @@images view provides a srcset method, that will output the full srcset of this image, using all available image scales. It has as required parameter the value of the sizes attribute that the user of this method has to provide and will be output as is in the generated HTML.
</p>
<p>
<img tal:replace="structure python:images.srcset('image', sizes='(min-width: 1400px) 550px, 90vw')" />
</p>
<p>
<code>
<img tal:replace="python:images.srcset('image', sizes='(min-width: 1400px) 550px, 90vw')" />
</code>
</p>
</section>

<section class="section"
id="stored"
>
Expand Down
111 changes: 111 additions & 0 deletions plone/namedfile/tests/test_scaling.py
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,117 @@ def testOversizedHighPixelDensityScale(self):
self.assertEqual(len(foo.srcset), 1)
self.assertEqual(foo.srcset[0]["scale"], 2)

def testImgSrcSet(self):
"""test rendered srcset values"""
self.scaling.available_sizes = {
"huge": (1600, 65536),
"great": (1200, 65536),
"larger": (1000, 65536),
"large": (800, 65536),
"teaser": (600, 65536),
"preview": (400, 65536),
"mini": (200, 65536),
"thumb": (128, 128),
"tile": (64, 64),
"icon": (32, 32),
"listing": (16, 16),
}
tag = self.scaling.srcset("image", sizes="50vw")
base = self.item.absolute_url()
expected = f"""<img title="foo" sizes="50vw" srcset="{base}/@@images/image-200-....png 200w, {base}/@@images/image-128-....png 128w, {base}/@@images/image-64-....png 64w, {base}/@@images/image-32-....png 32w, {base}/@@images/image-16-....png 16w" src="{base}/@@images/image-1600-....png".../>"""
self.assertTrue(_ellipsis_match(expected, tag.strip()))

def testImgSrcSetCustomSrc(self):
"""test that we can select a custom scale in the src attribute"""
self.scaling.available_sizes = {
"huge": (1600, 65536),
"great": (1200, 65536),
"larger": (1000, 65536),
"large": (800, 65536),
"teaser": (600, 65536),
"preview": (400, 65536),
"mini": (200, 65536),
"thumb": (128, 128),
"tile": (64, 64),
"icon": (32, 32),
"listing": (16, 16),
}
tag = self.scaling.srcset("image", sizes="50vw", scale_in_src="mini")
base = self.item.absolute_url()
expected = f"""<img title="foo" sizes="50vw" srcset="{base}/@@images/image-200-....png 200w, {base}/@@images/image-128-....png 128w, {base}/@@images/image-64-....png 64w, {base}/@@images/image-32-....png 32w, {base}/@@images/image-16-....png 16w" src="{base}/@@images/image-200-....png".../>"""
self.assertTrue(_ellipsis_match(expected, tag.strip()))

def testImgSrcSetInexistentScale(self):
"""test that when requesting an inexistent scale for the src attribute
we provide the biggest scale we can produce
"""
self.scaling.available_sizes = {
"huge": (1600, 65536),
"great": (1200, 65536),
"larger": (1000, 65536),
"large": (800, 65536),
"teaser": (600, 65536),
"preview": (400, 65536),
"mini": (200, 65536),
"thumb": (128, 128),
"tile": (64, 64),
"icon": (32, 32),
"listing": (16, 16),
}
tag = self.scaling.srcset(
"image", sizes="50vw", scale_in_src="inexistent-scale-name"
)
base = self.item.absolute_url()
expected = f"""<img title="foo" sizes="50vw" srcset="{base}/@@images/image-200-....png 200w, {base}/@@images/image-128-....png 128w, {base}/@@images/image-64-....png 64w, {base}/@@images/image-32-....png 32w, {base}/@@images/image-16-....png 16w" src="{base}/@@images/image-200-....png".../>"""
self.assertTrue(_ellipsis_match(expected, tag.strip()))

def testImgSrcSetCustomTitle(self):
"""test passing a custom title to the srcset method"""
self.scaling.available_sizes = {
"huge": (1600, 65536),
"great": (1200, 65536),
"larger": (1000, 65536),
"large": (800, 65536),
"teaser": (600, 65536),
"preview": (400, 65536),
"mini": (200, 65536),
"thumb": (128, 128),
"tile": (64, 64),
"icon": (32, 32),
"listing": (16, 16),
}
tag = self.scaling.srcset("image", sizes="50vw", title="My Custom Title")
base = self.item.absolute_url()
expected = f"""<img title="My Custom Title" sizes="50vw" srcset="{base}/@@images/image-200-....png 200w, {base}/@@images/image-128-....png 128w, {base}/@@images/image-64-....png 64w, {base}/@@images/image-32-....png 32w, {base}/@@images/image-16-....png 16w" src="{base}/@@images/image-1600-....png".../>"""
self.assertTrue(_ellipsis_match(expected, tag.strip()))

def testImgSrcSetAdditionalAttributes(self):
"""test that additional parameters are output as is, like alt, loading, ..."""
self.scaling.available_sizes = {
"huge": (1600, 65536),
"great": (1200, 65536),
"larger": (1000, 65536),
"large": (800, 65536),
"teaser": (600, 65536),
"preview": (400, 65536),
"mini": (200, 65536),
"thumb": (128, 128),
"tile": (64, 64),
"icon": (32, 32),
"listing": (16, 16),
}
tag = self.scaling.srcset(
"image",
sizes="50vw",
alt="This image shows nothing",
css_class="my-personal-class",
title="My Custom Title",
loading="lazy",
)
base = self.item.absolute_url()
expected = f"""<img title="My Custom Title" alt="This image shows nothing" class="my-personal-class" loading="lazy" sizes="50vw" srcset="{base}/@@images/image-200-....png 200w, {base}/@@images/image-128-....png 128w, {base}/@@images/image-64-....png 64w, {base}/@@images/image-32-....png 32w, {base}/@@images/image-16-....png 16w" src="{base}/@@images/image-1600-....png".../>"""
self.assertTrue(_ellipsis_match(expected, tag.strip()))


class ImageTraverseTests(unittest.TestCase):

Expand Down
40 changes: 40 additions & 0 deletions plone/namedfile/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -457,3 +457,43 @@ several ways that you may reference scales from page templates.
5. and lastly, the short-cut can also be used to render the unscaled image::

<img tal:replace="structure context/@@images/image" />


Image scales and srcset
-----------------------

Nowadays modern browsers are able to render different images depending on their width
if urls and widths are correctly provided in an attribute called `srcset` and the rendered
space is provided in the attribute `sizes`.

So one can do the following:

<img tal:define="images context/@@images;"
tal:replace="structure python:images.srcset(sizes='90vw')" />


This will render the `img` with the urls of all scales configured in Plone, calculating the width
of each of the scales and will add the `sizes="90vw"` attribute which instructs the browser to "render
the image that best fits as it will take the 90% of the current viewport-width" whichever is the current
viewport.

This will mean that for bigger screens the browser will download a bigger image while in small screens
a smaller scale is enough.

This also means that the developer does not need to worry on creating a specific scale, they only need to
provide the correct media query to signal the required width.

The `scrset` method of the `@@images` view takes also all other parameters that can be rendered in the `img`
tag such as `title`, `alt` or `loading`:


<img tal:define="images context/@@images;"
tal:replace="structure python:images.srcset(sizes='90vw',
alt='This is the alternative text',
loading='lazy',
css_class='rounded-img')" />


*NOTE*: while using this approach may be useful for projects, using it in reusable addons is not recommended
because it may require overriding it to your needs in a project. For such cases, we recommend using configurable
picture variants.