diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 86d86602f17..34e3e3d5972 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -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"): diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 80192f55e53..6eee878a266 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -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() @@ -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] = ( @@ -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)] @@ -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() @@ -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))