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
3 changes: 3 additions & 0 deletions Tests/test_imageops.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ def _check(
_check((10, 0), lambda im: ImageOps.cover(im, (5, 5)), (5, 0))
_check((5, 5), lambda im: ImageOps.cover(im, (2, 0)), (0, 0))

_check((10, 1), lambda im: ImageOps.scale(im, 0.4), (4, 0))
_check((1, 10), lambda im: ImageOps.scale(im, 0.4), (0, 4))


def test_1pxfit() -> None:
# Division by zero in equalize if image is 1 pixel high
Expand Down
2 changes: 2 additions & 0 deletions src/PIL/ImageOps.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,8 @@ def scale(
raise ValueError(msg)
else:
size = (round(factor * image.width), round(factor * image.height))
if min(size) == 0:
return _new_empty(image, size)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this path load the image before copying its metadata? Pillow’s existing Tests/images/hopper.png has text chunks after IDAT. With this PR’s method:

from PIL import Image, ImageOps

with Image.open("Tests/images/hopper.png") as im:
    small = ImageOps.scale(im, 0.001)
    print("comment" in small.info)  # False

A non-empty scale retains comment, and loading first retains it here too. PNG permits text chunks after IDAT. The existing zero-size ImageOps.contain() path also skips this metadata, so the current behavior may be intentional. Loading first would decode the source even when the result is empty, which is a performance trade-off.

If metadata preservation is preferred, this worked in the scoped check:

Suggested change
return _new_empty(image, size)
image.load()
return _new_empty(image, size)

I checked the exact PR Python methods against an existing Pillow 13.0.0 runtime, including 144 in-memory mode/size/factor controls; I did not build the current C extensions or run the full suite. OpenAI Codex (GPT-6) assisted with the verification and review.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a tradeoff for the sake of performance, and one that I'm willing to live with for now.

When a PR made sure that we were preserving palette and transparency here, my comment was #10299 (comment),

Creating an empty image in ImageOps contain() is pretty niche, and doing anything afterwards that would use palettes or transparency is even more niche.

I think opening a PNG, scaling it, and then trying to read the comment from after load() is still more obscure. If the user had strong expectations of correctly reading the comment, they would have to know about load(). So your scenario requires them to expect ImageOps to load the image? Or to think that they could load() after scaling to get the same effect?

Overall, there are some operations that you can only run on the original image in Pillow. You can't open a TIFF image, copy it, and then try to read the original TIFF tags

>>> from PIL import Image
>>> im = Image.open("ests/images/hopper.tif") 
>>> im.tag_v2
<PIL.TiffImagePlugin.ImageFileDirectory_v2 object at 0x10997fed0>
>>> im.copy().tag_v2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Image' object has no attribute 'tag_v2'

If anyone comes in saying that this feature would help them, then we can reconsider.

return image.resize(size, resample)


Expand Down
Loading