From ac0e9e1acb8bc1678a75aa26da710c17a068aaa2 Mon Sep 17 00:00:00 2001 From: Joby Elliott Date: Tue, 12 Sep 2023 00:23:25 +0000 Subject: [PATCH] started work on *magick driver and updated tests --- .github/workflows/phpstan.yaml | 14 ++ .github/workflows/phpunit.yaml | 13 ++ .gitignore | 3 +- README.md | 16 +- composer.json | 7 +- phpstan.neon | 4 + phpunit.xml | 31 ++++ src/DefaultDriver.php | 3 +- src/Drivers/AbstractCliDriver.php | 6 - src/Drivers/AbstractDriver.php | 14 +- src/Drivers/AbstractExtensionDriver.php | 10 +- src/Drivers/GDDriver.php | 100 ++++++++---- src/Drivers/MagickCliDriver.php | 8 +- src/Drivers/MagickDriver.php | 144 +++++++++++++++++ src/Image.php | 23 ++- src/Sizers/AbstractSizer.php | 4 +- src/Sizers/Cover.php | 10 +- src/Sizers/Fit.php | 8 +- tests/100x200.jpg | Bin 4962 -> 0 bytes tests/200x100.jpg | Bin 4067 -> 0 bytes tests/200x200.jpg | Bin 5747 -> 0 bytes tests/Sizers/CoverTest.php | 109 +++++++++++++ tests/input-100x200.png | Bin 0 -> 774 bytes tests/input-200x100.png | Bin 0 -> 679 bytes tests/input-200x200.png | Bin 0 -> 834 bytes tests/test.svg | 196 ++++++++++++++++++++++++ 26 files changed, 654 insertions(+), 69 deletions(-) create mode 100644 .github/workflows/phpstan.yaml create mode 100644 .github/workflows/phpunit.yaml create mode 100644 phpstan.neon create mode 100644 phpunit.xml create mode 100644 src/Drivers/MagickDriver.php delete mode 100644 tests/100x200.jpg delete mode 100644 tests/200x100.jpg delete mode 100644 tests/200x200.jpg create mode 100644 tests/Sizers/CoverTest.php create mode 100644 tests/input-100x200.png create mode 100644 tests/input-200x100.png create mode 100644 tests/input-200x200.png create mode 100644 tests/test.svg diff --git a/.github/workflows/phpstan.yaml b/.github/workflows/phpstan.yaml new file mode 100644 index 0000000..e362fbd --- /dev/null +++ b/.github/workflows/phpstan.yaml @@ -0,0 +1,14 @@ +name: PHPStan +on: push +jobs: + phpstan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: php-actions/composer@v6 + with: + dev: yes + - uses: php-actions/phpstan@v3 + with: + path: src + level: max diff --git a/.github/workflows/phpunit.yaml b/.github/workflows/phpunit.yaml new file mode 100644 index 0000000..a93faa6 --- /dev/null +++ b/.github/workflows/phpunit.yaml @@ -0,0 +1,13 @@ +name: PHPUnit +on: push +jobs: + phpunit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: php-actions/composer@v6 + with: + dev: yes + - uses: php-actions/phpunit@v3 + with: + version: 10 diff --git a/.gitignore b/.gitignore index b9c83f1..b81d74f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ composer.phar composer.lock debug.log /examples/out/* -!.gitkeep \ No newline at end of file +!.gitkeep +.phpunit.result.cache \ No newline at end of file diff --git a/README.md b/README.md index bae7426..e3e563f 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,11 @@ This library is under active development, and until a 1.0 release is made you sh ### Current progress -| Driver | Rotate | Mirror | Resize | Crop | -| :--------- | :----: | :----: | :----: | :--: | -| GD | X | X | X | X | -| Imagick | | | | | -| Gmagick | | | | | -| ImagickCLI | X | X | X | X | +| Driver | Rotate | Mirror | Resize | Crop | +| :-------------- | :----: | :----: | :----: | :---: | +| GDDriver | X | X | X | X | +| MagickDriver | | | | | +| MagickCliDriver | X | X | X | X | ## Roadmap @@ -22,9 +21,8 @@ This library is under active development, and until a 1.0 release is made you sh A 1.0 release will not be made until the following drivers are available and solidly tested: * GD -* Imagick -* Gmagick -* GmagickCLI +* Magick (unified driver that will use either ImageMagick or Gmagick automatically depending on what is available) +* MagickCliDriver (unified driver that can be configured to use either ImageMagick or Gmagick CLI tools) ### Transforms diff --git a/composer.json b/composer.json index 2d40691..aad378d 100644 --- a/composer.json +++ b/composer.json @@ -24,14 +24,13 @@ }, "require-dev": { "ext-gmagick": "*", - "ext-gd": "*" + "ext-gd": "*", + "phpunit/phpunit": "^10.3", + "phpstan/phpstan": "^1.10" }, "autoload-dev": { "psr-4": { "ByJoby\\ImageTransform\\tests\\": "tests/" } - }, - "scripts": { - "test": "./vendor/bin/atoum -d tests" } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..478362b --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: max + paths: + - src \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..bc2f7c1 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,31 @@ + + + + + + + + + + tests/ + + + + + src + + + + + + \ No newline at end of file diff --git a/src/DefaultDriver.php b/src/DefaultDriver.php index d7c2d06..7718952 100644 --- a/src/DefaultDriver.php +++ b/src/DefaultDriver.php @@ -10,7 +10,8 @@ use ByJoby\ImageTransform\Drivers\GDDriver; */ class DefaultDriver { - protected $driver; + /** @var DriverInterface|null */ + protected static $driver; public static function get(): DriverInterface { diff --git a/src/Drivers/AbstractCliDriver.php b/src/Drivers/AbstractCliDriver.php index 8a9021f..21f9642 100644 --- a/src/Drivers/AbstractCliDriver.php +++ b/src/Drivers/AbstractCliDriver.php @@ -4,10 +4,4 @@ namespace ByJoby\ImageTransform\Drivers; abstract class AbstractCliDriver extends AbstractDriver { - public function __construct() - { - if (!function_exists('exec')) { - throw new \Exception("CLI drivers can't be used with the current configuration because exec is disabled"); - } - } } diff --git a/src/Drivers/AbstractDriver.php b/src/Drivers/AbstractDriver.php index aaa8d41..211b827 100644 --- a/src/Drivers/AbstractDriver.php +++ b/src/Drivers/AbstractDriver.php @@ -5,20 +5,25 @@ namespace ByJoby\ImageTransform\Drivers; use ByJoby\ImageTransform\DriverInterface; use ByJoby\ImageTransform\Image; use ByJoby\ImageTransform\Sizers\AbstractSizer; +use Exception; abstract class AbstractDriver implements DriverInterface { + /** @var string|null */ protected $tempDir = null; + /** @var int */ protected $chmod_dir = 0775; + /** @var int */ protected $chmod_file = 0665; - abstract protected function doSave(Image $image, string $filename); + abstract protected function doSave(Image $image, string $filename): void; public function tempDir(): string { if (!$this->tempDir) { $this->setTempDir(sys_get_temp_dir() . '/byjoby_image-transform/' . uniqid("", true)); } + // @phpstan-ignore-next-line this is actually checked return $this->tempDir; } @@ -31,7 +36,7 @@ abstract class AbstractDriver implements DriverInterface return $this; } - protected function mkdir(string $dir) + protected function mkdir(string $dir): bool { // return true if dir exists and is writeable if (is_dir($dir) && is_writeable($dir)) { @@ -74,12 +79,15 @@ abstract class AbstractDriver implements DriverInterface } touch($filename); } - $this->doSave($image, realpath($filename)); + $filename = realpath($filename); + if (!$filename) throw new Exception("Invalid filename or path"); + $this->doSave($image, $filename); chmod($filename, $this->chmod_file); return null; } else { $filename = $this->tempDir() . '/' . uniqid() . '.jpg'; $this->doSave($image, $filename); + /** @var string we can count on this being a string because we just wrote it */ $output = file_get_contents($filename); unlink($filename); return $output; diff --git a/src/Drivers/AbstractExtensionDriver.php b/src/Drivers/AbstractExtensionDriver.php index b454259..00e0765 100644 --- a/src/Drivers/AbstractExtensionDriver.php +++ b/src/Drivers/AbstractExtensionDriver.php @@ -6,14 +6,20 @@ use ByJoby\ImageTransform\Image; abstract class AbstractExtensionDriver extends AbstractDriver { + // @phpstan-ignore-next-line we specify types in subclasses abstract protected function getImageObject(Image $image); + // @phpstan-ignore-next-line we specify types in subclasses abstract protected function doResize($object, Image $image); + // @phpstan-ignore-next-line we specify types in subclasses abstract protected function doCrop($object, Image $image); + // @phpstan-ignore-next-line we specify types in subclasses abstract protected function doFlip($object, Image $image); + // @phpstan-ignore-next-line we specify types in subclasses abstract protected function doRotation($object, Image $image); - abstract protected function saveImageObject($object, string $filename); + // @phpstan-ignore-next-line we specify types in subclasses + abstract protected function saveImageObject($object, string $filename): void; - public function doSave(Image $image, string $filename) + public function doSave(Image $image, string $filename): void { $object = $this->getImageObject($image); $object = $this->doRotation($object, $image); diff --git a/src/Drivers/GDDriver.php b/src/Drivers/GDDriver.php index 36bf4b3..07bb48b 100644 --- a/src/Drivers/GDDriver.php +++ b/src/Drivers/GDDriver.php @@ -3,6 +3,7 @@ namespace ByJoby\ImageTransform\Drivers; use ByJoby\ImageTransform\Image; +use GdImage; /** * This driver uses PHP's built-in GD libary. This is by far the slowest driver, @@ -18,42 +19,62 @@ class GDDriver extends AbstractExtensionDriver } } - protected function getImageObject(Image $image) + protected function getImageObject(Image $image): GdImage { $source = $image->source(); - $extension = strtolower(preg_replace('/^.+\./', '', $source)); + /** @var string */ + $extension = preg_replace('/^.+\./', '', $source); + $extension = strtolower($extension); switch ($extension) { case 'bmp': + // @phpstan-ignore-next-line this will throw an exception, which is good return imagecreatefrombmp($source); case 'gif': + // @phpstan-ignore-next-line this will throw an exception, which is good return imagecreatefromgif($source); case 'jpg': + // @phpstan-ignore-next-line this will throw an exception, which is good return imagecreatefromjpeg($source); case 'jpeg': + // @phpstan-ignore-next-line this will throw an exception, which is good return imagecreatefromjpeg($source); case 'png': + // @phpstan-ignore-next-line this will throw an exception, which is good return imagecreatefrompng($source); case 'wbmp': + // @phpstan-ignore-next-line this will throw an exception, which is good return imagecreatefromwbmp($source); case 'webp': + // @phpstan-ignore-next-line this will throw an exception, which is good return imagecreatefromwebp($source); case 'xbm': + // @phpstan-ignore-next-line this will throw an exception, which is good return imagecreatefromxbm($source); default: throw new \Exception("Unsupported input type: " . htmlentities($source)); } } - protected function doResize($object, Image $image) + /** + * @param GdImage $object + * @param Image $image + * @return GdImage + */ + protected function doResize($object, Image $image): GdImage { $sizer = $image->sizer(); if ($sizer->resizeToHeight() && $sizer->resizeToWidth()) { // sizer is calling for a resize + /** @var GdImage */ $new = imagecreatetruecolor($sizer->resizeToWidth(), $sizer->resizeToHeight()); imagecopyresampled( - $new, $object, - 0, 0,//dst x/y - 0, 0, + $new, + $object, + 0, + 0, + //dst x/y + 0, + 0, $sizer->resizeToWidth(), $sizer->resizeToHeight(), $sizer->originalWidth(), $sizer->originalHeight() ); @@ -64,17 +85,26 @@ class GDDriver extends AbstractExtensionDriver } } - protected function doCrop($object, Image $image) + /** + * @param GdImage $object + * @param Image $image + * @return GdImage + */ + protected function doCrop($object, Image $image): GdImage { $sizer = $image->sizer(); if ($sizer->cropToHeight() && $sizer->cropToWidth()) { // sizer is calling for a crop + /** @var GdImage */ $new = imagecreatetruecolor($sizer->cropToWidth(), $sizer->cropToHeight()); imagecopyresampled( - $new, $object, - ($sizer->cropToWidth()-$sizer->resizeToWidth())/2,($sizer->cropToHeight()-$sizer->resizeToHeight())/2, - 0,0, - $sizer->resizetoWidth(),$sizer->resizeToHeight(),$sizer->resizeToWidth(),$sizer->resizeToHeight() + $new, + $object, + ($sizer->cropToWidth() - $sizer->resizeToWidth()) / 2, ($sizer->cropToHeight() - $sizer->resizeToHeight()) / 2, + 0, + 0, + // @phpstan-ignore-next-line these are definitely set + $sizer->resizetoWidth(), $sizer->resizeToHeight(), $sizer->resizeToWidth(), $sizer->resizeToHeight() ); return $new; } else { @@ -83,47 +113,65 @@ class GDDriver extends AbstractExtensionDriver } } - protected function doFlip($object, Image $image) + /** + * @param GdImage $object + * @param Image $image + * @return GdImage + */ + protected function doFlip($object, Image $image): GdImage { if ($image->getFlipH()) { - imageflip($object,IMG_FLIP_HORIZONTAL); + imageflip($object, IMG_FLIP_HORIZONTAL); } if ($image->getFlipV()) { - imageflip($object,IMG_FLIP_VERTICAL); + imageflip($object, IMG_FLIP_VERTICAL); } return $object; } - protected function doRotation($object, Image $image) + /** + * @param GdImage $object + * @param Image $image + * @return GdImage + */ + protected function doRotation($object, Image $image): GdImage { if ($rotationAmount = 360 - $image->rotation() * 90) { + // @phpstan-ignore-next-line return imagerotate($object, $rotationAmount, 0); } return $object; } - protected function saveImageObject($object, string $filename) + /** + * @param GdImage $object + * @param string $filename + * @return void + */ + protected function saveImageObject($object, string $filename): void { - $extension = strtolower(preg_replace('/^.+\./', '', $filename)); + /** @var string */ + $extension = preg_replace('/^.+\./', '', $filename); + $extension = strtolower($extension); switch ($extension) { case 'bmp': - return imagebmp($object, $filename); + imagebmp($object, $filename); case 'gif': - return imagegif($object, $filename); + imagegif($object, $filename); case 'jpg': - return imagejpeg($object, $filename); + imagejpeg($object, $filename); case 'jpeg': - return imagejpeg($object, $filename); + imagejpeg($object, $filename); case 'png': - return imagepng($object, $filename); + imagepng($object, $filename); case 'wbmp': - return imagewbmp($object, $filename); + imagewbmp($object, $filename); case 'webp': - return imagewebp($object, $filename); + imagewebp($object, $filename); case 'xbm': - return imagexbm($object, $filename); + imagexbm($object, $filename); default: throw new \Exception("Unsupported output type: " . htmlentities($filename)); } } -} +} \ No newline at end of file diff --git a/src/Drivers/MagickCliDriver.php b/src/Drivers/MagickCliDriver.php index d9af730..92fc32b 100644 --- a/src/Drivers/MagickCliDriver.php +++ b/src/Drivers/MagickCliDriver.php @@ -13,12 +13,8 @@ use ByJoby\ImageTransform\Image; */ class MagickCliDriver extends AbstractCliDriver { - protected $mogrify_executable; - - public function __construct($mogrify_executable = 'magick mogrify') + public function __construct(protected string $mogrify_executable = 'magick mogrify') { - parent::__construct(); - $this->mogrify_executable = $mogrify_executable; } protected function mogrifyExecutable(): string @@ -26,7 +22,7 @@ class MagickCliDriver extends AbstractCliDriver return $this->mogrify_executable; } - protected function doSave(Image $image, string $filename) + protected function doSave(Image $image, string $filename): void { // basics of command $command = [ diff --git a/src/Drivers/MagickDriver.php b/src/Drivers/MagickDriver.php new file mode 100644 index 0000000..d979883 --- /dev/null +++ b/src/Drivers/MagickDriver.php @@ -0,0 +1,144 @@ +use_gmagick) return new Gmagick($image->source()); + else return new Imagick($image->source()); + } + + /** + * @param Gmagick|Imagick $object + * @param Image $image + * @return Gmagick|Imagick + */ + protected function doResize($object, Image $image): Gmagick|Imagick + { + $sizer = $image->sizer(); + if ($sizer->resizeToHeight() && $sizer->resizeToWidth()) { + if ($object instanceof Gmagick) { + $object->resizeimage( + $sizer->resizeToWidth(), + $sizer->resizeToHeight(), + Gmagick::FILTER_LANCZOS, + 0 + ); + } else { + $object->resizeImage( + $sizer->resizeToWidth(), + $sizer->resizeToHeight(), + Imagick::FILTER_LANCZOS, + 0 + ); + } + } + return $object; + } + + /** + * @param Gmagick|Imagick $object + * @param Image $image + * @return Gmagick|Imagick + */ + protected function doCrop($object, Image $image): Gmagick|Imagick + { + $sizer = $image->sizer(); + $height = $sizer->cropToHeight(); + $width = $sizer->cropToWidth(); + if ($height && $width) { + $x = intval(($sizer->resizetoWidth() - $width) / 2); + $y = intval(($sizer->resizetoHeight() - $height) / 2); + if ($object instanceof Gmagick) { + $object->cropimage( + $width, + $height, + $x, + $y + ); + } else { + $object->cropImage( + $width, + $height, + $x, + $y + ); + } + } + return $object; + } + + /** + * @param Gmagick|Imagick $object + * @param Image $image + * @return Gmagick|Imagick + */ + protected function doFlip($object, Image $image): Gmagick|Imagick + { + if ($image->getFlipH()) { + if ($object instanceof Gmagick) $object->flipimage(); + else $object->flipImage(); + } + if ($image->getFlipV()) { + if ($object instanceof Gmagick) $object->flopimage(); + else $object->flopImage(); + } + return $object; + } + + /** + * @param Gmagick|Imagick $object + * @param Image $image + * @return Gmagick|Imagick + */ + protected function doRotation($object, Image $image): Gmagick|Imagick + { + if ($image->rotation()) { + if ($object instanceof Gmagick) $object->rotateimage(new GmagickPixel("#000"), $image->rotation() * 90); + else $object->rotateImage(new ImagickPixel("#000"), $image->rotation() * 90); + } + return $object; + } + + /** + * @param Gmagick|Imagick $object + * @param string $filename + * @return void + */ + protected function saveImageObject($object, string $filename): void + { + /** @var string */ + $format = preg_replace('/^.+\./', '', $filename); + $format = strtoupper($format); + if ($format == 'JPG') $format = 'JPEG'; + if ($object instanceof Gmagick) { + $object->setimageformat($format); + $object->writeimage($filename, true); + } else { + $object->setImageFormat($format); + $object->writeImage($filename); + } + } +} \ No newline at end of file diff --git a/src/Image.php b/src/Image.php index 6564b4f..9dc3306 100644 --- a/src/Image.php +++ b/src/Image.php @@ -7,13 +7,21 @@ use ByJoby\ImageTransform\Sizers\Original; class Image { + /** @var string */ protected $source; + /** @var DriverInterface|null */ protected $driver; + /** @var int */ protected $originalWidth; + /** @var int */ protected $originalHeight; + /** @var int */ protected $rotation = 0; + /** @var boolean */ protected $flipH = false; + /** @var boolean */ protected $flipV = false; + /** @var AbstractSizer */ protected $sizer = null; public function __construct(string $source, AbstractSizer|null $sizer = null) @@ -30,10 +38,11 @@ class Image public function setSource(string $source): static { // set source - $this->source = realpath($source); - if (!$this->source) { - throw new \Exception("Source image not found: " . htmlentities($source)); + $source = realpath($source); + if (!$source) { + throw new \Exception("Source image not found"); } + $this->source = $source; // validate file if (!is_file($this->source)) { throw new \Exception("Image file doesn't exist: " . htmlentities($this->source)); @@ -42,7 +51,11 @@ class Image throw new \Exception("Invalid image file: " . htmlentities($this->source)); } // get height/width - list($this->originalWidth, $this->originalHeight) = getimagesize($this->source); + $size = getimagesize($this->source); + if (!$size) { + throw new \Exception("Couldn't get image size: " . htmlentities($this->source)); + } + list($this->originalWidth, $this->originalHeight) = $size; // return self return $this; } @@ -55,7 +68,7 @@ class Image public function setSizer(AbstractSizer $sizer): static { $this->sizer = clone $sizer; - $this->sizer->image($this); + $this->sizer->setImage($this); return $this; } diff --git a/src/Sizers/AbstractSizer.php b/src/Sizers/AbstractSizer.php index 08f8834..4ca31e2 100644 --- a/src/Sizers/AbstractSizer.php +++ b/src/Sizers/AbstractSizer.php @@ -6,6 +6,7 @@ use ByJoby\ImageTransform\Image; abstract class AbstractSizer { + /** @var Image */ protected $image; abstract public function width(): int; @@ -46,8 +47,9 @@ abstract class AbstractSizer return $this->originalWidth()/$this->originalHeight(); } - public function image(Image $image) + public function setImage(Image $image): static { $this->image = $image; + return $this; } } diff --git a/src/Sizers/Cover.php b/src/Sizers/Cover.php index 8cdb921..a2823cd 100644 --- a/src/Sizers/Cover.php +++ b/src/Sizers/Cover.php @@ -4,6 +4,7 @@ namespace ByJoby\ImageTransform\Sizers; class Cover extends AbstractSizer { + /** @var int */ protected $width, $height; public function __construct(int $width, int $height) @@ -17,14 +18,17 @@ class Cover extends AbstractSizer return $this->width / $this->height; } + /** + * @return array{height:int,width:int} + */ protected function calculateSize(): array { if ($this->targetRatio() < $this->originalRatio()) { $height = $this->height; - $width = round($height * $this->originalRatio()); + $width = intval(round($height * $this->originalRatio())); } else { $width = $this->width; - $height = round($width / $this->originalRatio()); + $height = intval(round($width / $this->originalRatio())); } return [ 'height' => $height, @@ -61,4 +65,4 @@ class Cover extends AbstractSizer { return $this->height; } -} +} \ No newline at end of file diff --git a/src/Sizers/Fit.php b/src/Sizers/Fit.php index 8c46839..5fe74dd 100644 --- a/src/Sizers/Fit.php +++ b/src/Sizers/Fit.php @@ -4,6 +4,7 @@ namespace ByJoby\ImageTransform\Sizers; class Fit extends AbstractSizer { + /** @var int */ protected $width, $height; public function __construct(int $width, int $height) @@ -27,14 +28,17 @@ class Fit extends AbstractSizer return $this->width / $this->height; } + /** + * @return array{height:int,width:int} + */ protected function calculateSize(): array { if ($this->targetRatio() > $this->originalRatio()) { $height = $this->height; - $width = round($height * $this->originalRatio()); + $width = intval(round($height * $this->originalRatio())); } else { $width = $this->width; - $height = round($width / $this->originalRatio()); + $height = intval(round($width / $this->originalRatio())); } return [ 'height' => $height, diff --git a/tests/100x200.jpg b/tests/100x200.jpg deleted file mode 100644 index aa3bcc59b36580b5330bc227f5ccd87c6e6c94cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4962 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 diff --git a/tests/200x200.jpg b/tests/200x200.jpg deleted file mode 100644 index 6a4cf3e0a48510db9693e0344ecebc315d26f62e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5747 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 diff --git a/tests/Sizers/CoverTest.php b/tests/Sizers/CoverTest.php new file mode 100644 index 0000000..276a47e --- /dev/null +++ b/tests/Sizers/CoverTest.php @@ -0,0 +1,109 @@ +createMock(Image::class); + $image->method("originalWidth")->willReturn(200); + $image->method("originalHeight")->willReturn(200); + // spin up a 50x50 sizer + $sizer = new Cover(50, 50); + $sizer->setImage($image); + // resized to cover + $this->assertEquals(50, $sizer->resizeToWidth()); + $this->assertEquals(50, $sizer->resizeToHeight()); + // cropped to size + $this->assertEquals(50, $sizer->cropToWidth()); + $this->assertEquals(50, $sizer->cropToHeight()); + } + + public function testSquareImageSizedLandscape(): void + { + // mock 200x200 image + $image = $this->createMock(Image::class); + $image->method("originalWidth")->willReturn(200); + $image->method("originalHeight")->willReturn(200); + // spin up a 100x50 sizer + $sizer = new Cover(100, 50); + $sizer->setImage($image); + // resized to cover + $this->assertEquals(100, $sizer->resizeToWidth()); + $this->assertEquals(100, $sizer->resizeToHeight()); + // cropped to size + $this->assertEquals(100, $sizer->cropToWidth()); + $this->assertEquals(50, $sizer->cropToHeight()); + } + public function testSquareImageSizedPortrait(): void + { + // mock 200x200 image + $image = $this->createMock(Image::class); + $image->method("originalWidth")->willReturn(200); + $image->method("originalHeight")->willReturn(200); + // spin up a 50x100 sizer + $sizer = new Cover(50, 100); + $sizer->setImage($image); + // resized to cover + $this->assertEquals(100, $sizer->resizeToWidth()); + $this->assertEquals(100, $sizer->resizeToHeight()); + // cropped to size + $this->assertEquals(50, $sizer->cropToWidth()); + $this->assertEquals(100, $sizer->cropToHeight()); + } + + public function testLandscapeImageSizedSquare(): void + { + // mock 200x100 image + $image = $this->createMock(Image::class); + $image->method("originalWidth")->willReturn(200); + $image->method("originalHeight")->willReturn(100); + // spin up a 50x50 sizer + $sizer = new Cover(50, 50); + $sizer->setImage($image); + // resized to cover + $this->assertEquals(100, $sizer->resizeToWidth()); + $this->assertEquals(50, $sizer->resizeToHeight()); + // cropped to size + $this->assertEquals(50, $sizer->cropToWidth()); + $this->assertEquals(50, $sizer->cropToHeight()); + } + + public function testLandscapeImageSizedLandscape(): void + { + // mock 200x100 image + $image = $this->createMock(Image::class); + $image->method("originalWidth")->willReturn(200); + $image->method("originalHeight")->willReturn(100); + // spin up a 100x50 sizer + $sizer = new Cover(100, 50); + $sizer->setImage($image); + // resized to cover + $this->assertEquals(100, $sizer->resizeToWidth()); + $this->assertEquals(50, $sizer->resizeToHeight()); + // cropped to size + $this->assertEquals(100, $sizer->cropToWidth()); + $this->assertEquals(50, $sizer->cropToHeight()); + } + public function testLandscapeImageSizedPortrait(): void + { + // mock 200x100 image + $image = $this->createMock(Image::class); + $image->method("originalWidth")->willReturn(200); + $image->method("originalHeight")->willReturn(100); + // spin up a 50x100 sizer + $sizer = new Cover(50, 100); + $sizer->setImage($image); + // resized to cover + $this->assertEquals(200, $sizer->resizeToWidth()); + $this->assertEquals(100, $sizer->resizeToHeight()); + // cropped to size + $this->assertEquals(50, $sizer->cropToWidth()); + $this->assertEquals(100, $sizer->cropToHeight()); + } +} \ No newline at end of file diff --git a/tests/input-100x200.png b/tests/input-100x200.png new file mode 100644 index 0000000000000000000000000000000000000000..8f08d3875f039473adf471d87b5d26dd49603d88 GIT binary patch literal 774 zcmeAS@N?(olHy`uVBq!ia0vp^DL{OJgAGW^*|;18Qk(@Ik;M!Qe1}1p@p%3UkP68X z*NBqf{Irtt#G+J&^73-M%)IR43(9ijLqx{5oQ}Q?>cm|CmC1^Tw`x5^*@j!Au&zlpFQX4=xbZHDSVdL*B`&*aOv0Q zmGSpxm*;Q3X>*2a`t;+sfB&{`_kOg&|NP;jn~!$Rw6wL}d-wnD z{Yl?WUp;g7(e8h<#BZJbCgm=iEIV`}{u>WE_)NIt5kS zCP)y^NXvhA`LLe&y}PwHp6;@&FWPsx_V)JMci-K9S2kast}*hNf1#d!N=)FiufR0S N;OXk;vd$@?2>`=&EtEO_ z@%_vG+r0`(B_H{6*$NB43tqYL?1C{z5nof9Y?In_vCOQTqF3kSq@2R{b_7^-sC7-4 zGw&dKRGrwKIR6KqAAhJWK3A}7?%cTxrEcAgo&WBAeN^VopX;iPcgyAJn@PXf_uaH6 zE^k|SX!h&uzb~$@xS1AyeDgxPE!E3Hziy24gjSdO|94t(OliIO) zZ|(o=(+3Y9{yF*aRol0BU(O7FzV_RyRr_qKOMX50zT@Vzf{%|k z{SIOW3C^?GTm9+B!r+PdQFUuBtv7k97ywk>OQ{MxHe!>U6= zOG~a?KjgIg?B2bRy1J&oAPSv5d-mm%DVM&y`*EZRclb1|dzJgRe&fG$H*db4ZRfP5 vF#qwgeu1y&e*-yJzW@LBbB`lMVS7;CZ)fPlZHIe736R0l)z4*}Q$iB}dUyFV literal 0 HcmV?d00001 diff --git a/tests/input-200x200.png b/tests/input-200x200.png new file mode 100644 index 0000000000000000000000000000000000000000..cbe4d4bec94f706b5998b44b8c48285794deffe9 GIT binary patch literal 834 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yaTa()7BevL9R^{>lpinR(g8$%zH2dih1^v)|cBF)%Ql_jGX#shIQj>O#M1g%a%# zp9i}iPFt8^ZkUlbuQ5I0gT#VY&(hl576r`coUHI!m0grmO;Hd!J85=&*nGKvIrH|#Bo_T(Du6&@JeE+suC#%}JU2lai{kmIqTYbsbU-$Q&=Vo$L5MUVy;-UU28H3E- TRwYWn^v~ew>gTe~DWM4f7-aNz literal 0 HcmV?d00001 diff --git a/tests/test.svg b/tests/test.svg new file mode 100644 index 0000000..1d0ad19 --- /dev/null +++ b/tests/test.svg @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +