diff --git a/README.rst b/README.rst index 8314989..98a9464 100644 --- a/README.rst +++ b/README.rst @@ -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 `_ 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 `_ diff --git a/news/170.feature b/news/170.feature new file mode 100644 index 0000000..a6c729d --- /dev/null +++ b/news/170.feature @@ -0,0 +1,2 @@ +Add a srcset method to the @@images view +[erral] diff --git a/plone/namedfile/scaling.py b/plone/namedfile/scaling.py index f79fc6a..ddf140c 100644 --- a/plone/namedfile/scaling.py +++ b/plone/namedfile/scaling.py @@ -739,6 +739,73 @@ 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 5568f1d..51c4b8c 100644 --- a/plone/namedfile/test.pt +++ b/plone/namedfile/test.pt @@ -42,6 +42,10 @@ i18n:name="" i18n:translate="" >picture tags, + img tag with srcset, +
+

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

+

+ +

+

+ + + +

+
+
diff --git a/plone/namedfile/tests/test_scaling.py b/plone/namedfile/tests/test_scaling.py index aa34934..47163b8 100644 --- a/plone/namedfile/tests/test_scaling.py +++ b/plone/namedfile/tests/test_scaling.py @@ -813,6 +813,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): diff --git a/plone/namedfile/usage.rst b/plone/namedfile/usage.rst index c18cf07..8dbb027 100644 --- a/plone/namedfile/usage.rst +++ b/plone/namedfile/usage.rst @@ -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:: - - - - 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:: - - - -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:: - - - - 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:: - - - -5. and lastly, the short-cut can also be used to render the unscaled image:: - -