From e9c9f19c26c3a361c4fa17947b6b5d860e516c2c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 4 Jun 2024 18:46:35 +1000 Subject: [PATCH] Do not use first frame duration for other frames when saving --- Tests/test_file_apng.py | 15 +++++++++++++-- src/PIL/PngImagePlugin.py | 13 ++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 1b393a3ff5d..e95850212a5 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -706,10 +706,21 @@ def test_different_modes_in_later_frames( assert reloaded.mode == mode -def test_apng_repeated_seeks_give_correct_info() -> None: +def test_different_durations(tmp_path: Path) -> None: + test_file = str(tmp_path / "temp.png") + with Image.open("Tests/images/apng/different_durations.png") as im: - for i in range(3): + for _ in range(3): im.seek(0) assert im.info["duration"] == 4000 + im.seek(1) assert im.info["duration"] == 1000 + + im.save(test_file, save_all=True) + + with Image.open(test_file) as reloaded: + assert reloaded.info["duration"] == 4000 + + reloaded.seek(1) + assert reloaded.info["duration"] == 1000 diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 0d5751962c0..dde45e587e1 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1105,7 +1105,7 @@ def write(self, data: bytes) -> None: def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images): - duration = im.encoderinfo.get("duration", im.info.get("duration", 0)) + duration = im.encoderinfo.get("duration") 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)) @@ -1126,6 +1126,8 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) encoderinfo = im.encoderinfo.copy() if isinstance(duration, (list, tuple)): encoderinfo["duration"] = duration[frame_count] + elif duration is None and "duration" in im_frame.info: + encoderinfo["duration"] = im_frame.info["duration"] if isinstance(disposal, (list, tuple)): encoderinfo["disposal"] = disposal[frame_count] if isinstance(blend, (list, tuple)): @@ -1160,15 +1162,12 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) not bbox and prev_disposal == encoderinfo.get("disposal") and prev_blend == encoderinfo.get("blend") + and "duration" in encoderinfo ): - previous["encoderinfo"]["duration"] += encoderinfo.get( - "duration", duration - ) + previous["encoderinfo"]["duration"] += encoderinfo["duration"] continue else: bbox = None - if "duration" not in encoderinfo: - encoderinfo["duration"] = duration im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) if len(im_frames) == 1 and not default_image: @@ -1198,7 +1197,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) im_frame = im_frame.crop(bbox) size = im_frame.size encoderinfo = frame_data["encoderinfo"] - frame_duration = int(round(encoderinfo["duration"])) + frame_duration = int(round(encoderinfo.get("duration", 0))) frame_disposal = encoderinfo.get("disposal", disposal) frame_blend = encoderinfo.get("blend", blend) # frame control