diff --git a/CHANGELOG.md b/CHANGELOG.md index e03a6e6b..55d1f44d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # CHANGELOG ### NEXT (YYYY-MM-DD) +- Fix the Imagick driver deactivating the alpha channel in `effects()->grayscale()` (transparent pixels were encoded as opaque gray); it now uses the alpha-preserving grayscale image type, like `usePalette()` (#880, @nlemoine) ### 1.5.3 (2026-06-03) - Fix the Imagick driver painting a "black box" when pasting an image with transparent areas at an alpha lower than 100; the opacity now scales the existing per-pixel alpha instead of overwriting it (#878, @nlemoine) diff --git a/src/Imagick/Effects.php b/src/Imagick/Effects.php index 1a1846ee..e9ad166f 100644 --- a/src/Imagick/Effects.php +++ b/src/Imagick/Effects.php @@ -95,7 +95,20 @@ public function grayscale() { static::getDriverInfo()->requireFeature(DriverInfo::FEATURE_GRAYSCALEEFFECT); try { - $this->imagick->setImageType(\Imagick::IMGTYPE_GRAYSCALE); + // IMGTYPE_GRAYSCALE is an alpha-less image type: ImageMagick deactivates the alpha + // channel when switching to it, so transparent pixels would be encoded as opaque + // gray. Use the alpha-preserving variant instead, as Image::setColorspace() already + // does (the constant was named IMGTYPE_GRAYSCALEMATTE before ImageMagick 7 / Imagick + // 3.4.3, and some combinations of Imagick and ImageMagick versions define neither, + // hence the hard-coded fallback value). + if (defined('\Imagick::IMGTYPE_GRAYSCALEALPHA')) { + $grayscaleType = \Imagick::IMGTYPE_GRAYSCALEALPHA; + } elseif (defined('\Imagick::IMGTYPE_GRAYSCALEMATTE')) { + $grayscaleType = \Imagick::IMGTYPE_GRAYSCALEMATTE; + } else { + $grayscaleType = 3; + } + $this->imagick->setImageType($grayscaleType); } catch (\ImagickException $e) { throw new RuntimeException('Failed to grayscale the image', $e->getCode(), $e); } diff --git a/tests/tests/Effects/AbstractEffectsTest.php b/tests/tests/Effects/AbstractEffectsTest.php index d6a2f7c3..4e173d93 100644 --- a/tests/tests/Effects/AbstractEffectsTest.php +++ b/tests/tests/Effects/AbstractEffectsTest.php @@ -13,6 +13,7 @@ use Imagine\Driver\Info; use Imagine\Driver\InfoProvider; +use Imagine\Exception\NotSupportedException; use Imagine\Image\Box; use Imagine\Image\Palette\RGB; use Imagine\Image\Point; @@ -117,6 +118,35 @@ public function testGrayscale() $this->assertEquals($greyG, $greyB); } + public function testGrayscalePreservesTransparency() + { + if (!$this->getDriverInfo()->hasFeature(Info::FEATURE_GRAYSCALEEFFECT)) { + $this->isGoingToThrowException('Imagine\Exception\NotSupportedException'); + } else { + try { + $this->getDriverInfo()->requireFeature(Info::FEATURE_TRANSPARENCY); + } catch (NotSupportedException $x) { + $this->markTestSkipped($x->getMessage()); + } + } + $palette = new RGB(); + $imagine = $this->getImagine(); + + // Grayscaling must desaturate the color channels without dropping the + // alpha channel (on the Imagick driver, IMGTYPE_GRAYSCALE used to + // deactivate it, so every transparent pixel was encoded as opaque gray). + // The image is saved and reloaded because the defect only shows at + // encode time: in-memory pixel reads still expose the stored alpha. + $image = $imagine->create(new Box(20, 20), $palette->color('f00', 0)); + $image->effects() + ->grayscale(); + + $reloaded = $imagine->load($image->get('png')); + $pixel = $reloaded->getColorAt(new Point(10, 10)); + + $this->assertSame(0, $pixel->getAlpha()); + } + public function brightnessProvider() { $color = '#145af0';