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 24 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
5 changes: 3 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ and BLOBs to be configured in zope.conf.

plone.supermodel handlers are registered.

See the ``usage.rst`` doctest for more details.
See the `image handling section of Plone documentation <https://6.docs.plone.org/classic-ui/images.html#all-image-scales-in-the-srcset>`_ to learn how to
use the features provided by this package.


Source Code
===========

Note: This packages is licensed under a *BSD license*.
Note: This packages is licensed under a *BSD license*.
Please do not add dependencies on GPL code!

Contributors please read the document `Process for Plone core's development <https://docs.plone.org/develop/coredev/docs/index.html>`_
Expand Down
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
54 changes: 0 additions & 54 deletions plone/namedfile/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -403,57 +403,3 @@ We will test this with a dummy request, faking traversal::
'text/plain'
>>> request.response.getHeader('Content-Disposition')
"attachment; filename*=UTF-8''test.txt"


Image scales
------------

This package can handle the creation, storage, and retrieval of arbitrarily
sized scaled versions of images stored in NamedImage or NamedBlobImage fields.

Image scales are accessed via an @@images view that is available for any item
providing ``plone.namedfile.interfaces.IImageScaleTraversable``. There are
several ways that you may reference scales from page templates.

1. for full control you may do the tag generation explicitly::

<img tal:define="images context/@@images;
thumbnail python: images.scale('image', width=64, height=64);"
tal:condition="thumbnail"
tal:attributes="src thumbnail/url;
width thumbnail/width;
height thumbnail/height" />

This would create an up to 64 by 64 pixel scaled down version of the image
stored in the "image" field. It also allows for passing in additional
parameters supported by the ``scaleImage`` function from ``plone.scale``,
e.g. ``mode`` or ``quality``.

.. _`plone.scale`: https://pypi.org/project/plone.scale/

2. for automatic tag generation with extra parameters you would use::

<img tal:define="images context/@@images"
tal:replace="structure python: images.tag('image',
width=1200, height=800, mode='contain')" />

3. It is possible to access scales via predefined named scale sizes, rather
than hardcoding the dimensions every time you access a scale. The scale
sizes are found via calling a utility providing
``plone.namedfile.interfaces.IAvailableSizes``, which should return a dict of
scale name => (width, height). A scale called 'mini' could then be accessed
like this::

<img tal:define="images context/@@images"
tal:replace="structure python: images.tag('image', scale='mini')" />

This would use the predefined scale size "mini" to determine the desired
image dimensions, but still allow to pass in extra parameters.

4. a convenience short-cut for option 3 can be used::

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

5. and lastly, the short-cut can also be used to render the unscaled image::

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