Skip to content

Commit b2d9bc3

Browse files
authored
Support saving APNG float durations (#9365)
2 parents f130c10 + 91f219f commit b2d9bc3

File tree

3 files changed

+28
-6
lines changed

3 files changed

+28
-6
lines changed

Tests/test_file_apng.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,24 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
518518
assert im.info["duration"] == 600
519519

520520

521+
def test_apng_save_duration_float(tmp_path: Path) -> None:
522+
test_file = tmp_path / "temp.png"
523+
im = Image.new("1", (1, 1))
524+
im2 = Image.new("1", (1, 1), 1)
525+
im.save(test_file, save_all=True, append_images=[im2], duration=0.5)
526+
527+
with Image.open(test_file) as reloaded:
528+
assert reloaded.info["duration"] == 0.5
529+
530+
531+
def test_apng_save_large_duration(tmp_path: Path) -> None:
532+
test_file = tmp_path / "temp.png"
533+
im = Image.new("1", (1, 1))
534+
im2 = Image.new("1", (1, 1), 1)
535+
with pytest.raises(ValueError, match="cannot write duration"):
536+
im.save(test_file, save_all=True, append_images=[im2], duration=65536000)
537+
538+
521539
def test_apng_save_disposal(tmp_path: Path) -> None:
522540
test_file = tmp_path / "temp.png"
523541
size = (128, 64)

docs/handbook/image-file-formats.rst

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,9 +1041,8 @@ following parameters can also be set:
10411041
Defaults to 0.
10421042

10431043
**duration**
1044-
Integer (or list or tuple of integers) length of time to display this APNG frame
1045-
(in milliseconds).
1046-
Defaults to 0.
1044+
The length of time (or list or tuple of lengths of time) to display this APNG frame
1045+
(in milliseconds). Defaults to 0.
10471046

10481047
**disposal**
10491048
An integer (or list or tuple of integers) specifying the APNG disposal

src/PIL/PngImagePlugin.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import warnings
4040
import zlib
4141
from enum import IntEnum
42+
from fractions import Fraction
4243
from typing import IO, NamedTuple, cast
4344

4445
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
@@ -1275,7 +1276,11 @@ def _write_multiple_frames(
12751276
im_frame = im_frame.crop(bbox)
12761277
size = im_frame.size
12771278
encoderinfo = frame_data.encoderinfo
1278-
frame_duration = int(round(encoderinfo.get("duration", 0)))
1279+
frame_duration = encoderinfo.get("duration", 0)
1280+
delay = Fraction(frame_duration / 1000).limit_denominator(65535)
1281+
if delay.numerator > 65535:
1282+
msg = "cannot write duration"
1283+
raise ValueError(msg)
12791284
frame_disposal = encoderinfo.get("disposal", disposal)
12801285
frame_blend = encoderinfo.get("blend", blend)
12811286
# frame control
@@ -1287,8 +1292,8 @@ def _write_multiple_frames(
12871292
o32(size[1]), # height
12881293
o32(bbox[0]), # x_offset
12891294
o32(bbox[1]), # y_offset
1290-
o16(frame_duration), # delay_numerator
1291-
o16(1000), # delay_denominator
1295+
o16(delay.numerator), # delay_numerator
1296+
o16(delay.denominator), # delay_denominator
12921297
o8(frame_disposal), # dispose_op
12931298
o8(frame_blend), # blend_op
12941299
)

0 commit comments

Comments
 (0)