Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Tests/test_file_apng.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 2 additions & 3 deletions docs/handbook/image-file-formats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions src/PIL/PngImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
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
Expand Down Expand Up @@ -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
Expand All @@ -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
)
Expand Down
Loading