Skip to content
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

Consider all frames when selecting mode for PNG save_all #6610

Merged
merged 1 commit into from Oct 18, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions Tests/test_file_apng.py
Expand Up @@ -648,6 +648,16 @@ def test_seek_after_close():
im.seek(0)


@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
def test_different_modes_in_later_frames(mode, tmp_path):
test_file = str(tmp_path / "temp.png")

im = Image.new("L", (1, 1))
im.save(test_file, save_all=True, append_images=[Image.new(mode, (1, 1))])
with Image.open(test_file) as reloaded:
assert reloaded.mode == mode


def test_constants_deprecation():
for enum, prefix in {
PngImagePlugin.Disposal: "APNG_DISPOSE_",
Expand Down
41 changes: 30 additions & 11 deletions src/PIL/PngImagePlugin.py
Expand Up @@ -1089,28 +1089,28 @@ def write(self, data):
self.seq_num += 1


def _write_multiple_frames(im, fp, chunk, rawmode):
default_image = im.encoderinfo.get("default_image", im.info.get("default_image"))
def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images):
duration = im.encoderinfo.get("duration", im.info.get("duration", 0))
loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE))
blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE))

if default_image:
chain = itertools.chain(im.encoderinfo.get("append_images", []))
chain = itertools.chain(append_images)
else:
chain = itertools.chain([im], im.encoderinfo.get("append_images", []))
chain = itertools.chain([im], append_images)

im_frames = []
frame_count = 0
for im_seq in chain:
for im_frame in ImageSequence.Iterator(im_seq):
im_frame = im_frame.copy()
if im_frame.mode != im.mode:
if im.mode == "P":
im_frame = im_frame.convert(im.mode, palette=im.palette)
if im_frame.mode == rawmode:
im_frame = im_frame.copy()
else:
if rawmode == "P":
im_frame = im_frame.convert(rawmode, palette=im.palette)
else:
im_frame = im_frame.convert(im.mode)
im_frame = im_frame.convert(rawmode)
encoderinfo = im.encoderinfo.copy()
if isinstance(duration, (list, tuple)):
encoderinfo["duration"] = duration[frame_count]
Expand Down Expand Up @@ -1221,7 +1221,26 @@ def _save_all(im, fp, filename):
def _save(im, fp, filename, chunk=putchunk, save_all=False):
# save an image to disk (called by the save method)

mode = im.mode
if save_all:
default_image = im.encoderinfo.get(
"default_image", im.info.get("default_image")
)
modes = set()
append_images = im.encoderinfo.get("append_images", [])
if default_image:
chain = itertools.chain(append_images)
else:
chain = itertools.chain([im], append_images)
for im_seq in chain:
for im_frame in ImageSequence.Iterator(im_seq):
modes.add(im_frame.mode)
for mode in ("RGBA", "RGB", "P"):
if mode in modes:
break
else:
mode = modes.pop()
else:
mode = im.mode

if mode == "P":

Expand Down Expand Up @@ -1373,7 +1392,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
chunk(fp, b"eXIf", exif)

if save_all:
_write_multiple_frames(im, fp, chunk, rawmode)
_write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
else:
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])

Expand Down