Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add possibility to use ImageInterface::fill() with colors or Images #1263

Draft
wants to merge 5 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
109 changes: 84 additions & 25 deletions src/Drivers/Gd/Modifiers/FillModifier.php
Expand Up @@ -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);
}
}
}
117 changes: 105 additions & 12 deletions src/Drivers/Imagick/Modifiers/FillModifier.php
Expand Up @@ -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;
Expand All @@ -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(),
Expand All @@ -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);
}
}
7 changes: 6 additions & 1 deletion src/Modifiers/FillModifier.php
Expand Up @@ -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);
Expand Down
38 changes: 38 additions & 0 deletions tests/Drivers/Gd/Modifiers/FillModifierTest.php
Expand Up @@ -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));
}
}