diff --git a/news/199.feature b/news/199.feature index a0dcbe9..55513a1 100644 --- a/news/199.feature +++ b/news/199.feature @@ -1,2 +1,2 @@ -Extract ``_scale_url()`` method on ``ImageScale`` and ``ImageScaling`` for overridable scale URL generation. This allows custom image backends (e.g. Thumbor) to generate direct URLs by overriding a single method. +Extract ``_scale_url()`` method on ``ImageScale`` and ``ImageScaling`` for overridable scale URL generation. Accepts an optional ``scale_info`` dict with scale metadata (width, height, mode, fieldname, mimetype, etc.) so custom image backends (e.g. Thumbor) can generate URLs with full context by overriding a single method. @jensens diff --git a/src/plone/namedfile/scaling.py b/src/plone/namedfile/scaling.py index 875afb5..0b2cc3c 100644 --- a/src/plone/namedfile/scaling.py +++ b/src/plone/namedfile/scaling.py @@ -102,10 +102,10 @@ def __init__(self, context, request, **info): else: name = info["fieldname"] self.__name__ = f"{name}.{extension}" - self.url = self._scale_url(name, extension) + self.url = self._scale_url(name, extension, scale_info=info) self.srcset = info.get("srcset", []) - def _scale_url(self, uid, extension, base_url=None): + def _scale_url(self, uid, extension, base_url=None, scale_info=None): """Build the URL for an image scale. Override this method to generate custom scale URLs, e.g. for @@ -115,6 +115,10 @@ def _scale_url(self, uid, extension, base_url=None): :param extension: The file extension (e.g. "jpeg", "png"). :param base_url: The base URL of the content object. Defaults to ``self.context.absolute_url()``. + :param scale_info: Optional dict with scale metadata (width, + height, mode, fieldname, mimetype, etc.). The contents + depend on the call site — ``__init__`` passes the full + info dict, ``srcset`` passes the ``pre_scale()`` result. :returns: The full URL to the image scale. """ if base_url is None: @@ -128,7 +132,7 @@ def srcset_attribute(self): _srcset_attr = [] extension = self.data.contentType.split("/")[-1].lower() for scale in self.srcset: - url = self._scale_url(scale["uid"], extension) + url = self._scale_url(scale["uid"], extension, scale_info=scale) _srcset_attr.append(f"{url} {scale['scale']}x") srcset_attr = ", ".join(_srcset_attr) return srcset_attr @@ -532,7 +536,7 @@ def available_sizes(self): def available_sizes(self, value): self._sizes = value - def _scale_url(self, uid, extension, base_url=None): + def _scale_url(self, uid, extension, base_url=None, scale_info=None): """Build the URL for an image scale. Override this method to generate custom scale URLs, e.g. for @@ -542,6 +546,10 @@ def _scale_url(self, uid, extension, base_url=None): :param extension: The file extension (e.g. "jpeg", "png"). :param base_url: The base URL of the content object. Defaults to ``self.context.absolute_url()``. + :param scale_info: Optional dict with scale metadata (width, + height, mode, fieldname, mimetype, etc.). The contents + depend on the call site — ``srcset`` passes the + ``pre_scale()`` result. :returns: The full URL to the image scale. """ if base_url is None: @@ -804,7 +812,7 @@ def srcset( ) if scale: extension = scale["mimetype"].split("/")[-1].lower() - url = self._scale_url(scale["uid"], extension) + url = self._scale_url(scale["uid"], extension, scale_info=scale) srcset_urls.append(f"{url} {scale['width']}w") # then get the urls of the scales that are smaller than the original @@ -814,7 +822,7 @@ def srcset( fieldname=fieldname, width=width, height=height, mode="scale" ) extension = scale["mimetype"].split("/")[-1].lower() - url = self._scale_url(scale["uid"], extension) + url = self._scale_url(scale["uid"], extension, scale_info=scale) srcset_urls.append(f"{url} {scale['width']}w") attributes = {} diff --git a/src/plone/namedfile/tests/test_scaling.py b/src/plone/namedfile/tests/test_scaling.py index 3b6abaf..83009f9 100644 --- a/src/plone/namedfile/tests/test_scaling.py +++ b/src/plone/namedfile/tests/test_scaling.py @@ -840,7 +840,7 @@ def test_image_scaling_scale_url_override(self): """Subclasses can override _scale_url to produce custom URLs.""" class CustomScaling(ImageScaling): - def _scale_url(self, uid, extension, base_url=None): + def _scale_url(self, uid, extension, base_url=None, scale_info=None): return f"https://thumbor.example.com/{uid}.{extension}" scaling = CustomScaling(self.item, None) @@ -859,7 +859,7 @@ def test_image_scale_override(self): """A custom ImageScale subclass can override _scale_url.""" class CustomImageScale(ImageScale): - def _scale_url(self, uid, extension, base_url=None): + def _scale_url(self, uid, extension, base_url=None, scale_info=None): return f"https://cdn.example.com/{uid}.{extension}" class CustomScaling(ImageScaling): @@ -869,11 +869,32 @@ class CustomScaling(ImageScaling): scale = scaling.scale("image", width=100, height=100, pre=True) self.assertTrue(scale.url.startswith("https://cdn.example.com/")) + def test_scale_info_passed_to_scale_url(self): + """_scale_url receives scale_info with metadata.""" + captured = {} + + class CustomImageScale(ImageScale): + def _scale_url(self, uid, extension, base_url=None, scale_info=None): + captured["scale_info"] = scale_info + return super()._scale_url(uid, extension, base_url, scale_info) + + class CustomScaling(ImageScaling): + _scale_view_class = CustomImageScale + + scaling = CustomScaling(self.item, None) + scale = scaling.scale("image", width=100, height=100, pre=True) + self.assertIsNotNone(scale) + info = captured["scale_info"] + self.assertIn("uid", info) + self.assertIn("width", info) + self.assertIn("height", info) + self.assertIn("fieldname", info) + def test_srcset_uses_scale_url(self): """ImageScaling.srcset should use _scale_url for srcset URLs.""" class CustomScaling(ImageScaling): - def _scale_url(self, uid, extension, base_url=None): + def _scale_url(self, uid, extension, base_url=None, scale_info=None): return f"https://thumbor.example.com/{uid}.{extension}" scaling = CustomScaling(self.item, None) @@ -886,6 +907,27 @@ def _scale_url(self, uid, extension, base_url=None): # The srcset attribute URLs should use the custom _scale_url self.assertIn("https://thumbor.example.com/", tag) + def test_srcset_passes_scale_info(self): + """ImageScaling.srcset passes scale_info to _scale_url.""" + captured_infos = [] + + class CustomScaling(ImageScaling): + def _scale_url(self, uid, extension, base_url=None, scale_info=None): + captured_infos.append(scale_info) + return super()._scale_url(uid, extension, base_url, scale_info) + + scaling = CustomScaling(self.item, None) + scaling.available_sizes = { + "mini": (200, 65536), + "thumb": (128, 128), + } + scaling.srcset("image", sizes="100vw", scale_in_src="mini") + self.assertTrue(len(captured_infos) > 0) + for info in captured_infos: + self.assertIsNotNone(info) + self.assertIn("uid", info) + self.assertIn("width", info) + class TestImgSrcSet(unittest.TestCase):