started work on *magick driver and updated tests
14
.github/workflows/phpstan.yaml
vendored
Normal file
|
@ -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
|
13
.github/workflows/phpunit.yaml
vendored
Normal file
|
@ -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
|
3
.gitignore
vendored
|
@ -3,4 +3,5 @@ composer.phar
|
|||
composer.lock
|
||||
debug.log
|
||||
/examples/out/*
|
||||
!.gitkeep
|
||||
!.gitkeep
|
||||
.phpunit.result.cache
|
16
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
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
4
phpstan.neon
Normal file
|
@ -0,0 +1,4 @@
|
|||
parameters:
|
||||
level: max
|
||||
paths:
|
||||
- src
|
31
phpunit.xml
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
colors="true"
|
||||
executionOrder="random"
|
||||
failOnWarning="true"
|
||||
failOnRisky="true"
|
||||
failOnEmptyTestSuite="true"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
verbose="true"
|
||||
bootstrap="vendor/autoload.php">
|
||||
<php>
|
||||
<ini name="display_errors" value="On" />
|
||||
<ini name="error_reporting" value="-1" />
|
||||
<ini name="xdebug.mode" value="coverage" />
|
||||
</php>
|
||||
<testsuites>
|
||||
<testsuite name="Tests">
|
||||
<directory>tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<coverage>
|
||||
<include>
|
||||
<directory suffix=".php">src</directory>
|
||||
</include>
|
||||
<report>
|
||||
<html outputDirectory="coverage" lowUpperBound="50" highLowerBound="90" />
|
||||
</report>
|
||||
</coverage>
|
||||
</phpunit>
|
|
@ -10,7 +10,8 @@ use ByJoby\ImageTransform\Drivers\GDDriver;
|
|||
*/
|
||||
class DefaultDriver
|
||||
{
|
||||
protected $driver;
|
||||
/** @var DriverInterface|null */
|
||||
protected static $driver;
|
||||
|
||||
public static function get(): DriverInterface
|
||||
{
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 = [
|
||||
|
|
144
src/Drivers/MagickDriver.php
Normal file
|
@ -0,0 +1,144 @@
|
|||
<?php
|
||||
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
|
||||
namespace ByJoby\ImageTransform\Drivers;
|
||||
|
||||
use ByJoby\ImageTransform\Image;
|
||||
use Gmagick;
|
||||
use GmagickPixel;
|
||||
use Imagick;
|
||||
use ImagickPixel;
|
||||
|
||||
/**
|
||||
* This driver uses either ImageMagick or Gmagick, depending on its constructor
|
||||
* argument.
|
||||
*/
|
||||
class MagickDriver extends AbstractExtensionDriver
|
||||
{
|
||||
/**
|
||||
* By default this driver will use Imagick, but if you pass a true value it
|
||||
* will instead use Gmagick.
|
||||
*
|
||||
* @param boolean $use_gmagick
|
||||
*/
|
||||
public function __construct(protected bool $use_gmagick = false)
|
||||
{
|
||||
}
|
||||
|
||||
protected function getImageObject(Image $image): Gmagick|Imagick
|
||||
{
|
||||
if ($this->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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 5.6 KiB |
109
tests/Sizers/CoverTest.php
Normal file
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
|
||||
namespace ByJoby\ImageTransform\Sizers;
|
||||
|
||||
use ByJoby\ImageTransform\Image;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class CoverTest extends TestCase
|
||||
{
|
||||
public function testSquareImageSizedSquare(): void
|
||||
{
|
||||
// mock 200x200 image
|
||||
$image = $this->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());
|
||||
}
|
||||
}
|
BIN
tests/input-100x200.png
Normal file
After Width: | Height: | Size: 774 B |
BIN
tests/input-200x100.png
Normal file
After Width: | Height: | Size: 679 B |
BIN
tests/input-200x200.png
Normal file
After Width: | Height: | Size: 834 B |
196
tests/test.svg
Normal file
|
@ -0,0 +1,196 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="200"
|
||||
height="200"
|
||||
viewBox="0 0 200 200"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
|
||||
sodipodi:docname="test.svg"
|
||||
inkscape:export-filename="input-200x200.png.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="false"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
showguides="true"
|
||||
inkscape:zoom="2.3267609"
|
||||
inkscape:cx="94.552044"
|
||||
inkscape:cy="68.980014"
|
||||
inkscape:window-width="1736"
|
||||
inkscape:window-height="1023"
|
||||
inkscape:window-x="156"
|
||||
inkscape:window-y="153"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<g
|
||||
id="g3">
|
||||
<rect
|
||||
style="fill:#0000ff;stroke-width:0.188982"
|
||||
id="rect1"
|
||||
width="28.571428"
|
||||
height="25"
|
||||
x="0"
|
||||
y="-150"
|
||||
transform="scale(1,-1)" />
|
||||
<rect
|
||||
style="fill:#000000;stroke-width:0.188982"
|
||||
id="rect1-6"
|
||||
width="28.571428"
|
||||
height="25"
|
||||
x="28.571428"
|
||||
y="-150"
|
||||
transform="scale(1,-1)" />
|
||||
<rect
|
||||
style="fill:#ff00ff;stroke-width:0.188982"
|
||||
id="rect1-61"
|
||||
width="28.571428"
|
||||
height="25"
|
||||
x="57.142857"
|
||||
y="-150"
|
||||
transform="scale(1,-1)" />
|
||||
<rect
|
||||
style="fill:#000000;stroke-width:0.188982"
|
||||
id="rect1-6-8"
|
||||
width="28.571428"
|
||||
height="25"
|
||||
x="85.714287"
|
||||
y="-150"
|
||||
transform="scale(1,-1)" />
|
||||
<rect
|
||||
style="fill:#00ffff;stroke-width:0.188982"
|
||||
id="rect1-61-0"
|
||||
width="28.571428"
|
||||
height="25"
|
||||
x="114.28571"
|
||||
y="-150"
|
||||
transform="scale(1,-1)" />
|
||||
<rect
|
||||
style="fill:#000000;stroke-width:0.188982"
|
||||
id="rect1-6-8-2"
|
||||
width="28.571428"
|
||||
height="25"
|
||||
x="142.85715"
|
||||
y="-150"
|
||||
transform="scale(1,-1)" />
|
||||
<rect
|
||||
style="fill:#000080;stroke-width:0.447214"
|
||||
id="rect2"
|
||||
width="40"
|
||||
height="50"
|
||||
x="0"
|
||||
y="150" />
|
||||
<rect
|
||||
style="fill:#f2f2f2;stroke-width:0.632456"
|
||||
id="rect3"
|
||||
width="40"
|
||||
height="50"
|
||||
x="40"
|
||||
y="150" />
|
||||
<rect
|
||||
style="fill:#cccccc;stroke-width:0.188982"
|
||||
id="rect1-61-0-9"
|
||||
width="28.571428"
|
||||
height="25"
|
||||
x="171.42857"
|
||||
y="-150"
|
||||
transform="scale(1,-1)" />
|
||||
<rect
|
||||
style="fill:#cccccc;stroke-width:0.422577"
|
||||
id="rect1-1"
|
||||
width="28.571428"
|
||||
height="125"
|
||||
x="0"
|
||||
y="-125"
|
||||
transform="scale(1,-1)" />
|
||||
<rect
|
||||
style="fill:#808000;stroke-width:0.422577"
|
||||
id="rect1-6-2"
|
||||
width="28.571428"
|
||||
height="125"
|
||||
x="28.571428"
|
||||
y="-125"
|
||||
transform="scale(1,-1)" />
|
||||
<rect
|
||||
style="fill:#008080;stroke-width:0.422577"
|
||||
id="rect1-61-9"
|
||||
width="28.571428"
|
||||
height="125"
|
||||
x="57.142857"
|
||||
y="-125"
|
||||
transform="scale(1,-1)" />
|
||||
<rect
|
||||
style="fill:#008000;stroke-width:0.422577"
|
||||
id="rect1-6-8-3"
|
||||
width="28.571428"
|
||||
height="125"
|
||||
x="85.714287"
|
||||
y="-125"
|
||||
transform="scale(1,-1)" />
|
||||
<rect
|
||||
style="fill:#800080;stroke-width:0.422577"
|
||||
id="rect1-61-0-1"
|
||||
width="28.571428"
|
||||
height="125"
|
||||
x="114.28571"
|
||||
y="-125"
|
||||
transform="scale(1,-1)" />
|
||||
<rect
|
||||
style="fill:#800000;stroke-width:0.422577"
|
||||
id="rect1-6-8-2-9"
|
||||
width="28.571428"
|
||||
height="125"
|
||||
x="142.85715"
|
||||
y="-125"
|
||||
transform="scale(1,-1)" />
|
||||
<rect
|
||||
style="fill:#000080;stroke-width:0.422577"
|
||||
id="rect1-61-0-9-4"
|
||||
width="28.571428"
|
||||
height="125"
|
||||
x="171.42857"
|
||||
y="-125"
|
||||
transform="scale(1,-1)" />
|
||||
<rect
|
||||
style="fill:#800080;stroke-width:0.632456"
|
||||
id="rect3-5"
|
||||
width="40"
|
||||
height="50"
|
||||
x="80"
|
||||
y="150" />
|
||||
<rect
|
||||
style="fill:#4d4d4d;stroke-width:0.632456"
|
||||
id="rect3-5-2"
|
||||
width="40"
|
||||
height="50"
|
||||
x="120"
|
||||
y="150" />
|
||||
<rect
|
||||
style="fill:#1a1a1a;stroke-width:0.632456"
|
||||
id="rect3-5-2-7"
|
||||
width="40"
|
||||
height="50"
|
||||
x="160"
|
||||
y="150" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5.1 KiB |