From a0b691a219d274a561784781476cc952e79c1b8b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 May 2023 12:12:16 +1000 Subject: [PATCH 1/2] Fixed combining single duration across duplicate PNG frames --- Tests/test_file_apng.py | 6 ++++++ src/PIL/PngImagePlugin.py | 9 ++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index f78c086eb0c..c62231cd4ea 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -440,6 +440,12 @@ def test_apng_save_duration_loop(tmp_path): assert im.n_frames == 1 assert im.info.get("duration") == 750 + # test removal of duplicated frames with a single duration + frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500) + with Image.open(test_file) as im: + assert im.n_frames == 1 + assert im.info.get("duration") == 1500 + # test info duration frame.info["duration"] = 750 frame.save(test_file, save_all=True) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 82a74b26785..aaf242b1d54 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1146,11 +1146,14 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) and prev_disposal == encoderinfo.get("disposal") and prev_blend == encoderinfo.get("blend") ): - if isinstance(duration, (list, tuple)): - previous["encoderinfo"]["duration"] += encoderinfo["duration"] + previous["encoderinfo"]["duration"] += encoderinfo.get( + "duration", duration + ) continue else: bbox = None + if "duration" not in encoderinfo: + encoderinfo["duration"] = duration im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) # animation control @@ -1175,7 +1178,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.get("duration", duration))) + frame_duration = int(round(encoderinfo["duration"])) frame_disposal = encoderinfo.get("disposal", disposal) frame_blend = encoderinfo.get("blend", blend) # frame control From 97df237dc81c930d983b4025b7b3a97d043dfd7c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Jun 2023 18:04:39 +1000 Subject: [PATCH 2/2] Moved test into separate function --- Tests/test_file_apng.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index c62231cd4ea..a22ac581d8d 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -440,12 +440,6 @@ def test_apng_save_duration_loop(tmp_path): assert im.n_frames == 1 assert im.info.get("duration") == 750 - # test removal of duplicated frames with a single duration - frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500) - with Image.open(test_file) as im: - assert im.n_frames == 1 - assert im.info.get("duration") == 1500 - # test info duration frame.info["duration"] = 750 frame.save(test_file, save_all=True) @@ -453,6 +447,17 @@ def test_apng_save_duration_loop(tmp_path): assert im.info.get("duration") == 750 +def test_apng_save_duplicate_duration(tmp_path): + test_file = str(tmp_path / "temp.png") + frame = Image.new("RGB", (1, 1)) + + # Test a single duration is correctly combined across duplicate frames + frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500) + with Image.open(test_file) as im: + assert im.n_frames == 1 + assert im.info.get("duration") == 1500 + + def test_apng_save_disposal(tmp_path): test_file = str(tmp_path / "temp.png") size = (128, 64)