Skip to content

Fix Imagick grayscale flattening saturated colors - #1516

Open
nlemoine wants to merge 1 commit into
Intervention:developfrom
nlemoine:fix/imagick-grayscale-luma
Open

Fix Imagick grayscale flattening saturated colors#1516
nlemoine wants to merge 1 commit into
Intervention:developfrom
nlemoine:fix/imagick-grayscale-luma

Conversation

@nlemoine

@nlemoine nlemoine commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

GrayscaleModifier on the Imagick driver turns every saturated color into the same mid grey. All the tonal information is gone.

Repro, on a 200x200 PNG with four flat quadrants (red, green, blue, yellow), sampling each quadrant centre after ->grayscale():

driver red green blue yellow
GD 76 149 29 225
Imagick (before) 128 128 128 128
Imagick (after) 54 182 18 237

Cause: the modifier calls modulateImage(100, 0, 100), which sets HSL saturation to zero. HSL lightness of a fully saturated color is (max+min)/2 = 127.5, so any saturated hue lands on 128 no matter how bright it looks.

Fix: transformImageColorspace() to COLORSPACE_GRAY, then back to COLORSPACE_SRGB. The vips driver already does the same with Interpretation::B_W then Interpretation::SRGB.

I first tried a color matrix with Rec. 601 coefficients, it would have matched GD almost exactly. But it breaks on CMYK sources. The matrix hits the C, M and Y channels, so the tonality comes out inverted, red becomes light and yellow becomes dark. The colorspace transform handles CMYK fine.

Other things I checked: alpha and semi transparency survive, animated GIFs get every frame converted, colorspace() still reports Rgb\Colorspace. Encoding to png/jpeg/gif/webp is unchanged.

Tests: testColorChange only asserted isGrayscale(), and 128,128,128 satisfies that, so nothing caught it. I added testSaturatedColorsKeepDistinctBrightness on both drivers, using the existing blocks.png fixture. It checks that pure blue, red and green stay distinct and in the right order (blue darkest, green brightest). That ordering should hold for any luma based conversion, so the same test works for GD and Imagick. On the old Imagick code it fails with Failed asserting that 128 is less than 128.

The Imagick GrayscaleModifier used modulateImage(100, 0, 100), which sets
HSL saturation to zero. HSL lightness of any fully saturated color is
(max+min)/2, so every saturated hue ended up on the same mid grey and all
tonal information was lost.

Use transformImageColorspace() to GRAY and back to sRGB instead, the same
approach the vips driver already uses. This produces a luma based grey and
also handles CMYK sources correctly.

The existing tests only asserted that the result is grayscale, which the
flat mid grey satisfied. Added a test on both drivers checking that pure
blue, red and green keep a distinct and correctly ordered brightness.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant