diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 1697d3d6ff9..cc2ce15ead7 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -52,6 +52,27 @@ def test_transparency() -> None: assert converted.getpixel((2, 0)) == (0, 0, 255, 255) +@pytest.mark.parametrize( + "colour, expected", + ( + (b"#F00", (240, 0, 0)), + (b"#FF8000", (255, 128, 0)), + (b"#123456789", (18, 69, 120)), + (b"#C71BC30BC71B", (199, 195, 199)), + (b"#FFFF00000000", (255, 0, 0)), + ), +) +def test_hex_colour_lengths(colour: bytes, expected: tuple[int, int, int]) -> None: + data = b"""/* XPM */ +static char *test[] = { +"1 1 1 1", +"a\tc %s", +"a"}; +""" % colour + with Image.open(BytesIO(data)) as im: + assert im.convert("RGB").getpixel((0, 0)) == expected + + def test_transparency_rgba() -> None: # With more than 256 colours and a transparent colour, # the image is opened as RGBA @@ -77,6 +98,26 @@ def test_truncated_header() -> None: pass +@pytest.mark.parametrize( + "colour, expected", ((b"#1234", (0, 18, 52)), (b"#12345678", (52, 86, 120))) +) +def test_other_hex_colour_lengths( + colour: bytes, expected: tuple[int, int, int] +) -> None: + # Other lengths keep using the lowest 24 bits, without shifting the next colour + data = b"""/* XPM */ +static char *test[] = { +"2 1 2 1", +"a\tc %s", +"b\tc #00ff00", +"ab"}; +""" % colour + with Image.open(BytesIO(data)) as im: + rgb = im.convert("RGB") + assert rgb.getpixel((0, 0)) == expected + assert rgb.getpixel((1, 0)) == (0, 255, 0) + + def test_cannot_read_color() -> None: with open(TEST_FILE, "rb") as fp: data = fp.read().split(b"#")[0] diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index ed277478066..e95eafcb3e3 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -78,6 +78,16 @@ def _open(self) -> None: if rgb == b"None": transparent_key = c palette[c] = b"\0\0\0" + elif rgb.startswith(b"#") and len(rgb) in (4, 7, 10, 13): + # #RGB, #RRGGBB, #RRRGGGBBB or #RRRRGGGGBBBB + # libXpm parses colours with XParseColor, + # which uses the digits as the most significant bits + # of each channel instead of scaling them + digits = (len(rgb) - 1) // 3 + palette[c] = bytes( + int(rgb[i : i + digits], 16) << (16 - 4 * digits) >> 8 + for i in range(1, len(rgb), digits) + ) elif rgb.startswith(b"#"): rgb_int = int(rgb[1:], 16) palette[c] = (