diff --git a/src/Drivers/Gd/Modifiers/FillModifier.php b/src/Drivers/Gd/Modifiers/FillModifier.php index 00265248..c5ce09ca 100644 --- a/src/Drivers/Gd/Modifiers/FillModifier.php +++ b/src/Drivers/Gd/Modifiers/FillModifier.php @@ -4,61 +4,120 @@ namespace Intervention\Image\Drivers\Gd\Modifiers; +use GdImage; +use Intervention\Image\Drivers\Gd\Cloner; use Intervention\Image\Drivers\DriverSpecialized; use Intervention\Image\Drivers\Gd\Frame; use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Geometry\Point; +use Intervention\Image\Interfaces\ColorInterface; use Intervention\Image\Interfaces\ModifierInterface; /** * @method bool hasPosition() - * @property mixed $color + * @property mixed $filling * @property null|Point $position */ class FillModifier extends DriverSpecialized implements ModifierInterface { public function apply(ImageInterface $image): ImageInterface { - $color = $this->color($image); + $filling = $this->resolveFilling($image); foreach ($image as $frame) { - if ($this->hasPosition()) { - $this->floodFillWithColor($frame, $color); + if (is_int($filling)) { + $this->fillWithColor($frame, $filling); } else { - $this->fillAllWithColor($frame, $color); + $this->fillWithImage($frame, $filling); } } return $image; } - private function color(ImageInterface $image): int + /** + * Resolve filling to its native version which can either be a + * color (integer) or an image (GdImage) + * + * @param ImageInterface $image + * @return GdImage|int + */ + private function resolveFilling(ImageInterface $image): int|GdImage { - return $this->driver()->colorProcessor($image->colorspace())->colorToNative( - $this->driver()->handleInput($this->color) - ); + $filling = $this->driver()->handleInput($this->filling); + + return match (true) { + $filling instanceof ColorInterface => $this->driver() + ->colorProcessor($image->colorspace()) + ->colorToNative($filling), + default => $filling->core()->native(), + }; } - private function floodFillWithColor(Frame $frame, int $color): void + /** + * Fill frame with given color + * + * @param Frame $frame + * @param int $color + * @return void + */ + private function fillWithColor(Frame $frame, int $color): void { - imagefill( - $frame->native(), - $this->position->x(), - $this->position->y(), - $color - ); + if ($this->hasPosition()) { + // flood fill if position is set + imagefill( + $frame->native(), + $this->position->x(), + $this->position->y(), + $color + ); + } else { + // fill image completely if no position is set + imagealphablending($frame->native(), true); + imagefilledrectangle( + $frame->native(), + 0, + 0, + $frame->size()->width() - 1, + $frame->size()->height() - 1, + $color + ); + } } - private function fillAllWithColor(Frame $frame, int $color): void + /** + * Fill frame with given image texture + * + * @param Frame $frame + * @param GdImage $gd + * @return void + */ + private function fillWithImage(Frame $frame, GdImage $gd): void { imagealphablending($frame->native(), true); - imagefilledrectangle( - $frame->native(), - 0, - 0, - $frame->size()->width() - 1, - $frame->size()->height() - 1, - $color - ); + + imagesettile($frame->native(), $gd); + $filling = IMG_COLOR_TILED; + + $width = imagesx($frame->native()); + $height = imagesy($frame->native()); + + // flood fill if position is set + if ($this->hasPosition()) { + // create new image + $base = Cloner::clone($frame->native()); + + // flood fill at exact position + imagefill($frame->native(), $this->position->x(), $this->position->y(), $filling); + + // copy filled original over base + imagecopy($base, $frame->native(), 0, 0, 0, 0, $width, $height); + + // set base as new resource-core + $frame->setNative($base); + } else { + // fill image completely if no position is set + imagefilledrectangle($frame->native(), 0, 0, $width - 1, $height - 1, $filling); + } } } diff --git a/src/Drivers/Imagick/Modifiers/FillModifier.php b/src/Drivers/Imagick/Modifiers/FillModifier.php index 141b8f3c..f5bef8c8 100644 --- a/src/Drivers/Imagick/Modifiers/FillModifier.php +++ b/src/Drivers/Imagick/Modifiers/FillModifier.php @@ -6,7 +6,10 @@ use Imagick; use ImagickDraw; +use ImagickDrawException; +use ImagickException; use ImagickPixel; +use ImagickPixelException; use Intervention\Image\Drivers\DriverSpecialized; use Intervention\Image\Drivers\Imagick\Frame; use Intervention\Image\Interfaces\ImageInterface; @@ -15,30 +18,52 @@ /** * @method bool hasPosition() - * @property mixed $color + * @property mixed $filling * @property null|Point $position */ class FillModifier extends DriverSpecialized implements ModifierInterface { public function apply(ImageInterface $image): ImageInterface { - $color = $this->driver()->handleInput($this->color); - $pixel = $this->driver() - ->colorProcessor($image->colorspace()) - ->colorToNative($color); + $filling = $this->resolveFilling($image); + $call = $this->hasPosition() ? 'floodFill' : 'fillAll'; + $call .= is_a($filling, ImagickPixel::class) ? 'WithColor' : 'WithImage'; foreach ($image as $frame) { - if ($this->hasPosition()) { - $this->floodFillWithColor($frame, $pixel); - } else { - $this->fillAllWithColor($frame, $pixel); - } + call_user_func([$this, $call], $frame, $filling); } return $image; } - private function floodFillWithColor(Frame $frame, ImagickPixel $pixel): void + /** + * Resolve filling to its native version which can either be a + * color (ImagickPixel) or an image (Imagick) + * + * @param ImageInterface $image + * @return ImagickPixel|Imagick + */ + private function resolveFilling(ImageInterface $image): ImagickPixel|Imagick + { + $filling = $this->driver()->handleInput($this->filling); + + return match (true) { + $filling instanceof ImageInterface => $filling->core()->native(), + default => $this->driver() + ->colorProcessor($image->colorspace()) + ->colorToNative($filling), + }; + } + + /** + * Modify given frame by flood filling with given color at the modifier's position + * + * @param Frame $frame + * @param ImagickPixel $pixel + * @return Imagick + * @throws ImagickException + */ + private function floodFillWithColor(Frame $frame, ImagickPixel $pixel): Imagick { $target = $frame->native()->getImagePixelColor( $this->position->x(), @@ -54,18 +79,86 @@ private function floodFillWithColor(Frame $frame, ImagickPixel $pixel): void false, Imagick::CHANNEL_ALL ); + + return $frame->native(); } - private function fillAllWithColor(Frame $frame, ImagickPixel $pixel): void + /** + * Modify given frame by filling it completely with given color + * + * @param Frame $frame + * @param ImagickPixel $pixel + * @return Imagick + * @throws ImagickDrawException + * @throws ImagickException + */ + private function fillAllWithColor(Frame $frame, ImagickPixel $pixel): Imagick { $draw = new ImagickDraw(); $draw->setFillColor($pixel); + $draw->rectangle( 0, 0, $frame->native()->getImageWidth(), $frame->native()->getImageHeight() ); + $frame->native()->drawImage($draw); + + return $frame->native(); + } + + /** + * Modify given frame by flood filling it with given texture at the modifier's position + * + * @param Frame $frame + * @param Imagick $texture + * @return void + * @throws ImagickException + * @throws ImagickPixelException + */ + private function floodFillWithImage(Frame $frame, Imagick $texture): void + { + // create tile + $tile = clone $frame->native(); + + // get color at position + $targetColor = $tile->getImagePixelColor($this->position->x(), $this->position->y()); + + // mask away color at position + // does not work becaue there might be other transparent areas + $tile->transparentPaintImage($targetColor, 0, 0, false); + + // fill canvas with texture + $canvas = $frame->native()->textureImage($texture); + + // merge canvas and tile + $canvas->compositeImage($tile, Imagick::COMPOSITE_DEFAULT, 0, 0); + + // copy original alpha channel only if position is not completely transparent + if ($targetColor->getColorValue(Imagick::COLOR_ALPHA) != 0) { + $canvas->compositeImage($frame->native(), Imagick::COMPOSITE_DSTIN, 0, 0); + } + + // replace imagick of frame + $frame->native()->compositeImage($canvas, Imagick::COMPOSITE_SRCOVER, 0, 0); + } + + /** + * Fill given frame completely with given texture + * + * @param Frame $frame + * @param Imagick $texture + * @return void + * @throws ImagickException + */ + private function fillAllWithImage(Frame $frame, Imagick $texture): void + { + // fill completely with texture + $modified = $frame->native()->textureImage($texture); + + // replace imagick of frame + $frame->native()->compositeImage($modified, Imagick::COMPOSITE_SRCOVER, 0, 0); } } diff --git a/src/Modifiers/FillModifier.php b/src/Modifiers/FillModifier.php index 7ee088b0..7c7c7a8a 100644 --- a/src/Modifiers/FillModifier.php +++ b/src/Modifiers/FillModifier.php @@ -9,11 +9,16 @@ class FillModifier extends SpecializableModifier { public function __construct( - public mixed $color, + public mixed $filling, public ?Point $position = null ) { } + /** + * Determine if the fill modifier has a position + * + * @return bool + */ public function hasPosition(): bool { return !empty($this->position); diff --git a/tests/Drivers/Gd/Modifiers/FillModifierTest.php b/tests/Drivers/Gd/Modifiers/FillModifierTest.php index f16f5de5..24823589 100644 --- a/tests/Drivers/Gd/Modifiers/FillModifierTest.php +++ b/tests/Drivers/Gd/Modifiers/FillModifierTest.php @@ -38,4 +38,42 @@ public function testFillAllColor(): void $this->assertEquals('cccccc', $image->pickColor(420, 270)->toHex()); $this->assertEquals('cccccc', $image->pickColor(540, 400)->toHex()); } + + public function testFloodFillImage(): void + { + $image = $this->readTestImage('blocks.png'); + $this->assertTransparency($image->pickColor(445, 11)); + $this->assertTransparency($image->pickColor(454, 4)); + $this->assertTransparency($image->pickColor(460, 28)); + $this->assertTransparency($image->pickColor(470, 20)); + $this->assertTransparency($image->pickColor(470, 30)); + $image->modify(new FillModifier($this->getTestImagePath('tile.png'), new Point(500, 0))); + $this->assertEquals('445160', $image->pickColor(445, 11)->toHex()); + $this->assertEquals('b4e000', $image->pickColor(454, 4)->toHex()); + $this->assertEquals('445160', $image->pickColor(460, 28)->toHex()); + $this->assertEquals('b4e000', $image->pickColor(470, 20)->toHex()); + $this->assertTransparency($image->pickColor(470, 30)); + } + + public function testFillAllImage(): void + { + $image = $this->readTestImage('blocks.png'); + $this->assertEquals('0000ff', $image->pickColor(0, 0)->toHex()); + $this->assertEquals('0000ff', $image->pickColor(12, 5)->toHex()); + $this->assertEquals('0000ff', $image->pickColor(12, 12)->toHex()); + $this->assertTransparency($image->pickColor(445, 11)); + $this->assertTransparency($image->pickColor(454, 4)); + $this->assertTransparency($image->pickColor(460, 28)); + $this->assertTransparency($image->pickColor(470, 20)); + $this->assertTransparency($image->pickColor(470, 30)); + $image->modify(new FillModifier($this->getTestImagePath('tile.png'))); + $this->assertEquals('b4e000', $image->pickColor(0, 0)->toHex()); + $this->assertEquals('0000ff', $image->pickColor(12, 5)->toHex()); + $this->assertEquals('445160', $image->pickColor(12, 12)->toHex()); + $this->assertEquals('445160', $image->pickColor(445, 11)->toHex()); + $this->assertEquals('b4e000', $image->pickColor(454, 4)->toHex()); + $this->assertEquals('445160', $image->pickColor(460, 28)->toHex()); + $this->assertEquals('b4e000', $image->pickColor(470, 20)->toHex()); + $this->assertTransparency($image->pickColor(470, 30)); + } } diff --git a/tests/Drivers/Imagick/Modifiers/FillModifierTest.php b/tests/Drivers/Imagick/Modifiers/FillModifierTest.php index cfc9b73d..377cd8ce 100644 --- a/tests/Drivers/Imagick/Modifiers/FillModifierTest.php +++ b/tests/Drivers/Imagick/Modifiers/FillModifierTest.php @@ -38,4 +38,42 @@ public function testFillAllColor(): void $this->assertEquals('cccccc', $image->pickColor(420, 270)->toHex()); $this->assertEquals('cccccc', $image->pickColor(540, 400)->toHex()); } + + public function testFloodFillImage(): void + { + $image = $this->readTestImage('blocks.png'); + $this->assertTransparency($image->pickColor(445, 11)); + $this->assertTransparency($image->pickColor(454, 4)); + $this->assertTransparency($image->pickColor(460, 28)); + $this->assertTransparency($image->pickColor(470, 20)); + $this->assertTransparency($image->pickColor(470, 30)); + $image->modify(new FillModifier($this->getTestImagePath('tile.png'), new Point(500, 0))); + $this->assertEquals('445160', $image->pickColor(445, 11)->toHex()); + $this->assertEquals('b4e000', $image->pickColor(454, 4)->toHex()); + $this->assertEquals('445160', $image->pickColor(460, 28)->toHex()); + $this->assertEquals('b4e000', $image->pickColor(470, 20)->toHex()); + $this->assertTransparency($image->pickColor(470, 30)); + } + + public function testFillAllImage(): void + { + $image = $this->readTestImage('blocks.png'); + $this->assertEquals('0000ff', $image->pickColor(0, 0)->toHex()); + $this->assertEquals('0000ff', $image->pickColor(12, 5)->toHex()); + $this->assertEquals('0000ff', $image->pickColor(12, 12)->toHex()); + $this->assertTransparency($image->pickColor(445, 11)); + $this->assertTransparency($image->pickColor(454, 4)); + $this->assertTransparency($image->pickColor(460, 28)); + $this->assertTransparency($image->pickColor(470, 20)); + $this->assertTransparency($image->pickColor(470, 30)); + $image->modify(new FillModifier($this->getTestImagePath('tile.png'))); + $this->assertEquals('b4e000', $image->pickColor(0, 0)->toHex()); + $this->assertEquals('0000ff', $image->pickColor(12, 5)->toHex()); + $this->assertEquals('445160', $image->pickColor(12, 12)->toHex()); + $this->assertEquals('445160', $image->pickColor(445, 11)->toHex()); + $this->assertEquals('b4e000', $image->pickColor(454, 4)->toHex()); + $this->assertEquals('445160', $image->pickColor(460, 28)->toHex()); + $this->assertEquals('b4e000', $image->pickColor(470, 20)->toHex()); + $this->assertTransparency($image->pickColor(470, 30)); + } }