diff --git a/news/182.feature b/news/182.feature new file mode 100644 index 00000000..6465efa7 --- /dev/null +++ b/news/182.feature @@ -0,0 +1 @@ +Add `srcset` method @erral diff --git a/plone/namedfile/scaling.py b/plone/namedfile/scaling.py index 8f2c9c4b..1450e370 100644 --- a/plone/namedfile/scaling.py +++ b/plone/namedfile/scaling.py @@ -740,6 +740,72 @@ def picture( fieldname=fieldname, ).prettify() + def srcset( + 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 _marker: + attributes["alt"] = self.context.Title() + else: + 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 diff --git a/plone/namedfile/test.pt b/plone/namedfile/test.pt index a1ad17d1..92a9aa93 100644 --- a/plone/namedfile/test.pt +++ b/plone/namedfile/test.pt @@ -26,6 +26,10 @@ contain, picture tags, stored scales, + img tag with srcset, clear

@@ -138,6 +142,27 @@

+ +
+

img with srcset attributes

+

+srcset allows the browser to select the correct image, depending on the space the image has on a page. +

+

+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. +

+

+ +

+

+ + + +

+
+

Stored scales

diff --git a/plone/namedfile/tests/test_scaling.py b/plone/namedfile/tests/test_scaling.py index c23846d6..7b4ee4c6 100644 --- a/plone/namedfile/tests/test_scaling.py +++ b/plone/namedfile/tests/test_scaling.py @@ -814,6 +814,118 @@ 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"""foo""" + 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"""foo""" + 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"""foo""" + 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"""foo""" + 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"""This image shows nothing""" + self.assertTrue(_ellipsis_match(expected, tag.strip())) + class ImageTraverseTests(unittest.TestCase):