This commit is contained in:
Andrew Murray 2025-07-08 17:02:07 +00:00 committed by GitHub
commit 73e9a30bcb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 414 additions and 31 deletions

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
import os
from io import BytesIO
from pathlib import Path from pathlib import Path
import pytest import pytest
@ -708,6 +710,56 @@ def test_apng_save_blend(tmp_path: Path) -> None:
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
def test_save_all_progress() -> None:
out = BytesIO()
progress = []
def callback(state) -> None:
if state["image_filename"]:
state["image_filename"] = os.path.basename(state["image_filename"])
progress.append(state)
Image.new("RGB", (1, 1)).save(out, "PNG", save_all=True, progress=callback)
assert progress == [
{
"image_index": 0,
"image_filename": None,
"completed_frames": 1,
"total_frames": 1,
}
]
out = BytesIO()
progress = []
with Image.open("Tests/images/apng/single_frame.png") as im:
with Image.open("Tests/images/apng/delay.png") as im2:
im.save(
out, "PNG", save_all=True, append_images=[im, im2], progress=callback
)
expected = []
for i in range(2):
expected.append(
{
"image_index": i,
"image_filename": "single_frame.png",
"completed_frames": i + 1,
"total_frames": 7,
}
)
for i in range(5):
expected.append(
{
"image_index": 2,
"image_filename": "delay.png",
"completed_frames": i + 3,
"total_frames": 7,
}
)
assert progress == expected
def test_apng_save_size(tmp_path: Path) -> None: def test_apng_save_size(tmp_path: Path) -> None:
test_file = tmp_path / "temp.png" test_file = tmp_path / "temp.png"

View File

@ -217,6 +217,52 @@ class TestFileAvif:
with Image.open(blob) as im: with Image.open(blob) as im:
im.load() im.load()
def test_save_all_progress(self) -> None:
out = BytesIO()
progress = []
def callback(state) -> None:
if state["image_filename"]:
state["image_filename"] = os.path.basename(state["image_filename"])
progress.append(state)
Image.new("RGB", (1, 1)).save(out, "AVIF", save_all=True, progress=callback)
assert progress == [
{
"image_index": 0,
"image_filename": None,
"completed_frames": 1,
"total_frames": 1,
}
]
out = BytesIO()
progress = []
with Image.open("Tests/images/avif/star.avifs") as im:
im2 = Image.new(im.mode, im.size)
im.save(out, "AVIF", save_all=True, append_images=[im2], progress=callback)
expected = []
for i in range(5):
expected.append(
{
"image_index": 0,
"image_filename": "star.avifs",
"completed_frames": i + 1,
"total_frames": 6,
}
)
expected.append(
{
"image_index": 1,
"image_filename": None,
"completed_frames": 6,
"total_frames": 6,
}
)
assert progress == expected
def test_background_from_gif(self, tmp_path: Path) -> None: def test_background_from_gif(self, tmp_path: Path) -> None:
with Image.open("Tests/images/chi.gif") as im: with Image.open("Tests/images/chi.gif") as im:
original_value = im.convert("RGB").getpixel((1, 1)) original_value = im.convert("RGB").getpixel((1, 1))

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import os
import warnings import warnings
from collections.abc import Generator from collections.abc import Generator
from io import BytesIO from io import BytesIO
@ -309,6 +310,52 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None:
assert reloaded.getpixel((0, 0)) == 255 assert reloaded.getpixel((0, 0)) == 255
def test_save_all_progress() -> None:
out = BytesIO()
progress = []
def callback(state) -> None:
if state["image_filename"]:
state["image_filename"] = os.path.basename(state["image_filename"])
progress.append(state)
Image.new("RGB", (1, 1)).save(out, "GIF", save_all=True, progress=callback)
assert progress == [
{
"image_index": 0,
"image_filename": None,
"completed_frames": 1,
"total_frames": 1,
}
]
out = BytesIO()
progress = []
with Image.open("Tests/images/chi.gif") as im2:
im = Image.new("RGB", im2.size)
im.save(out, "GIF", save_all=True, append_images=[im2], progress=callback)
expected: list[dict[str, int | str | None]] = [
{
"image_index": 0,
"image_filename": None,
"completed_frames": 1,
"total_frames": 32,
}
]
for i in range(31):
expected.append(
{
"image_index": 1,
"image_filename": "chi.gif",
"completed_frames": i + 2,
"total_frames": 32,
}
)
assert progress == expected
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path, mode", "path, mode",
( (

View File

@ -314,6 +314,48 @@ def test_save_all() -> None:
assert "mp" not in jpg.info assert "mp" not in jpg.info
def test_save_all_progress() -> None:
out = BytesIO()
progress = []
def callback(state) -> None:
if state["image_filename"]:
state["image_filename"] = (
state["image_filename"].replace("\\", "/").split("Tests/images/")[-1]
)
progress.append(state)
Image.new("RGB", (1, 1)).save(out, "MPO", save_all=True, progress=callback)
assert progress == [
{
"image_index": 0,
"image_filename": None,
"completed_frames": 1,
"total_frames": 1,
}
]
out = BytesIO()
progress = []
with Image.open("Tests/images/sugarshack.mpo") as im:
with Image.open("Tests/images/frozenpond.mpo") as im2:
im.save(out, "MPO", save_all=True, append_images=[im2], progress=callback)
expected = []
for i, filename in enumerate(["sugarshack.mpo", "frozenpond.mpo"]):
for j in range(2):
expected.append(
{
"image_index": i,
"image_filename": filename,
"completed_frames": i * 2 + j + 1,
"total_frames": 4,
}
)
assert progress == expected
def test_save_xmp() -> None: def test_save_xmp() -> None:
im = Image.new("RGB", (1, 1)) im = Image.new("RGB", (1, 1))
im2 = Image.new("RGB", (1, 1), "#f00") im2 = Image.new("RGB", (1, 1), "#f00")

View File

@ -1,11 +1,11 @@
from __future__ import annotations from __future__ import annotations
import io
import os import os
import os.path import os.path
import tempfile import tempfile
import time import time
from collections.abc import Generator from collections.abc import Generator
from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -179,6 +179,46 @@ def test_save_all(tmp_path: Path) -> None:
assert os.path.getsize(outfile) > 0 assert os.path.getsize(outfile) > 0
def test_save_all_progress() -> None:
out = BytesIO()
progress = []
def callback(state) -> None:
if state["image_filename"]:
state["image_filename"] = os.path.basename(state["image_filename"])
progress.append(state)
Image.new("RGB", (1, 1)).save(out, "PDF", save_all=True, progress=callback)
assert progress == [
{
"image_index": 0,
"image_filename": None,
"completed_frames": 1,
"total_frames": 1,
}
]
out = BytesIO()
progress = []
with Image.open("Tests/images/sugarshack.mpo") as im:
with Image.open("Tests/images/frozenpond.mpo") as im2:
im.save(out, "PDF", save_all=True, append_images=[im2], progress=callback)
expected = []
for i, filename in enumerate(["sugarshack.mpo", "frozenpond.mpo"]):
for j in range(2):
expected.append(
{
"image_index": i,
"image_filename": filename,
"completed_frames": i * 2 + j + 1,
"total_frames": 4,
}
)
assert progress == expected
def test_multiframe_normal_save(tmp_path: Path) -> None: def test_multiframe_normal_save(tmp_path: Path) -> None:
# Test saving a multiframe image without save_all # Test saving a multiframe image without save_all
with Image.open("Tests/images/dispose_bgnd.gif") as im: with Image.open("Tests/images/dispose_bgnd.gif") as im:
@ -334,12 +374,12 @@ def test_pdf_info(tmp_path: Path) -> None:
def test_pdf_append_to_bytesio() -> None: def test_pdf_append_to_bytesio() -> None:
im = hopper("RGB") im = hopper("RGB")
f = io.BytesIO() f = BytesIO()
im.save(f, format="PDF") im.save(f, format="PDF")
initial_size = len(f.getvalue()) initial_size = len(f.getvalue())
assert initial_size > 0 assert initial_size > 0
im = hopper("P") im = hopper("P")
f = io.BytesIO(f.getvalue()) f = BytesIO(f.getvalue())
im.save(f, format="PDF", append=True) im.save(f, format="PDF", append=True)
assert len(f.getvalue()) > initial_size assert len(f.getvalue()) > initial_size

View File

@ -752,7 +752,7 @@ class TestFileTiff:
with Image.open(outfile) as reloaded: with Image.open(outfile) as reloaded:
assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
def test_tiff_save_all(self) -> None: def test_save_all(self) -> None:
mp = BytesIO() mp = BytesIO()
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
im.save(mp, format="tiff", save_all=True) im.save(mp, format="tiff", save_all=True)
@ -785,6 +785,53 @@ class TestFileTiff:
assert isinstance(reread, TiffImagePlugin.TiffImageFile) assert isinstance(reread, TiffImagePlugin.TiffImageFile)
assert reread.n_frames == 3 assert reread.n_frames == 3
def test_save_all_progress(self) -> None:
out = BytesIO()
progress = []
def callback(state) -> None:
if state["image_filename"]:
state["image_filename"] = os.path.basename(state["image_filename"])
progress.append(state)
Image.new("RGB", (1, 1)).save(out, "TIFF", save_all=True, progress=callback)
assert progress == [
{
"image_index": 0,
"image_filename": None,
"completed_frames": 1,
"total_frames": 1,
}
]
out = BytesIO()
progress = []
with Image.open("Tests/images/hopper.tif") as im:
with Image.open("Tests/images/multipage.tiff") as im2:
im.save(
out, "TIFF", save_all=True, append_images=[im2], progress=callback
)
expected = [
{
"image_index": 0,
"image_filename": "hopper.tif",
"completed_frames": 1,
"total_frames": 4,
}
]
for i in range(3):
expected.append(
{
"image_index": 1,
"image_filename": "multipage.tiff",
"completed_frames": i + 2,
"total_frames": 4,
}
)
assert progress == expected
def test_fixoffsets(self) -> None: def test_fixoffsets(self) -> None:
b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00") b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00")
with TiffImagePlugin.AppendingTiffWriter(b) as a: with TiffImagePlugin.AppendingTiffWriter(b) as a:

View File

@ -1,9 +1,10 @@
from __future__ import annotations from __future__ import annotations
import io import os
import re import re
import sys import sys
import warnings import warnings
from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -105,10 +106,10 @@ class TestFileWebp:
def test_write_method(self, tmp_path: Path) -> None: def test_write_method(self, tmp_path: Path) -> None:
self._roundtrip(tmp_path, self.rgb_mode, 12.0, {"method": 6}) self._roundtrip(tmp_path, self.rgb_mode, 12.0, {"method": 6})
buffer_no_args = io.BytesIO() buffer_no_args = BytesIO()
hopper().save(buffer_no_args, format="WEBP") hopper().save(buffer_no_args, format="WEBP")
buffer_method = io.BytesIO() buffer_method = BytesIO()
hopper().save(buffer_method, format="WEBP", method=6) hopper().save(buffer_method, format="WEBP", method=6)
assert buffer_no_args.getbuffer() != buffer_method.getbuffer() assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
@ -129,6 +130,52 @@ class TestFileWebp:
with pytest.raises(ValueError): with pytest.raises(ValueError):
_webp.WebPEncode(im.getim(), False, 0, 0, "", 4, 0, b"", "") _webp.WebPEncode(im.getim(), False, 0, 0, "", 4, 0, b"", "")
def test_save_all_progress(self) -> None:
out = BytesIO()
progress = []
def callback(state) -> None:
if state["image_filename"]:
state["image_filename"] = os.path.basename(state["image_filename"])
progress.append(state)
Image.new("RGB", (1, 1)).save(out, "WEBP", save_all=True, progress=callback)
assert progress == [
{
"image_index": 0,
"image_filename": None,
"completed_frames": 1,
"total_frames": 1,
}
]
out = BytesIO()
progress = []
with Image.open("Tests/images/iss634.webp") as im:
im2 = Image.new("RGB", im.size)
im.save(out, "WEBP", save_all=True, append_images=[im2], progress=callback)
expected = []
for i in range(42):
expected.append(
{
"image_index": 0,
"image_filename": "iss634.webp",
"completed_frames": i + 1,
"total_frames": 43,
}
)
expected.append(
{
"image_index": 1,
"image_filename": None,
"completed_frames": 43,
"total_frames": 43,
}
)
assert progress == expected
def test_icc_profile(self, tmp_path: Path) -> None: def test_icc_profile(self, tmp_path: Path) -> None:
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
self._roundtrip( self._roundtrip(

View File

@ -235,8 +235,9 @@ def _save(
frame_duration = 0 frame_duration = 0
cur_idx = im.tell() cur_idx = im.tell()
is_single_frame = total == 1 is_single_frame = total == 1
progress = info.get("progress")
try: try:
for ims in [im] + append_images: for i, ims in enumerate([im] + append_images):
# Get number of frames in this image # Get number of frames in this image
nfr = getattr(ims, "n_frames", 1) nfr = getattr(ims, "n_frames", 1)
@ -267,6 +268,7 @@ def _save(
# Update frame index # Update frame index
frame_idx += 1 frame_idx += 1
im._save_all_progress(progress, ims, i, frame_idx, total)
if not save_all: if not save_all:
break break

View File

@ -25,7 +25,6 @@
# #
from __future__ import annotations from __future__ import annotations
import itertools
import math import math
import os import os
import subprocess import subprocess
@ -662,12 +661,17 @@ def _write_multiple_frames(
duration = im.encoderinfo.get("duration") duration = im.encoderinfo.get("duration")
disposal = im.encoderinfo.get("disposal", im.info.get("disposal")) disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
im_sequences = [im, *im.encoderinfo.get("append_images", [])]
progress = im.encoderinfo.get("progress")
if progress:
total = sum(getattr(seq, "n_frames", 1) for seq in im_sequences)
im_frames: list[_Frame] = [] im_frames: list[_Frame] = []
previous_im: Image.Image | None = None previous_im: Image.Image | None = None
frame_count = 0 frame_count = 0
background_im = None background_im = None
for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])): for i, seq in enumerate(im_sequences):
for im_frame in ImageSequence.Iterator(imSequence): for im_frame in ImageSequence.Iterator(seq):
# a copy is required here since seek can still mutate the image # a copy is required here since seek can still mutate the image
im_frame = _normalize_mode(im_frame.copy()) im_frame = _normalize_mode(im_frame.copy())
if frame_count == 0: if frame_count == 0:
@ -697,6 +701,8 @@ def _write_multiple_frames(
# This frame is identical to the previous frame # This frame is identical to the previous frame
if encoderinfo.get("duration"): if encoderinfo.get("duration"):
im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"] im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"]
if progress:
im._save_all_progress(progress, seq, i, frame_count, total)
continue continue
if im_frames[-1].encoderinfo.get("disposal") == 2: if im_frames[-1].encoderinfo.get("disposal") == 2:
# To appear correctly in viewers using a convention, # To appear correctly in viewers using a convention,
@ -760,6 +766,8 @@ def _write_multiple_frames(
bbox = None bbox = None
previous_im = im_frame previous_im = im_frame
im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo)) im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo))
if progress:
im._save_all_progress(progress, seq, i, frame_count, total)
if len(im_frames) == 1: if len(im_frames) == 1:
if "duration" in im.encoderinfo: if "duration" in im.encoderinfo:

View File

@ -2586,6 +2586,26 @@ class Image:
self.encoderinfo = {**im._default_encoderinfo, **encoderinfo} self.encoderinfo = {**im._default_encoderinfo, **encoderinfo}
return encoderinfo return encoderinfo
def _save_all_progress(
self,
progress,
im: Image | None = None,
im_index: int = 0,
completed: int = 1,
total: int = 1,
) -> None:
if not progress:
return
progress(
{
"image_index": im_index,
"image_filename": getattr(im or self, "filename", None),
"completed_frames": completed,
"total_frames": total,
}
)
def seek(self, frame: int) -> None: def seek(self, frame: int) -> None:
""" """
Seeks to the given frame in this sequence file. If you seek Seeks to the given frame in this sequence file. If you seek

View File

@ -40,16 +40,20 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
append_images = im.encoderinfo.get("append_images", []) append_images = im.encoderinfo.get("append_images", [])
progress = im.encoderinfo.get("progress")
if not append_images and not getattr(im, "is_animated", False): if not append_images and not getattr(im, "is_animated", False):
_save(im, fp, filename) _save(im, fp, filename)
im._save_all_progress(progress)
return return
mpf_offset = 28 mpf_offset = 28
offsets: list[int] = [] offsets: list[int] = []
im_sequences = [im, *append_images] im_sequences = [im, *append_images]
total = sum(getattr(seq, "n_frames", 1) for seq in im_sequences) total = sum(getattr(seq, "n_frames", 1) for seq in im_sequences)
for im_sequence in im_sequences: if progress:
for im_frame in ImageSequence.Iterator(im_sequence): completed = 0
for i, seq in enumerate(im_sequences):
for im_frame in ImageSequence.Iterator(seq):
if not offsets: if not offsets:
# APP2 marker # APP2 marker
ifd_length = 66 + 16 * total ifd_length = 66 + 16 * total
@ -73,6 +77,9 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
im_frame.save(fp, "JPEG") im_frame.save(fp, "JPEG")
im_frame.encoderinfo = encoderinfo im_frame.encoderinfo = encoderinfo
offsets.append(fp.tell() - offsets[-1]) offsets.append(fp.tell() - offsets[-1])
if progress:
completed += 1
im._save_all_progress(progress, seq, i, completed, total)
ifd = TiffImagePlugin.ImageFileDirectory_v2() ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[0xB000] = b"0100" ifd[0xB000] = b"0100"

View File

@ -254,7 +254,8 @@ def _save(
existing_pdf.write_catalog() existing_pdf.write_catalog()
page_number = 0 page_number = 0
for im_sequence in ims: progress = im.encoderinfo.get("progress")
for i, im_sequence in enumerate(ims):
im_pages: ImageSequence.Iterator | list[Image.Image] = ( im_pages: ImageSequence.Iterator | list[Image.Image] = (
ImageSequence.Iterator(im_sequence) if save_all else [im_sequence] ImageSequence.Iterator(im_sequence) if save_all else [im_sequence]
) )
@ -290,6 +291,9 @@ def _save(
existing_pdf.write_obj(contents_refs[page_number], stream=page_contents) existing_pdf.write_obj(contents_refs[page_number], stream=page_contents)
page_number += 1 page_number += 1
im._save_all_progress(
progress, im_sequence, i, page_number, number_of_pages
)
# #
# trailer # trailer

View File

@ -1169,16 +1169,19 @@ def _write_multiple_frames(
loop = im.encoderinfo.get("loop", im.info.get("loop", 0)) loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE)) disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE))
blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE)) blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE))
progress = im.encoderinfo.get("progress")
if default_image: im_sequences = []
chain = itertools.chain(append_images) if not default_image:
else: im_sequences.append(im)
chain = itertools.chain([im], append_images) im_sequences += append_images
if progress:
total = sum(getattr(seq, "n_frames", 1) for seq in im_sequences)
im_frames: list[_Frame] = [] im_frames: list[_Frame] = []
frame_count = 0 frame_count = 0
for im_seq in chain: for i, seq in enumerate(im_sequences):
for im_frame in ImageSequence.Iterator(im_seq): for im_frame in ImageSequence.Iterator(seq):
if im_frame.mode == mode: if im_frame.mode == mode:
im_frame = im_frame.copy() im_frame = im_frame.copy()
else: else:
@ -1225,10 +1228,14 @@ def _write_multiple_frames(
and "duration" in encoderinfo and "duration" in encoderinfo
): ):
previous.encoderinfo["duration"] += encoderinfo["duration"] previous.encoderinfo["duration"] += encoderinfo["duration"]
if progress:
im._save_all_progress(progress, seq, i, frame_count, total)
continue continue
else: else:
bbox = None bbox = None
im_frames.append(_Frame(im_frame, bbox, encoderinfo)) im_frames.append(_Frame(im_frame, bbox, encoderinfo))
if progress:
im._save_all_progress(progress, seq, i, frame_count, total)
if len(im_frames) == 1 and not default_image: if len(im_frames) == 1 and not default_image:
return im_frames[0].im return im_frames[0].im

View File

@ -2296,25 +2296,36 @@ class AppendingTiffWriter(io.BytesIO):
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
progress = im.encoderinfo.get("progress")
append_images = list(im.encoderinfo.get("append_images", [])) append_images = list(im.encoderinfo.get("append_images", []))
if not hasattr(im, "n_frames") and not append_images: if not hasattr(im, "n_frames") and not append_images:
return _save(im, fp, filename) _save(im, fp, filename)
im._save_all_progress(progress)
return
cur_idx = im.tell() cur_idx = im.tell()
im_sequences = [im] + append_images
if progress:
completed = 0
total = sum(getattr(seq, "n_frames", 1) for seq in im_sequences)
try: try:
with AppendingTiffWriter(fp) as tf: with AppendingTiffWriter(fp) as tf:
for ims in [im] + append_images: for i, seq in enumerate(im_sequences):
encoderinfo = ims._attach_default_encoderinfo(im) encoderinfo = seq._attach_default_encoderinfo(im)
if not hasattr(ims, "encoderconfig"): if not hasattr(seq, "encoderconfig"):
ims.encoderconfig = () seq.encoderconfig = ()
nfr = getattr(ims, "n_frames", 1) nfr = getattr(seq, "n_frames", 1)
for idx in range(nfr): for idx in range(nfr):
ims.seek(idx) seq.seek(idx)
ims.load() seq.load()
_save(ims, tf, filename) _save(seq, tf, filename)
if progress:
completed += 1
im._save_all_progress(progress, seq, i, completed, total)
tf.newFrame() tf.newFrame()
ims.encoderinfo = encoderinfo seq.encoderinfo = encoderinfo
finally: finally:
im.seek(cur_idx) im.seek(cur_idx)

View File

@ -156,6 +156,7 @@ def _convert_frame(im: Image.Image) -> Image.Image:
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
encoderinfo = im.encoderinfo.copy() encoderinfo = im.encoderinfo.copy()
progress = encoderinfo.get("progress")
append_images = list(encoderinfo.get("append_images", [])) append_images = list(encoderinfo.get("append_images", []))
# If total frame count is 1, then save using the legacy API, which # If total frame count is 1, then save using the legacy API, which
@ -165,6 +166,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
total += getattr(ims, "n_frames", 1) total += getattr(ims, "n_frames", 1)
if total == 1: if total == 1:
_save(im, fp, filename) _save(im, fp, filename)
im._save_all_progress(progress)
return return
background: int | tuple[int, ...] = (0, 0, 0, 0) background: int | tuple[int, ...] = (0, 0, 0, 0)
@ -237,7 +239,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
timestamp = 0 timestamp = 0
cur_idx = im.tell() cur_idx = im.tell()
try: try:
for ims in [im] + append_images: for i, ims in enumerate([im] + append_images):
# Get number of frames in this image # Get number of frames in this image
nfr = getattr(ims, "n_frames", 1) nfr = getattr(ims, "n_frames", 1)
@ -262,6 +264,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
else: else:
timestamp += duration timestamp += duration
frame_idx += 1 frame_idx += 1
im._save_all_progress(progress, ims, i, frame_idx, total)
finally: finally:
im.seek(cur_idx) im.seek(cur_idx)