diff --git a/CHANGELOG.md b/CHANGELOG.md index c973a24e..181865a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # CHANGELOG ### NEXT (YYYY-MM-DD) +- 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 - Fix the Imagick driver inverting the alpha channel in `effects()->negative()` (an opaque image became fully transparent); it now excludes the alpha channel, matching the GD driver ### 1.5.2 (2026-01-09) diff --git a/src/Imagick/Image.php b/src/Imagick/Image.php index bab72106..188e2d95 100644 --- a/src/Imagick/Image.php +++ b/src/Imagick/Image.php @@ -29,7 +29,6 @@ use Imagine\Image\Point; use Imagine\Image\PointInterface; use Imagine\Image\ProfileInterface; -use Imagine\Utils\ErrorHandling; /** * Image implementation using the Imagick PHP extension. @@ -246,14 +245,14 @@ public function paste(ImageInterface $image, PointInterface $start, $alpha = 100 $pasteMe = $image->imagick; } elseif ($alpha > 0) { $pasteMe = $image->cloneImagick(); - // setImageOpacity was replaced with setImageAlpha in php-imagick v3.4.3 - if (method_exists($pasteMe, 'setImageAlpha')) { - $pasteMe->setImageAlpha($alpha / 100); - } else { - ErrorHandling::ignoring(E_DEPRECATED, function () use ($pasteMe, $alpha) { - $pasteMe->setImageOpacity($alpha / 100); - }); - } + // Scale the existing per-pixel alpha by the opacity factor instead of + // overwriting it. setImageAlpha()/setImageOpacity() set every pixel's + // alpha to the same value, so transparent areas of $image become a + // semi-opaque rectangle - a "black box" painted over the destination. + // Multiplying the alpha channel keeps fully-transparent pixels + // transparent while still fading the opaque ones. + $pasteMe->setImageAlphaChannel(\Imagick::ALPHACHANNEL_ACTIVATE); + $pasteMe->evaluateImage(\Imagick::EVALUATE_MULTIPLY, $alpha / 100, \Imagick::CHANNEL_ALPHA); } else { $pasteMe = null; } diff --git a/tests/tests/Image/AbstractImageTest.php b/tests/tests/Image/AbstractImageTest.php index d6a8de7b..3e7b0337 100644 --- a/tests/tests/Image/AbstractImageTest.php +++ b/tests/tests/Image/AbstractImageTest.php @@ -1082,6 +1082,49 @@ public function testPasteWithAlpha($alpha) $this->assertColorSimilar($expectedColor, $finalColor, '', 1.74); } + /** + * Pasting an image that has transparent areas with alpha < 100 must keep + * those areas transparent: the opacity has to scale the existing per-pixel + * alpha, not overwrite it. Regression test for the Imagick driver, where + * setImageAlpha() used to flatten the alpha of every pixel and paint a + * semi-opaque "black box" over the transparent surroundings. + */ + public function testPasteWithAlphaPreservesTransparency() + { + try { + $this->getDriverInfo()->requireFeature(Info::FEATURE_TRANSPARENCY); + } catch (NotSupportedException $x) { + $this->markTestSkipped($x->getMessage()); + } + $palette = new RGB(); + $imagine = $this->getImagine(); + + // Destination: opaque blue. + $destination = $imagine->create(new Box(20, 20), $palette->color(array(0, 0, 255))); + + // Source: a fully-transparent canvas with an opaque red square in the + // middle, so the corners exercise the transparent-area code path. + $source = $imagine->create(new Box(10, 10), $palette->color(array(255, 0, 0), 0)); + $source->draw()->rectangle(new Point(3, 3), new Point(6, 6), $palette->color(array(255, 0, 0)), true); + + try { + $destination->paste($source, new Point(0, 0), 50); + } catch (NotSupportedException $x) { + // e.g. Gmagick, which does not support pasting with alpha. + $this->markTestSkipped($x->getMessage()); + } + + // The transparent corner must leave the blue destination untouched + // (the bug turned it into a ~50% black overlay, e.g. #000080). + $corner = $destination->getColorAt(new Point(1, 1)); + $this->assertColorSimilar($palette->color(array(0, 0, 255)), $corner, 'transparent area was contaminated', 1.74); + $this->assertSame(100, $corner->getAlpha()); + + // The opaque centre is still blended over the destination (~50% red). + $centre = $destination->getColorAt(new Point(4, 4)); + $this->assertColorSimilar($palette->color(array(128, 0, 128)), $centre, 'opaque area was not blended', 2); + } + public function testPasteOutOfBoundaries() { $imagine = $this->getImagine();