Apply encoder options when saving multiple PNG frames (#9300)

This commit is contained in:
Hugo van Kemenade 2025-11-30 22:21:42 +02:00 committed by GitHub
commit 77e16b1030
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 34 additions and 11 deletions

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO
from pathlib import Path from pathlib import Path
import pytest import pytest
@ -718,6 +719,25 @@ def test_apng_save_size(tmp_path: Path) -> None:
assert reloaded.size == (200, 200) assert reloaded.size == (200, 200)
def test_compress_level() -> None:
compress_level_sizes = {}
for compress_level in (0, 9):
out = BytesIO()
im = Image.new("L", (100, 100))
im.save(
out,
"PNG",
save_all=True,
append_images=[Image.new("L", (200, 200))],
compress_level=compress_level,
)
compress_level_sizes[compress_level] = len(out.getvalue())
assert compress_level_sizes[0] > compress_level_sizes[9]
def test_seek_after_close() -> None: def test_seek_after_close() -> None:
im = Image.open("Tests/images/apng/delay.png") im = Image.open("Tests/images/apng/delay.png")
im.seek(1) im.seek(1)

View File

@ -1154,6 +1154,15 @@ class _fdat:
self.seq_num += 1 self.seq_num += 1
def _apply_encoderinfo(im: Image.Image, encoderinfo: dict[str, Any]) -> None:
im.encoderconfig = (
encoderinfo.get("optimize", False),
encoderinfo.get("compress_level", -1),
encoderinfo.get("compress_type", -1),
encoderinfo.get("dictionary", b""),
)
class _Frame(NamedTuple): class _Frame(NamedTuple):
im: Image.Image im: Image.Image
bbox: tuple[int, int, int, int] | None bbox: tuple[int, int, int, int] | None
@ -1247,10 +1256,10 @@ def _write_multiple_frames(
# default image IDAT (if it exists) # default image IDAT (if it exists)
if default_image: if default_image:
if im.mode != mode: default_im = im if im.mode == mode else im.convert(mode)
im = im.convert(mode) _apply_encoderinfo(default_im, im.encoderinfo)
ImageFile._save( ImageFile._save(
im, default_im,
cast(IO[bytes], _idat(fp, chunk)), cast(IO[bytes], _idat(fp, chunk)),
[ImageFile._Tile("zip", (0, 0) + im.size, 0, rawmode)], [ImageFile._Tile("zip", (0, 0) + im.size, 0, rawmode)],
) )
@ -1284,6 +1293,7 @@ def _write_multiple_frames(
) )
seq_num += 1 seq_num += 1
# frame data # frame data
_apply_encoderinfo(im_frame, im.encoderinfo)
if frame == 0 and not default_image: if frame == 0 and not default_image:
# first frame must be in IDAT chunks for backwards compatibility # first frame must be in IDAT chunks for backwards compatibility
ImageFile._save( ImageFile._save(
@ -1359,14 +1369,6 @@ def _save(
bits = 4 bits = 4
outmode += f";{bits}" outmode += f";{bits}"
# encoder options
im.encoderconfig = (
im.encoderinfo.get("optimize", False),
im.encoderinfo.get("compress_level", -1),
im.encoderinfo.get("compress_type", -1),
im.encoderinfo.get("dictionary", b""),
)
# get the corresponding PNG mode # get the corresponding PNG mode
try: try:
rawmode, bit_depth, color_type = _OUTMODES[outmode] rawmode, bit_depth, color_type = _OUTMODES[outmode]
@ -1496,6 +1498,7 @@ def _save(
im, fp, chunk, mode, rawmode, default_image, append_images im, fp, chunk, mode, rawmode, default_image, append_images
) )
if single_im: if single_im:
_apply_encoderinfo(single_im, im.encoderinfo)
ImageFile._save( ImageFile._save(
single_im, single_im,
cast(IO[bytes], _idat(fp, chunk)), cast(IO[bytes], _idat(fp, chunk)),