-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added progress callback when save_all is used #7435
base: main
Are you sure you want to change the base?
Changes from 1 commit
6850465
3465a59
b5f3228
fe7c9d3
fae0653
19c2721
4a0a011
69680db
344c300
3c80ec0
b7a5b59
14560e1
d52b3a2
682a9ae
2f27173
26e2a9f
8b4b7ce
ebbdc6e
fc5c096
62786fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
from io import BytesIO | ||
|
||
import pytest | ||
|
||
from PIL import Image, ImageSequence, PngImagePlugin | ||
|
@@ -663,6 +665,36 @@ def test_apng_save_blend(tmp_path): | |
assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||
|
||
|
||
def test_save_all_progress(): | ||
out = BytesIO() | ||
progress = [] | ||
|
||
def callback(filename, frame_number, n_frames): | ||
progress.append((filename, frame_number, n_frames)) | ||
|
||
Image.new("RGB", (1, 1)).save(out, "PNG", save_all=True, progress=callback) | ||
assert progress == [(None, 1, 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 | ||
) | ||
|
||
assert progress == [ | ||
("Tests/images/apng/single_frame.png", 1, 7), | ||
("Tests/images/apng/single_frame.png", 2, 7), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line could use a comment that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
You no longer think this is necessary? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The comment was meant to clarify that the image is repeated because it occurs twice in the input; providing explicit indices makes pointing that out unnecessary. |
||
("Tests/images/apng/delay.png", 3, 7), | ||
("Tests/images/apng/delay.png", 4, 7), | ||
("Tests/images/apng/delay.png", 5, 7), | ||
("Tests/images/apng/delay.png", 6, 7), | ||
("Tests/images/apng/delay.png", 7, 7), | ||
] | ||
|
||
|
||
def test_seek_after_close(): | ||
im = Image.open("Tests/images/apng/delay.png") | ||
im.seek(1) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
import io | ||
import re | ||
import sys | ||
import warnings | ||
from io import BytesIO | ||
|
||
import pytest | ||
|
||
|
@@ -102,10 +102,10 @@ def test_write_rgb(self, tmp_path): | |
def test_write_method(self, tmp_path): | ||
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") | ||
|
||
buffer_method = io.BytesIO() | ||
buffer_method = BytesIO() | ||
hopper().save(buffer_method, format="WEBP", method=6) | ||
assert buffer_no_args.getbuffer() != buffer_method.getbuffer() | ||
|
||
|
@@ -122,6 +122,30 @@ def test_save_all(self, tmp_path): | |
reloaded.seek(1) | ||
assert_image_similar(im2, reloaded, 1) | ||
|
||
@skip_unless_feature("webp_anim") | ||
def test_save_all_progress(self): | ||
out = BytesIO() | ||
progress = [] | ||
|
||
def callback(filename, frame_number, n_frames): | ||
progress.append((filename, frame_number, n_frames)) | ||
|
||
Image.new("RGB", (1, 1)).save(out, "WEBP", save_all=True, progress=callback) | ||
assert progress == [(None, 1, 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(("Tests/images/iss634.webp", i + 1, 43)) | ||
expected.append((None, 43, 43)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Likewise, saying that the last frame comes from image #2 would be more informative than "don't know" – especially when there's multiple images involved There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've pushed a commit to switch to an index. |
||
assert progress == expected | ||
|
||
def test_icc_profile(self, tmp_path): | ||
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) | ||
if _webp.HAVE_WEBPANIM: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe indicate index here (instead of
None
)?1
for the 1st input image?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you be open to the idea of having an index all of the time? Or, we could pass back the image instance, and let the user figure out the index or filename as they see fit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having index always is probably simpler. As for passing the image, it wouldn't be much help with identifying the index if an image is reused in the input (like in that one test… come to think of it, having explicit index there would make adding a comment somewhat redundant).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've pushed a commit to switch to an index.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...Why did you remove the filename though? I thought you were talking about adding an index, not removing the filename (as my own suggestion was to provide index when the filename isn't available)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use a
namedtuple
instead (i.e. defineProcess = namedtuple('Process', "index filename frame n_frames")
somewhere global, and pass its value to the progress callback).And I'd say that the name of the input file being currently processed is a quite appropriate piece of information to be included, as it's the most user-friendly way of showing, well, which file is being processed (whether you're logging your progress to a terminal or displaying it in GUI on top of the progressbar).
Why make the most likely usecase inconvenient? The name of the game shouldn't be "provide as little information as possible and force the calling code to figure out the rest of it every single time", it should aim at the most likely general purpose case (which I imagine more often than not would be logging/displaying something in the lines of
Converting: input/image2.png [#2] -> output.png (frame 12 of 84)
; and the only thing here that would be instantly available for the calling code without complication in any situation would be the output file name, as it's passed in as a raw string and never changes throughout the entire process).Suppose the code that wants to log progress isn't concerned with input at all – it only has a generator that supplies these images; why would it need to implement workarounds like instantiating all values from that generator at once, or wrapping it into a generator object that mucks around with the data being yielded, when all it wants to do is display progress on screen? And like I said, filename is the clearest way possible of identifying an image that was loaded from a file.
I'm pretty sure these can be obtained with
Image.open()
, just like the first image.The index can be necessary in case of duplicate files in the input, but the filename is likely to be necessary for the purpose of displaying the progress in user-friendly manner. (Now passing the entire image object, that would actually be "passing back everything to the user just in case something is useful" 😆)
Incidentally, passing progress status as a
namedtuple
(or the like) would help with that.Filename and/or index are necessary to show which image this current frame comes from, in case when more than one is on the input – it shouldn't be a stretch to expect it to be necessary information for the vast majority of usecases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still not convinced that a filename or image index is necessary, but I've pushed a commit to restore the filename.
Because namedtuples still have order, and I'm concerned that there could be different views about what the best order is, or that we might want to add more items in the future, I've used a dictionary instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The keys are a bit overly verbose, but that should do the job at least.
Still, I believe having a single fixed class with predefined attributes would make for better API than using free-form
dict
s in every implementation (not to mention avoiding potential issues if there's a need to make changes at some point in the future). If you're concerned with possibility of using indices to access anamedtuple
field (which I honestly find rather unlikely as attribute access is much clearer), you can use a@dataclass
instead.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either way, it would be a good idea to have this
getattr(im, "filename", None)
thing in one place in the codebase (and if you keep thedict
, it would still be a good idea to have one piece of code defining its creation, at least for sake of ensuring there won't be any discrepancies in key names).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've pushed a commit to move the creation of the dictionary into a common method.