Skip to content
Open
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
42 changes: 42 additions & 0 deletions Tests/test_file_xpm.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,48 @@ def test_rgb() -> None:
assert_image_similar(im, hopper(), 16)


def test_transparency() -> None:
data = b"""/* XPM */
static char *test[] = {
"2 2 3 1",
" \tc None",
"r\tc #FF0000",
"b\tc #0000FF",
" r",
"b "};
"""
with Image.open(BytesIO(data)) as im:
assert im.mode == "P"
assert im.info["transparency"] == 0

converted = im.convert("RGBA")
assert converted.getpixel((0, 0)) == (0, 0, 0, 0)
assert converted.getpixel((1, 0)) == (255, 0, 0, 255)
assert converted.getpixel((0, 1)) == (0, 0, 255, 255)


def test_transparency_rgba() -> None:
# With more than 256 colours and a transparent colour,
# the image is opened as RGBA
chars = "0123456789abcdefghijklmnopqrstuvwxy"
colours = [
b'"%s\tc #%06x",' % ((chars[i // 35] + chars[i % 35]).encode(), i + 1)
for i in range(300)
]
colours.append(b'"zz\tc None",')
data = (
b'/* XPM */\nstatic char *test[] = {\n"2 1 301 2",\n'
+ b"\n".join(colours)
+ b'\n"00zz"};\n'
)
with Image.open(BytesIO(data)) as im:
assert im.mode == "RGBA"
assert "transparency" not in im.info

assert im.getpixel((0, 0)) == (0, 0, 1, 255)
assert im.getpixel((1, 0)) == (0, 0, 0, 0)


def test_truncated_header() -> None:
data = b"/* XPM */"
with pytest.raises(SyntaxError, match="broken XPM file"):
Expand Down
24 changes: 18 additions & 6 deletions src/PIL/XpmImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def _open(self) -> None:
# load palette description

palette = {}
transparent_key = None

for _ in range(palette_length):
line = self.fp.readline().rstrip()
Expand All @@ -73,7 +74,8 @@ def _open(self) -> None:
# process colour key
rgb = s[i + 1]
if rgb == b"None":
self.info["transparency"] = c
transparent_key = c
palette[c] = b"\0\0\0"
elif rgb.startswith(b"#"):
rgb_int = int(rgb[1:], 16)
palette[c] = (
Expand All @@ -94,12 +96,22 @@ def _open(self) -> None:

args: tuple[int, dict[bytes, bytes] | tuple[bytes, ...]]
if palette_length > 256:
self._mode = "RGB"
if transparent_key is not None:
self._mode = "RGBA"
palette = {
c: rgb + (b"\0" if c == transparent_key else b"\xff")
for c, rgb in palette.items()
}
else:
self._mode = "RGB"
args = (bpp, palette)
else:
self._mode = "P"
self.palette = ImagePalette.raw("RGB", b"".join(palette.values()))
args = (bpp, tuple(palette.keys()))
palette_keys = tuple(palette.keys())
args = (bpp, palette_keys)
if transparent_key is not None:
self.info["transparency"] = palette_keys.index(transparent_key)

self.tile = [ImageFile._Tile("xpm", (0, 0) + self.size, self.fp.tell(), args)]

Expand All @@ -124,8 +136,8 @@ def decode(self, buffer: Image.DecoderInput) -> tuple[int, int]:
data = bytearray()
bpp, palette = self.args
dest_length = self.state.xsize * self.state.ysize
if self.mode == "RGB":
dest_length *= 3
if self.mode in ("RGB", "RGBA"):
dest_length *= len(self.mode)
pixel_header = False
while len(data) < dest_length:
line = self.fd.readline()
Expand All @@ -137,7 +149,7 @@ def decode(self, buffer: Image.DecoderInput) -> tuple[int, int]:
line = b'"'.join(line.split(b'"')[1:-1])
for i in range(0, len(line), bpp):
key = line[i : i + bpp]
if self.mode == "RGB":
if self.mode in ("RGB", "RGBA"):
data += palette[key]
else:
data += o8(palette.index(key))
Expand Down