Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Tests/test_file_apng.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,30 @@ def test_apng_mode() -> None:
assert im_rgba.getpixel((64, 32)) == (0, 0, 255, 128)


def test_apng_save_different_palettes(tmp_path: Path) -> None:
# APNG does not support multiple palettes
test_file = tmp_path / "temp.png"
red = Image.new("P", (1, 1), (255, 0, 0))
green = Image.new("P", (1, 1), (0, 255, 0))
red.save(test_file, save_all=True, append_images=[green])

with Image.open(test_file) as reloaded:
# So the frames must be converted to another mode
assert reloaded.mode == "RGB"
assert reloaded.getpixel((0, 0)) == (255, 0, 0)

reloaded.seek(1)
assert reloaded.mode == "RGB"
assert reloaded.getpixel((0, 0)) == (0, 255, 0)

# Test that RGBA palettes result in an RGBA image
green = Image.new("L", (1, 1))
green.putpalette([], "RGBA")
red.save(test_file, save_all=True, append_images=[green])
with Image.open(test_file) as reloaded:
assert reloaded.mode == "RGBA"


def test_apng_chunk_errors() -> None:
with Image.open("Tests/images/apng/chunk_no_actl.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
Expand Down
43 changes: 31 additions & 12 deletions src/PIL/PngImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1332,25 +1332,44 @@ def _save(
)
modes = set()
sizes = set()
palette = None
palette_frame_with_alpha = None
different_palette = None
append_images = im.encoderinfo.get("append_images", [])
for im_seq in itertools.chain([im], append_images):
for im_frame in ImageSequence.Iterator(im_seq):
modes.add(im_frame.mode)
sizes.add(im_frame.size)
for mode in ("RGBA", "RGB", "P"):
if mode in modes:
break
if im_frame.mode == "P":
if im_frame.palette and im_frame.palette.mode == "RGBA":
palette_frame_with_alpha = im_frame
if different_palette is None:
frame_palette = im_frame.getpalette() or []
if palette is not None:
if frame_palette != palette:
different_palette = True
else:
palette = frame_palette
elif im_frame.mode in ("RGBA", "RGBA"):
different_palette = False
if different_palette:
# APNG can only contain a single PLTE, so use another mode.
mode = "RGBA" if palette_frame_with_alpha else "RGB"
else:
mode = modes.pop()
for mode in ("RGBA", "RGB", "P"):
if mode in modes:
break
else:
mode = modes.pop()
size = tuple(max(frame_size[i] for frame_size in sizes) for i in range(2))
else:
size = im.size
mode = im.mode
if mode == "P" and im.palette:
palette = im.getpalette() or []
palette_frame_with_alpha = im if im.palette.mode == "RGBA" else None

outmode = mode
palette = []
if im.palette:
palette = im.getpalette() or []
if mode == "P":
#
# attempt to minimize storage requirements for palette images
Expand All @@ -1359,7 +1378,7 @@ def _save(
colors = min(1 << im.encoderinfo["bits"], 256)
else:
# check palette contents
if im.palette:
if palette:
colors = max(min(len(palette) // 3, 256), 1)
else:
colors = 256
Expand Down Expand Up @@ -1429,7 +1448,7 @@ def _save(
if not after_idat:
chunk(fp, cid, data)

if im.mode == "P":
if mode == "P" and palette is not None:
palette_byte_number = colors * 3
palette_bytes = bytes(palette[:palette_byte_number])
while len(palette_bytes) < palette_byte_number:
Expand All @@ -1439,7 +1458,7 @@ def _save(
transparency = im.encoderinfo.get("transparency", im.info.get("transparency"))

if transparency is not None:
if im.mode == "P":
if mode == "P":
# limit to actual palette size
alpha_bytes = colors
if isinstance(transparency, bytes):
Expand Down Expand Up @@ -1473,8 +1492,8 @@ def _save(
# and it's in the info dict. It's probably just stale.
msg = "cannot use transparency for this mode"
raise OSError(msg)
elif im.mode == "P" and im.im.getpalettemode() == "RGBA":
alpha = im.im.getpalette("RGBA", "A")
elif mode == "P" and palette_frame_with_alpha:
alpha = palette_frame_with_alpha.im.getpalette("RGBA", "A")
alpha_bytes = colors
chunk(fp, b"tRNS", alpha[:alpha_bytes])

Expand Down