From 58f0afd3c77fbc8533ac6cde26296d479615b3a8 Mon Sep 17 00:00:00 2001 From: Oliver Vogel Date: Sun, 12 Jan 2025 08:24:23 +0100 Subject: [PATCH] Optimize & Refactor Crop- and ResizeCanvasModifiers (#1418) * Refactor and simplify CropModifier::class * Refactor and simplify ResizeCanvasModifier::class --- src/Collection.php | 4 +- src/Colors/Cmyk/Colorspace.php | 2 +- src/Colors/Hsl/Colorspace.php | 2 +- src/Colors/Hsv/Colorspace.php | 2 +- src/Colors/Rgb/Colorspace.php | 2 +- src/Drivers/Gd/Modifiers/CropModifier.php | 63 ++---------- .../Gd/Modifiers/ResizeCanvasModifier.php | 70 ++----------- .../Imagick/Modifiers/CropModifier.php | 97 +++++++------------ .../Imagick/Modifiers/FillModifier.php | 32 +++--- .../Modifiers/ResizeCanvasModifier.php | 76 ++------------- src/Format.php | 5 +- tests/ImagickTestCase.php | 10 ++ .../Drivers/Gd/Modifiers/CropModifierTest.php | 51 ++++++++++ .../Gd/Modifiers/ResizeCanvasModifierTest.php | 14 +++ .../Imagick/Encoders/PngEncoderTest.php | 2 +- .../Imagick/Modifiers/CropModifierTest.php | 60 ++++++++++++ .../Modifiers/ResizeCanvasModifierTest.php | 2 +- 17 files changed, 220 insertions(+), 274 deletions(-) diff --git a/src/Collection.php b/src/Collection.php index 539855a8..a5ae6d82 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -185,7 +185,7 @@ class Collection implements CollectionInterface, IteratorAggregate, Countable return new self( array_map( - fn($item) => $callback($item), + fn(mixed $item) => $callback($item), $this->items, ) ); @@ -202,7 +202,7 @@ class Collection implements CollectionInterface, IteratorAggregate, Countable return new self( array_filter( $this->items, - fn($item) => $callback($item), + fn(mixed $item) => $callback($item), ) ); } diff --git a/src/Colors/Cmyk/Colorspace.php b/src/Colors/Cmyk/Colorspace.php index fc5445bb..fcabf8ed 100644 --- a/src/Colors/Cmyk/Colorspace.php +++ b/src/Colors/Cmyk/Colorspace.php @@ -35,7 +35,7 @@ class Colorspace implements ColorspaceInterface public function colorFromNormalized(array $normalized): ColorInterface { return new Color(...array_map( - fn($classname, $value_normalized) => (new $classname(normalized: $value_normalized))->value(), + fn(string $classname, float $value_normalized) => (new $classname(normalized: $value_normalized))->value(), self::$channels, $normalized, )); diff --git a/src/Colors/Hsl/Colorspace.php b/src/Colors/Hsl/Colorspace.php index 431f3980..5de1f6a7 100644 --- a/src/Colors/Hsl/Colorspace.php +++ b/src/Colors/Hsl/Colorspace.php @@ -34,7 +34,7 @@ class Colorspace implements ColorspaceInterface public function colorFromNormalized(array $normalized): ColorInterface { return new Color(...array_map( - fn($classname, $value_normalized) => (new $classname(normalized: $value_normalized))->value(), + fn(string $classname, float $value_normalized) => (new $classname(normalized: $value_normalized))->value(), self::$channels, $normalized )); diff --git a/src/Colors/Hsv/Colorspace.php b/src/Colors/Hsv/Colorspace.php index 132c88a9..d4987b6f 100644 --- a/src/Colors/Hsv/Colorspace.php +++ b/src/Colors/Hsv/Colorspace.php @@ -34,7 +34,7 @@ class Colorspace implements ColorspaceInterface public function colorFromNormalized(array $normalized): ColorInterface { return new Color(...array_map( - fn($classname, $value_normalized) => (new $classname(normalized: $value_normalized))->value(), + fn(string $classname, float $value_normalized) => (new $classname(normalized: $value_normalized))->value(), self::$channels, $normalized )); diff --git a/src/Colors/Rgb/Colorspace.php b/src/Colors/Rgb/Colorspace.php index 9d963ecb..248eb67d 100644 --- a/src/Colors/Rgb/Colorspace.php +++ b/src/Colors/Rgb/Colorspace.php @@ -34,7 +34,7 @@ class Colorspace implements ColorspaceInterface public function colorFromNormalized(array $normalized): ColorInterface { return new Color(...array_map( - fn($classname, $value_normalized) => (new $classname(normalized: $value_normalized))->value(), + fn($classname, float $value_normalized) => (new $classname(normalized: $value_normalized))->value(), self::$channels, $normalized, )); diff --git a/src/Drivers/Gd/Modifiers/CropModifier.php b/src/Drivers/Gd/Modifiers/CropModifier.php index fabdf7f7..c1ea4825 100644 --- a/src/Drivers/Gd/Modifiers/CropModifier.php +++ b/src/Drivers/Gd/Modifiers/CropModifier.php @@ -6,6 +6,7 @@ namespace Intervention\Image\Drivers\Gd\Modifiers; use Intervention\Image\Drivers\Gd\Cloner; use Intervention\Image\Exceptions\ColorException; +use Intervention\Image\Interfaces\ColorInterface; use Intervention\Image\Interfaces\FrameInterface; use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Interfaces\SizeInterface; @@ -23,9 +24,7 @@ class CropModifier extends GenericCropModifier implements SpecializedInterface { $originalSize = $image->size(); $crop = $this->crop($image); - $background = $this->driver()->colorProcessor($image->colorspace())->colorToNative( - $this->driver()->handleInput($this->background) - ); + $background = $this->driver()->handleInput($this->background); foreach ($image as $frame) { $this->cropFrame($frame, $originalSize, $crop, $background); @@ -41,10 +40,10 @@ class CropModifier extends GenericCropModifier implements SpecializedInterface FrameInterface $frame, SizeInterface $originalSize, SizeInterface $resizeTo, - int $background + ColorInterface $background ): void { // create new image with transparent background - $modified = Cloner::cloneEmpty($frame->native(), $resizeTo); + $modified = Cloner::cloneEmpty($frame->native(), $resizeTo, $background); // define offset $offset_x = $resizeTo->pivot()->x() + $this->offset_x; @@ -56,6 +55,9 @@ class CropModifier extends GenericCropModifier implements SpecializedInterface $targetWidth = $targetWidth < $originalSize->width() ? $targetWidth + $offset_x : $targetWidth; $targetHeight = $targetHeight < $originalSize->height() ? $targetHeight + $offset_y : $targetHeight; + // don't alpha blend for copy operation to keep transparent areas of original image + imagealphablending($modified, false); + // copy content from resource imagecopyresampled( $modified, @@ -70,57 +72,6 @@ class CropModifier extends GenericCropModifier implements SpecializedInterface $targetHeight ); - // don't alpha blend for covering areas - imagealphablending($modified, false); - - // cover the possible newly created areas with background color - if ($resizeTo->width() > $originalSize->width() || $this->offset_x > 0) { - imagefilledrectangle( - $modified, - $originalSize->width() + ($this->offset_x * -1) - $resizeTo->pivot()->x(), - 0, - $resizeTo->width(), - $resizeTo->height(), - $background - ); - } - - // cover the possible newly created areas with background color - if ($resizeTo->height() > $originalSize->height() || $this->offset_y > 0) { - imagefilledrectangle( - $modified, - ($this->offset_x * -1) - $resizeTo->pivot()->x(), - $originalSize->height() + ($this->offset_y * -1) - $resizeTo->pivot()->y(), - ($this->offset_x * -1) + $originalSize->width() - 1 - $resizeTo->pivot()->x(), - $resizeTo->height(), - $background - ); - } - - // cover the possible newly created areas with background color - if ((($this->offset_x * -1) - $resizeTo->pivot()->x() - 1) > 0) { - imagefilledrectangle( - $modified, - 0, - 0, - ($this->offset_x * -1) - $resizeTo->pivot()->x() - 1, - $resizeTo->height(), - $background - ); - } - - // cover the possible newly created areas with background color - if ((($this->offset_y * -1) - $resizeTo->pivot()->y() - 1) > 0) { - imagefilledrectangle( - $modified, - ($this->offset_x * -1) - $resizeTo->pivot()->x(), - 0, - ($this->offset_x * -1) + $originalSize->width() - $resizeTo->pivot()->x() - 1, - ($this->offset_y * -1) - $resizeTo->pivot()->y() - 1, - $background - ); - } - // set new content as resource $frame->setNative($modified); } diff --git a/src/Drivers/Gd/Modifiers/ResizeCanvasModifier.php b/src/Drivers/Gd/Modifiers/ResizeCanvasModifier.php index 665dd09a..7f73cc73 100644 --- a/src/Drivers/Gd/Modifiers/ResizeCanvasModifier.php +++ b/src/Drivers/Gd/Modifiers/ResizeCanvasModifier.php @@ -4,15 +4,7 @@ declare(strict_types=1); namespace Intervention\Image\Drivers\Gd\Modifiers; -use Intervention\Image\Colors\Rgb\Channels\Blue; -use Intervention\Image\Colors\Rgb\Channels\Green; -use Intervention\Image\Colors\Rgb\Channels\Red; -use Intervention\Image\Drivers\Gd\Cloner; -use Intervention\Image\Exceptions\ColorException; -use Intervention\Image\Interfaces\ColorInterface; -use Intervention\Image\Interfaces\FrameInterface; use Intervention\Image\Interfaces\ImageInterface; -use Intervention\Image\Interfaces\SizeInterface; use Intervention\Image\Interfaces\SpecializedInterface; use Intervention\Image\Modifiers\ResizeCanvasModifier as GenericResizeCanvasModifier; @@ -25,62 +17,16 @@ class ResizeCanvasModifier extends GenericResizeCanvasModifier implements Specia */ public function apply(ImageInterface $image): ImageInterface { - $resize = $this->cropSize($image); - $background = $this->driver()->handleInput($this->background); + $cropSize = $this->cropSize($image); - foreach ($image as $frame) { - $this->modify($frame, $resize, $background); - } + $image->modify(new CropModifier( + $cropSize->width(), + $cropSize->height(), + $cropSize->pivot()->x(), + $cropSize->pivot()->y(), + $this->background, + )); return $image; } - - /** - * @throws ColorException - */ - protected function modify( - FrameInterface $frame, - SizeInterface $resize, - ColorInterface $background, - ): void { - // create new canvas with target size & transparent background color - $modified = Cloner::cloneEmpty($frame->native(), $resize, $background); - - // make image area transparent to keep transparency - // even if background-color is set - $transparent = imagecolorallocatealpha( - $modified, - $background->channel(Red::class)->value(), - $background->channel(Green::class)->value(), - $background->channel(Blue::class)->value(), - 127, - ); - - // create transparent area to place the original on top - imagealphablending($modified, false); // do not blend / just overwrite - imagecolortransparent($modified, $transparent); - imagefilledrectangle( - $modified, - $resize->pivot()->x() * -1, - $resize->pivot()->y() * -1, - abs($resize->pivot()->x()) + $frame->size()->width() - 1, - abs($resize->pivot()->y()) + $frame->size()->height() - 1, - $transparent, - ); - - // place original - imagecopy( - $modified, - $frame->native(), - $resize->pivot()->x() * -1, - $resize->pivot()->y() * -1, - 0, - 0, - $frame->size()->width(), - $frame->size()->height(), - ); - - // set new content as resource - $frame->setNative($modified); - } } diff --git a/src/Drivers/Imagick/Modifiers/CropModifier.php b/src/Drivers/Imagick/Modifiers/CropModifier.php index 067c1fd4..df5a30af 100644 --- a/src/Drivers/Imagick/Modifiers/CropModifier.php +++ b/src/Drivers/Imagick/Modifiers/CropModifier.php @@ -4,8 +4,8 @@ declare(strict_types=1); namespace Intervention\Image\Drivers\Imagick\Modifiers; -use ImagickDraw; -use ImagickPixel; +use Imagick; +use Intervention\Image\Drivers\Imagick\Driver; use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Interfaces\SpecializedInterface; use Intervention\Image\Modifiers\CropModifier as GenericCropModifier; @@ -14,80 +14,55 @@ class CropModifier extends GenericCropModifier implements SpecializedInterface { public function apply(ImageInterface $image): ImageInterface { - $originalSize = $image->size(); $crop = $this->crop($image); $background = $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->driver()->handleInput($this->background) ); - $transparent = new ImagickPixel('transparent'); - - $draw = new ImagickDraw(); - $draw->setFillColor($background); + // create empty container imagick to rebuild core + $imagick = new Imagick(); + $resolution = $image->resolution()->perInch(); foreach ($image as $frame) { - $frame->native()->setBackgroundColor($transparent); - $frame->native()->setImageBackgroundColor($transparent); + // create new frame canvas with modifiers background + $canvas = new Imagick(); + $canvas->newImage($crop->width(), $crop->height(), $background, 'png'); + $canvas->setImageResolution($resolution->x(), $resolution->y()); - // crop image - $frame->native()->extentImage( - $crop->width(), - $crop->height(), - $crop->pivot()->x() + $this->offset_x, - $crop->pivot()->y() + $this->offset_y + // set animation details + if ($image->isAnimated()) { + $canvas->setImageDelay($frame->native()->getImageDelay()); + $canvas->setImageIterations($frame->native()->getImageIterations()); + $canvas->setImageDispose($frame->native()->getImageDispose()); + } + + // place original frame content onto the empty colored frame canvas + $canvas->compositeImage( + $frame->native(), + Imagick::COMPOSITE_DEFAULT, + ($crop->pivot()->x() + $this->offset_x) * -1, + ($crop->pivot()->y() + $this->offset_y) * -1, ); - // repage - $frame->native()->setImagePage( - $crop->width(), - $crop->height(), - 0, - 0, - ); - - // cover the possible newly created areas with background color - if ($crop->width() > $originalSize->width() || $this->offset_x > 0) { - $draw->rectangle( - $originalSize->width() + ($this->offset_x * -1) - $crop->pivot()->x(), - 0, - $crop->width(), - $crop->height() + // copy alpha channel if available + if ($frame->native()->getImageAlphaChannel()) { + $canvas->compositeImage( + $frame->native(), + version_compare(Driver::version(), '7.0.0', '>=') ? + Imagick::COMPOSITE_COPYOPACITY : + Imagick::COMPOSITE_DSTIN, + ($crop->pivot()->x() + $this->offset_x) * -1, + ($crop->pivot()->y() + $this->offset_y) * -1, ); } - // cover the possible newly created areas with background color - if ($crop->height() > $originalSize->height() || $this->offset_y > 0) { - $draw->rectangle( - ($this->offset_x * -1) - $crop->pivot()->x(), - $originalSize->height() + ($this->offset_y * -1) - $crop->pivot()->y(), - ($this->offset_x * -1) + $originalSize->width() - 1 - $crop->pivot()->x(), - $crop->height() - ); - } - - // cover the possible newly created areas with background color - if ((($this->offset_x * -1) - $crop->pivot()->x() - 1) > 0) { - $draw->rectangle( - 0, - 0, - ($this->offset_x * -1) - $crop->pivot()->x() - 1, - $crop->height() - ); - } - - // cover the possible newly created areas with background color - if ((($this->offset_y * -1) - $crop->pivot()->y() - 1) > 0) { - $draw->rectangle( - ($this->offset_x * -1) - $crop->pivot()->x(), - 0, - ($this->offset_x * -1) + $originalSize->width() - $crop->pivot()->x() - 1, - ($this->offset_y * -1) - $crop->pivot()->y() - 1, - ); - } - - $frame->native()->drawImage($draw); + // add newly built frame to container imagick + $imagick->addImage($canvas); } + // replace imagick + $image->core()->setNative($imagick); + return $image; } } diff --git a/src/Drivers/Imagick/Modifiers/FillModifier.php b/src/Drivers/Imagick/Modifiers/FillModifier.php index 0eee743d..274745fa 100644 --- a/src/Drivers/Imagick/Modifiers/FillModifier.php +++ b/src/Drivers/Imagick/Modifiers/FillModifier.php @@ -7,7 +7,6 @@ namespace Intervention\Image\Drivers\Imagick\Modifiers; use Imagick; use ImagickDraw; use ImagickPixel; -use Intervention\Image\Interfaces\FrameInterface; use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Interfaces\SpecializedInterface; use Intervention\Image\Modifiers\FillModifier as ModifiersFillModifier; @@ -16,12 +15,11 @@ class FillModifier extends ModifiersFillModifier implements SpecializedInterface { public function apply(ImageInterface $image): ImageInterface { - $color = $this->driver()->handleInput($this->color); - $pixel = $this->driver() - ->colorProcessor($image->colorspace()) - ->colorToNative($color); + $pixel = $this->driver()->colorProcessor($image->colorspace())->colorToNative( + $this->driver()->handleInput($this->color) + ); - foreach ($image as $frame) { + foreach ($image->core()->native() as $frame) { if ($this->hasPosition()) { $this->floodFillWithColor($frame, $pixel); } else { @@ -32,14 +30,14 @@ class FillModifier extends ModifiersFillModifier implements SpecializedInterface return $image; } - private function floodFillWithColor(FrameInterface $frame, ImagickPixel $pixel): void + private function floodFillWithColor(Imagick $frame, ImagickPixel $pixel): void { - $target = $frame->native()->getImagePixelColor( + $target = $frame->getImagePixelColor( $this->position->x(), $this->position->y() ); - $frame->native()->floodfillPaintImage( + $frame->floodfillPaintImage( $pixel, 100, $target, @@ -50,16 +48,16 @@ class FillModifier extends ModifiersFillModifier implements SpecializedInterface ); } - private function fillAllWithColor(FrameInterface $frame, ImagickPixel $pixel): void + private function fillAllWithColor(Imagick $frame, ImagickPixel $pixel): void { $draw = new ImagickDraw(); $draw->setFillColor($pixel); - $draw->rectangle( - 0, - 0, - $frame->native()->getImageWidth(), - $frame->native()->getImageHeight() - ); - $frame->native()->drawImage($draw); + $draw->rectangle(0, 0, $frame->getImageWidth(), $frame->getImageHeight()); + $frame->drawImage($draw); + + // deactive alpha channel when image was filled with opaque color + if ($pixel->getColorValue(Imagick::COLOR_ALPHA) == 1) { + $frame->setImageAlphaChannel(Imagick::ALPHACHANNEL_DEACTIVATE); + } } } diff --git a/src/Drivers/Imagick/Modifiers/ResizeCanvasModifier.php b/src/Drivers/Imagick/Modifiers/ResizeCanvasModifier.php index 3baf10d2..4459f329 100644 --- a/src/Drivers/Imagick/Modifiers/ResizeCanvasModifier.php +++ b/src/Drivers/Imagick/Modifiers/ResizeCanvasModifier.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Intervention\Image\Drivers\Imagick\Modifiers; -use ImagickDraw; use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Interfaces\SpecializedInterface; use Intervention\Image\Modifiers\ResizeCanvasModifier as GenericResizeCanvasModifier; @@ -13,74 +12,15 @@ class ResizeCanvasModifier extends GenericResizeCanvasModifier implements Specia { public function apply(ImageInterface $image): ImageInterface { - $size = $image->size(); - $resize = $this->cropSize($image); + $cropSize = $this->cropSize($image); - $background = $this->driver()->colorProcessor($image->colorspace())->colorToNative( - $this->driver()->handleInput($this->background) - ); - - foreach ($image as $frame) { - $frame->native()->extentImage( - $resize->width(), - $resize->height(), - $resize->pivot()->x(), - $resize->pivot()->y() - ); - - if ($resize->width() > $size->width()) { - // fill new emerged background - $draw = new ImagickDraw(); - $draw->setFillColor($background); - - $delta_width = abs($resize->pivot()->x()); - $delta_height = $resize->pivot()->y() * -1; - - if ($delta_width > 0) { - $draw->rectangle( - 0, - $delta_height, - $delta_width - 1, - $delta_height + $size->height() - 1 - ); - } - - $draw->rectangle( - $size->width() + $delta_width, - $delta_height, - $resize->width(), - $delta_height + $size->height() - 1 - ); - - $frame->native()->drawImage($draw); - } - - if ($resize->height() > $size->height()) { - // fill new emerged background - $draw = new ImagickDraw(); - $draw->setFillColor($background); - - $delta = abs($resize->pivot()->y()); - - if ($delta > 0) { - $draw->rectangle( - 0, - 0, - $resize->width(), - $delta - 1 - ); - } - - $draw->rectangle( - 0, - $size->height() + $delta, - $resize->width(), - $resize->height() - ); - - $frame->native()->drawImage($draw); - } - } + $image->modify(new CropModifier( + $cropSize->width(), + $cropSize->height(), + $cropSize->pivot()->x(), + $cropSize->pivot()->y(), + $this->background, + )); return $image; } diff --git a/src/Format.php b/src/Format.php index dc9be3e1..40acf442 100644 --- a/src/Format.php +++ b/src/Format.php @@ -17,6 +17,7 @@ use Intervention\Image\Encoders\WebpEncoder; use Intervention\Image\Exceptions\NotSupportedException; use Intervention\Image\Interfaces\EncoderInterface; use ReflectionClass; +use ReflectionParameter; enum Format { @@ -155,7 +156,7 @@ enum Format $reflectionClass = new ReflectionClass($classname); if ($constructor = $reflectionClass->getConstructor()) { $parameters = array_map( - fn($parameter): string => $parameter->getName(), + fn(ReflectionParameter $parameter): string => $parameter->getName(), $constructor->getParameters(), ); } @@ -163,7 +164,7 @@ enum Format // filter out unavailable options of target encoder $options = array_filter( $options, - fn($key): bool => in_array($key, $parameters), + fn(mixed $key): bool => in_array($key, $parameters), ARRAY_FILTER_USE_KEY, ); diff --git a/tests/ImagickTestCase.php b/tests/ImagickTestCase.php index bef7b674..27489097 100644 --- a/tests/ImagickTestCase.php +++ b/tests/ImagickTestCase.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Intervention\Image\Tests; use Imagick; +use ImagickException; use ImagickPixel; use Intervention\Image\Decoders\FilePathImageDecoder; use Intervention\Image\Drivers\Imagick\Core; @@ -20,6 +21,14 @@ abstract class ImagickTestCase extends BaseTestCase ); } + /** + * Create test image with red (#ff0000) background + * + * @param int $width + * @param int $height + * @return Image + * @throws ImagickException + */ public static function createTestImage(int $width, int $height): Image { $background = new ImagickPixel('rgb(255, 0, 0)'); @@ -29,6 +38,7 @@ abstract class ImagickTestCase extends BaseTestCase $imagick->setImageType(Imagick::IMGTYPE_UNDEFINED); $imagick->setColorspace(Imagick::COLORSPACE_SRGB); $imagick->setImageResolution(96, 96); + $imagick->setImageBackgroundColor($background); return new Image( new Driver(), diff --git a/tests/Unit/Drivers/Gd/Modifiers/CropModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/CropModifierTest.php index 4a5e268d..24fcc734 100644 --- a/tests/Unit/Drivers/Gd/Modifiers/CropModifierTest.php +++ b/tests/Unit/Drivers/Gd/Modifiers/CropModifierTest.php @@ -36,4 +36,55 @@ final class CropModifierTest extends GdTestCase $this->assertColor(0, 0, 255, 255, $image->pickColor(445, 16)); $this->assertTransparency($image->pickColor(460, 16)); } + + public function testModifySinglePixel(): void + { + $image = $this->createTestImage(1, 1); + $this->assertEquals(1, $image->width()); + $this->assertEquals(1, $image->height()); + $image->modify(new CropModifier(3, 3, 0, 0, 'ff0', 'center')); + $this->assertEquals(3, $image->width()); + $this->assertEquals(3, $image->height()); + $this->assertColor(255, 255, 0, 255, $image->pickColor(0, 0)); + $this->assertColor(255, 0, 0, 255, $image->pickColor(1, 1)); + $this->assertColor(255, 255, 0, 255, $image->pickColor(2, 2)); + } + + public function testModifyKeepsResolution(): void + { + $image = $this->readTestImage('300dpi.png'); + $this->assertEquals(300, round($image->resolution()->perInch()->x())); + $image = $image->modify(new CropModifier(800, 100, -10, -10, 'ff0000')); + $this->assertEquals(300, round($image->resolution()->perInch()->x())); + } + + public function testHalfTransparent(): void + { + $image = $this->createTestImage(16, 16); + $image->modify(new CropModifier(32, 32, 0, 0, '00f5', 'center')); + $this->assertEquals(32, $image->width()); + $this->assertEquals(32, $image->height()); + $this->assertColor(0, 0, 255, 85, $image->pickColor(5, 5)); + $this->assertColor(0, 0, 255, 85, $image->pickColor(16, 5)); + $this->assertColor(0, 0, 255, 85, $image->pickColor(30, 5)); + $this->assertColor(0, 0, 255, 85, $image->pickColor(5, 16)); + $this->assertColor(255, 0, 0, 255, $image->pickColor(16, 16)); + $this->assertColor(0, 0, 255, 85, $image->pickColor(30, 16)); + $this->assertColor(0, 0, 255, 85, $image->pickColor(5, 30)); + $this->assertColor(0, 0, 255, 85, $image->pickColor(16, 30)); + $this->assertColor(0, 0, 255, 85, $image->pickColor(30, 30)); + } + + public function testMergeTransparentBackgrounds(): void + { + $image = $this->createTestImage(1, 1)->fill('f00'); + $this->assertEquals(1, $image->width()); + $this->assertEquals(1, $image->height()); + $image->modify(new CropModifier(3, 3, 0, 0, '00f7', 'center')); + $this->assertEquals(3, $image->width()); + $this->assertEquals(3, $image->height()); + $this->assertColor(0, 0, 255, 119, $image->pickColor(0, 0)); + $this->assertColor(255, 0, 0, 255, $image->pickColor(1, 1)); + $this->assertColor(0, 0, 255, 119, $image->pickColor(2, 2)); + } } diff --git a/tests/Unit/Drivers/Gd/Modifiers/ResizeCanvasModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/ResizeCanvasModifierTest.php index 04a2d886..56cc7aad 100644 --- a/tests/Unit/Drivers/Gd/Modifiers/ResizeCanvasModifierTest.php +++ b/tests/Unit/Drivers/Gd/Modifiers/ResizeCanvasModifierTest.php @@ -40,6 +40,20 @@ final class ResizeCanvasModifierTest extends GdTestCase $this->assertColor(180, 224, 0, 255, $image->pickColor(2, 2)); $this->assertColor(255, 255, 0, 255, $image->pickColor(17, 17)); $this->assertTransparency($image->pickColor(12, 1)); + + $image = $this->createTestImage(16, 16); + $image->modify(new ResizeCanvasModifier(32, 32, '00f5', 'center')); + $this->assertEquals(32, $image->width()); + $this->assertEquals(32, $image->height()); + $this->assertColor(0, 0, 255, 85, $image->pickColor(5, 5)); + $this->assertColor(0, 0, 255, 85, $image->pickColor(16, 5)); + $this->assertColor(0, 0, 255, 85, $image->pickColor(30, 5)); + $this->assertColor(0, 0, 255, 85, $image->pickColor(5, 16)); + $this->assertColor(255, 0, 0, 255, $image->pickColor(16, 16)); + $this->assertColor(0, 0, 255, 85, $image->pickColor(30, 16)); + $this->assertColor(0, 0, 255, 85, $image->pickColor(5, 30)); + $this->assertColor(0, 0, 255, 85, $image->pickColor(16, 30)); + $this->assertColor(0, 0, 255, 85, $image->pickColor(30, 30)); } public function testModifyEdge(): void diff --git a/tests/Unit/Drivers/Imagick/Encoders/PngEncoderTest.php b/tests/Unit/Drivers/Imagick/Encoders/PngEncoderTest.php index 64d99f38..742cd3b5 100644 --- a/tests/Unit/Drivers/Imagick/Encoders/PngEncoderTest.php +++ b/tests/Unit/Drivers/Imagick/Encoders/PngEncoderTest.php @@ -64,7 +64,7 @@ final class PngEncoderTest extends ImagickTestCase yield [ static::createTestImage(3, 2)->fill('ccc'), // new grayscale new PngEncoder(indexed: true), - 'grayscale', // result should be 'indexed' but there seems to be no way to force this with imagick + 'indexed', ]; yield [ static::readTestImage('circle.png'), // truecolor-alpha diff --git a/tests/Unit/Drivers/Imagick/Modifiers/CropModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/CropModifierTest.php index 2b3691f7..e5bce68e 100644 --- a/tests/Unit/Drivers/Imagick/Modifiers/CropModifierTest.php +++ b/tests/Unit/Drivers/Imagick/Modifiers/CropModifierTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Intervention\Image\Tests\Unit\Drivers\Imagick\Modifiers; +use Intervention\Image\Colors\Cmyk\Colorspace; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\RequiresPhpExtension; use Intervention\Image\Modifiers\CropModifier; @@ -36,4 +37,63 @@ final class CropModifierTest extends ImagickTestCase $this->assertColor(0, 0, 255, 255, $image->pickColor(445, 16)); $this->assertTransparency($image->pickColor(460, 16)); } + + public function testModifySinglePixel(): void + { + $image = $this->createTestImage(1, 1); + $this->assertEquals(1, $image->width()); + $this->assertEquals(1, $image->height()); + $image->modify(new CropModifier(3, 3, 0, 0, 'ff0', 'center')); + $this->assertEquals(3, $image->width()); + $this->assertEquals(3, $image->height()); + $this->assertColor(255, 255, 0, 255, $image->pickColor(0, 0)); + $this->assertColor(255, 0, 0, 255, $image->pickColor(1, 1)); + $this->assertColor(255, 255, 0, 255, $image->pickColor(2, 2)); + } + + public function testModifyKeepsColorspace(): void + { + $image = $this->readTestImage('cmyk.jpg'); + $this->assertInstanceOf(Colorspace::class, $image->colorspace()); + $image = $image->modify(new CropModifier(800, 100, -10, -10, 'ff0000')); + $this->assertInstanceOf(Colorspace::class, $image->colorspace()); + } + + public function testModifyKeepsResolution(): void + { + $image = $this->readTestImage('300dpi.png'); + $this->assertEquals(300, round($image->resolution()->perInch()->x())); + $image = $image->modify(new CropModifier(800, 100, -10, -10, 'ff0000')); + $this->assertEquals(300, round($image->resolution()->perInch()->x())); + } + + public function testHalfTransparent(): void + { + $image = $this->createTestImage(16, 16); + $image->modify(new CropModifier(32, 32, 0, 0, '00f5', 'center')); + $this->assertEquals(32, $image->width()); + $this->assertEquals(32, $image->height()); + $this->assertColor(0, 0, 255, 77, $image->pickColor(5, 5)); + $this->assertColor(0, 0, 255, 77, $image->pickColor(16, 5)); + $this->assertColor(0, 0, 255, 77, $image->pickColor(30, 5)); + $this->assertColor(0, 0, 255, 77, $image->pickColor(5, 16)); + $this->assertColor(255, 0, 0, 255, $image->pickColor(16, 16)); + $this->assertColor(0, 0, 255, 77, $image->pickColor(30, 16)); + $this->assertColor(0, 0, 255, 77, $image->pickColor(5, 30)); + $this->assertColor(0, 0, 255, 77, $image->pickColor(16, 30)); + $this->assertColor(0, 0, 255, 77, $image->pickColor(30, 30)); + } + + public function testMergeTransparentBackgrounds(): void + { + $image = $this->createTestImage(1, 1)->fill('f00'); + $this->assertEquals(1, $image->width()); + $this->assertEquals(1, $image->height()); + $image->modify(new CropModifier(3, 3, 0, 0, '00f7', 'center')); + $this->assertEquals(3, $image->width()); + $this->assertEquals(3, $image->height()); + $this->assertColor(0, 0, 255, 127, $image->pickColor(0, 0), 1); + $this->assertColor(255, 0, 0, 255, $image->pickColor(1, 1)); + $this->assertColor(0, 0, 255, 127, $image->pickColor(2, 2), 1); + } } diff --git a/tests/Unit/Drivers/Imagick/Modifiers/ResizeCanvasModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/ResizeCanvasModifierTest.php index a4d940e0..c9bac210 100644 --- a/tests/Unit/Drivers/Imagick/Modifiers/ResizeCanvasModifierTest.php +++ b/tests/Unit/Drivers/Imagick/Modifiers/ResizeCanvasModifierTest.php @@ -41,7 +41,7 @@ final class ResizeCanvasModifierTest extends ImagickTestCase $this->assertColor(255, 255, 0, 255, $image->pickColor(17, 17)); $this->assertTransparency($image->pickColor(12, 1)); - $image = $this->createTestImage(16, 16)->fill('f00'); + $image = $this->createTestImage(16, 16); $image->modify(new ResizeCanvasModifier(32, 32, '00f5', 'center')); $this->assertEquals(32, $image->width()); $this->assertEquals(32, $image->height());