diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 644b7807a..b57a1d1ad 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -518,6 +518,24 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: assert im.info["duration"] == 600 +def test_apng_save_duration_float(tmp_path: Path) -> None: + test_file = tmp_path / "temp.png" + im = Image.new("1", (1, 1)) + im2 = Image.new("1", (1, 1), 1) + im.save(test_file, save_all=True, append_images=[im2], duration=0.5) + + with Image.open(test_file) as reloaded: + assert reloaded.info["duration"] == 0.5 + + +def test_apng_save_large_duration(tmp_path: Path) -> None: + test_file = tmp_path / "temp.png" + im = Image.new("1", (1, 1)) + im2 = Image.new("1", (1, 1), 1) + with pytest.raises(ValueError, match="cannot write duration"): + im.save(test_file, save_all=True, append_images=[im2], duration=65536000) + + def test_apng_save_disposal(tmp_path: Path) -> None: test_file = tmp_path / "temp.png" size = (128, 64) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 03ee96c0f..d26c0fbae 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1041,9 +1041,8 @@ following parameters can also be set: Defaults to 0. **duration** - Integer (or list or tuple of integers) length of time to display this APNG frame - (in milliseconds). - Defaults to 0. + The length of time (or list or tuple of lengths of time) to display this APNG frame + (in milliseconds). Defaults to 0. **disposal** An integer (or list or tuple of integers) specifying the APNG disposal diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 2508f8566..9826a4cd1 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -39,6 +39,7 @@ import struct import warnings import zlib from enum import IntEnum +from fractions import Fraction from typing import IO, NamedTuple, cast from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence @@ -1275,7 +1276,11 @@ def _write_multiple_frames( im_frame = im_frame.crop(bbox) size = im_frame.size encoderinfo = frame_data.encoderinfo - frame_duration = int(round(encoderinfo.get("duration", 0))) + frame_duration = encoderinfo.get("duration", 0) + delay = Fraction(frame_duration / 1000).limit_denominator(65535) + if delay.numerator > 65535: + msg = "cannot write duration" + raise ValueError(msg) frame_disposal = encoderinfo.get("disposal", disposal) frame_blend = encoderinfo.get("blend", blend) # frame control @@ -1287,8 +1292,8 @@ def _write_multiple_frames( o32(size[1]), # height o32(bbox[0]), # x_offset o32(bbox[1]), # y_offset - o16(frame_duration), # delay_numerator - o16(1000), # delay_denominator + o16(delay.numerator), # delay_numerator + o16(delay.denominator), # delay_denominator o8(frame_disposal), # dispose_op o8(frame_blend), # blend_op )