From 8f23fe131032c48dad80040805e82e09a51f7906 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Tue, 5 Sep 2023 13:28:15 +0200 Subject: [PATCH] add fill --- composer.json | 3 +- src/Actions/CalculateFitSizeAction.php | 1 + src/Drivers/BaseColor.php | 106 ++++++++++ src/Drivers/ColorFormat.php | 12 ++ src/Drivers/GdColor.php | 147 ++++++++++++++ src/Drivers/GdImageDriver.php | 109 ++++++++++ src/Drivers/ImageDriver.php | 13 ++ src/Drivers/ImagickColor.php | 172 ++++++++++++++++ src/Drivers/ImagickImageDriver.php | 123 +++++++++++- src/Enums/AlignPosition.php | 40 ++++ src/Exceptions/InvalidColor.php | 18 ++ src/Point.php | 19 ++ src/Size.php | 112 ++++++++++- tests/ArchTest.php | 4 +- tests/GdColorTest.php | 263 +++++++++++++++++++++++++ tests/ImagickColorTest.php | 194 ++++++++++++++++++ tests/Manipulations/BlurTest.php | 5 +- tests/Manipulations/BrightnessTest.php | 5 +- tests/Manipulations/FitTest.php | 45 ++++- tests/Pest.php | 2 +- 20 files changed, 1363 insertions(+), 30 deletions(-) create mode 100644 src/Drivers/BaseColor.php create mode 100644 src/Drivers/ColorFormat.php create mode 100644 src/Drivers/GdColor.php create mode 100644 src/Drivers/ImagickColor.php create mode 100644 src/Enums/AlignPosition.php create mode 100644 src/Exceptions/InvalidColor.php create mode 100644 src/Point.php create mode 100644 tests/GdColorTest.php create mode 100644 tests/ImagickColorTest.php diff --git a/composer.json b/composer.json index 7ae3a8ba..0b3fc80f 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,8 @@ "ext-gd": "*" }, "require-dev": { - "pestphp/pest": "^1.0", + "pestphp/pest": "^2.0", + "pestphp/pest-plugin-drift": "^2.4", "spatie/ray": "^1.37", "symfony/var-dumper": "^4.0|^5.0|^6.0" }, diff --git a/src/Actions/CalculateFitSizeAction.php b/src/Actions/CalculateFitSizeAction.php index 357acd9a..aced5d74 100644 --- a/src/Actions/CalculateFitSizeAction.php +++ b/src/Actions/CalculateFitSizeAction.php @@ -22,6 +22,7 @@ public function execute( return match ($fit) { Fit::Contain => $size->resize($desiredWidth, $desiredHeight, [Constraint::PreserveAspectRatio]), + Fit::Fill => $size->resize($desiredWidth, $desiredHeight, [Constraint::PreserveAspectRatio, Constraint::Upsize]), }; } } diff --git a/src/Drivers/BaseColor.php b/src/Drivers/BaseColor.php new file mode 100644 index 00000000..5b8a649b --- /dev/null +++ b/src/Drivers/BaseColor.php @@ -0,0 +1,106 @@ +parse($value); + } + + public function parse(mixed $colorValue): self + { + match (true) { + is_string($colorValue) => $this->initFromString($colorValue), + is_int($colorValue) => $this->initFromInteger($colorValue), + is_array($colorValue) => $this->initFromArray($colorValue), + is_object($colorValue) => $this->initFromObject($colorValue), + is_null($colorValue) => $this->initFromArray([255, 255, 255, 0]), + default => throw InvalidColor::make($colorValue), + }; + + return $this; + } + + /** + * Formats current color instance into given format + * + * @param string $type + * + * @return mixed + */ + public function format(ColorFormat $colorFormat): mixed + { + return match ($colorFormat) { + ColorFormat::RGBA => $this->getRgba(), + ColorFormat::HEX => $this->getHex('#'), + ColorFormat::INT => $this->getInt(), + ColorFormat::ARRAY => $this->getArray(), + ColorFormat::OBJECT => $this, + }; + } + + protected function rgbaFromString(string $colorValue): array + { + $result = false; + + // parse color string in hexidecimal format like #cccccc or cccccc or ccc + $hexPattern = '/^#?([a-f0-9]{1,2})([a-f0-9]{1,2})([a-f0-9]{1,2})$/i'; + + // parse color string in format rgb(140, 140, 140) + $rgbPattern = '/^rgb ?\(([0-9]{1,3}), ?([0-9]{1,3}), ?([0-9]{1,3})\)$/i'; + + // parse color string in format rgba(255, 0, 0, 0.5) + $rgbaPattern = '/^rgba ?\(([0-9]{1,3}), ?([0-9]{1,3}), ?([0-9]{1,3}), ?([0-9.]{1,4})\)$/i'; + + if (preg_match($hexPattern, $colorValue, $matches)) { + $result = []; + $result[0] = strlen($matches[1]) == '1' ? hexdec($matches[1] . $matches[1]) : hexdec($matches[1]); + $result[1] = strlen($matches[2]) == '1' ? hexdec($matches[2] . $matches[2]) : hexdec($matches[2]); + $result[2] = strlen($matches[3]) == '1' ? hexdec($matches[3] . $matches[3]) : hexdec($matches[3]); + $result[3] = 1; + } elseif (preg_match($rgbPattern, $colorValue, $matches)) { + $result = []; + $result[0] = ($matches[1] >= 0 && $matches[1] <= 255) ? intval($matches[1]) : 0; + $result[1] = ($matches[2] >= 0 && $matches[2] <= 255) ? intval($matches[2]) : 0; + $result[2] = ($matches[3] >= 0 && $matches[3] <= 255) ? intval($matches[3]) : 0; + $result[3] = 1; + } elseif (preg_match($rgbaPattern, $colorValue, $matches)) { + $result = []; + $result[0] = ($matches[1] >= 0 && $matches[1] <= 255) ? intval($matches[1]) : 0; + $result[1] = ($matches[2] >= 0 && $matches[2] <= 255) ? intval($matches[2]) : 0; + $result[2] = ($matches[3] >= 0 && $matches[3] <= 255) ? intval($matches[3]) : 0; + $result[3] = ($matches[4] >= 0 && $matches[4] <= 1) ? $matches[4] : 0; + } else { + throw InvalidColor::make($colorValue); + } + + return $result; + } +} diff --git a/src/Drivers/ColorFormat.php b/src/Drivers/ColorFormat.php new file mode 100644 index 00000000..f2621643 --- /dev/null +++ b/src/Drivers/ColorFormat.php @@ -0,0 +1,12 @@ +alpha = ($value >> 24) & 0xFF; + $this->red = ($value >> 16) & 0xFF; + $this->green = ($value >> 8) & 0xFF; + $this->blue = $value & 0xFF; + + return $this; + } + + public function initFromArray(array $value): self + { + $value = array_values($value); + + if (count($value) == 4) { + + [$red, $green, $blue, $alpha] = $value; + $this->alpha = $this->alpha2gd($alpha); + + } elseif (count($value) == 3) { + + [$red, $green, $blue] = $value; + $this->alpha = 0; + + } + + $this->red = $red; + $this->green = $green; + $this->blue = $blue; + + return $this; + } + + public function initFromString(string $value): self + { + if ($color = $this->rgbaFromString($value)) { + $this->red = $color[0]; + $this->green = $color[1]; + $this->blue = $color[2]; + $this->alpha = $this->alpha2gd($color[3]); + } + + return $this; + } + + public function initFromRgb(int $red, int $green, int $blue): self + { + $this->red = $red; + $this->green = $green; + $this->blue = $blue; + $this->alpha = 0; + + return $this; + } + + public function initFromRgba(int $red, int $green, int $blue, float $alpha = 1): self + { + $this->red = $red; + $this->green = $green; + $this->blue = $blue; + $this->alpha = $this->alpha2gd($alpha); + + return $this; + } + + public function initFromObject(ImagickPixel $value): never + { + throw InvalidColor::cannotConvertImagickColorToGd(); + } + + public function getInt(): int + { + return ($this->alpha << 24) + ($this->red << 16) + ($this->green << 8) + $this->blue; + } + + public function getHex(string $prefix = ''): string + { + return sprintf('%s%02x%02x%02x', $prefix, $this->red, $this->green, $this->blue); + } + + + public function getArray(): array + { + return [ + $this->red, + $this->green, + $this->blue, + round(1 - $this->alpha / 127, 2) + ]; + } + + public function getRgba(): string + { + return sprintf('rgba(%d, %d, %d, %.2F)', + $this->red, + $this->green, + $this->blue, + round(1 - $this->alpha / 127, 2) + ); + } + + public function differs(BaseColor $color, int $tolerance = 0): bool + { + $color_tolerance = round($tolerance * 2.55); + $alpha_tolerance = round($tolerance * 1.27); + + $delta = [ + 'r' => abs($color->red - $this->red), + 'g' => abs($color->green - $this->green), + 'b' => abs($color->blue - $this->blue), + 'a' => abs($color->alpha - $this->alpha) + ]; + + return ( + $delta['r'] > $color_tolerance || + $delta['g'] > $color_tolerance || + $delta['b'] > $color_tolerance || + $delta['a'] > $alpha_tolerance + ); + } + + private function alpha2gd(float $input): int + { + $oldMin = 0; + $oldMax = 1; + + $newMin = 127; + $newMax = 0; + + return ceil(((($input- $oldMin) * ($newMax - $newMin)) / ($oldMax - $oldMin)) + $newMin); + } +} diff --git a/src/Drivers/GdImageDriver.php b/src/Drivers/GdImageDriver.php index b9d71833..369687b1 100644 --- a/src/Drivers/GdImageDriver.php +++ b/src/Drivers/GdImageDriver.php @@ -3,8 +3,11 @@ namespace Spatie\Image\Drivers; use GdImage; +use Intervention\Image\Gd\Color; +use Intervention\Image\Image; use Spatie\Image\Actions\CalculateFitSizeAction; use Spatie\Image\Drivers\Concerns\ValidatesArguments; +use Spatie\Image\Enums\AlignPosition; use Spatie\Image\Enums\Fit; use Spatie\Image\Exceptions\CouldNotLoadImage; use Spatie\Image\Size; @@ -15,6 +18,24 @@ class GdImageDriver implements ImageDriver private GdImage $image; + public function new(int $width, int $height, string $backgroundColor = null): self + { + $image = imagecreatetruecolor($width, $height); + + $backgroundColor = new GdColor($backgroundColor); + + imagefill($image, 0, 0, $backgroundColor->getInt()); + + return (new self)->setImage($image); + } + + protected function setImage(GdImage $image): self + { + $this->image = $image; + + return $this; + } + public function load(string $path): ImageDriver { $handle = fopen($path, 'r'); @@ -97,6 +118,11 @@ public function fit(Fit $fit, int $desiredWidth = null, int $desiredHeight = nul $this->modify($this->getWidth(), $this->getHeight(), $resize->width, $resize->height); + + if ($fit === Fit::Fill) { + $this->resizeCanvas($desiredWidth, $desiredHeight, AlignPosition::Center); + } + return $this; } @@ -136,4 +162,87 @@ protected function modify($originalWidth, $originalHeight, $desiredWidth, $desir return $result; } + + public function pickColor(int $x, int $y, ColorFormat $colorFormat): mixed + { + $color = imagecolorat($this->image, $x, $y); + + if ( ! imageistruecolor($this->image)) { + $color = imagecolorsforindex($this->image, $color); + $color['alpha'] = round(1 - $color['alpha'] / 127, 2); + } + + $color = new GdColor($color); + + return $color->format($colorFormat); + } + + public function resizeCanvas( + int $width = null, + int $height = null, + AlignPosition $position = null, + bool $relative = false, + string $backgroundColor = '#ffffff' + ): ImageDriver + { + $position ??= AlignPosition::Center; + + $originalWidth = $this->getWidth(); + $originalHeight = $this->getHeight(); + + $width ??= $originalWidth; + $height ??= $originalHeight; + + if ($relative) { + $width = $originalWidth + $width; + $height = $originalHeight + $height; + } + + // check for negative width/height + $width = ($width <= 0) ? $width + $originalWidth : $width; + $height = ($height <= 0) ? $height + $originalHeight : $height; + + // create new canvas + $canvas = $this->new($width, $height, $backgroundColor); + + // set copy position + $canvasSize = $canvas->getSize()->align($position); + $imageSize = $this->getSize()->align($position); + $canvasPosition = $imageSize->relativePosition($canvasSize); + $imagePosition = $canvasSize->relativePosition($imageSize); + + if ($width <= $originalWidth) { + $destinationX = 0; + $sourceX = $canvasPosition->x; + $sourceWidth = $canvasSize->width; + } else { + $destinationX = $imagePosition->x; + $sourceX = 0; + $sourceWidth = $originalWidth; + } + + if ($height <= $originalHeight) { + $destinationY = 0; + $sourceY = $canvasPosition->y; + $sourceHeight = $canvasSize->height; + } else { + $destinationY = $imagePosition->y; + $sourceY = 0; + $sourceHeight = $originalHeight; + } + + // make image area transparent to keep transparency + // even if background-color is set + $transparent = imagecolorallocatealpha($canvas->image, 255, 255, 255, 127); + imagealphablending($canvas->image, false); // do not blend / just overwrite + imagefilledrectangle($canvas->image, $destinationX, $destinationY, $destinationX + $sourceWidth - 1, $destinationY + $sourceHeight - 1, $transparent); + + // copy image into new canvas + imagecopy($canvas->image, $this->image, $destinationX, $destinationY, $sourceX, $sourceY, $sourceWidth, $sourceHeight); + + // set new core to canvas + $this->image = $canvas->image; + + return $this; + } } diff --git a/src/Drivers/ImageDriver.php b/src/Drivers/ImageDriver.php index e7c59959..3b3de3fb 100644 --- a/src/Drivers/ImageDriver.php +++ b/src/Drivers/ImageDriver.php @@ -2,11 +2,14 @@ namespace Spatie\Image\Drivers; +use Spatie\Image\Enums\AlignPosition; use Spatie\Image\Enums\Fit; use Spatie\Image\Size; interface ImageDriver { + public function new(int $width, int $height, string $backgroundColor = null): self; + public function driverName(): string; public function load(string $path): self; @@ -30,4 +33,14 @@ public function blur(int $blur): self; public function getSize(): Size; public function fit(Fit $fit, int $desiredWidth = null, int $desiredHeight = null): self; + + public function pickColor(int $x, int $y, ColorFormat $colorFormat): mixed; + + public function resizeCanvas( + int $width = null, + int $height = null, + AlignPosition $position = null, + bool $relative = false, + string $backgroundColor = '#000000' + ): self; } diff --git a/src/Drivers/ImagickColor.php b/src/Drivers/ImagickColor.php new file mode 100644 index 00000000..515b61c2 --- /dev/null +++ b/src/Drivers/ImagickColor.php @@ -0,0 +1,172 @@ +> 24) & 0xFF; + $r = ($value >> 16) & 0xFF; + $g = ($value >> 8) & 0xFF; + $b = $value & 0xFF; + $a = $this->rgb2alpha($a); + + $this->setPixel($r, $g, $b, $a); + + return $this; + } + + public function initFromArray(array $value): self + { + $value = array_values($value); + + if (count($value) == 4) { + + [$red, $green, $blue, $alpha] = $value; + + } elseif (count($value) == 3) { + + // color array without alpha value + [$red, $green, $blue] = $value; + $alpha = 1; + } + + $this->setPixel($red, $green, $blue, $alpha); + + return $this; + } + + public function initFromString(string $value): self + { + if ($color = $this->rgbaFromString($value)) { + $this->setPixel($color[0], $color[1], $color[2], $color[3]); + } + + return $this; + } + + public function initFromObject(ImagickPixel $value): self + { + $this->pixel = $value; + + return $this; + } + + public function initFromRgb(int $red, int $green, int $blue): self + { + $this->setPixel($red, $green, $blue); + + return $this; + } + + public function initFromRgba(int $red, int $green, int $blue, float $alpha): self + { + $this->setPixel($red, $green, $blue, $alpha); + + return $this; + } + + public function getInt(): int + { + $r = $this->getRedValue(); + $g = $this->getGreenValue(); + $b = $this->getBlueValue(); + $a = intval(round($this->getAlphaValue() * 255)); + + return ($a << 24) + ($r << 16) + ($g << 8) + $b; + } + + public function getHex(string $prefix = ''): string + { + return sprintf('%s%02x%02x%02x', $prefix, + $this->getRedValue(), + $this->getGreenValue(), + $this->getBlueValue() + ); + } + + public function getArray(): array + { + return [ + $this->getRedValue(), + $this->getGreenValue(), + $this->getBlueValue(), + $this->getAlphaValue() + ]; + } + + public function getRgba(): string + { + return sprintf('rgba(%d, %d, %d, %.2F)', + $this->getRedValue(), + $this->getGreenValue(), + $this->getBlueValue(), + $this->getAlphaValue() + ); + } + + public function differs(BaseColor $color, int $tolerance = 0): bool + { + $color_tolerance = round($tolerance * 2.55); + $alpha_tolerance = round($tolerance); + + $delta = [ + 'r' => abs($color->getRedValue() - $this->getRedValue()), + 'g' => abs($color->getGreenValue() - $this->getGreenValue()), + 'b' => abs($color->getBlueValue() - $this->getBlueValue()), + 'a' => abs($color->getAlphaValue() - $this->getAlphaValue()) + ]; + + return ( + $delta['r'] > $color_tolerance || + $delta['g'] > $color_tolerance || + $delta['b'] > $color_tolerance || + $delta['a'] > $alpha_tolerance + ); + } + + public function getRedValue(): int + { + return round($this->pixel->getColorValue(Imagick::COLOR_RED) * 255); + } + + public function getGreenValue(): int + { + return round($this->pixel->getColorValue(Imagick::COLOR_GREEN) * 255); + } + + public function getBlueValue(): int + { + return round($this->pixel->getColorValue(Imagick::COLOR_BLUE) * 255); + } + + public function getAlphaValue(): float + { + return round($this->pixel->getColorValue(Imagick::COLOR_ALPHA), 2); + } + + private function setPixel($r, $g, $b, $a = null): ImagickPixel + { + $a = is_null($a) ? 1 : $a; + + return $this->pixel = new \ImagickPixel( + sprintf('rgba(%d, %d, %d, %.2F)', $r, $g, $b, $a) + ); + } + + public function getPixel(): ImagickPixel + { + return $this->pixel; + } + + private function rgb2alpha(int $value): float + { + return round($value/255, 2); + } +} diff --git a/src/Drivers/ImagickImageDriver.php b/src/Drivers/ImagickImageDriver.php index e5628afd..8e1a0784 100644 --- a/src/Drivers/ImagickImageDriver.php +++ b/src/Drivers/ImagickImageDriver.php @@ -3,8 +3,11 @@ namespace Spatie\Image\Drivers; use Imagick; +use ImagickDraw; +use Intervention\Image\Imagick\Color; use Spatie\Image\Actions\CalculateFitSizeAction; use Spatie\Image\Drivers\Concerns\ValidatesArguments; +use Spatie\Image\Enums\AlignPosition; use Spatie\Image\Enums\Fit; use Spatie\Image\Size; @@ -14,7 +17,29 @@ class ImagickImageDriver implements ImageDriver protected Imagick $image; - public function load(string $path): ImageDriver + public function new(int $width, int $height, string $backgroundColor = null): self + { + $backgroundColor = new ImagickColor($backgroundColor); + + $image = new Imagick(); + + $image->newImage($width, $height, $backgroundColor->getPixel(), 'png'); + $image->setType(Imagick::IMGTYPE_UNDEFINED); + $image->setImageType(Imagick::IMGTYPE_UNDEFINED); + $image->setColorspace(Imagick::COLORSPACE_UNDEFINED); + + return (new self())->setImage($image); + } + + protected function setImage(Imagick $image): self + { + $this->image = $image; + + return $this; + } + + + public function load(string $path): self { $this->image = new Imagick($path); @@ -31,7 +56,7 @@ public function getHeight(): int return $this->image->getImageHeight(); } - public function brightness(int $brightness): ImageDriver + public function brightness(int $brightness): self { $this->ensureNumberBetween($brightness, -100, 100, 'brightness'); @@ -40,7 +65,7 @@ public function brightness(int $brightness): ImageDriver return $this; } - public function blur(int $blur): ImageDriver + public function blur(int $blur): self { $this->ensureNumberBetween($blur, 0, 100, 'blur'); @@ -49,7 +74,7 @@ public function blur(int $blur): ImageDriver return $this; } - public function fit(Fit $fit, int $desiredWidth = null, int $desiredHeight = null): ImageDriver + public function fit(Fit $fit, int $desiredWidth = null, int $desiredHeight = null): self { $resize = (new CalculateFitSizeAction())->execute( $this->getWidth(), @@ -61,9 +86,99 @@ public function fit(Fit $fit, int $desiredWidth = null, int $desiredHeight = nul $this->image->scaleImage($resize->width, $resize->height); + if ($fit === Fit::Fill) { + $this->resizeCanvas($desiredWidth, $desiredHeight, AlignPosition::Center); + } + return $this; } + public function resizeCanvas( + int $width = null, + int $height = null, + AlignPosition $position = null, + bool $relative = false, + string $backgroundColor = '#ffffff' + ): self + { + $position ??= AlignPosition::Center; + + $originalWidth = $this->getWidth(); + $originalHeight = $this->getHeight(); + + $width ??= $originalWidth; + $height ??= $originalHeight; + + if ($relative) { + $width = $originalWidth + $width; + $height = $originalHeight + $height; + } + + $width = $width <= 0 + ? $width + $originalWidth + : $width; + + $height = $height <= 0 + ? $height + $originalHeight + : $height; + + $canvas = $this->new($width, $height, $backgroundColor); + + $canvasSize = $canvas->getSize()->align($position); + $imageSize = $this->getSize()->align($position); + $canvasPosition = $imageSize->relativePosition($canvasSize); + $imagePosition = $canvasSize->relativePosition($imageSize); + + if ($width <= $originalWidth) { + $destinationX = 0; + $sourceX = $canvasPosition->x; + $sourceWidth = $canvasSize->width; + } else { + $destinationX = $imagePosition->x; + $sourceX = 0; + $sourceWidth = $originalWidth; + } + + if ($height <= $originalHeight) { + $destinationY = 0; + $sourceY = $canvasPosition->y; + $sourceHeight = $canvasSize->height; + } else { + $destinationY = $imagePosition->y; + $sourceY = 0; + $sourceHeight = $originalHeight; + } + + // make image area transparent to keep transparency + // even if background-color is set + $rect = new ImagickDraw; + $fill = $canvas->pickColor(0, 0, ColorFormat::HEX); + $fill = $fill == '#ff0000' ? '#00ff00' : '#ff0000'; + $rect->setFillColor($fill); + $rect->rectangle($destinationX, $destinationY, $destinationX + $sourceWidth - 1, $destinationY + $sourceHeight - 1); + $canvas->image->drawImage($rect); + $canvas->image->transparentPaintImage($fill, 0, 0, false); + + $canvas->image->setImageColorspace($this->image->getImageColorspace()); + + // copy image into new canvas + $this->image->cropImage($sourceWidth, $sourceHeight, $sourceX, $sourceY); + $canvas->image->compositeImage($this->image, Imagick::COMPOSITE_DEFAULT, $destinationX, $destinationY); + $canvas->image->setImagePage(0,0,0,0); + + // set new core to canvas + $this->image = $canvas->image; + + return $this; + } + + public function pickColor(int $x, int $y, ColorFormat $colorFormat): mixed + { + $color = new ImagickColor($this->image->getImagePixelColor($x, $y)); + + return $color->format($colorFormat); + } + public function save(string $path): ImageDriver { $this->image->writeImage($path); diff --git a/src/Enums/AlignPosition.php b/src/Enums/AlignPosition.php new file mode 100644 index 00000000..3316763f --- /dev/null +++ b/src/Enums/AlignPosition.php @@ -0,0 +1,40 @@ +x = $x; + $this->y = $y; + + return $this; + } +} diff --git a/src/Size.php b/src/Size.php index ac8aaf25..20608ff4 100644 --- a/src/Size.php +++ b/src/Size.php @@ -2,11 +2,17 @@ namespace Spatie\Image; +use Exception; +use Spatie\Image\Enums\AlignPosition; use Spatie\Image\Enums\Constraint; class Size { - public function __construct(public $width, public $height) + public function __construct( + public $width, + public $height, + public $pivot = new Point() + ) { } @@ -17,10 +23,9 @@ public function aspectRatio(): float public function resize(int $desiredWidth = null, int $desiredHeight = null, array $constraints = []): self { - // TODO: improve this check and exception if ($desiredWidth === null && $desiredHeight === null) { - throw new \Exception("Width and height can't both be null"); + throw new Exception("Width and height can't both be null"); } $dominantWidthSize = clone $this; @@ -34,9 +39,11 @@ public function resize(int $desiredWidth = null, int $desiredHeight = null, arra ->resizeWidth($desiredWidth, $desiredHeight, $constraints) ->resizeHeight($desiredWidth, $desiredHeight, $constraints); - return $dominantHeightSize->fitsInto(new Size($desiredWidth, $desiredHeight)) + $result = $dominantHeightSize->fitsInto(new Size($desiredWidth, $desiredHeight)) ? $dominantHeightSize : $dominantWidthSize; + + return $result; } public function resizeWidth(int $desiredWidth = null, int $desiredHeight = null, array $constraints = []): self @@ -49,8 +56,8 @@ public function resizeWidth(int $desiredWidth = null, int $desiredHeight = null, } if (in_array(Constraint::Upsize, $constraints)) { - $maximumWidth = $desiredWidth; - $maximumHeight = $desiredHeight; + $maximumWidth = $this->width; + $maximumHeight = $this->height; $this->width = min($desiredWidth, $maximumWidth); } else { @@ -82,8 +89,9 @@ public function resizeHeight(int $desiredWidth = null, int $desiredHeight = null } if (in_array(Constraint::Upsize, $constraints)) { - $maximumHeight = $desiredHeight; - $maximumWidth = $desiredWidth; + // TODO: is this correct? + $maximumHeight = $this->height; + $maximumWidth = $this->width; $this->height = $desiredHeight > $maximumHeight ? $maximumHeight @@ -111,4 +119,92 @@ public function fitsInto(Size $size): bool { return ($this->width <= $size->width) && ($this->height <= $size->height); } + + public function align(AlignPosition $position, $offsetX = 0, $offsetY = 0): self + { + + switch ($position) { + + case AlignPosition::Top: + case AlignPosition::TopCenter: + case AlignPosition::TopMiddle: + case AlignPosition::CenterTop: + case AlignPosition::MiddleTop: + $x = intval($this->width / 2); + $y = 0 + $offsetY; + break; + + case AlignPosition::TopRight: + case AlignPosition::RightTop: + $x = $this->width - $offsetX; + $y = 0 + $offsetY; + break; + + case AlignPosition::Left: + case AlignPosition::LeftCenter: + case AlignPosition::LeftMiddle: + case AlignPosition::CenterLeft: + case AlignPosition::MiddleLeft: + $x = 0 + $offsetX; + $y = intval($this->height / 2); + break; + + case AlignPosition::Right: + case AlignPosition::RightCenter: + case AlignPosition::RightMiddle: + case AlignPosition::CenterRight: + case AlignPosition::MiddleRight: + $x = $this->width - $offsetX; + $y = intval($this->height / 2); + break; + + case AlignPosition::BottomLeft: + case AlignPosition::LeftBottom: + $x = 0 + $offsetX; + $y = $this->height - $offsetY; + break; + + case AlignPosition::Bottom: + case AlignPosition::BottomCenter: + case AlignPosition::BottomMiddle: + case AlignPosition::CenterBottom: + case AlignPosition::MiddleBottom: + $x = intval($this->width / 2); + $y = $this->height - $offsetY; + break; + + case AlignPosition::BottomRight: + case AlignPosition::RightBottom: + $x = $this->width - $offsetX; + $y = $this->height - $offsetY; + break; + + case AlignPosition::Center: + case AlignPosition::Middle: + case AlignPosition::CenterCenter: + case AlignPosition::MiddleMiddle: + $x = intval($this->width / 2) + $offsetX; + $y = intval($this->height / 2) + $offsetY; + break; + + default: + case 'top-left': + case 'left-top': + $x = 0 + $offsetX; + $y = 0 + $offsetY; + break; + } + + $this->pivot->setCoordinates($x, $y); + + return $this; + } + + public function relativePosition(Size $size): Point + { + $x = $this->pivot->x - $size->pivot->x; + $y = $this->pivot->y - $size->pivot->y; + + return new Point($x, $y); + } } diff --git a/tests/ArchTest.php b/tests/ArchTest.php index 56283ea2..e9c8d621 100644 --- a/tests/ArchTest.php +++ b/tests/ArchTest.php @@ -1,8 +1,6 @@ expect(['dd', 'dump', 'ray']) ->not->toBeUsed(); - -*/ diff --git a/tests/GdColorTest.php b/tests/GdColorTest.php new file mode 100644 index 00000000..73b3065f --- /dev/null +++ b/tests/GdColorTest.php @@ -0,0 +1,263 @@ +parse(null); + + validateGdColor($color, 255, 255, 255, 127); +}); + +it('can parse an integer', function () { + $color = (new GdColor())->parse(850736919); + + validateGdColor($color, 181, 55, 23, 50); +}); + +it('can parse an array', function () { + $color = (new GdColor())->parse([181, 55, 23, 0.5]); + + validateGdColor($color, 181, 55, 23, 64); +}); + +it('can parse a hex string', function () { + $color = new GdColor(); + $color->parse('#b53717'); + validateGdColor($color, 181, 55, 23, 0); +}); + +it('can parse an rgba string', function () { + $color = (new GdColor())->parse('rgba(181, 55, 23, 1)'); + + validateGdColor($color, 181, 55, 23, 0); +}); + +it('can initialize from an integer', function () { + $color = (new GdColor())->initFromInteger(0); + validateGdColor($color, 0, 0, 0, 0); + + $color->initFromInteger(2147483647); + validateGdColor($color, 255, 255, 255, 127); + + $color->initFromInteger(16777215); + validateGdColor($color, 255, 255, 255, 0); + + $color->initFromInteger(2130706432); + validateGdColor($color, 0, 0, 0, 127); + + $color->initFromInteger(850736919); + validateGdColor($color, 181, 55, 23, 50); +}); + +it('can initialize from array', function () { + $color = (new GdColor())->initFromArray([0, 0, 0, 0]); + validateGdColor($color, 0, 0, 0, 127); + + $color->initFromArray([0, 0, 0, 1]); + validateGdColor($color, 0, 0, 0, 0); + + $color->initFromArray([255, 255, 255, 1]); + validateGdColor($color, 255, 255, 255, 0); + + $color->initFromArray([255, 255, 255, 0]); + validateGdColor($color, 255, 255, 255, 127); + + $color->initFromArray([255, 255, 255, 0.5]); + validateGdColor($color, 255, 255, 255, 64); + + $color->initFromArray([0, 0, 0]); + validateGdColor($color, 0, 0, 0, 0); + + $color->initFromArray([255, 255, 255]); + validateGdColor($color, 255, 255, 255, 0); + + $color->initFromArray([181, 55, 23]); + validateGdColor($color, 181, 55, 23, 0); + + $color->initFromArray([181, 55, 23, 0.5]); + validateGdColor($color, 181, 55, 23, 64); +}); + +it('init can initialize from a hex string', function () { + $color = (new GdColor())->initFromString('#cccccc'); + validateGdColor($color, 204, 204, 204, 0); + + $color->initFromString('#b53717'); + validateGdColor($color, 181, 55, 23, 0); + + $color->initFromString('ffffff'); + validateGdColor($color, 255, 255, 255, 0); + + $color->initFromString('ff00ff'); + validateGdColor($color, 255, 0, 255, 0); + + $color->initFromString('#000'); + validateGdColor($color, 0, 0, 0, 0); + + $color->initFromString('000'); + validateGdColor($color, 0, 0, 0, 0); +}); + +it('can initialize from an rgb string', function () { + $color = (new GdColor())->initFromString('rgb(1, 14, 144)'); + validateGdColor($color, 1, 14, 144, 0); + + $color->initFromString('rgb (255, 255, 255)'); + validateGdColor($color, 255, 255, 255, 0); + + $color->initFromString('rgb(0,0,0)'); + validateGdColor($color, 0, 0, 0, 0); + + $color->initFromString('rgba(0,0,0,0)'); + validateGdColor($color, 0, 0, 0, 127); + + $color->initFromString('rgba(0,0,0,0.5)'); + validateGdColor($color, 0, 0, 0, 64); + + $color->initFromString('rgba(255, 0, 0, 0.5)'); + validateGdColor($color, 255, 0, 0, 64); + + $color->initFromString('rgba(204, 204, 204, 0.9)'); + validateGdColor($color, 204, 204, 204, 13); +}); + +it('can initialize from rgb value', function () { + $color = (new GdColor())->initFromRgb(0, 0, 0); + validateGdColor($color, 0, 0, 0, 0); + + $color->initFromRgb(255, 255, 255); + validateGdColor($color, 255, 255, 255, 0); + + $color->initFromRgb(181, 55, 23); + validateGdColor($color, 181, 55, 23, 0); +}); + +it('can initialize from rgba values', function () { + $color = (new GdColor())->initFromRgba(0, 0, 0, 1); + validateGdColor($color, 0, 0, 0, 0); + + $color->initFromRgba(255, 255, 255, 1); + validateGdColor($color, 255, 255, 255, 0); + + $color->initFromRgba(181, 55, 23, 1); + validateGdColor($color, 181, 55, 23, 0); + + $color->initFromRgba(181, 55, 23, 0); + validateGdColor($color, 181, 55, 23, 127); + + $color->initFromRgba(181, 55, 23, 0.5); + validateGdColor($color, 181, 55, 23, 64); +}); + +it('can get the int value of a color', function () { + $color = new GdColor(); + expect($color->getInt())->toEqual(2147483647); + + $color = new GdColor([255, 255, 255]); + expect($color->getInt())->toEqual(16777215); + + $color = new GdColor([255, 255, 255, 1]); + expect($color->getInt())->toEqual(16777215); + + $color = new GdColor([181, 55, 23, 0.5]); + expect($color->getInt())->toEqual(1085617943); + + $color = new GdColor([181, 55, 23, 1]); + expect($color->getInt())->toEqual(11876119); + + $color = new GdColor([0, 0, 0, 0]); + expect($color->getInt())->toEqual(2130706432); +}); + +it('can get the hex value from a color', function () { + $color = new GdColor(); + expect($color->getHex())->toEqual('ffffff'); + + $color = new GdColor([255, 255, 255, 1]); + expect($color->getHex())->toEqual('ffffff'); + + $color = new GdColor([181, 55, 23, 0.5]); + expect($color->getHex())->toEqual('b53717'); + + $color = new GdColor([0, 0, 0, 0]); + expect($color->getHex('#'))->toEqual('#000000'); +}); + +it('can convert the color to an array', function () { + $color = new GdColor(); + $i = $color->getArray(); + + expect([255, 255, 255, 0])->toEqual($i); + + $color = new GdColor([255, 255, 255, 1]); + $i = $color->getArray(); + + expect([255, 255, 255, 1])->toEqual($i); + + $color = new GdColor([181, 55, 23, 0.5]); + $i = $color->getArray(); + + expect([181, 55, 23, 0.5])->toEqual($i); + + $color = new GdColor([0, 0, 0, 1]); + $i = $color->getArray(); + + expect([0, 0, 0, 1])->toEqual($i); +}); + +it('can get the rgba values', function () { + $color = (new GdColor()); + expect($color->getRgba())->toEqual('rgba(255, 255, 255, 0.00)'); + + $color = new GdColor([255, 255, 255, 1]); + expect($color->getRgba())->toEqual('rgba(255, 255, 255, 1.00)'); + + $color = new GdColor([181, 55, 23, 0.5]); + expect($color->getRgba())->toEqual('rgba(181, 55, 23, 0.50)'); + + $color = new GdColor([0, 0, 0, 1]); + expect($color->getRgba())->toEqual('rgba(0, 0, 0, 1.00)'); +}); + +it('can check if colors differ from each other', function () { + $color1 = new GdColor([0, 0, 0]); + $color2 = new GdColor([0, 0, 0]); + expect($color1->differs($color2))->toBeFalse(); + + $color1 = new GdColor([1, 0, 0]); + $color2 = new GdColor([0, 0, 0]); + expect($color1->differs($color2))->toBeTrue(); + + $color1 = new GdColor([1, 0, 0]); + $color2 = new GdColor([0, 0, 0]); + expect($color1->differs($color2, 10))->toBeFalse(); + + $color1 = new GdColor([127, 127, 127]); + $color2 = new GdColor([0, 0, 0]); + expect($color1->differs($color2, 49))->toBeTrue(); + + $color1 = new GdColor([127, 127, 127]); + $color2 = new GdColor([0, 0, 0]); + expect($color1->differs($color2, 50))->toBeFalse(); +}); + +it('will thrown an exception for an invalid color', function () { + new GdColor('invalid-color'); +})->throws(InvalidColor::class); + +function validateGdColor(GdColor $color, $red, $green, $blue, $alpha): void +{ + expect($color)->toBeInstanceOf(GdColor::class) + ->and($color) + ->red->toEqual($red) + ->green->toEqual($green) + ->blue->toEqual($blue) + ->alpha->toEqual($alpha); +} diff --git a/tests/ImagickColorTest.php b/tests/ImagickColorTest.php new file mode 100644 index 00000000..6bb5bb70 --- /dev/null +++ b/tests/ImagickColorTest.php @@ -0,0 +1,194 @@ +parse(null); + validateImagickColor($color, 255, 255, 255, 0); +}); + +it('can parse an integer', function () { + $color = (new ImagickColor())->parse(16777215); + validateImagickColor($color, 255, 255, 255, 0); + + $color = (new ImagickColor())->parse(4294967295); + validateImagickColor($color, 255, 255, 255, 1); +}); + +it('can parse an array', function () { + $color = (new ImagickColor())->parse([181, 55, 23, 0.5]); + validateImagickColor($color, 181, 55, 23, 0.5); +}); + +it('can parse a hex string', function () { + $color = (new ImagickColor())->parse('#b53717'); + validateImagickColor($color, 181, 55, 23, 1); +}); + +it('can parse an rgba string', function () { + $color = (new ImagickColor())->parse('rgba(181, 55, 23, 1)'); + validateImagickColor($color, 181, 55, 23, 1); +}); + +it('can init from an integer', function (int $int, int $red, int $green, int $blue, float $alpha) { + $color = (new ImagickColor())->initFromInteger($int); + validateImagickColor($color, $red, $green, $blue, $alpha); +})->with([ + [0, 0, 0, 0, 0], + [2147483647, 255, 255, 255, 0.5], + [16777215, 255, 255, 255, 0], + [2130706432, 0, 0, 0, 0.5], + [867514135, 181, 55, 23, 0.2], +]); + +it('can init from an array', function (array $input, int $red, int $green, int $blue, float $alpha) { + $color = (new ImagickColor())->initFromArray($input); + validateImagickColor($color, $red, $green, $blue, $alpha); +})->with([ + [[0, 0, 0, 0], 0, 0, 0, 0], + [[0, 0, 0, 1], 0, 0, 0, 1], + [[255, 255, 255, 1], 255, 255, 255, 1], + [[255, 255, 255, 0], 255, 255, 255, 0], + [[255, 255, 255, 0.5], 255, 255, 255, 0.5], + [[0, 0, 0], 0, 0, 0, 1], + [[255, 255, 255], 255, 255, 255, 1], + [[181, 55, 23], 181, 55, 23, 1], + [[181, 55, 23, 0.5], 181, 55, 23, 0.5], + +]); + +it('can init from a hex string', function (string $hexColor, int $red, int $green, int $blue, float $alpha) { + $color = (new ImagickColor())->initFromString($hexColor); + validateImagickColor($color, $red, $green, $blue, $alpha); +})->with([ + ['#cccccc', 204, 204, 204, 1], + ['#b53717', 181, 55, 23, 1], + ['ffffff', 255, 255, 255, 1], + ['ff00ff', 255, 0, 255, 1], + ['#000', 0, 0, 0, 1], + ['000', 0, 0, 0, 1], +]); + +it('can init from an rgb string', function (string $rgbString, int $red, int $green, int $blue, float $alpha) { + $color = (new ImagickColor())->initFromString($rgbString); + validateImagickColor($color, $red, $green, $blue, $alpha); +})->with([ + ['rgb(1, 14, 144)', 1, 14, 144, 1], + ['rgb (255, 255, 255)', 255, 255, 255, 1], + ['rgb(0,0,0)', 0, 0, 0, 1], + ['rgba(0,0,0,0)', 0, 0, 0, 0], + ['rgba(0,0,0,0.5)', 0, 0, 0, 0.5], + ['rgba(255, 0, 0, 0.5)', 255, 0, 0, 0.5], + ['rgba(204, 204, 204, 0.9)', 204, 204, 204, 0.9], +]); + +it('can init from rgb values', function (array $inputValues, array $expectedValues) { + $color = (new ImagickColor())->initFromRgb(...$inputValues); + validateImagickColor($color, ...$expectedValues); +})->with([ + [[0, 0, 0], [0, 0, 0, 1]], + [[255, 255, 255], [255, 255, 255, 1]], + [[181, 55, 23], [181, 55, 23, 1]], +]); + +it('can init from an rgba value', function (array $inputValues, array $expectedValues) { + $color = (new ImagickColor())->initFromRgba(...$inputValues); + validateImagickColor($color, ...$expectedValues); +})->with([ + [[0, 0, 0, 1], [0, 0, 0, 1]], + [[255, 255, 255, 1], [255, 255, 255, 1]], + [[181, 55, 23, 1], [181, 55, 23, 1]], + [[181, 55, 23, 0], [181, 55, 23, 0]], + [[181, 55, 23, 0.5], [181, 55, 23, 0.5]], +]); + +test('it can get an int', function (array|null $input, int $expected) { + $color = new ImagickColor($input); + expect($color->getInt())->toEqual($expected); +})->with([ + [null, 16777215], + [[255, 255, 255], 4294967295], + [[255, 255, 255, 1], 4294967295], + [[181, 55, 23, 0.2], 867514135], + [[255, 255, 255, 0.5], 2164260863], + [[181, 55, 23, 1], 4290066199], + [[0, 0, 0, 0], 0], +]); + +it('can get the hex value', function (array|null $input, string $expectedHex) { + $color = new ImagickColor($input); + expect($color->getHex())->toEqual($expectedHex); +})->with([ + [null, 'ffffff'], + [[255, 255, 255], 'ffffff'], + [[255, 255, 255, 1], 'ffffff'], + [[181, 55, 23, 0.2], 'b53717'], + [[255, 255, 255, 0.5], 'ffffff'], + [[181, 55, 23, 1], 'b53717'], + [[0, 0, 0, 0], '000000'], +]); + +it('can get the array value', function (array|null $input, array $expected) { + $color = new ImagickColor($input); + expect($color->getArray())->toEqual($expected); +})->with([ + [null, [255, 255, 255, 0]], + [[255, 255, 255], [255, 255, 255, 1]], + [[255, 255, 255, 1], [255, 255, 255, 1]], + [[181, 55, 23, 0.2], [181, 55, 23, 0.2]], + [[255, 255, 255, 0.5], [255, 255, 255, 0.5]], + [[181, 55, 23, 0.5], [181, 55, 23, 0.5]], + [[0, 0, 0, 1], [0, 0, 0, 1]], +]); + +it('can get the rgba value', function (array|null $input, string $expected) { + $rgbaString = (new ImagickColor($input))->getRgba(); + expect($rgbaString)->toEqual($expected); +})->with([ + [[255, 255, 255, 1], 'rgba(255, 255, 255, 1.00)'], + [[181, 55, 23, 0.5], 'rgba(181, 55, 23, 0.50)'], + [[0, 0, 0, 1], 'rgba(0, 0, 0, 1.00)'], + [[255, 255, 255, 0.5], 'rgba(255, 255, 255, 0.50)'], +]); + +it('can check if a color is different', function () { + $color1 = new ImagickColor([0, 0, 0]); + $color2 = new ImagickColor([0, 0, 0]); + expect($color1->differs($color2))->toBeFalse(); + + $color1 = new ImagickColor([1, 0, 0]); + $color2 = new ImagickColor([0, 0, 0]); + expect($color1->differs($color2))->toBeTrue(); + + $color1 = new ImagickColor([1, 0, 0]); + $color2 = new ImagickColor([0, 0, 0]); + expect($color1->differs($color2, 10))->toBeFalse(); + + $color1 = new ImagickColor([127, 127, 127]); + $color2 = new ImagickColor([0, 0, 0]); + expect($color1->differs($color2, 49))->toBeTrue(); + + $color1 = new ImagickColor([127, 127, 127]); + $color2 = new ImagickColor([0, 0, 0]); + expect($color1->differs($color2, 50))->toBeFalse(); +}); + +it('will throw an exception for an invalid value', function () { + new ImagickColor('invalid value'); +})->throws(InvalidColor::class); + +function validateImagickColor($color, int $red, int $green, int $blue, float $alpha): void +{ + expect($color)->toBeInstanceOf(ImagickColor::class); + + expect(round($color->getRedValue(), 2))->toEqual($red); + expect(round($color->getGreenValue(), 2))->toEqual($green); + expect(round($color->getBlueValue(), 2))->toEqual($blue); + expect(round($color->getAlphaValue(), 2))->toEqual($alpha); +} diff --git a/tests/Manipulations/BlurTest.php b/tests/Manipulations/BlurTest.php index fe47904d..f5d0cb29 100644 --- a/tests/Manipulations/BlurTest.php +++ b/tests/Manipulations/BlurTest.php @@ -1,10 +1,9 @@ tempDir->path("{$driver->driverName()}/blur.jpg"); @@ -15,4 +14,4 @@ it('will throw an exception when passing an invalid blur value', function (ImageDriver $driver) { $driver->load(getTestJpg())->brightness(101); -})->with('drivers')->throws(InvalidManipulation::class); +})->with('drivers')->throws(InvalidManipulation::class); \ No newline at end of file diff --git a/tests/Manipulations/BrightnessTest.php b/tests/Manipulations/BrightnessTest.php index 990c2cd1..5d8f90e9 100644 --- a/tests/Manipulations/BrightnessTest.php +++ b/tests/Manipulations/BrightnessTest.php @@ -1,10 +1,9 @@ tempDir->path("{$driver->driverName()}/brightness.jpg"); @@ -15,4 +14,4 @@ it('will throw an exception when passing an invalid brightness', function (ImageDriver $driver) { $driver->load(getTestJpg())->brightness(-101); -})->with('drivers')->throws(InvalidManipulation::class); +})->with('drivers')->throws(InvalidManipulation::class); \ No newline at end of file diff --git a/tests/Manipulations/FitTest.php b/tests/Manipulations/FitTest.php index f2fef8dc..02e8bb3c 100644 --- a/tests/Manipulations/FitTest.php +++ b/tests/Manipulations/FitTest.php @@ -1,16 +1,47 @@ tempDir->path("{$driver->driverName()}/fit.jpg"); +it('can contain an image in the given dimensions', function ( + ImageDriver $driver, + array $fitDimensions, + int $expectedWidth, + int $expectedHeight, +) { + $targetFile = $this->tempDir->path("{$driver->driverName()}/fit-contain.jpg"); - $driver->load(getTestJpg())->fit(Fit::Contain, 100, 60)->save($targetFile); + $driver->load(getTestJpg())->fit(Fit::Contain, ...$fitDimensions)->save($targetFile); expect($targetFile)->toBeFile(); - // TODO: add assertions around the dimensions of the image -})->with('drivers'); + $savedImage = $driver->load($targetFile); + expect($savedImage->getWidth())->toBe($expectedWidth); + expect($savedImage->getHeight())->toBe($expectedHeight); + +})->with('drivers')->with([ + [[100, 60], 73, 60], + [[60, 100], 60, 50], + [[200, 200], 200, 165], +]); + +it('can fill an image in the given dimensions', function ( + ImageDriver $driver, + array $fitDimensions, + int $expectedWidth, + int $expectedHeight, +) { + $targetFile = $this->tempDir->path("{$driver->driverName()}/fit-fill.jpg"); + + $driver->load(getTestJpg())->fit(Fit::Fill, ...$fitDimensions)->save($targetFile); + + $savedImage = $driver->load($targetFile); + expect($savedImage->getWidth())->toBe($expectedWidth); + expect($savedImage->getHeight())->toBe($expectedHeight); +})->with('drivers')->with([ + [[500, 500], 500, 500], + [[250, 300], 250, 300], + [[100, 100], 100, 100], + +]); diff --git a/tests/Pest.php b/tests/Pest.php index 5be76f82..e7ca0262 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -8,7 +8,7 @@ (new TemporaryDirectory(getTempPath()))->delete(); }) ->beforeEach(function () { - ray()->newScreen($this->getName()); + ray()->newScreen($this->name()); $this ->tempDir = (new TemporaryDirectory(getTestSupportPath()))