Support saving APNG float durations (#9365)

This commit is contained in:
Hugo van Kemenade 2026-01-01 15:49:03 +02:00 committed by GitHub
commit b2d9bc3c76
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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
@ -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
)