1
0
mirror of https://github.com/Intervention/image.git synced 2025-08-31 01:29:51 +02:00

Merge branch 'feature/blending-color' into next

This commit is contained in:
Oliver Vogel
2024-01-15 09:36:50 +01:00
33 changed files with 580 additions and 199 deletions

View File

@@ -18,6 +18,6 @@ class TransparentColorDecoder extends HexColorDecoder
throw new DecoderException('Unable to decode input');
}
return parent::decode('#ff00ff00');
return parent::decode('#ffffff00');
}
}

89
src/Drivers/Gd/Cloner.php Normal file
View File

@@ -0,0 +1,89 @@
<?php
namespace Intervention\Image\Drivers\Gd;
use GdImage;
use Intervention\Image\Colors\Rgb\Color;
use Intervention\Image\Geometry\Rectangle;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\SizeInterface;
class Cloner
{
/**
* Create a clone of the given GdImage
*
* @param GdImage $gd
* @return GdImage
*/
public static function clone(GdImage $gd): GdImage
{
// create empty canvas with same size
$clone = static::cloneEmpty($gd);
// transfer actual image to clone
imagecopy($clone, $gd, 0, 0, 0, 0, imagesx($gd), imagesy($gd));
return $clone;
}
/**
* Create an "empty" clone of the given GdImage
*
* This only retains the basic data without transferring the actual image.
* It is optionally possible to change the size of the result and set a
* background color.
*
* @param GdImage $gd
* @param null|SizeInterface $size
* @param ColorInterface $background
* @return GdImage
*/
public static function cloneEmpty(
GdImage $gd,
?SizeInterface $size = null,
ColorInterface $background = new Color(255, 255, 255, 0)
): GdImage {
// define size
$size = match (true) {
is_null($size) => 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;
}
}

View File

@@ -41,7 +41,6 @@ class BinaryImageDecoder extends AbstractDecoder implements DecoderInterface
if (!imageistruecolor($gd)) {
imagepalettetotruecolor($gd);
}
imagesavealpha($gd, true);
// build image instance

View File

@@ -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);

View File

@@ -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');

View File

@@ -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);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Drivers\DriverSpecialized;
use Intervention\Image\Drivers\Gd\Cloner;
use Intervention\Image\Interfaces\ImageInterface;
/**
* @property mixed $color
*/
class BlendTransparencyModifier extends DriverSpecialized
{
public function apply(ImageInterface $image): ImageInterface
{
// decode blending color
$color = $this->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;
}
}

View File

@@ -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

View File

@@ -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(),

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -1,18 +0,0 @@
<?php
namespace Intervention\Image\Drivers\Gd;
use GdImage;
use Intervention\Image\Drivers\DriverSpecialized;
use Intervention\Image\Interfaces\ModifierInterface;
abstract class SpecializedModifier extends DriverSpecialized implements ModifierInterface
{
protected function copyResolution(GdImage $source, GdImage $target): void
{
$resolution = imageresolution($source);
if (is_array($resolution) && array_key_exists(0, $resolution) && array_key_exists(1, $resolution)) {
imageresolution($target, $resolution[0], $resolution[1]);
}
}
}

View File

@@ -46,7 +46,7 @@ class Driver extends AbstractDriver
*/
public function createImage(int $width, int $height): ImageInterface
{
$background = new ImagickPixel('rgba(0, 0, 0, 0)');
$background = new ImagickPixel('rgba(255, 255, 255, 0)');
$imagick = new Imagick();
$imagick->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));
}

View File

@@ -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');
}

View File

@@ -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);
}
/**

View File

@@ -0,0 +1,35 @@
<?php
namespace Intervention\Image\Drivers\Imagick\Modifiers;
use Imagick;
use Intervention\Image\Drivers\DriverSpecialized;
use Intervention\Image\Interfaces\ImageInterface;
/**
* @property mixed $color
*/
class BlendTransparencyModifier extends DriverSpecialized
{
public function apply(ImageInterface $image): ImageInterface
{
// decode blending color
$color = $this->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;
}
}

View File

@@ -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;

View File

@@ -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));
}
/**

View File

@@ -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;

View File

@@ -0,0 +1,10 @@
<?php
namespace Intervention\Image\Modifiers;
class BlendTransparencyModifier extends AbstractModifier
{
public function __construct(public mixed $color = null)
{
}
}

View File

@@ -13,6 +13,7 @@ class CropModifier extends AbstractModifier
public int $height,
public int $offset_x = 0,
public int $offset_y = 0,
public mixed $background = 'f00',
public string $position = 'top-left'
) {
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Intervention\Image\Tests\Drivers\Gd;
use Intervention\Image\Colors\Rgb\Color;
use Intervention\Image\Drivers\Gd\Cloner;
use Intervention\Image\Geometry\Rectangle;
use Intervention\Image\Tests\TestCase;
class ClonerTest extends TestCase
{
public function testClone(): void
{
$gd = imagecreatefromgif($this->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))
);
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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));
}
}