Skip to content

Commit 589f687

Browse files
authored
Merge pull request #170 from plone/erral-img-srcset
Add a method to the @@images view to render a img tag with srcset
2 parents ccefeb3 + 68c3e06 commit 589f687

File tree

6 files changed

+208
-56
lines changed

6 files changed

+208
-56
lines changed

README.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ and BLOBs to be configured in zope.conf.
1212

1313
plone.supermodel handlers are registered.
1414

15-
See the ``usage.rst`` doctest for more details.
15+
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
16+
use the features provided by this package.
1617

1718

1819
Source Code
1920
===========
2021

21-
Note: This packages is licensed under a *BSD license*.
22+
Note: This packages is licensed under a *BSD license*.
2223
Please do not add dependencies on GPL code!
2324

2425
Contributors please read the document `Process for Plone core's development <https://docs.plone.org/develop/coredev/docs/index.html>`_

news/170.feature

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add a srcset method to the @@images view
2+
[erral]

plone/namedfile/scaling.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,73 @@ def picture(
739739
fieldname=fieldname,
740740
).prettify()
741741

742+
def srcset(
743+
self,
744+
fieldname=None,
745+
scale_in_src="huge",
746+
sizes="",
747+
alt=_marker,
748+
css_class=None,
749+
title=_marker,
750+
**kwargs,
751+
):
752+
if fieldname is None:
753+
try:
754+
primary = IPrimaryFieldInfo(self.context, None)
755+
except TypeError:
756+
return
757+
if primary is None:
758+
return # 404
759+
fieldname = primary.fieldname
760+
761+
original_width, original_height = self.getImageSize(fieldname)
762+
763+
storage = getMultiAdapter(
764+
(self.context, functools.partial(self.modified, fieldname)),
765+
IImageScaleStorage,
766+
)
767+
768+
srcset_urls = []
769+
for width, height in self.available_sizes.values():
770+
if width <= original_width:
771+
scale = storage.pre_scale(
772+
fieldname=fieldname, width=width, height=height, mode="scale"
773+
)
774+
extension = scale["mimetype"].split("/")[-1].lower()
775+
srcset_urls.append(
776+
f'{self.context.absolute_url()}/@@images/{scale["uid"]}.{extension} {scale["width"]}w'
777+
)
778+
attributes = {}
779+
if title is _marker:
780+
attributes["title"] = self.context.Title()
781+
elif title:
782+
attributes["title"] = title
783+
if alt is _marker:
784+
attributes["alt"] = self.context.Title()
785+
else:
786+
attributes["alt"] = alt
787+
788+
if css_class is not None:
789+
attributes["class"] = css_class
790+
791+
attributes.update(**kwargs)
792+
793+
attributes["sizes"] = sizes
794+
795+
srcset_string = ", ".join(srcset_urls)
796+
attributes["srcset"] = srcset_string
797+
798+
if scale_in_src not in self.available_sizes:
799+
for key, (width, height) in self.available_sizes.items():
800+
if width <= original_width:
801+
scale_in_src = key
802+
break
803+
804+
scale = self.scale(fieldname=fieldname, scale=scale_in_src)
805+
attributes["src"] = scale.url
806+
807+
return _image_tag_from_values(*attributes.items())
808+
742809

743810
class NavigationRootScaling(ImageScaling):
744811
@lazy_property

plone/namedfile/test.pt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@
4242
i18n:name=""
4343
i18n:translate=""
4444
>picture tags</a>,
45+
<a href="#srcset"
46+
i18n="name"
47+
i18n:translate=""
48+
>img tag with srcset</a>,
4549
<a href="#stored"
4650
i18n:name=""
4751
i18n:translate=""
@@ -198,6 +202,26 @@
198202
</p>
199203
</section>
200204

205+
<section class="section"
206+
id="srcset"
207+
>
208+
<h2 i18n:translate="">img with srcset attributes</h2>
209+
<p i18n:translate="">
210+
srcset allows the browser to select the correct image, depending on the space the image has on a page.
211+
</p>
212+
<p i18n:translate="msg_images_test_srcset">
213+
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.
214+
</p>
215+
<p>
216+
<img tal:replace="structure python:images.srcset('image', sizes='(min-width: 1400px) 550px, 90vw')" />
217+
</p>
218+
<p>
219+
<code>
220+
<img tal:replace="python:images.srcset('image', sizes='(min-width: 1400px) 550px, 90vw')" />
221+
</code>
222+
</p>
223+
</section>
224+
201225
<section class="section"
202226
id="stored"
203227
>

plone/namedfile/tests/test_scaling.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,118 @@ def testOversizedHighPixelDensityScale(self):
813813
self.assertEqual(len(foo.srcset), 1)
814814
self.assertEqual(foo.srcset[0]["scale"], 2)
815815

816+
def testImgSrcSet(self):
817+
"""test rendered srcset values"""
818+
self.scaling.available_sizes = {
819+
"huge": (1600, 65536),
820+
"great": (1200, 65536),
821+
"larger": (1000, 65536),
822+
"large": (800, 65536),
823+
"teaser": (600, 65536),
824+
"preview": (400, 65536),
825+
"mini": (200, 65536),
826+
"thumb": (128, 128),
827+
"tile": (64, 64),
828+
"icon": (32, 32),
829+
"listing": (16, 16),
830+
}
831+
tag = self.scaling.srcset("image", sizes="50vw")
832+
base = self.item.absolute_url()
833+
expected = f"""<img title="foo" alt="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".../>"""
834+
self.assertTrue(_ellipsis_match(expected, tag.strip()))
835+
836+
def testImgSrcSetCustomSrc(self):
837+
"""test that we can select a custom scale in the src attribute"""
838+
self.scaling.available_sizes = {
839+
"huge": (1600, 65536),
840+
"great": (1200, 65536),
841+
"larger": (1000, 65536),
842+
"large": (800, 65536),
843+
"teaser": (600, 65536),
844+
"preview": (400, 65536),
845+
"mini": (200, 65536),
846+
"thumb": (128, 128),
847+
"tile": (64, 64),
848+
"icon": (32, 32),
849+
"listing": (16, 16),
850+
}
851+
tag = self.scaling.srcset("image", sizes="50vw", scale_in_src="mini")
852+
base = self.item.absolute_url()
853+
expected = f"""<img title="foo" alt="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".../>"""
854+
self.assertTrue(_ellipsis_match(expected, tag.strip()))
855+
856+
def testImgSrcSetInexistentScale(self):
857+
"""test that when requesting an inexistent scale for the src attribute
858+
we provide the biggest scale we can produce
859+
"""
860+
self.scaling.available_sizes = {
861+
"huge": (1600, 65536),
862+
"great": (1200, 65536),
863+
"larger": (1000, 65536),
864+
"large": (800, 65536),
865+
"teaser": (600, 65536),
866+
"preview": (400, 65536),
867+
"mini": (200, 65536),
868+
"thumb": (128, 128),
869+
"tile": (64, 64),
870+
"icon": (32, 32),
871+
"listing": (16, 16),
872+
}
873+
tag = self.scaling.srcset(
874+
"image", sizes="50vw", scale_in_src="inexistent-scale-name"
875+
)
876+
base = self.item.absolute_url()
877+
expected = f"""<img title="foo" alt="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".../>"""
878+
self.assertTrue(_ellipsis_match(expected, tag.strip()))
879+
880+
def testImgSrcSetCustomTitle(self):
881+
"""test passing a custom title to the srcset method"""
882+
self.scaling.available_sizes = {
883+
"huge": (1600, 65536),
884+
"great": (1200, 65536),
885+
"larger": (1000, 65536),
886+
"large": (800, 65536),
887+
"teaser": (600, 65536),
888+
"preview": (400, 65536),
889+
"mini": (200, 65536),
890+
"thumb": (128, 128),
891+
"tile": (64, 64),
892+
"icon": (32, 32),
893+
"listing": (16, 16),
894+
}
895+
tag = self.scaling.srcset("image", sizes="50vw", title="My Custom Title")
896+
base = self.item.absolute_url()
897+
expected = f"""<img title="My Custom Title" alt="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".../>"""
898+
self.assertTrue(_ellipsis_match(expected, tag.strip()))
899+
900+
def testImgSrcSetAdditionalAttributes(self):
901+
"""test that additional parameters are output as is, like alt, loading, ..."""
902+
self.scaling.available_sizes = {
903+
"huge": (1600, 65536),
904+
"great": (1200, 65536),
905+
"larger": (1000, 65536),
906+
"large": (800, 65536),
907+
"teaser": (600, 65536),
908+
"preview": (400, 65536),
909+
"mini": (200, 65536),
910+
"thumb": (128, 128),
911+
"tile": (64, 64),
912+
"icon": (32, 32),
913+
"listing": (16, 16),
914+
}
915+
tag = self.scaling.srcset(
916+
"image",
917+
sizes="50vw",
918+
alt="This image shows nothing",
919+
css_class="my-personal-class",
920+
title="My Custom Title",
921+
loading="lazy",
922+
)
923+
base = self.item.absolute_url()
924+
925+
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".../>"""
926+
self.assertTrue(_ellipsis_match(expected, tag.strip()))
927+
816928

817929
class ImageTraverseTests(unittest.TestCase):
818930

plone/namedfile/usage.rst

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -403,57 +403,3 @@ We will test this with a dummy request, faking traversal::
403403
'text/plain'
404404
>>> request.response.getHeader('Content-Disposition')
405405
"attachment; filename*=UTF-8''test.txt"
406-
407-
408-
Image scales
409-
------------
410-
411-
This package can handle the creation, storage, and retrieval of arbitrarily
412-
sized scaled versions of images stored in NamedImage or NamedBlobImage fields.
413-
414-
Image scales are accessed via an @@images view that is available for any item
415-
providing ``plone.namedfile.interfaces.IImageScaleTraversable``. There are
416-
several ways that you may reference scales from page templates.
417-
418-
1. for full control you may do the tag generation explicitly::
419-
420-
<img tal:define="images context/@@images;
421-
thumbnail python: images.scale('image', width=64, height=64);"
422-
tal:condition="thumbnail"
423-
tal:attributes="src thumbnail/url;
424-
width thumbnail/width;
425-
height thumbnail/height" />
426-
427-
This would create an up to 64 by 64 pixel scaled down version of the image
428-
stored in the "image" field. It also allows for passing in additional
429-
parameters supported by the ``scaleImage`` function from ``plone.scale``,
430-
e.g. ``mode`` or ``quality``.
431-
432-
.. _`plone.scale`: https://pypi.org/project/plone.scale/
433-
434-
2. for automatic tag generation with extra parameters you would use::
435-
436-
<img tal:define="images context/@@images"
437-
tal:replace="structure python: images.tag('image',
438-
width=1200, height=800, mode='contain')" />
439-
440-
3. It is possible to access scales via predefined named scale sizes, rather
441-
than hardcoding the dimensions every time you access a scale. The scale
442-
sizes are found via calling a utility providing
443-
``plone.namedfile.interfaces.IAvailableSizes``, which should return a dict of
444-
scale name => (width, height). A scale called 'mini' could then be accessed
445-
like this::
446-
447-
<img tal:define="images context/@@images"
448-
tal:replace="structure python: images.tag('image', scale='mini')" />
449-
450-
This would use the predefined scale size "mini" to determine the desired
451-
image dimensions, but still allow to pass in extra parameters.
452-
453-
4. a convenience short-cut for option 3 can be used::
454-
455-
<img tal:replace="structure context/@@images/image/mini" />
456-
457-
5. and lastly, the short-cut can also be used to render the unscaled image::
458-
459-
<img tal:replace="structure context/@@images/image" />

0 commit comments

Comments
 (0)