Skip to content

Saving all frames of a paletted PNG with few colors can fail #8081

@Yay295

Description

@Yay295
>>> image.save('copy.png',save_all=True)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\...\.venv\Lib\site-packages\PIL\Image.py", line 2459, in save
    save_handler(self, fp, filename)
  File "C:\...\.venv\Lib\site-packages\PIL\PngImagePlugin.py", line 1230, in _save_all
    _save(im, fp, filename, save_all=True)
  File "C:\...\.venv\Lib\site-packages\PIL\PngImagePlugin.py", line 1408, in _save
    im = _write_multiple_frames(
         ^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\...\.venv\Lib\site-packages\PIL\PngImagePlugin.py", line 1117, in _write_multiple_frames
    im_frame = im_frame.convert(rawmode)
               ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\...\.venv\Lib\site-packages\PIL\Image.py", line 1087, in convert
    im = self.im.convert(mode, dither)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: conversion not supported

The image isn't mine to share, but it's a P mode image with only 9 colors. This causes it to hit this bit of code:

if mode == "P":
#
# attempt to minimize storage requirements for palette images
if "bits" in im.encoderinfo:
# number of bits specified by user
colors = min(1 << im.encoderinfo["bits"], 256)
else:
# check palette contents
if im.palette:
colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 1)
else:
colors = 256
if colors <= 16:
if colors <= 2:
bits = 1
elif colors <= 4:
bits = 2
else:
bits = 4
mode = f"{mode};{bits}"

Which changes mode to P;4. Then when getting the output rawmode:

rawmode, mode = _OUTMODES[mode]

_OUTMODES = {
# supported PIL modes, and corresponding rawmodes/bits/color combinations
"1": ("1", b"\x01\x00"),
"L;1": ("L;1", b"\x01\x00"),
"L;2": ("L;2", b"\x02\x00"),
"L;4": ("L;4", b"\x04\x00"),
"L": ("L", b"\x08\x00"),
"LA": ("LA", b"\x08\x04"),
"I": ("I;16B", b"\x10\x00"),
"I;16": ("I;16B", b"\x10\x00"),
"I;16B": ("I;16B", b"\x10\x00"),
"P;1": ("P;1", b"\x01\x03"),
"P;2": ("P;2", b"\x02\x03"),
"P;4": ("P;4", b"\x04\x03"),
"P": ("P", b"\x08\x03"),
"RGB": ("RGB", b"\x08\x02"),
"RGBA": ("RGBA", b"\x08\x06"),
}

It gets a rawmode of P;4. This would all be fine, except I passed True for the save_all parameter, which causes a branch here:

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

Inside _write_multiple_frames is this bit of code:

if im_frame.mode == rawmode:
im_frame = im_frame.copy()
else:
im_frame = im_frame.convert(rawmode)

Now since the actual image mode is P, but the requested rawmode is P;4, it tries to convert from P to P;4, which fails, because Image.convert() takes modes, not rawmodes, and P;4 is not a valid mode.

This method also attempts to convert to a rawmode a bit further on as well:

if default_image:
if im.mode != rawmode:
im = im.convert(rawmode)

A basic search of the rest of Pillow's code only shows one other instance of ".convert(rawmode)" (in WebPImagePlugin), but it's okay in that case because the rawmode is set on the prior line to be either "RGB" or "RGBA".

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions