Support saving float durations

This commit is contained in:
Andrew Murray 2026-01-01 13:02:21 +11:00
parent 900636e7db
commit 91f219fdcf
3 changed files with 28 additions and 6 deletions

View File

@ -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)

View File

@ -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

View File

@ -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
@ -1272,7 +1273,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
@ -1284,8 +1289,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
)