Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion news/199.feature
Original file line number Diff line number Diff line change
@@ -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
20 changes: 14 additions & 6 deletions src/plone/namedfile/scaling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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 = {}
Expand Down
48 changes: 45 additions & 3 deletions src/plone/namedfile/tests/test_scaling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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):
Expand All @@ -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)
Expand All @@ -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):

Expand Down