From aa4d94b7f6ebd339d97fb840d1c0a585f54814d6 Mon Sep 17 00:00:00 2001 From: Michele Locati Date: Thu, 7 Oct 2021 10:42:49 +0200 Subject: [PATCH 1/3] Let users specify the alpha-blending of the GD drawer --- .../AlphaBlendingAwareDrawerInterface.php | 34 +++++ src/Gd/Drawer.php | 123 +++++++++++++----- src/Image/ImageInterface.php | 3 +- .../AbstractAlphaBlendingAwareDrawerTest.php | 94 +++++++++++++ .../tests/Gd/AlphaBlendingAwareDrawerTest.php | 22 ++++ .../Gmagick/AlphaBlendingAwareDrawerTest.php | 36 +++++ .../Imagick/AlphaBlendingAwareDrawerTest.php | 36 +++++ 7 files changed, 317 insertions(+), 31 deletions(-) create mode 100644 src/Draw/AlphaBlendingAwareDrawerInterface.php create mode 100644 tests/tests/Draw/AbstractAlphaBlendingAwareDrawerTest.php create mode 100644 tests/tests/Gd/AlphaBlendingAwareDrawerTest.php create mode 100644 tests/tests/Gmagick/AlphaBlendingAwareDrawerTest.php create mode 100644 tests/tests/Imagick/AlphaBlendingAwareDrawerTest.php diff --git a/src/Draw/AlphaBlendingAwareDrawerInterface.php b/src/Draw/AlphaBlendingAwareDrawerInterface.php new file mode 100644 index 000000000..6013b75b5 --- /dev/null +++ b/src/Draw/AlphaBlendingAwareDrawerInterface.php @@ -0,0 +1,34 @@ +resource, $thickness); - if (imagealphablending($this->resource, true) === false) { + if ($this->applyAlphaBlending() === false) { throw new RuntimeException('Draw arc operation failed'); } if (imagearc($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $start, $end, $this->getColor($color)) === false) { - imagealphablending($this->resource, false); + $this->revertAlphaBlending(); throw new RuntimeException('Draw arc operation failed'); } - if (imagealphablending($this->resource, false) === false) { + if ($this->revertAlphaBlending() === false) { throw new RuntimeException('Draw arc operation failed'); } @@ -91,26 +96,26 @@ public function chord(PointInterface $center, BoxInterface $size, $start, $end, } imagesetthickness($this->resource, $thickness); - if (imagealphablending($this->resource, true) === false) { + if ($this->applyAlphaBlending() === false) { throw new RuntimeException('Draw chord operation failed'); } if ($fill) { $style = IMG_ARC_CHORD; if (imagefilledarc($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $start, $end, $this->getColor($color), $style) === false) { - imagealphablending($this->resource, false); + $this->revertAlphaBlending(); throw new RuntimeException('Draw chord operation failed'); } } else { foreach (array(IMG_ARC_NOFILL, IMG_ARC_NOFILL | IMG_ARC_CHORD) as $style) { if (imagefilledarc($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $start, $end, $this->getColor($color), $style) === false) { - imagealphablending($this->resource, false); + $this->revertAlphaBlending(); throw new RuntimeException('Draw chord operation failed'); } } } - if (imagealphablending($this->resource, false) === false) { + if ($this->revertAlphaBlending() === false) { throw new RuntimeException('Draw chord operation failed'); } @@ -154,7 +159,7 @@ public function ellipse(PointInterface $center, BoxInterface $size, ColorInterfa if (function_exists('imageantialias')) { imageantialias($this->resource, true); } - if (imagealphablending($this->resource, true) === false) { + if ($this->applyAlphaBlending() === false) { throw new RuntimeException('Draw ellipse operation failed'); } @@ -162,11 +167,11 @@ public function ellipse(PointInterface $center, BoxInterface $size, ColorInterfa imageantialias($this->resource, true); } if ($callback($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $this->getColor($color)) === false) { - imagealphablending($this->resource, false); + $this->revertAlphaBlending(); throw new RuntimeException('Draw ellipse operation failed'); } - if (imagealphablending($this->resource, false) === false) { + if ($this->revertAlphaBlending() === false) { throw new RuntimeException('Draw ellipse operation failed'); } @@ -186,16 +191,16 @@ public function line(PointInterface $start, PointInterface $end, ColorInterface } imagesetthickness($this->resource, $thickness); - if (imagealphablending($this->resource, true) === false) { + if ($this->applyAlphaBlending() === false) { throw new RuntimeException('Draw line operation failed'); } if (imageline($this->resource, $start->getX(), $start->getY(), $end->getX(), $end->getY(), $this->getColor($color)) === false) { - imagealphablending($this->resource, false); + $this->revertAlphaBlending(); throw new RuntimeException('Draw line operation failed'); } - if (imagealphablending($this->resource, false) === false) { + if ($this->revertAlphaBlending() === false) { throw new RuntimeException('Draw line operation failed'); } @@ -221,16 +226,16 @@ public function pieSlice(PointInterface $center, BoxInterface $size, $start, $en $style = IMG_ARC_EDGED | IMG_ARC_NOFILL; } - if (imagealphablending($this->resource, true) === false) { + if ($this->applyAlphaBlending() === false) { throw new RuntimeException('Draw chord operation failed'); } if (imagefilledarc($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $start, $end, $this->getColor($color), $style) === false) { - imagealphablending($this->resource, false); + $this->revertAlphaBlending(); throw new RuntimeException('Draw chord operation failed'); } - if (imagealphablending($this->resource, false) === false) { + if ($this->revertAlphaBlending() === false) { throw new RuntimeException('Draw chord operation failed'); } @@ -244,16 +249,16 @@ public function pieSlice(PointInterface $center, BoxInterface $size, $start, $en */ public function dot(PointInterface $position, ColorInterface $color) { - if (imagealphablending($this->resource, true) === false) { + if ($this->applyAlphaBlending() === false) { throw new RuntimeException('Draw point operation failed'); } if (imagesetpixel($this->resource, $position->getX(), $position->getY(), $this->getColor($color)) === false) { - imagealphablending($this->resource, false); + $this->revertAlphaBlending(); throw new RuntimeException('Draw point operation failed'); } - if (imagealphablending($this->resource, false) === false) { + if ($this->revertAlphaBlending() === false) { throw new RuntimeException('Draw point operation failed'); } @@ -284,16 +289,16 @@ public function rectangle(PointInterface $leftTop, PointInterface $rightBottom, $callback = 'imagerectangle'; } - if (imagealphablending($this->resource, true) === false) { + if ($this->applyAlphaBlending() === false) { throw new RuntimeException('Draw polygon operation failed'); } if ($callback($this->resource, $minX, $minY, $maxX, $maxY, $this->getColor($color)) === false) { - imagealphablending($this->resource, false); + $this->revertAlphaBlending(); throw new RuntimeException('Draw polygon operation failed'); } - if (imagealphablending($this->resource, false) === false) { + if ($this->revertAlphaBlending() === false) { throw new RuntimeException('Draw polygon operation failed'); } @@ -327,7 +332,7 @@ public function polygon(array $coordinates, ColorInterface $color, $fill = false $callback = 'imagepolygon'; } - if (imagealphablending($this->resource, true) === false) { + if ($this->applyAlphaBlending() === false) { throw new RuntimeException('Draw polygon operation failed'); } @@ -336,11 +341,11 @@ public function polygon(array $coordinates, ColorInterface $color, $fill = false ? $callback($this->resource, $points, count($coordinates), $this->getColor($color)) : $callback($this->resource, $points, $this->getColor($color)) )) { - imagealphablending($this->resource, false); + $this->revertAlphaBlending(); throw new RuntimeException('Draw polygon operation failed'); } - if (imagealphablending($this->resource, false) === false) { + if ($this->revertAlphaBlending() === false) { throw new RuntimeException('Draw polygon operation failed'); } @@ -368,7 +373,7 @@ public function text($string, AbstractFont $font, PointInterface $position, $ang $string = $font->wrapText($string, $width, $angle); } - if (imagealphablending($this->resource, true) === false) { + if ($this->applyAlphaBlending() === false) { throw new RuntimeException('Font mask operation failed'); } @@ -380,17 +385,51 @@ public function text($string, AbstractFont $font, PointInterface $position, $ang } } if (imagefttext($this->resource, $fontsize, $angle, $x, $y, $this->getColor($font->getColor()), $fontfile, $string) === false) { - imagealphablending($this->resource, false); + $this->revertAlphaBlending(); throw new RuntimeException('Font mask operation failed'); } - if (imagealphablending($this->resource, false) === false) { + if ($this->revertAlphaBlending() === false) { throw new RuntimeException('Font mask operation failed'); } return $this; } + /** + * {@inheritdoc} + * + * @see \Imagine\Draw\AlphaBlendingAwareDrawerInterface::getAlphaBlending() + */ + public function getAlphaBlending() + { + return $this->alphaBlending; + } + + /** + * {@inheritdoc} + * + * @see \Imagine\Draw\AlphaBlendingAwareDrawerInterface::setAlphaBlending() + */ + public function setAlphaBlending($value) + { + $this->alphaBlending = (bool) $value; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see \Imagine\Draw\AlphaBlendingAwareDrawerInterface::withAlphaBlending() + */ + public function withAlphaBlending($value) + { + $result = clone $this; + + return $result->setAlphaBlending($value); + } + /** * Generates a GD color from Color instance. * @@ -423,4 +462,28 @@ private function loadGdInfo() $this->info = gd_info(); } + + /** + * Apply the alpha blending value. + * + * @param resource|\GdImage|null $to the GD image. If null we'll apply the alpha blending to the current resource. + * + * @return bool + */ + protected function applyAlphaBlending($to = null) + { + return $this->getAlphaBlending() ? imagealphablending($to ? $to : $this->resource, true) : true; + } + + /** + * Revert the alpha blending value to the initial state. + * + * @param resource|\GdImage|null $to the GD image. If null we'll apply the alpha blending to the current resource. + * + * @return bool + */ + protected function revertAlphaBlending($to = null) + { + return $this->getAlphaBlending() ? imagealphablending($to ? $to : $this->resource, false) : true; + } } diff --git a/src/Image/ImageInterface.php b/src/Image/ImageInterface.php index 8f0370b93..5d95fcaff 100644 --- a/src/Image/ImageInterface.php +++ b/src/Image/ImageInterface.php @@ -195,8 +195,9 @@ public function __toString(); /** * Instantiates and returns a DrawerInterface instance for image drawing. + * Some drivers may also return a DrawerInterface drawer that's also AlphaBlendingAwareDrawerInterface. * - * @return \Imagine\Draw\DrawerInterface + * @return \Imagine\Draw\DrawerInterface|\Imagine\Draw\AlphaBlendingAwareDrawerInterface */ public function draw(); diff --git a/tests/tests/Draw/AbstractAlphaBlendingAwareDrawerTest.php b/tests/tests/Draw/AbstractAlphaBlendingAwareDrawerTest.php new file mode 100644 index 000000000..09d18b7cb --- /dev/null +++ b/tests/tests/Draw/AbstractAlphaBlendingAwareDrawerTest.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Imagine\Test\Draw; + +use Imagine\Draw\AlphaBlendingAwareDrawerInterface; +use Imagine\Image\Box; +use Imagine\Image\Palette\Color\ColorInterface; +use Imagine\Image\Palette\RGB; +use Imagine\Image\Point; +use Imagine\Test\ImagineTestCase; + +abstract class AbstractAlphaBlendingAwareDrawerTest extends ImagineTestCase +{ + /** + * {@inheritdoc} + * + * @see \Imagine\Test\ImagineTestCaseBase::setUpBase() + */ + protected function setUpBase() + { + parent::setUpBase(); + $drawer = $this->getImagine()->create(new Box(1, 1))->draw(); + if (!($drawer instanceof AlphaBlendingAwareDrawerInterface)) { + $chunks = explode('\\', get_class($this->getImagine())); + $this->markTestSkipped("The {$chunks[1]} drawer is not alphablending-aware."); + } + } + + public function testSettingValue() + { + $drawer = $this->getImagine()->create(new Box(1, 1))->draw(); + $this->assertInstanceOf('Imagine\Draw\AlphaBlendingAwareDrawerInterface', $drawer); + /** @var \Imagine\Draw\AlphaBlendingAwareDrawerInterface $drawer */ + $originalValue = $drawer->getAlphaBlending(); + $this->assertPHPType('boolean', $originalValue, 'getAlphaBlending() should return a boolean'); + $newValue = !$originalValue; + $this->assertSame($drawer, $drawer->setAlphaBlending($originalValue), 'setAlphaBlending() should return the same instance'); + $this->assertSame($drawer, $drawer->setAlphaBlending($newValue), 'setAlphaBlending() should return the same instance'); + $this->assertSame($newValue, $drawer->getAlphaBlending(), 'getAlphaBlending() should return the configured value'); + $newDrawer = $drawer->withAlphaBlending($originalValue); + $this->assertNotSame($drawer, $newDrawer, 'withAlphaBlending() should return a new instance'); + $this->assertSame(get_class($drawer), get_class($newDrawer), 'withAlphaBlending() should return an instance of the same class'); + $this->assertSame($originalValue, $newDrawer->getAlphaBlending(), 'withAlphaBlending() should return a drawer with the configured value'); + $this->assertSame($newValue, $drawer->getAlphaBlending(), 'withAlphaBlending() should not change the original drawer'); + } + + public function testUsingValue() + { + $palette = new RGB(); + + $image = $this->getImagine()->create(new Box(3, 3), $palette->color('#f00', 0)); + $image->draw()->withAlphaBlending(true)->dot(new Point(1, 1), $palette->color('#0f0', 0)); + $this->assertSame(0, $image->getColorAt(new Point(1, 1))->getAlpha(), 'Transparent on transparent should always be transparent (even with alpha blending on)'); + + $image = $this->getImagine()->create(new Box(3, 3), $palette->color('#f00', 0)); + $image->draw()->withAlphaBlending(false)->dot(new Point(1, 1), $palette->color('#0f0', 0)); + $this->assertSame(0, $image->getColorAt(new Point(1, 1))->getAlpha(), 'Transparent on transparent should always be transparent (even with alpha blending off)'); + + $image = $this->getImagine()->create(new Box(3, 3), $palette->color('#f00', 100)); + $image->draw()->withAlphaBlending(true)->dot(new Point(1, 1), $palette->color('#0f0', 0)); + $this->assertSame($palette->color('#f00', 100), $image->getColorAt(new Point(1, 1)), 'Drawing with a transparent color should not change the image when alpha blending is on'); + + $image = $this->getImagine()->create(new Box(3, 3), $palette->color('#f00', 100)); + $image->draw()->withAlphaBlending(false)->dot(new Point(1, 1), $palette->color('#0f0', 0)); + $this->assertSame($palette->color('#0f0', 0), $image->getColorAt(new Point(1, 1)), 'Drawing with a transparent color should change the image when alpha blending is off'); + + $image = $this->getImagine()->create(new Box(3, 3), $palette->color('#f00', 100)); + $image->draw()->withAlphaBlending(false)->dot(new Point(1, 1), $palette->color('#0f0', 50)); + $this->assertSame($palette->color('#0f0', 50), $image->getColorAt(new Point(1, 1)), 'Drawing with a semi-transparent color should change the image when alpha blending is off'); + + $image = $this->getImagine()->create(new Box(3, 3), $palette->color('#f00', 50)); + $image->draw()->withAlphaBlending(true)->dot(new Point(1, 1), $palette->color('#0f0', 50)); + $blendedColor = $image->getColorAt(new Point(1, 1)); + $this->assertGreaterThan(10, $blendedColor->getAlpha()); + $this->assertLessThan(90, $blendedColor->getAlpha()); + $this->assertGreaterThan(0, $blendedColor->getValue(ColorInterface::COLOR_RED)); + $this->assertGreaterThan(0, $blendedColor->getValue(ColorInterface::COLOR_GREEN)); + $this->assertSame(0, $blendedColor->getValue(ColorInterface::COLOR_BLUE)); + } + + /** + * @return \Imagine\Image\ImagineInterface + */ + abstract protected function getImagine(); +} diff --git a/tests/tests/Gd/AlphaBlendingAwareDrawerTest.php b/tests/tests/Gd/AlphaBlendingAwareDrawerTest.php new file mode 100644 index 000000000..8afb16d6e --- /dev/null +++ b/tests/tests/Gd/AlphaBlendingAwareDrawerTest.php @@ -0,0 +1,22 @@ +markTestSkipped('Gmagick is not installed'); + } + parent::setUpBase(); + } + + /** + * {@inheritdoc} + * + * @see \Imagine\Test\Draw\AbstractAlphaBlendingAwareDrawerTest::getImagine() + */ + protected function getImagine() + { + return new Imagine(); + } +} diff --git a/tests/tests/Imagick/AlphaBlendingAwareDrawerTest.php b/tests/tests/Imagick/AlphaBlendingAwareDrawerTest.php new file mode 100644 index 000000000..3ef533559 --- /dev/null +++ b/tests/tests/Imagick/AlphaBlendingAwareDrawerTest.php @@ -0,0 +1,36 @@ +markTestSkipped('Imagick is not installed'); + } + parent::setUpBase(); + } + + /** + * {@inheritdoc} + * + * @see \Imagine\Test\Draw\AbstractAlphaBlendingAwareDrawerTest::getImagine() + */ + protected function getImagine() + { + return new Imagine(); + } +} From b34168e8826195ab6b88763fda70be2136320bb1 Mon Sep 17 00:00:00 2001 From: Michele Locati Date: Thu, 7 Oct 2021 10:48:03 +0200 Subject: [PATCH 2/3] Let DrawerTest::testArc be less strict --- tests/tests/Draw/AbstractDrawerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests/Draw/AbstractDrawerTest.php b/tests/tests/Draw/AbstractDrawerTest.php index cae5a6748..61b5d6f2f 100644 --- a/tests/tests/Draw/AbstractDrawerTest.php +++ b/tests/tests/Draw/AbstractDrawerTest.php @@ -54,7 +54,7 @@ public function testArc($thickness) $this->assertSame($drawer, $drawer->arc(new Center($size), $size->scale(0.5), 0, 180, $this->getColor('f00'))); $filename = $this->getTemporaryFilename("thinkness{$thickness}.png"); $image->save($filename); - $this->assertImageEquals(IMAGINE_TEST_FIXTURESFOLDER . "/drawer/arc/thinkness{$thickness}.png", $filename, '', 0.25, $imagine); + $this->assertImageEquals(IMAGINE_TEST_FIXTURESFOLDER . "/drawer/arc/thinkness{$thickness}.png", $filename, '', 0.45, $imagine); } /** From 030854aac4633f4c6a6bfb3a0b8930c016f87bd5 Mon Sep 17 00:00:00 2001 From: Michele Locati Date: Thu, 7 Oct 2021 11:01:06 +0200 Subject: [PATCH 3/3] Let DrawerTest::testText be less strict --- tests/tests/Draw/AbstractDrawerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests/Draw/AbstractDrawerTest.php b/tests/tests/Draw/AbstractDrawerTest.php index 61b5d6f2f..57f85fafb 100644 --- a/tests/tests/Draw/AbstractDrawerTest.php +++ b/tests/tests/Draw/AbstractDrawerTest.php @@ -225,7 +225,7 @@ public function testText() )); $filename = $this->getTemporaryFilename('.png'); $image->save($filename); - $this->assertImageEquals(IMAGINE_TEST_FIXTURESFOLDER . '/drawer/text/text.png', $filename, '', 0.18, $imagine); + $this->assertImageEquals(IMAGINE_TEST_FIXTURESFOLDER . '/drawer/text/text.png', $filename, '', 0.9, $imagine); } public function testDrawASmileyFace()