From 072f1e94e62a85d329533fe59bfd7aafdd478dae Mon Sep 17 00:00:00 2001 From: Andrew Chen <48723787+chuenchen309@users.noreply.github.com> Date: Fri, 17 Jul 2026 14:57:34 +0800 Subject: [PATCH 1/5] Handle the premultiplied La and RGBa modes in ImageColor.getcolor() Both alpha checks test mode[-1] == "A", uppercase only, so the premultiplied modes La and RGBa fall through and their alpha is dropped: Image.new("La", s, "red") comes out fully transparent, and RGBa silently forces alpha to 255. Premultiply with rounding to match convert(); verified against convert() over all 256 alpha values. Co-Authored-By: Claude Opus 4.8 --- Tests/test_imagecolor.py | 13 +++++++++++++ src/PIL/ImageColor.py | 9 +++++++++ 2 files changed, 22 insertions(+) diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index 6eea7886dd8..fdae64fd348 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -199,6 +199,19 @@ def test_color_hsv() -> None: assert (170, 255, 255) == ImageColor.getcolor("hsv(240, 100%, 100%)", "HSV") +@pytest.mark.parametrize("mode, premultiplied_mode", (("LA", "La"), ("RGBA", "RGBa"))) +@pytest.mark.parametrize("color", ("red", "#ff000080")) +def test_color_premultiplied(mode: str, premultiplied_mode: str, color: str) -> None: + # The premultiplied modes are spelled with a lowercase "a", so an + # uppercase-only check drops their alpha. Compare against convert(). + expected = ( + Image.new(mode, (1, 1), color).convert(premultiplied_mode).getpixel((0, 0)) + ) + + assert ImageColor.getcolor(color, premultiplied_mode) == expected + assert Image.new(premultiplied_mode, (1, 1), color).getpixel((0, 0)) == expected + + def test_color_too_long() -> None: # Arrange color_too_long = "hsl(" + "1" * 40 + "," + "1" * 40 + "%," + "1" * 40 + "%)" diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index 9a15a8eb759..bce90b585b3 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -159,12 +159,21 @@ def getcolor(color: str, mode: str) -> int | tuple[int, ...]: graylevel = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16 if mode[-1] == "A": return graylevel, alpha + if mode[-1] == "a": + return _premultiply(graylevel, alpha), alpha return graylevel elif mode[-1] == "A": return rgb + (alpha,) + elif mode[-1] == "a": + return tuple(_premultiply(band, alpha) for band in rgb) + (alpha,) return rgb +def _premultiply(band: int, alpha: int) -> int: + # Rounded, to match convert()'s premultiplication. + return (band * alpha + 127) // 255 + + colormap: dict[str, str | tuple[int, int, int]] = { # X11 colour table from https://drafts.csswg.org/css-color-4/, with # gray/grey spelling issues fixed. This is a superset of HTML 4.0 From 16fc5ec57eee8fd00e8134e388b1333e9413f99d Mon Sep 17 00:00:00 2001 From: chuenchen309 <48723787+chuenchen309@users.noreply.github.com> Date: Sun, 19 Jul 2026 09:15:28 +0800 Subject: [PATCH 2/5] Make LA transform fill opaque, consistent with RGBA test_fill[LA] asserted (76, 0): a transparent fill for an opaque "red" fillcolor. That was a side effect of getcolor("red", "La") returning a bare graylevel int, so the transform's internal La fill defaulted alpha to 0. With getcolor now returning (76, 255), the LA fill matches both the RGBA case and Image.new("LA", ..., "red"). --- Tests/test_image_transform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 12a05ec18b1..298ffe527e4 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -94,7 +94,7 @@ def test_quad(self) -> None: ( ("RGB", (255, 0, 0)), ("RGBA", (255, 0, 0, 255)), - ("LA", (76, 0)), + ("LA", (76, 255)), ), ) def test_fill(self, mode: str, expected_pixel: tuple[int, ...]) -> None: From 3f9d6f185c636bdaca1681651d11f44aebc3ff31 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 19 Jul 2026 23:49:43 +1000 Subject: [PATCH 3/5] Remove unrelated assertion --- Tests/test_imagecolor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index fdae64fd348..e7f8dca5e7d 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -209,7 +209,6 @@ def test_color_premultiplied(mode: str, premultiplied_mode: str, color: str) -> ) assert ImageColor.getcolor(color, premultiplied_mode) == expected - assert Image.new(premultiplied_mode, (1, 1), color).getpixel((0, 0)) == expected def test_color_too_long() -> None: From 89beb8fe66166d4cc7d900ef1162c906ebc4d28e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 20 Jul 2026 00:06:47 +1000 Subject: [PATCH 4/5] Simplified code --- Tests/test_imagecolor.py | 14 +++++++------- src/PIL/ImageColor.py | 26 ++++++++++---------------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index e7f8dca5e7d..1208a426953 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -200,13 +200,13 @@ def test_color_hsv() -> None: @pytest.mark.parametrize("mode, premultiplied_mode", (("LA", "La"), ("RGBA", "RGBa"))) -@pytest.mark.parametrize("color", ("red", "#ff000080")) -def test_color_premultiplied(mode: str, premultiplied_mode: str, color: str) -> None: - # The premultiplied modes are spelled with a lowercase "a", so an - # uppercase-only check drops their alpha. Compare against convert(). - expected = ( - Image.new(mode, (1, 1), color).convert(premultiplied_mode).getpixel((0, 0)) - ) +@pytest.mark.parametrize("color", ("#f00", "#ff000080")) +def test_color_premultiplied_alpha( + mode: str, premultiplied_mode: str, color: str +) -> None: + im = Image.new(mode, (1, 1), color) + im_premultiplied = im.convert(premultiplied_mode) + expected = im_premultiplied.getpixel((0, 0)) assert ImageColor.getcolor(color, premultiplied_mode) == expected diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index bce90b585b3..24a1225cdcb 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -152,26 +152,20 @@ def getcolor(color: str, mode: str) -> int | tuple[int, ...]: r, g, b = rgb h, s, v = rgb_to_hsv(r / 255, g / 255, b / 255) return int(h * 255), int(s * 255), int(v * 255) - elif Image.getmodebase(mode) == "L": + if Image.getmodebase(mode) == "L": r, g, b = rgb # ITU-R Recommendation 601-2 for nonlinear RGB # scaled to 24 bits to match the convert's implementation. graylevel = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16 - if mode[-1] == "A": - return graylevel, alpha - if mode[-1] == "a": - return _premultiply(graylevel, alpha), alpha - return graylevel - elif mode[-1] == "A": - return rgb + (alpha,) - elif mode[-1] == "a": - return tuple(_premultiply(band, alpha) for band in rgb) + (alpha,) - return rgb - - -def _premultiply(band: int, alpha: int) -> int: - # Rounded, to match convert()'s premultiplication. - return (band * alpha + 127) // 255 + value: tuple[int, ...] = (graylevel,) + else: + value = rgb + if mode[-1] == "a": + # Rounded, to match convert()'s premultiplication. + value = tuple((band * alpha + 127) // 255 for band in value) + if mode[-1] in ("A", "a"): + return value + (alpha,) + return value[0] if len(value) == 1 else value colormap: dict[str, str | tuple[int, int, int]] = { From c96bbf34ce92272bd86a7dc3bfdea6b70a70b7ba Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 21 Jul 2026 10:27:58 +1000 Subject: [PATCH 5/5] Use round() --- Tests/test_imagecolor.py | 2 +- src/PIL/ImageColor.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index 1208a426953..2b191521613 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -200,7 +200,7 @@ def test_color_hsv() -> None: @pytest.mark.parametrize("mode, premultiplied_mode", (("LA", "La"), ("RGBA", "RGBa"))) -@pytest.mark.parametrize("color", ("#f00", "#ff000080")) +@pytest.mark.parametrize("color", ("#f00", "#ff000080", "#ff000050")) def test_color_premultiplied_alpha( mode: str, premultiplied_mode: str, color: str ) -> None: diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index 24a1225cdcb..ef1548c7e23 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -161,8 +161,7 @@ def getcolor(color: str, mode: str) -> int | tuple[int, ...]: else: value = rgb if mode[-1] == "a": - # Rounded, to match convert()'s premultiplication. - value = tuple((band * alpha + 127) // 255 for band in value) + value = tuple(round(band * alpha / 255) for band in value) if mode[-1] in ("A", "a"): return value + (alpha,) return value[0] if len(value) == 1 else value