From b2b3b62be7248043da7716c0ea5e6b56f58e6c90 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Sep 2022 20:06:08 +1000 Subject: [PATCH] Consider all frames when selecting mode for PNG save_all --- Tests/test_file_apng.py | 10 ++++++++++ src/PIL/PngImagePlugin.py | 41 ++++++++++++++++++++++++++++----------- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 0ff05f608c2..bc9b9161983 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -648,6 +648,16 @@ def test_seek_after_close(): im.seek(0) +@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P")) +def test_different_modes_in_later_frames(mode, tmp_path): + test_file = str(tmp_path / "temp.png") + + im = Image.new("L", (1, 1)) + im.save(test_file, save_all=True, append_images=[Image.new(mode, (1, 1))]) + with Image.open(test_file) as reloaded: + assert reloaded.mode == mode + + def test_constants_deprecation(): for enum, prefix in { PngImagePlugin.Disposal: "APNG_DISPOSE_", diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 442c65e6f1b..18100242224 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1089,28 +1089,28 @@ def write(self, data): self.seq_num += 1 -def _write_multiple_frames(im, fp, chunk, rawmode): - default_image = im.encoderinfo.get("default_image", im.info.get("default_image")) +def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images): duration = im.encoderinfo.get("duration", im.info.get("duration", 0)) loop = im.encoderinfo.get("loop", im.info.get("loop", 0)) disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE)) blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE)) if default_image: - chain = itertools.chain(im.encoderinfo.get("append_images", [])) + chain = itertools.chain(append_images) else: - chain = itertools.chain([im], im.encoderinfo.get("append_images", [])) + chain = itertools.chain([im], append_images) im_frames = [] frame_count = 0 for im_seq in chain: for im_frame in ImageSequence.Iterator(im_seq): - im_frame = im_frame.copy() - if im_frame.mode != im.mode: - if im.mode == "P": - im_frame = im_frame.convert(im.mode, palette=im.palette) + if im_frame.mode == rawmode: + im_frame = im_frame.copy() + else: + if rawmode == "P": + im_frame = im_frame.convert(rawmode, palette=im.palette) else: - im_frame = im_frame.convert(im.mode) + im_frame = im_frame.convert(rawmode) encoderinfo = im.encoderinfo.copy() if isinstance(duration, (list, tuple)): encoderinfo["duration"] = duration[frame_count] @@ -1221,7 +1221,26 @@ def _save_all(im, fp, filename): def _save(im, fp, filename, chunk=putchunk, save_all=False): # save an image to disk (called by the save method) - mode = im.mode + if save_all: + default_image = im.encoderinfo.get( + "default_image", im.info.get("default_image") + ) + modes = set() + append_images = im.encoderinfo.get("append_images", []) + if default_image: + chain = itertools.chain(append_images) + else: + chain = itertools.chain([im], append_images) + for im_seq in chain: + for im_frame in ImageSequence.Iterator(im_seq): + modes.add(im_frame.mode) + for mode in ("RGBA", "RGB", "P"): + if mode in modes: + break + else: + mode = modes.pop() + else: + mode = im.mode if mode == "P": @@ -1373,7 +1392,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): chunk(fp, b"eXIf", exif) if save_all: - _write_multiple_frames(im, fp, chunk, rawmode) + _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) else: ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])