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: 2 additions & 0 deletions news/199.feature
Original file line number Diff line number Diff line change
@@ -0,0 +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.
@jensens
52 changes: 39 additions & 13 deletions src/plone/namedfile/scaling.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ def __init__(self, context, request, **info):
if self.data is None:
self.data = getattr(self.context, self.fieldname)

url = self.context.absolute_url()
extension = self.data.contentType.split("/")[-1].lower()
if self.data.contentType == "image/svg+xml":
extension = "svg"
Expand All @@ -103,21 +102,34 @@ def __init__(self, context, request, **info):
else:
name = info["fieldname"]
self.__name__ = f"{name}.{extension}"
self.url = f"{url}/@@images/{self.__name__}"
self.url = self._scale_url(name, extension)
self.srcset = info.get("srcset", [])

def _scale_url(self, uid, extension, base_url=None):
"""Build the URL for an image scale.

Override this method to generate custom scale URLs, e.g. for
external image services like Thumbor.

:param uid: The unique scale identifier.
: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()``.
:returns: The full URL to the image scale.
"""
if base_url is None:
base_url = self.context.absolute_url()
return f"{base_url}/@@images/{uid}.{extension}"

def absolute_url(self):
return self.url

def srcset_attribute(self):
_srcset_attr = []
extension = self.data.contentType.split("/")[-1].lower()
for scale in self.srcset:
_srcset_attr.append(
"{}/@@images/{}.{} {}x".format(
self.context.absolute_url(), scale["uid"], extension, scale["scale"]
)
)
url = self._scale_url(scale["uid"], extension)
_srcset_attr.append(f"{url} {scale['scale']}x")
srcset_attr = ", ".join(_srcset_attr)
return srcset_attr

Expand Down Expand Up @@ -520,6 +532,22 @@ def available_sizes(self):
def available_sizes(self, value):
self._sizes = value

def _scale_url(self, uid, extension, base_url=None):
"""Build the URL for an image scale.

Override this method to generate custom scale URLs, e.g. for
external image services like Thumbor.

:param uid: The unique scale identifier.
: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()``.
:returns: The full URL to the image scale.
"""
if base_url is None:
base_url = self.context.absolute_url()
return f"{base_url}/@@images/{uid}.{extension}"

def getImageSize(self, fieldname=None):
if fieldname is not None:
try:
Expand Down Expand Up @@ -776,9 +804,8 @@ def srcset(
)
if scale:
extension = scale["mimetype"].split("/")[-1].lower()
srcset_urls.append(
f'{self.context.absolute_url()}/@@images/{scale["uid"]}.{extension} {scale["width"]}w'
)
url = self._scale_url(scale["uid"], extension)
srcset_urls.append(f"{url} {scale['width']}w")

# then get the urls of the scales that are smaller than the original
for width, height in self.available_sizes.values():
Expand All @@ -787,9 +814,8 @@ def srcset(
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'
)
url = self._scale_url(scale["uid"], extension)
srcset_urls.append(f"{url} {scale['width']}w")

attributes = {}
if title is _marker:
Expand Down
73 changes: 73 additions & 0 deletions src/plone/namedfile/tests/test_scaling.py
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,79 @@ def testOversizedHighPixelDensityScale(self):
self.assertEqual(foo.srcset[0]["scale"], 2)


class TestScaleUrl(unittest.TestCase):
"""Test the _scale_url override mechanism on ImageScale and ImageScaling."""

layer = PLONE_NAMEDFILE_INTEGRATION_TESTING

def setUp(self):
data = getFile("image.png")
item = DummyContent()
item.image = MockNamedImage(data, "image/png", "image.png")
self.layer["app"]._setOb("item", item)
self.item = self.layer["app"].item

def test_image_scaling_scale_url_default(self):
scaling = ImageScaling(self.item, None)
url = scaling._scale_url("abc-123", "jpeg")
self.assertEqual(url, f"{self.item.absolute_url()}/@@images/abc-123.jpeg")

def test_image_scaling_scale_url_custom_base(self):
scaling = ImageScaling(self.item, None)
url = scaling._scale_url("abc-123", "jpeg", base_url="http://example.com")
self.assertEqual(url, "http://example.com/@@images/abc-123.jpeg")

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):
return f"https://thumbor.example.com/{uid}.{extension}"

scaling = CustomScaling(self.item, None)
url = scaling._scale_url("abc-123", "jpeg")
self.assertEqual(url, "https://thumbor.example.com/abc-123.jpeg")

def test_image_scale_uses_scale_url(self):
"""ImageScale.url should use _scale_url."""
scaling = ImageScaling(self.item, None)
scale = scaling.scale("image", width=100, height=100, pre=True)
# The url should follow the _scale_url pattern
self.assertIn("/@@images/", scale.url)
self.assertTrue(scale.url.endswith(".png"))

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):
return f"https://cdn.example.com/{uid}.{extension}"

class CustomScaling(ImageScaling):
_scale_view_class = CustomImageScale

scaling = CustomScaling(self.item, None)
scale = scaling.scale("image", width=100, height=100, pre=True)
self.assertTrue(scale.url.startswith("https://cdn.example.com/"))

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):
return f"https://thumbor.example.com/{uid}.{extension}"

scaling = CustomScaling(self.item, None)
scaling.available_sizes = {
"mini": (200, 65536),
"thumb": (128, 128),
"tile": (64, 64),
}
tag = scaling.srcset("image", sizes="100vw", scale_in_src="mini")
# The srcset attribute URLs should use the custom _scale_url
self.assertIn("https://thumbor.example.com/", tag)


class TestImgSrcSet(unittest.TestCase):

layer = PLONE_NAMEDFILE_INTEGRATION_TESTING
Expand Down