From 0bb99e5561e1e2b87f94d6ae30f07eba1744421c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Jun 2025 15:08:16 +1000 Subject: [PATCH 1/2] Use save parameters as encoderinfo defaults --- Tests/test_file_mpo.py | 22 ++++++++++++++++------ Tests/test_file_tiff.py | 13 +++++++++---- src/PIL/Image.py | 9 ++++++++- src/PIL/MpoImagePlugin.py | 1 + src/PIL/TiffImagePlugin.py | 3 +-- 5 files changed, 35 insertions(+), 13 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 73838ef44..c192f017f 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -312,10 +312,20 @@ def test_save_all() -> None: def test_save_xmp() -> None: im = Image.new("RGB", (1, 1)) im2 = Image.new("RGB", (1, 1), "#f00") + + def roundtrip_xmp(): + im_reloaded = roundtrip(im, xmp=b"Default", save_all=True, append_images=[im2]) + xmp = [im_reloaded.info["xmp"]] + im_reloaded.seek(1) + return xmp + [im_reloaded.info["xmp"]] + + # Use the save parameters for all frames by default + assert roundtrip_xmp() == [b"Default", b"Default"] + + # Specify a value for the first frame + im.encoderinfo = {"xmp": b"First frame"} + assert roundtrip_xmp() == [b"First frame", b"Default"] + + # Specify value for the second frame im2.encoderinfo = {"xmp": b"Second frame"} - im_reloaded = roundtrip(im, xmp=b"First frame", save_all=True, append_images=[im2]) - - assert im_reloaded.info["xmp"] == b"First frame" - - im_reloaded.seek(1) - assert im_reloaded.info["xmp"] == b"Second frame" + assert roundtrip_xmp() == [b"Default", b"Second frame"] diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index d0d394aa9..d192e9685 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -695,16 +695,21 @@ class TestFileTiff: assert im.tag_v2[278] == 256 im = hopper() + im.encoderinfo = {"tiffinfo": {278: 100}} im2 = Image.new("L", (128, 128)) - im2.encoderinfo = {"tiffinfo": {278: 256}} - im.save(outfile, save_all=True, append_images=[im2]) + im3 = im2.copy() + im3.encoderinfo = {"tiffinfo": {278: 300}} + im.save(outfile, save_all=True, tiffinfo={278: 200}, append_images=[im2, im3]) with Image.open(outfile) as im: assert isinstance(im, TiffImagePlugin.TiffImageFile) - assert im.tag_v2[278] == 128 + assert im.tag_v2[278] == 100 im.seek(1) - assert im.tag_v2[278] == 256 + assert im.tag_v2[278] == 200 + + im.seek(2) + assert im.tag_v2[278] == 300 def test_strip_raw(self) -> None: infile = "Tests/images/tiff_strip_raw.tif" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ed2f728aa..7a8d93793 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2555,7 +2555,8 @@ class Image: self.load() save_all = params.pop("save_all", None) - self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params} + self._default_encoderinfo = params + self._attach_default_encoderinfo(self) self.encoderconfig: tuple[Any, ...] = () if format.upper() not in SAVE: @@ -2600,6 +2601,12 @@ class Image: if open_fp: fp.close() + def _attach_default_encoderinfo(self, im: Image) -> Any: + self.encoderinfo = { + **im._default_encoderinfo, + **getattr(self, "encoderinfo", {}), + } + def seek(self, frame: int) -> None: """ Seeks to the given frame in this sequence file. If you seek diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index f7393eac0..f1f44c0ff 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -64,6 +64,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: JpegImagePlugin._save(im_frame, fp, filename) offsets.append(fp.tell()) else: + im_frame._attach_default_encoderinfo(im) im_frame.save(fp, "JPEG") offsets.append(fp.tell() - offsets[-1]) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 88af9162e..e1b10fea5 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -2310,8 +2310,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: try: with AppendingTiffWriter(fp) as tf: for ims in [im] + append_images: - if not hasattr(ims, "encoderinfo"): - ims.encoderinfo = {} + ims._attach_default_encoderinfo(im) if not hasattr(ims, "encoderconfig"): ims.encoderconfig = () nfr = getattr(ims, "n_frames", 1) From d4162f85056223098fef0ba3f87e58519ba2955f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Jun 2025 18:27:49 +1000 Subject: [PATCH 2/2] Updated return type --- Tests/test_file_mpo.py | 2 +- src/PIL/Image.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 682fc7361..6c9c541f1 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -316,7 +316,7 @@ def test_save_xmp() -> None: im = Image.new("RGB", (1, 1)) im2 = Image.new("RGB", (1, 1), "#f00") - def roundtrip_xmp(): + def roundtrip_xmp() -> list[Any]: im_reloaded = roundtrip(im, xmp=b"Default", save_all=True, append_images=[im2]) xmp = [im_reloaded.info["xmp"]] im_reloaded.seek(1) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7b1b575a0..9fc1c7067 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2600,7 +2600,7 @@ class Image: if open_fp: fp.close() - def _attach_default_encoderinfo(self, im: Image) -> Any: + def _attach_default_encoderinfo(self, im: Image) -> dict[str, Any]: encoderinfo = getattr(self, "encoderinfo", {}) self.encoderinfo = {**im._default_encoderinfo, **encoderinfo} return encoderinfo