Skip to content

Commit ddcf065

Browse files
committed
Added progress callback when save_all is used
1 parent aaa7587 commit ddcf065

12 files changed

Lines changed: 233 additions & 19 deletions

Tests/test_file_apng.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from io import BytesIO
2+
13
import pytest
24

35
from PIL import Image, ImageSequence, PngImagePlugin
@@ -663,6 +665,33 @@ def test_apng_save_blend(tmp_path):
663665
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
664666

665667

668+
def test_save_all_progress():
669+
out = BytesIO()
670+
progress = []
671+
672+
def callback(filename, frame_number, n_frames):
673+
progress.append((filename, frame_number, n_frames))
674+
675+
Image.new("RGB", (1, 1)).save(out, "PNG", save_all=True, progress=callback)
676+
assert progress == [(None, 1, 1)]
677+
678+
out = BytesIO()
679+
progress = []
680+
681+
with Image.open("Tests/images/apng/single_frame.png") as im:
682+
with Image.open("Tests/images/apng/delay.png") as im2:
683+
im.save(out, "PNG", save_all=True, append_images=[im2], progress=callback)
684+
685+
assert progress == [
686+
("Tests/images/apng/single_frame.png", 1, 6),
687+
("Tests/images/apng/delay.png", 2, 6),
688+
("Tests/images/apng/delay.png", 3, 6),
689+
("Tests/images/apng/delay.png", 4, 6),
690+
("Tests/images/apng/delay.png", 5, 6),
691+
("Tests/images/apng/delay.png", 6, 6),
692+
]
693+
694+
666695
def test_seek_after_close():
667696
im = Image.open("Tests/images/apng/delay.png")
668697
im.seek(1)

Tests/test_file_gif.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,33 @@ def test_roundtrip_save_all_1(tmp_path):
265265
assert reloaded.getpixel((0, 0)) == 255
266266

267267

268+
def test_save_all_progress():
269+
out = BytesIO()
270+
progress = []
271+
272+
def callback(filename, frame_number, n_frames):
273+
progress.append((filename, frame_number, n_frames))
274+
275+
Image.new("RGB", (1, 1)).save(out, "GIF", save_all=True, progress=callback)
276+
assert progress == [(None, 1, 1)]
277+
278+
out = BytesIO()
279+
progress = []
280+
281+
with Image.open("Tests/images/hopper.gif") as im:
282+
with Image.open("Tests/images/dispose_bgnd.gif") as im2:
283+
im.save(out, "GIF", save_all=True, append_images=[im2], progress=callback)
284+
285+
assert progress == [
286+
("Tests/images/hopper.gif", 1, 6),
287+
("Tests/images/dispose_bgnd.gif", 2, 6),
288+
("Tests/images/dispose_bgnd.gif", 3, 6),
289+
("Tests/images/dispose_bgnd.gif", 4, 6),
290+
("Tests/images/dispose_bgnd.gif", 5, 6),
291+
("Tests/images/dispose_bgnd.gif", 6, 6),
292+
]
293+
294+
268295
@pytest.mark.parametrize(
269296
"path, mode",
270297
(

Tests/test_file_mpo.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,3 +278,28 @@ def test_save_all():
278278
# Test that a single frame image will not be saved as an MPO
279279
jpg = roundtrip(im, save_all=True)
280280
assert "mp" not in jpg.info
281+
282+
283+
def test_save_all_progress():
284+
out = BytesIO()
285+
progress = []
286+
287+
def callback(filename, frame_number, n_frames):
288+
progress.append((filename, frame_number, n_frames))
289+
290+
Image.new("RGB", (1, 1)).save(out, "MPO", save_all=True, progress=callback)
291+
assert progress == [(None, 1, 1)]
292+
293+
out = BytesIO()
294+
progress = []
295+
296+
with Image.open("Tests/images/sugarshack.mpo") as im:
297+
with Image.open("Tests/images/frozenpond.mpo") as im2:
298+
im.save(out, "MPO", save_all=True, append_images=[im2], progress=callback)
299+
300+
assert progress == [
301+
("Tests/images/sugarshack.mpo", 1, 4),
302+
("Tests/images/sugarshack.mpo", 2, 4),
303+
("Tests/images/frozenpond.mpo", 3, 4),
304+
("Tests/images/frozenpond.mpo", 4, 4),
305+
]

Tests/test_file_pdf.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import io
21
import os
32
import os.path
43
import tempfile
54
import time
5+
from io import BytesIO
66

77
import pytest
88

@@ -169,6 +169,31 @@ def im_generator(ims):
169169
assert os.path.getsize(outfile) > 0
170170

171171

172+
def test_save_all_progress():
173+
out = BytesIO()
174+
progress = []
175+
176+
def callback(filename, frame_number, n_frames):
177+
progress.append((filename, frame_number, n_frames))
178+
179+
Image.new("RGB", (1, 1)).save(out, "PDF", save_all=True, progress=callback)
180+
assert progress == [(None, 1, 1)]
181+
182+
out = BytesIO()
183+
progress = []
184+
185+
with Image.open("Tests/images/sugarshack.mpo") as im:
186+
with Image.open("Tests/images/frozenpond.mpo") as im2:
187+
im.save(out, "PDF", save_all=True, append_images=[im2], progress=callback)
188+
189+
assert progress == [
190+
("Tests/images/sugarshack.mpo", 1, 4),
191+
("Tests/images/sugarshack.mpo", 2, 4),
192+
("Tests/images/frozenpond.mpo", 3, 4),
193+
("Tests/images/frozenpond.mpo", 4, 4),
194+
]
195+
196+
172197
def test_multiframe_normal_save(tmp_path):
173198
# Test saving a multiframe image without save_all
174199
with Image.open("Tests/images/dispose_bgnd.gif") as im:
@@ -323,12 +348,12 @@ def test_pdf_info(tmp_path):
323348

324349
def test_pdf_append_to_bytesio():
325350
im = hopper("RGB")
326-
f = io.BytesIO()
351+
f = BytesIO()
327352
im.save(f, format="PDF")
328353
initial_size = len(f.getvalue())
329354
assert initial_size > 0
330355
im = hopper("P")
331-
f = io.BytesIO(f.getvalue())
356+
f = BytesIO(f.getvalue())
332357
im.save(f, format="PDF", append=True)
333358
assert len(f.getvalue()) > initial_size
334359

Tests/test_file_tiff.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -658,7 +658,7 @@ def test_palette(self, mode, tmp_path):
658658
with Image.open(outfile) as reloaded:
659659
assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
660660

661-
def test_tiff_save_all(self):
661+
def test_save_all(self):
662662
mp = BytesIO()
663663
with Image.open("Tests/images/multipage.tiff") as im:
664664
im.save(mp, format="tiff", save_all=True)
@@ -688,6 +688,32 @@ def im_generator(ims):
688688
with Image.open(mp) as reread:
689689
assert reread.n_frames == 3
690690

691+
def test_save_all_progress(self):
692+
out = BytesIO()
693+
progress = []
694+
695+
def callback(filename, frame_number, n_frames):
696+
progress.append((filename, frame_number, n_frames))
697+
698+
Image.new("RGB", (1, 1)).save(out, "TIFF", save_all=True, progress=callback)
699+
assert progress == [(None, 1, 1)]
700+
701+
out = BytesIO()
702+
progress = []
703+
704+
with Image.open("Tests/images/hopper.tif") as im:
705+
with Image.open("Tests/images/multipage.tiff") as im2:
706+
im.save(
707+
out, "TIFF", save_all=True, append_images=[im2], progress=callback
708+
)
709+
710+
assert progress == [
711+
("Tests/images/hopper.tif", 1, 4),
712+
("Tests/images/multipage.tiff", 2, 4),
713+
("Tests/images/multipage.tiff", 3, 4),
714+
("Tests/images/multipage.tiff", 4, 4),
715+
]
716+
691717
def test_saving_icc_profile(self, tmp_path):
692718
# Tests saving TIFF with icc_profile set.
693719
# At the time of writing this will only work for non-compressed tiffs

Tests/test_file_webp.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import io
21
import re
32
import sys
43
import warnings
4+
from io import BytesIO
55

66
import pytest
77

@@ -102,10 +102,10 @@ def test_write_rgb(self, tmp_path):
102102
def test_write_method(self, tmp_path):
103103
self._roundtrip(tmp_path, self.rgb_mode, 12.0, {"method": 6})
104104

105-
buffer_no_args = io.BytesIO()
105+
buffer_no_args = BytesIO()
106106
hopper().save(buffer_no_args, format="WEBP")
107107

108-
buffer_method = io.BytesIO()
108+
buffer_method = BytesIO()
109109
hopper().save(buffer_method, format="WEBP", method=6)
110110
assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
111111

@@ -122,6 +122,30 @@ def test_save_all(self, tmp_path):
122122
reloaded.seek(1)
123123
assert_image_similar(im2, reloaded, 1)
124124

125+
@skip_unless_feature("webp_anim")
126+
def test_save_all_progress(self):
127+
out = BytesIO()
128+
progress = []
129+
130+
def callback(filename, frame_number, n_frames):
131+
progress.append((filename, frame_number, n_frames))
132+
133+
Image.new("RGB", (1, 1)).save(out, "WEBP", save_all=True, progress=callback)
134+
assert progress == [(None, 1, 1)]
135+
136+
out = BytesIO()
137+
progress = []
138+
139+
with Image.open("Tests/images/iss634.webp") as im:
140+
im2 = Image.new("RGB", im.size)
141+
im.save(out, "WEBP", save_all=True, append_images=[im2], progress=callback)
142+
143+
expected = []
144+
for i in range(42):
145+
expected.append(("Tests/images/iss634.webp", i + 1, 43))
146+
expected.append((None, 43, 43))
147+
assert progress == expected
148+
125149
def test_icc_profile(self, tmp_path):
126150
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
127151
if _webp.HAVE_WEBPANIM:

src/PIL/GifImagePlugin.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
# See the README file for information on usage and redistribution.
2525
#
2626

27-
import itertools
2827
import math
2928
import os
3029
import subprocess
@@ -578,10 +577,17 @@ def _write_multiple_frames(im, fp, palette):
578577
duration = im.encoderinfo.get("duration")
579578
disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
580579

580+
progress = im.encoderinfo.get("progress")
581+
imSequences = [im] + list(im.encoderinfo.get("append_images", []))
582+
if progress:
583+
n_frames = 0
584+
for imSequence in imSequences:
585+
n_frames += getattr(imSequence, "n_frames", 1)
586+
581587
im_frames = []
582588
frame_count = 0
583589
background_im = None
584-
for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):
590+
for imSequence in imSequences:
585591
for im_frame in ImageSequence.Iterator(imSequence):
586592
# a copy is required here since seek can still mutate the image
587593
im_frame = _normalize_mode(im_frame.copy())
@@ -611,6 +617,10 @@ def _write_multiple_frames(im, fp, palette):
611617
# This frame is identical to the previous frame
612618
if encoderinfo.get("duration"):
613619
previous["encoderinfo"]["duration"] += encoderinfo["duration"]
620+
if progress:
621+
progress(
622+
getattr(imSequence, "filename", None), frame_count, n_frames
623+
)
614624
continue
615625
if encoderinfo.get("disposal") == 2:
616626
if background_im is None:
@@ -624,6 +634,8 @@ def _write_multiple_frames(im, fp, palette):
624634
else:
625635
bbox = None
626636
im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
637+
if progress:
638+
progress(getattr(imSequence, "filename", None), frame_count, n_frames)
627639

628640
if len(im_frames) > 1:
629641
for frame_data in im_frames:

src/PIL/MpoImagePlugin.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
# See the README file for information on usage and redistribution.
1919
#
2020

21-
import itertools
2221
import os
2322
import struct
2423

@@ -42,6 +41,7 @@ def _save(im, fp, filename):
4241

4342

4443
def _save_all(im, fp, filename):
44+
progress = im.encoderinfo.get("progress")
4545
append_images = im.encoderinfo.get("append_images", [])
4646
if not append_images:
4747
try:
@@ -50,11 +50,19 @@ def _save_all(im, fp, filename):
5050
animated = False
5151
if not animated:
5252
_save(im, fp, filename)
53+
if progress:
54+
progress(getattr(im, "filename", None), 1, 1)
5355
return
5456

5557
mpf_offset = 28
5658
offsets = []
57-
for imSequence in itertools.chain([im], append_images):
59+
imSequences = [im] + list(append_images)
60+
if progress:
61+
frame_number = 0
62+
n_frames = 0
63+
for imSequence in imSequences:
64+
n_frames += getattr(imSequence, "n_frames", 1)
65+
for imSequence in imSequences:
5866
for im_frame in ImageSequence.Iterator(imSequence):
5967
if not offsets:
6068
# APP2 marker
@@ -73,6 +81,9 @@ def _save_all(im, fp, filename):
7381
else:
7482
im_frame.save(fp, "JPEG")
7583
offsets.append(fp.tell() - offsets[-1])
84+
if progress:
85+
frame_number += 1
86+
progress(getattr(imSequence, "filename", None), frame_number, n_frames)
7687

7788
ifd = TiffImagePlugin.ImageFileDirectory_v2()
7889
ifd[0xB000] = b"0100"

src/PIL/PdfImagePlugin.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ def _save(im, fp, filename, save_all=False):
246246
# catalog and list of pages
247247
existing_pdf.write_catalog()
248248

249+
progress = im.encoderinfo.get("progress")
249250
page_number = 0
250251
for im_sequence in ims:
251252
im_pages = ImageSequence.Iterator(im_sequence) if save_all else [im_sequence]
@@ -281,6 +282,10 @@ def _save(im, fp, filename, save_all=False):
281282
existing_pdf.write_obj(contents_refs[page_number], stream=page_contents)
282283

283284
page_number += 1
285+
if progress:
286+
progress(
287+
getattr(im_sequence, "filename", None), page_number, number_of_pages
288+
)
284289

285290
#
286291
# trailer

0 commit comments

Comments
 (0)