Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
17 changes: 8 additions & 9 deletions src/Imagick/Image.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
Expand Down
43 changes: 43 additions & 0 deletions tests/tests/Image/AbstractImageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading