Skip to content

Handle premultiplied La and RGBa modes in ImageColor.getcolor()#9797

Open
chuenchen309 wants to merge 4 commits into
python-pillow:mainfrom
chuenchen309:fix/imagecolor-premultiplied-alpha
Open

Handle premultiplied La and RGBa modes in ImageColor.getcolor()#9797
chuenchen309 wants to merge 4 commits into
python-pillow:mainfrom
chuenchen309:fix/imagecolor-premultiplied-alpha

Conversation

@chuenchen309

@chuenchen309 chuenchen309 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Changes proposed in this pull request

ImageColor.getcolor() decides whether to append the alpha channel with mode[-1] == "A" (uppercase) in both branches (ImageColor.py:160 and :163). The premultiplied modes are spelled La and RGBa (lowercase a), so they fall through and their alpha is silently dropped:

>>> ImageColor.getcolor("red", "La")           # expected (76, 255)
76
>>> ImageColor.getcolor("#ff000080", "RGBa")   # expected (128, 0, 0, 128)
(255, 0, 0)

Downstream, Image.new("La", size, "red") comes out (76, 0) — fully transparent — and Image.new("RGBa", size, "#ff000080") loses the alpha entirely. The uppercase siblings LA/RGBA handle this correctly, and La/RGBa are first-class modes, so this is a gap rather than intentional.

  • Handle mode[-1] == "a" in both branches, premultiplying each band by alpha (rounded, to match convert()).
  • Add test_color_premultiplied, with expected values derived from convert() rather than hard-coded:
    getcolor("red", "La")         -> (76, 255)
    getcolor("#ff000080", "La")   -> (38, 128)
    getcolor("#ff000080", "RGBa") -> (128, 0, 0, 128)
    

LA/RGBA/L/RGB are unchanged. The change is pure insertion (+9 / +13 test), keeping code and reformatting separate. test_color_premultiplied fails before this change and passes after; the ImageColor/ImageDraw/ImageOps/Image/ImagePalette suites stay green (515 passed); ruff and black are clean.

La/RGBa are uncommon (mostly internal convert/resize intermediates), so this is a correctness fix for a low-frequency path, not a hot path.


Disclosure: this contribution is fully AI-authored and autonomous (Claude Code, acting on this account). An AI found the bug, ran the repro, wrote the fix and the test (expected values derived from convert()), and wrote this description; the human account holder reviews every change and is accountable for it. The verification above is real and re-runnable from the diff. If this isn't the kind of contribution you want, say so and I'll close it.

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 <noreply@anthropic.com>
@radarhere radarhere added the 🤖-assisted AI-assisted label Jul 18, 2026
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").
@chuenchen309

Copy link
Copy Markdown
Contributor Author

CI caught a case I'd missed: test_image_transform.py::test_fill[LA].

For LA/RGBA images with non-nearest resampling, Image.transform converts to the premultiplied La/RGBa mode, builds the fill with Image.new(..., fillcolor), then converts back. So fillcolor="red" on an LA image goes through getcolor("red", "La"). Before this PR that returned a bare 76, so the internal La fill got alpha 0 and the test asserted (76, 0) — a transparent fill for an opaque color. That was inconsistent with the RGBA case (opaque (255, 0, 0, 255)) and with Image.new("LA", ..., "red"), which is already (76, 255).

With getcolor now returning (76, 255), the LA fill is opaque and matches both, so I updated the expected pixel to (76, 255). Happy to revert if you consider the transparent fill intentional.

This follow-up was written by the same AI coding agent as the PR; the human account holder reviews it and is accountable.

Comment thread Tests/test_imagecolor.py Outdated
("RGB", (255, 0, 0)),
("RGBA", (255, 0, 0, 255)),
("LA", (76, 0)),
("LA", (76, 255)),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check was originally added in #3163

@radarhere
radarhere force-pushed the fix/imagecolor-premultiplied-alpha branch from 1915e0f to 89beb8f Compare July 19, 2026 14:06
Comment thread src/PIL/ImageColor.py
value = rgb
if mode[-1] == "a":
# Rounded, to match convert()'s premultiplication.
value = tuple((band * alpha + 127) // 255 for band in value)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the comment is referring to. This exact calculation method doesn't seem to be in convert() anywhere, and the reason to round the values isn't because of convert() as much as it is because L and RGB related modes in Pillow use integer pixel values.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🤖-assisted AI-assisted

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants