diff --git a/src/Colors/Rgb/Decoders/TransparentColorDecoder.php b/src/Colors/Rgb/Decoders/TransparentColorDecoder.php index a921e1a5..156a48a5 100644 --- a/src/Colors/Rgb/Decoders/TransparentColorDecoder.php +++ b/src/Colors/Rgb/Decoders/TransparentColorDecoder.php @@ -18,6 +18,6 @@ class TransparentColorDecoder extends HexColorDecoder throw new DecoderException('Unable to decode input'); } - return parent::decode('#ff00ff00'); + return parent::decode('#ffffff00'); } } diff --git a/src/Drivers/Gd/Cloner.php b/src/Drivers/Gd/Cloner.php new file mode 100644 index 00000000..6b6fd0ad --- /dev/null +++ b/src/Drivers/Gd/Cloner.php @@ -0,0 +1,89 @@ + new Rectangle(imagesx($gd), imagesy($gd)), + default => $size, + }; + + // create new gd image with same size or new given size + $clone = imagecreatetruecolor($size->width(), $size->height()); + + // copy resolution to clone + $resolution = imageresolution($gd); + if (is_array($resolution) && array_key_exists(0, $resolution) && array_key_exists(1, $resolution)) { + imageresolution($clone, $resolution[0], $resolution[1]); + } + + // fill with background + $processor = new ColorProcessor(); + imagefill($clone, 0, 0, $processor->colorToNative($background)); + imagealphablending($clone, true); + imagesavealpha($clone, true); + + return $clone; + } + + /** + * Create a clone of an GdImage that is positioned on the specified background color. + * Possible transparent areas are mixed with this color. + * + * @param GdImage $gd + * @param ColorInterface $background + * @return GdImage + */ + public static function cloneBlended(GdImage $gd, ColorInterface $background): GdImage + { + // create empty canvas with same size + $clone = static::cloneEmpty($gd, background: $background); + + // transfer actual image to clone + imagecopy($clone, $gd, 0, 0, 0, 0, imagesx($gd), imagesy($gd)); + + return $clone; + } +} diff --git a/src/Drivers/Gd/Decoders/BinaryImageDecoder.php b/src/Drivers/Gd/Decoders/BinaryImageDecoder.php index d8f3f846..cb1ed6c8 100644 --- a/src/Drivers/Gd/Decoders/BinaryImageDecoder.php +++ b/src/Drivers/Gd/Decoders/BinaryImageDecoder.php @@ -41,7 +41,6 @@ class BinaryImageDecoder extends AbstractDecoder implements DecoderInterface if (!imageistruecolor($gd)) { imagepalettetotruecolor($gd); } - imagesavealpha($gd, true); // build image instance diff --git a/src/Drivers/Gd/Driver.php b/src/Drivers/Gd/Driver.php index 9268887d..e6bcfac4 100644 --- a/src/Drivers/Gd/Driver.php +++ b/src/Drivers/Gd/Driver.php @@ -47,7 +47,7 @@ class Driver extends AbstractDriver // build new transparent GDImage $data = imagecreatetruecolor($width, $height); imagesavealpha($data, true); - $background = imagecolorallocatealpha($data, 255, 0, 255, 127); + $background = imagecolorallocatealpha($data, 255, 255, 255, 127); imagealphablending($data, false); imagefill($data, 0, 0, $background); imagecolortransparent($data, $background); diff --git a/src/Drivers/Gd/Encoders/JpegEncoder.php b/src/Drivers/Gd/Encoders/JpegEncoder.php index 3493f67e..e9a456cb 100644 --- a/src/Drivers/Gd/Encoders/JpegEncoder.php +++ b/src/Drivers/Gd/Encoders/JpegEncoder.php @@ -3,6 +3,7 @@ namespace Intervention\Image\Drivers\Gd\Encoders; use Intervention\Image\Drivers\DriverSpecializedEncoder; +use Intervention\Image\Drivers\Gd\Cloner; use Intervention\Image\EncodedImage; use Intervention\Image\Interfaces\ImageInterface; @@ -13,9 +14,10 @@ class JpegEncoder extends DriverSpecializedEncoder { public function encode(ImageInterface $image): EncodedImage { - $gd = $image->core()->native(); - $data = $this->getBuffered(function () use ($gd) { - imagejpeg($gd, null, $this->quality); + $output = Cloner::cloneBlended($image->core()->native(), background: $image->blendingColor()); + + $data = $this->getBuffered(function () use ($output) { + imagejpeg($output, null, $this->quality); }); return new EncodedImage($data, 'image/jpeg'); diff --git a/src/Drivers/Gd/Frame.php b/src/Drivers/Gd/Frame.php index cb0a2e33..62f4ea17 100644 --- a/src/Drivers/Gd/Frame.php +++ b/src/Drivers/Gd/Frame.php @@ -107,35 +107,6 @@ class Frame implements FrameInterface */ public function __clone(): void { - // create new clone image - $width = imagesx($this->native); - $height = imagesy($this->native); - $clone = match (imageistruecolor($this->native)) { - true => imagecreatetruecolor($width, $height), - default => imagecreate($width, $height), - }; - - // transfer resolution to clone - $resolution = imageresolution($this->native); - if (is_array($resolution) && array_key_exists(0, $resolution) && array_key_exists(1, $resolution)) { - imageresolution($clone, $resolution[0], $resolution[1]); - } - - // transfer transparency to clone - $transIndex = imagecolortransparent($this->native); - if ($transIndex != -1) { - $rgba = imagecolorsforindex($clone, $transIndex); - $transColor = imagecolorallocatealpha($clone, $rgba['red'], $rgba['green'], $rgba['blue'], 127); - imagefill($clone, 0, 0, $transColor); - imagecolortransparent($clone, $transColor); - } else { - imagealphablending($clone, false); - imagesavealpha($clone, true); - } - - // transfer actual image to clone - imagecopy($clone, $this->native, 0, 0, 0, 0, $width, $height); - - $this->native = $clone; + $this->native = Cloner::clone($this->native); } } diff --git a/src/Drivers/Gd/Modifiers/BlendTransparencyModifier.php b/src/Drivers/Gd/Modifiers/BlendTransparencyModifier.php new file mode 100644 index 00000000..82af1493 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/BlendTransparencyModifier.php @@ -0,0 +1,34 @@ +driver()->handleInput( + $this->color ? $this->color : $image->blendingColor() + ); + + foreach ($image as $frame) { + // create new canvas with blending color as background + $modified = Cloner::cloneBlended( + $frame->native(), + background: $color + ); + + // set new gd image + $frame->setNative($modified); + } + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/ContainModifier.php b/src/Drivers/Gd/Modifiers/ContainModifier.php index 5d7e3b3e..f0592acb 100644 --- a/src/Drivers/Gd/Modifiers/ContainModifier.php +++ b/src/Drivers/Gd/Modifiers/ContainModifier.php @@ -5,12 +5,12 @@ 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\SpecializedModifier; +use Intervention\Image\Drivers\DriverSpecialized; +use Intervention\Image\Drivers\Gd\Cloner; use Intervention\Image\Interfaces\ColorInterface; use Intervention\Image\Interfaces\FrameInterface; use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Interfaces\SizeInterface; -use Intervention\Image\Modifiers\FillModifier; /** * @method SizeInterface getCropSize(ImageInterface $image) @@ -20,16 +20,17 @@ use Intervention\Image\Modifiers\FillModifier; * @property mixed $background * @property string $position */ -class ContainModifier extends SpecializedModifier +class ContainModifier extends DriverSpecialized { public function apply(ImageInterface $image): ImageInterface { $crop = $this->getCropSize($image); $resize = $this->getResizeSize($image); $background = $this->driver()->handleInput($this->background); + $blendingColor = $image->blendingColor(); foreach ($image as $frame) { - $this->modify($frame, $crop, $resize, $background); + $this->modify($frame, $crop, $resize, $background, $blendingColor); } return $image; @@ -39,26 +40,19 @@ class ContainModifier extends SpecializedModifier FrameInterface $frame, SizeInterface $crop, SizeInterface $resize, - ColorInterface $background + ColorInterface $background, + ColorInterface $blendingColor ): void { // create new gd image - $modified = $this->driver()->createImage( - $resize->width(), - $resize->height() - )->modify( - new FillModifier($background) - )->core()->native(); - - // retain resolution - $this->copyResolution($frame->native(), $modified); + $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(), + $blendingColor->channel(Red::class)->value(), + $blendingColor->channel(Green::class)->value(), + $blendingColor->channel(Blue::class)->value(), 127, ); imagealphablending($modified, false); // do not blend / just overwrite diff --git a/src/Drivers/Gd/Modifiers/CoverModifier.php b/src/Drivers/Gd/Modifiers/CoverModifier.php index ce2ef59d..b4cb224c 100644 --- a/src/Drivers/Gd/Modifiers/CoverModifier.php +++ b/src/Drivers/Gd/Modifiers/CoverModifier.php @@ -2,7 +2,8 @@ namespace Intervention\Image\Drivers\Gd\Modifiers; -use Intervention\Image\Drivers\Gd\SpecializedModifier; +use Intervention\Image\Drivers\DriverSpecialized; +use Intervention\Image\Drivers\Gd\Cloner; use Intervention\Image\Interfaces\FrameInterface; use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Interfaces\SizeInterface; @@ -11,7 +12,7 @@ use Intervention\Image\Interfaces\SizeInterface; * @method SizeInterface getResizeSize(ImageInterface $image) * @method SizeInterface getCropSize(ImageInterface $image) */ -class CoverModifier extends SpecializedModifier +class CoverModifier extends DriverSpecialized { public function apply(ImageInterface $image): ImageInterface { @@ -28,31 +29,12 @@ class CoverModifier extends SpecializedModifier protected function modifyFrame(FrameInterface $frame, SizeInterface $crop, SizeInterface $resize): void { // create new image - $modified = $this->driver()->createImage( - $resize->width(), - $resize->height() - )->core()->native(); - - // get original image - $original = $frame->native(); - - // retain resolution - $this->copyResolution($original, $modified); - - // preserve transparency - $transIndex = imagecolortransparent($original); - - if ($transIndex != -1) { - $rgba = imagecolorsforindex($modified, $transIndex); - $transColor = imagecolorallocatealpha($modified, $rgba['red'], $rgba['green'], $rgba['blue'], 127); - imagefill($modified, 0, 0, $transColor); - imagecolortransparent($modified, $transColor); - } + $modified = Cloner::cloneEmpty($frame->native(), $resize); // copy content from resource imagecopyresampled( $modified, - $original, + $frame->native(), 0, 0, $crop->pivot()->x(), diff --git a/src/Drivers/Gd/Modifiers/CropModifier.php b/src/Drivers/Gd/Modifiers/CropModifier.php index 4d4b8c65..9139b074 100644 --- a/src/Drivers/Gd/Modifiers/CropModifier.php +++ b/src/Drivers/Gd/Modifiers/CropModifier.php @@ -2,7 +2,8 @@ namespace Intervention\Image\Drivers\Gd\Modifiers; -use Intervention\Image\Drivers\Gd\SpecializedModifier; +use Intervention\Image\Drivers\DriverSpecialized; +use Intervention\Image\Drivers\Gd\Cloner; use Intervention\Image\Interfaces\FrameInterface; use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Interfaces\SizeInterface; @@ -11,58 +12,109 @@ use Intervention\Image\Interfaces\SizeInterface; * @method SizeInterface crop(ImageInterface $image) * @property int $offset_x * @property int $offset_y + * @property mixed $background */ -class CropModifier extends SpecializedModifier +class CropModifier extends DriverSpecialized { 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) + ); foreach ($image as $frame) { - $this->cropFrame($frame, $crop); + $this->cropFrame($frame, $originalSize, $crop, $background); } return $image; } - protected function cropFrame(FrameInterface $frame, SizeInterface $resizeTo): void - { - // create new image - $modified = $this->driver() - ->createImage($resizeTo->width(), $resizeTo->height()) - ->core() - ->native(); + protected function cropFrame( + FrameInterface $frame, + SizeInterface $originalSize, + SizeInterface $resizeTo, + int $background + ): void { + // create new image with transparent background + $modified = Cloner::cloneEmpty($frame->native(), $resizeTo); - // get original image - $original = $frame->native(); + // define offset + $offset_x = ($resizeTo->pivot()->x() + $this->offset_x); + $offset_y = ($resizeTo->pivot()->y() + $this->offset_y); - // retain resolution - $this->copyResolution($original, $modified); - - // preserve transparency - $transIndex = imagecolortransparent($original); - - if ($transIndex != -1) { - $rgba = imagecolorsforindex($modified, $transIndex); - $transColor = imagecolorallocatealpha($modified, $rgba['red'], $rgba['green'], $rgba['blue'], 127); - imagefill($modified, 0, 0, $transColor); - imagecolortransparent($modified, $transColor); - } + // define target width & height + $targetWidth = min($resizeTo->width(), $originalSize->width()); + $targetHeight = min($resizeTo->height(), $originalSize->height()); + $targetWidth = $targetWidth < $originalSize->width() ? $targetWidth + $offset_x : $targetWidth; + $targetHeight = $targetHeight < $originalSize->height() ? $targetHeight + $offset_y : $targetHeight; // copy content from resource imagecopyresampled( $modified, - $original, + $frame->native(), + $offset_x * -1, + $offset_y * -1, 0, 0, - $resizeTo->pivot()->x() + $this->offset_x, - $resizeTo->pivot()->y() + $this->offset_y, - $resizeTo->width(), - $resizeTo->height(), - $resizeTo->width(), - $resizeTo->height(), + $targetWidth, + $targetHeight, + $targetWidth, + $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 recource $frame->setNative($modified); } diff --git a/src/Drivers/Gd/Modifiers/QuantizeColorsModifier.php b/src/Drivers/Gd/Modifiers/QuantizeColorsModifier.php index 8dbf1bd0..56939205 100644 --- a/src/Drivers/Gd/Modifiers/QuantizeColorsModifier.php +++ b/src/Drivers/Gd/Modifiers/QuantizeColorsModifier.php @@ -2,7 +2,8 @@ namespace Intervention\Image\Drivers\Gd\Modifiers; -use Intervention\Image\Drivers\Gd\SpecializedModifier; +use Intervention\Image\Drivers\DriverSpecialized; +use Intervention\Image\Drivers\Gd\Cloner; use Intervention\Image\Exceptions\InputException; use Intervention\Image\Interfaces\ImageInterface; @@ -10,7 +11,7 @@ use Intervention\Image\Interfaces\ImageInterface; * @property int $limit * @property mixed $background */ -class QuantizeColorsModifier extends SpecializedModifier +class QuantizeColorsModifier extends DriverSpecialized { public function apply(ImageInterface $image): ImageInterface { @@ -33,10 +34,7 @@ class QuantizeColorsModifier extends SpecializedModifier foreach ($image as $frame) { // create new image for color quantization - $reduced = imagecreatetruecolor($width, $height); - - // retain resolution - $this->copyResolution($frame->native(), $reduced); + $reduced = Cloner::cloneEmpty($frame->native(), background: $image->blendingColor()); // fill with background imagefill($reduced, 0, 0, $background); diff --git a/src/Drivers/Gd/Modifiers/ResizeCanvasModifier.php b/src/Drivers/Gd/Modifiers/ResizeCanvasModifier.php index 5035f339..fb69460a 100644 --- a/src/Drivers/Gd/Modifiers/ResizeCanvasModifier.php +++ b/src/Drivers/Gd/Modifiers/ResizeCanvasModifier.php @@ -5,18 +5,18 @@ 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\SpecializedModifier; +use Intervention\Image\Drivers\DriverSpecialized; +use Intervention\Image\Drivers\Gd\Cloner; use Intervention\Image\Interfaces\ColorInterface; use Intervention\Image\Interfaces\FrameInterface; use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Interfaces\SizeInterface; -use Intervention\Image\Modifiers\FillModifier; /** * @method SizeInterface cropSize(ImageInterface $image) * @property mixed $background */ -class ResizeCanvasModifier extends SpecializedModifier +class ResizeCanvasModifier extends DriverSpecialized { public function apply(ImageInterface $image): ImageInterface { @@ -35,16 +35,8 @@ class ResizeCanvasModifier extends SpecializedModifier SizeInterface $resize, ColorInterface $background, ): void { - // create new gd image - $modified = $this->driver()->createImage( - $resize->width(), - $resize->height() - )->modify( - new FillModifier($background) - )->core()->native(); - - // retain resolution - $this->copyResolution($frame->native(), $modified); + // create new canvas with target size & target background color + $modified = Cloner::cloneEmpty($frame->native(), $resize, $background); // make image area transparent to keep transparency // even if background-color is set @@ -57,7 +49,7 @@ class ResizeCanvasModifier extends SpecializedModifier ); imagealphablending($modified, false); // do not blend / just overwrite - imagecolortransparent($modified, $transparent); + // imagecolortransparent($modified, $transparent); imagefilledrectangle( $modified, $resize->pivot()->x() * -1, diff --git a/src/Drivers/Gd/Modifiers/ResizeModifier.php b/src/Drivers/Gd/Modifiers/ResizeModifier.php index 20d76494..b173ce8f 100644 --- a/src/Drivers/Gd/Modifiers/ResizeModifier.php +++ b/src/Drivers/Gd/Modifiers/ResizeModifier.php @@ -2,7 +2,8 @@ namespace Intervention\Image\Drivers\Gd\Modifiers; -use Intervention\Image\Drivers\Gd\SpecializedModifier; +use Intervention\Image\Drivers\DriverSpecialized; +use Intervention\Image\Drivers\Gd\Cloner; use Intervention\Image\Interfaces\FrameInterface; use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Interfaces\SizeInterface; @@ -11,7 +12,7 @@ use Intervention\Image\Interfaces\SizeInterface; * @property null|int $width * @property null|int $height */ -class ResizeModifier extends SpecializedModifier +class ResizeModifier extends DriverSpecialized { public function apply(ImageInterface $image): ImageInterface { @@ -25,35 +26,13 @@ class ResizeModifier extends SpecializedModifier private function resizeFrame(FrameInterface $frame, SizeInterface $resizeTo): void { - // create new image - $modified = imagecreatetruecolor( - $resizeTo->width(), - $resizeTo->height() - ); - - // get current GDImage - $current = $frame->native(); - - // retain resolution - $this->copyResolution($current, $modified); - - // preserve transparency - $transIndex = imagecolortransparent($current); - - if ($transIndex != -1) { - $rgba = imagecolorsforindex($modified, $transIndex); - $transColor = imagecolorallocatealpha($modified, $rgba['red'], $rgba['green'], $rgba['blue'], 127); - imagefill($modified, 0, 0, $transColor); - imagecolortransparent($modified, $transColor); - } else { - imagealphablending($modified, false); - imagesavealpha($modified, true); - } + // create empty canvas in target size + $modified = Cloner::cloneEmpty($frame->native(), $resizeTo); // copy content from resource imagecopyresampled( $modified, - $current, + $frame->native(), $resizeTo->pivot()->x(), $resizeTo->pivot()->y(), 0, diff --git a/src/Drivers/Gd/Modifiers/RotateModifier.php b/src/Drivers/Gd/Modifiers/RotateModifier.php index d59b74e8..0ed7c987 100644 --- a/src/Drivers/Gd/Modifiers/RotateModifier.php +++ b/src/Drivers/Gd/Modifiers/RotateModifier.php @@ -5,18 +5,18 @@ 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\SpecializedModifier; +use Intervention\Image\Drivers\DriverSpecialized; +use Intervention\Image\Drivers\Gd\Cloner; use Intervention\Image\Geometry\Rectangle; use Intervention\Image\Interfaces\ColorInterface; use Intervention\Image\Interfaces\FrameInterface; use Intervention\Image\Interfaces\ImageInterface; -use Intervention\Image\Modifiers\FillModifier; /** * @method mixed rotationAngle() * @property mixed $background */ -class RotateModifier extends SpecializedModifier +class RotateModifier extends DriverSpecialized { public function apply(ImageInterface $image): ImageInterface { @@ -74,15 +74,7 @@ class RotateModifier extends SpecializedModifier ->rotate($this->rotationAngle() * -1); // create new gd image - $modified = $this->driver()->createImage( - imagesx($rotated), - imagesy($rotated) - )->modify(new FillModifier($background)) - ->core() - ->native(); - - // retain resolution - $this->copyResolution($frame->native(), $modified); + $modified = Cloner::cloneEmpty($frame->native(), $container, $background); // draw the cutout on new gd image to have a transparent // background where the rotated image will be placed diff --git a/src/Drivers/Gd/SpecializedModifier.php b/src/Drivers/Gd/SpecializedModifier.php deleted file mode 100644 index e6d2f655..00000000 --- a/src/Drivers/Gd/SpecializedModifier.php +++ /dev/null @@ -1,18 +0,0 @@ -newImage($width, $height, $background, 'png'); @@ -54,6 +54,7 @@ class Driver extends AbstractDriver $imagick->setImageType(Imagick::IMGTYPE_UNDEFINED); $imagick->setColorspace(Imagick::COLORSPACE_SRGB); $imagick->setImageResolution(96, 96); + $imagick->setImageBackgroundColor($background); return new Image($this, new Core($imagick)); } diff --git a/src/Drivers/Imagick/Encoders/JpegEncoder.php b/src/Drivers/Imagick/Encoders/JpegEncoder.php index cf9719bb..acedb9f6 100644 --- a/src/Drivers/Imagick/Encoders/JpegEncoder.php +++ b/src/Drivers/Imagick/Encoders/JpegEncoder.php @@ -17,15 +17,25 @@ class JpegEncoder extends DriverSpecializedEncoder $format = 'jpeg'; $compression = Imagick::COMPRESSION_JPEG; + // resolve blending color because jpeg has no transparency + $background = $this->driver() + ->colorProcessor($image->colorspace()) + ->colorToNative($image->blendingColor()); + + // set alpha value to 1 because Imagick renders + // possible full transparent colors as black + $background->setColorValue(Imagick::COLOR_ALPHA, 1); + $imagick = $image->core()->native(); - $imagick->setImageBackgroundColor('white'); - $imagick->setBackgroundColor('white'); + $imagick->setImageBackgroundColor($background); + $imagick->setBackgroundColor($background); $imagick->setFormat($format); $imagick->setImageFormat($format); $imagick->setCompression($compression); $imagick->setImageCompression($compression); $imagick->setCompressionQuality($this->quality); $imagick->setImageCompressionQuality($this->quality); + $imagick->setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE); return new EncodedImage($imagick->getImagesBlob(), 'image/jpeg'); } diff --git a/src/Drivers/Imagick/Frame.php b/src/Drivers/Imagick/Frame.php index f957b962..342169b9 100644 --- a/src/Drivers/Imagick/Frame.php +++ b/src/Drivers/Imagick/Frame.php @@ -3,6 +3,7 @@ namespace Intervention\Image\Drivers\Imagick; use Imagick; +use ImagickPixel; use Intervention\Image\Geometry\Rectangle; use Intervention\Image\Image; use Intervention\Image\Interfaces\DriverInterface; @@ -14,6 +15,9 @@ class Frame implements FrameInterface { public function __construct(protected Imagick $native) { + $background = new ImagickPixel('rgba(255, 255, 255, 0)'); + $this->native->setImageBackgroundColor($background); + $this->native->setBackgroundColor($background); } /** diff --git a/src/Drivers/Imagick/Modifiers/BlendTransparencyModifier.php b/src/Drivers/Imagick/Modifiers/BlendTransparencyModifier.php new file mode 100644 index 00000000..77da839f --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/BlendTransparencyModifier.php @@ -0,0 +1,35 @@ +driver()->handleInput( + $this->color ? $this->color : $image->blendingColor() + ); + + // get imagickpixel from color + $pixel = $this->driver() + ->colorProcessor($image->colorspace()) + ->colorToNative($color); + + // merge transparent areas with the background color + foreach ($image as $frame) { + $frame->native()->setImageBackgroundColor($pixel); + $frame->native()->setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE); + $frame->native()->mergeImageLayers(Imagick::LAYERMETHOD_FLATTEN); + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/CropModifier.php b/src/Drivers/Imagick/Modifiers/CropModifier.php index 5e87b4cd..a2e24159 100644 --- a/src/Drivers/Imagick/Modifiers/CropModifier.php +++ b/src/Drivers/Imagick/Modifiers/CropModifier.php @@ -2,6 +2,8 @@ namespace Intervention\Image\Drivers\Imagick\Modifiers; +use ImagickDraw; +use ImagickPixel; use Intervention\Image\Drivers\DriverSpecialized; use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Interfaces\ModifierInterface; @@ -11,20 +13,84 @@ use Intervention\Image\Interfaces\SizeInterface; * @method SizeInterface crop(ImageInterface $image) * @property int $offset_x * @property int $offset_y + * @property mixed $background */ class CropModifier extends DriverSpecialized implements ModifierInterface { 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); foreach ($image as $frame) { + $frame->native()->setBackgroundColor($transparent); + $frame->native()->setImageBackgroundColor($transparent); + + // crop image $frame->native()->extentImage( $crop->width(), $crop->height(), $crop->pivot()->x() + $this->offset_x, $crop->pivot()->y() + $this->offset_y ); + + // 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() + ); + } + + // 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); } return $image; diff --git a/src/Image.php b/src/Image.php index 5d9310e2..9d406d76 100644 --- a/src/Image.php +++ b/src/Image.php @@ -10,6 +10,7 @@ use Intervention\Image\Analyzers\PixelColorsAnalyzer; use Intervention\Image\Analyzers\ProfileAnalyzer; use Intervention\Image\Analyzers\ResolutionAnalyzer; use Intervention\Image\Analyzers\WidthAnalyzer; +use Intervention\Image\Colors\Rgb\Color; use Intervention\Image\Encoders\AutoEncoder; use Intervention\Image\Encoders\AvifEncoder; use Intervention\Image\Encoders\BmpEncoder; @@ -45,6 +46,7 @@ use Intervention\Image\Interfaces\ModifierInterface; use Intervention\Image\Interfaces\ProfileInterface; use Intervention\Image\Interfaces\ResolutionInterface; use Intervention\Image\Interfaces\SizeInterface; +use Intervention\Image\Modifiers\BlendTransparencyModifier; use Intervention\Image\Modifiers\BlurModifier; use Intervention\Image\Modifiers\BrightnessModifier; use Intervention\Image\Modifiers\ColorizeModifier; @@ -93,6 +95,14 @@ final class Image implements ImageInterface */ protected Origin $origin; + /** + * Color is mixed with transparent areas when converting to a format which + * does not support transparency. + * + * @var ColorInterface + */ + protected ColorInterface $blendingColor; + /** * Create new instance * @@ -107,6 +117,9 @@ final class Image implements ImageInterface protected CollectionInterface $exif = new Collection() ) { $this->origin = new Origin(); + $this->blendingColor = $this->colorspace()->importColor( + new Color(255, 255, 255, 0) + ); } /** @@ -369,6 +382,38 @@ final class Image implements ImageInterface return $this->analyze(new PixelColorsAnalyzer($x, $y)); } + /** + * {@inheritdoc} + * + * @see ImageInterface::blendingColor() + */ + public function blendingColor(): ColorInterface + { + return $this->blendingColor; + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::setBlendingColor() + */ + public function setBlendingColor(mixed $color): ImageInterface + { + $this->blendingColor = $this->driver()->handleInput($color); + + return $this; + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::blendTransparency() + */ + public function blendTransparency(mixed $color = null): ImageInterface + { + return $this->modify(new BlendTransparencyModifier($color)); + } + /** * {@inheritdoc} * @@ -671,9 +716,10 @@ final class Image implements ImageInterface int $height, int $offset_x = 0, int $offset_y = 0, + mixed $background = 'ffffff', string $position = 'top-left' ): ImageInterface { - return $this->modify(new CropModifier($width, $height, $offset_x, $offset_y, $position)); + return $this->modify(new CropModifier($width, $height, $offset_x, $offset_y, $background, $position)); } /** diff --git a/src/Interfaces/ImageInterface.php b/src/Interfaces/ImageInterface.php index 0206acca..88986ff1 100644 --- a/src/Interfaces/ImageInterface.php +++ b/src/Interfaces/ImageInterface.php @@ -185,6 +185,31 @@ interface ImageInterface extends IteratorAggregate, Countable */ public function pickColors(int $x, int $y): CollectionInterface; + /** + * Return color that is mixed with transparent areas when converting to a format which + * does not support transparency. + * + * @return ColorInterface + */ + public function blendingColor(): ColorInterface; + + /** + * Set blending color will have no effect unless image is converted into a format + * which does not support transparency. + * + * @param mixed $color + * @return ImageInterface + */ + public function setBlendingColor(mixed $color): ImageInterface; + + /** + * Replace transparent areas of the image with given color + * + * @param mixed $color + * @return ImageInterface + */ + public function blendTransparency(mixed $color = null): ImageInterface; + /** * Retrieve ICC color profile of image * @@ -466,6 +491,7 @@ interface ImageInterface extends IteratorAggregate, Countable * @param int $height * @param int $offset_x * @param int $offset_y + * @param mixed $background * @param string $position * @return ImageInterface */ @@ -474,6 +500,7 @@ interface ImageInterface extends IteratorAggregate, Countable int $height, int $offset_x = 0, int $offset_y = 0, + mixed $background = 'ffffff', string $position = 'top-left' ): ImageInterface; diff --git a/src/Modifiers/BlendTransparencyModifier.php b/src/Modifiers/BlendTransparencyModifier.php new file mode 100644 index 00000000..5f7a782e --- /dev/null +++ b/src/Modifiers/BlendTransparencyModifier.php @@ -0,0 +1,10 @@ +getTestImagePath('gradient.gif')); + $clone = Cloner::clone($gd); + + $this->assertEquals(16, imagesx($gd)); + $this->assertEquals(16, imagesy($gd)); + $this->assertEquals(16, imagesx($clone)); + $this->assertEquals(16, imagesy($clone)); + + $this->assertEquals( + imagecolorsforindex($gd, imagecolorat($gd, 10, 10)), + imagecolorsforindex($clone, imagecolorat($clone, 10, 10)) + ); + } + + public function testCloneEmpty(): void + { + $gd = imagecreatefromgif($this->getTestImagePath('gradient.gif')); + $clone = Cloner::cloneEmpty($gd, new Rectangle(12, 12), new Color(255, 0, 0, 0)); + + $this->assertEquals(16, imagesx($gd)); + $this->assertEquals(16, imagesy($gd)); + $this->assertEquals(12, imagesx($clone)); + $this->assertEquals(12, imagesy($clone)); + + $this->assertEquals( + ['red' => 0, 'green' => 255, 'blue' => 2, 'alpha' => 0], + imagecolorsforindex($gd, imagecolorat($gd, 10, 10)), + ); + + $this->assertEquals( + ['red' => 255, 'green' => 0, 'blue' => 0, 'alpha' => 127], + imagecolorsforindex($clone, imagecolorat($clone, 10, 10)) + ); + } + + public function testCLoneBlended(): void + { + $gd = imagecreatefromgif($this->getTestImagePath('gradient.gif')); + $clone = Cloner::cloneBlended($gd, new Color(255, 0, 255, 255)); + + $this->assertEquals(16, imagesx($gd)); + $this->assertEquals(16, imagesy($gd)); + $this->assertEquals(16, imagesx($clone)); + $this->assertEquals(16, imagesy($clone)); + + $this->assertEquals( + ['red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 127], + imagecolorsforindex($gd, imagecolorat($gd, 1, 0)), + ); + + $this->assertEquals( + ['red' => 255, 'green' => 0, 'blue' => 255, 'alpha' => 0], + imagecolorsforindex($clone, imagecolorat($clone, 1, 0)) + ); + } +} diff --git a/tests/Drivers/Gd/ImageTest.php b/tests/Drivers/Gd/ImageTest.php index 3f319472..a3c1385f 100644 --- a/tests/Drivers/Gd/ImageTest.php +++ b/tests/Drivers/Gd/ImageTest.php @@ -4,6 +4,7 @@ namespace Intervention\Image\Tests\Drivers\Gd; use Intervention\Image\Analyzers\WidthAnalyzer; use Intervention\Image\Collection; +use Intervention\Image\Colors\Rgb\Color; use Intervention\Image\Drivers\Gd\Core; use Intervention\Image\Drivers\Gd\Driver; use Intervention\Image\Drivers\Gd\Frame; @@ -53,10 +54,10 @@ class ImageTest extends TestCase $this->assertEquals(4, $result->width()); $this->assertEquals('ff0000', $image->pickColor(0, 0)->toHex()); - $this->assertEquals('00000000', $image->pickColor(1, 0)->toHex()); + $this->assertTransparency($image->pickColor(1, 0)); $this->assertEquals('ff0000', $clone->pickColor(0, 0)->toHex()); - $this->assertEquals('00000000', $clone->pickColor(1, 0)->toHex()); + $this->assertTransparency($image->pickColor(1, 0)); } public function testDriver(): void @@ -210,4 +211,14 @@ class ImageTest extends TestCase { $this->assertInstanceOf(Image::class, $this->image->text('test', 0, 0, new Font())); } + + public function testSetGetBlendingColor(): void + { + $image = $this->readTestImage('gradient.gif'); + $this->assertInstanceOf(ColorInterface::class, $image->blendingColor()); + $this->assertColor(255, 255, 255, 0, $image->blendingColor()); + $result = $image->setBlendingColor(new Color(1, 2, 3, 4)); + $this->assertColor(1, 2, 3, 4, $result->blendingColor()); + $this->assertColor(1, 2, 3, 4, $image->blendingColor()); + } } diff --git a/tests/Drivers/Gd/InputHandlerTest.php b/tests/Drivers/Gd/InputHandlerTest.php index 1857b20e..4e8ca18c 100644 --- a/tests/Drivers/Gd/InputHandlerTest.php +++ b/tests/Drivers/Gd/InputHandlerTest.php @@ -138,6 +138,6 @@ class GdInputHandlerTest extends TestCase $input = 'transparent'; $result = $handler->handle($input); $this->assertInstanceOf(RgbColor::class, $result); - $this->assertEquals([255, 0, 255, 0], $result->toArray()); + $this->assertEquals([255, 255, 255, 0], $result->toArray()); } } diff --git a/tests/Drivers/Gd/Modifiers/ContainModifierTest.php b/tests/Drivers/Gd/Modifiers/ContainModifierTest.php index e648b5ed..4b999296 100644 --- a/tests/Drivers/Gd/Modifiers/ContainModifierTest.php +++ b/tests/Drivers/Gd/Modifiers/ContainModifierTest.php @@ -23,7 +23,7 @@ class ContainModifierTest extends TestCase $this->assertEquals(200, $image->width()); $this->assertEquals(100, $image->height()); $this->assertColor(255, 255, 0, 255, $image->pickColor(0, 0)); - $this->assertColor(255, 255, 0, 0, $image->pickColor(140, 10)); // transparent + $this->assertTransparency($image->pickColor(140, 10)); $this->assertColor(255, 255, 0, 255, $image->pickColor(175, 10)); } } diff --git a/tests/Drivers/Gd/Modifiers/CropModifierTest.php b/tests/Drivers/Gd/Modifiers/CropModifierTest.php index a51e7d6f..6fd5d1b9 100644 --- a/tests/Drivers/Gd/Modifiers/CropModifierTest.php +++ b/tests/Drivers/Gd/Modifiers/CropModifierTest.php @@ -17,11 +17,23 @@ class CropModifierTest extends TestCase public function testModify(): void { $image = $this->readTestImage('blocks.png'); - $image = $image->modify(new CropModifier(200, 200, 0, 0, 'bottom-right')); + $image = $image->modify(new CropModifier(200, 200, 0, 0, 'ffffff', 'bottom-right')); $this->assertEquals(200, $image->width()); $this->assertEquals(200, $image->height()); $this->assertColor(255, 0, 0, 255, $image->pickColor(5, 5)); $this->assertColor(255, 0, 0, 255, $image->pickColor(100, 100)); $this->assertColor(255, 0, 0, 255, $image->pickColor(190, 190)); } + + public function testModifyExtend(): void + { + $image = $this->readTestImage('blocks.png'); + $image = $image->modify(new CropModifier(800, 100, -10, -10, 'ff0000', 'top-left')); + $this->assertEquals(800, $image->width()); + $this->assertEquals(100, $image->height()); + $this->assertColor(255, 0, 0, 255, $image->pickColor(9, 9)); + $this->assertColor(0, 0, 255, 255, $image->pickColor(16, 16)); + $this->assertColor(0, 0, 255, 255, $image->pickColor(445, 16)); + $this->assertTransparency($image->pickColor(460, 16)); + } } diff --git a/tests/Drivers/Gd/Modifiers/FlipFlopModifierTest.php b/tests/Drivers/Gd/Modifiers/FlipFlopModifierTest.php index 93279eb5..8d0fb5cf 100644 --- a/tests/Drivers/Gd/Modifiers/FlipFlopModifierTest.php +++ b/tests/Drivers/Gd/Modifiers/FlipFlopModifierTest.php @@ -21,7 +21,7 @@ class FlipFlopModifierTest extends TestCase $image = $this->readTestImage('tile.png'); $this->assertEquals('b4e000', $image->pickColor(0, 0)->toHex()); $image->modify(new FlipModifier()); - $this->assertEquals('00000000', $image->pickColor(0, 0)->toHex()); + $this->assertTransparency($image->pickColor(0, 0)); } public function testFlopImage(): void @@ -29,6 +29,6 @@ class FlipFlopModifierTest extends TestCase $image = $this->readTestImage('tile.png'); $this->assertEquals('b4e000', $image->pickColor(0, 0)->toHex()); $image->modify(new FlopModifier()); - $this->assertEquals('00000000', $image->pickColor(0, 0)->toHex()); + $this->assertTransparency($image->pickColor(0, 0)); } } diff --git a/tests/Drivers/Imagick/ImageTest.php b/tests/Drivers/Imagick/ImageTest.php index 69257b07..5ecf956e 100644 --- a/tests/Drivers/Imagick/ImageTest.php +++ b/tests/Drivers/Imagick/ImageTest.php @@ -5,6 +5,7 @@ namespace Intervention\Image\Tests\Drivers\Imagick; use Imagick; use Intervention\Image\Analyzers\WidthAnalyzer; use Intervention\Image\Collection; +use Intervention\Image\Colors\Rgb\Color; use Intervention\Image\Drivers\Imagick\Core; use Intervention\Image\Drivers\Imagick\Driver; use Intervention\Image\Drivers\Imagick\Frame; @@ -52,10 +53,10 @@ class ImageTest extends TestCase $this->assertEquals(4, $result->width()); $this->assertEquals('ff0000', $image->pickColor(0, 0)->toHex()); - $this->assertEquals('00000000', $image->pickColor(1, 0)->toHex()); + $this->assertTransparency($image->pickColor(1, 0)); $this->assertEquals('ff0000', $clone->pickColor(0, 0)->toHex()); - $this->assertEquals('00000000', $clone->pickColor(1, 0)->toHex()); + $this->assertTransparency($clone->pickColor(1, 0)); } public function testDriver(): void @@ -204,4 +205,14 @@ class ImageTest extends TestCase { $this->assertInstanceOf(Image::class, $this->image->sharpen(12)); } + + public function testSetGetBlendingColor(): void + { + $image = $this->readTestImage('gradient.gif'); + $this->assertInstanceOf(ColorInterface::class, $image->blendingColor()); + $this->assertColor(255, 255, 255, 0, $image->blendingColor()); + $result = $image->setBlendingColor(new Color(1, 2, 3, 4)); + $this->assertColor(1, 2, 3, 4, $result->blendingColor()); + $this->assertColor(1, 2, 3, 4, $image->blendingColor()); + } } diff --git a/tests/Drivers/Imagick/InputHandlerTest.php b/tests/Drivers/Imagick/InputHandlerTest.php index 1175858a..ce3b3ed4 100644 --- a/tests/Drivers/Imagick/InputHandlerTest.php +++ b/tests/Drivers/Imagick/InputHandlerTest.php @@ -138,6 +138,6 @@ class InputHandlerTest extends TestCase $input = 'transparent'; $result = $handler->handle($input); $this->assertInstanceOf(RgbColor::class, $result); - $this->assertEquals([255, 0, 255, 0], $result->toArray()); + $this->assertEquals([255, 255, 255, 0], $result->toArray()); } } diff --git a/tests/Drivers/Imagick/Modifiers/CropModifierTest.php b/tests/Drivers/Imagick/Modifiers/CropModifierTest.php index 6889c247..57c735d0 100644 --- a/tests/Drivers/Imagick/Modifiers/CropModifierTest.php +++ b/tests/Drivers/Imagick/Modifiers/CropModifierTest.php @@ -17,11 +17,23 @@ class CropModifierTest extends TestCase public function testModify(): void { $image = $this->readTestImage('blocks.png'); - $image = $image->modify(new CropModifier(200, 200, 0, 0, 'bottom-right')); + $image = $image->modify(new CropModifier(200, 200, 0, 0, 'ffffff', 'bottom-right')); $this->assertEquals(200, $image->width()); $this->assertEquals(200, $image->height()); $this->assertColor(255, 0, 0, 255, $image->pickColor(5, 5)); $this->assertColor(255, 0, 0, 255, $image->pickColor(100, 100)); $this->assertColor(255, 0, 0, 255, $image->pickColor(190, 190)); } + + public function testModifyExtend(): void + { + $image = $this->readTestImage('blocks.png'); + $image = $image->modify(new CropModifier(800, 100, -10, -10, 'ff0000', 'top-left')); + $this->assertEquals(800, $image->width()); + $this->assertEquals(100, $image->height()); + $this->assertColor(255, 0, 0, 255, $image->pickColor(9, 9)); + $this->assertColor(0, 0, 255, 255, $image->pickColor(16, 16)); + $this->assertColor(0, 0, 255, 255, $image->pickColor(445, 16)); + $this->assertTransparency($image->pickColor(460, 16)); + } }