Compare commits

...

2 commits
main ... v0.2

39 changed files with 821 additions and 562 deletions

View file

@ -1,30 +0,0 @@
<?php
use mageekguy\atoum\reports;
$runner
->addTestsFromDirectory(__DIR__ . '/tests/units');
$runner->getScore()->getCoverage()->excludeDirectory(__DIR__ . '/tests/units/mock');
$travis = getenv('TRAVIS');
if ($travis) {
$script->addDefaultReport();
$coverallsToken = getenv('COVERALLS_REPO_TOKEN');
if ($coverallsToken) {
$coverallsReport = new reports\asynchronous\coveralls('classes', $coverallsToken);
$defaultFinder = $coverallsReport->getBranchFinder();
$coverallsReport
->setBranchFinder(function () use ($defaultFinder) {
if (($branch = getenv('TRAVIS_BRANCH')) === false) {
$branch = $defaultFinder();
}
return $branch;
}
)
->setServiceName('travis-ci')
->setServiceJobId(getenv('TRAVIS_JOB_ID'))
->addDefaultWriter()
;
$runner->addReport($coverallsReport);
}
}

19
.devcontainer/Dockerfile Normal file
View file

@ -0,0 +1,19 @@
FROM ubuntu:22.04
# prepare to install php 8.2
RUN apt update && apt install -y software-properties-common
RUN add-apt-repository ppa:ondrej/php
RUN apt update
# install php 8.2 and other fundamental packages
RUN export DEBIAN_FRONTEND=noninteractive; apt install -y --no-install-recommends php8.2 php-curl git openssl unzip
# install composer and its CA certificates
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
COPY --from=composer:latest /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
# install the PHP extensions that basically all PHP projects should need
RUN export DEBIAN_FRONTEND=noninteractive; apt install -y php8.2-opcache php-xdebug php-mbstring php-zip php-gd php-xml
# install extensions that are more project-specific
RUN export DEBIAN_FRONTEND=noninteractive; apt install -y php-gmagick

View file

@ -0,0 +1,37 @@
{
// build from dockerfile
"build": {
"dockerfile": "Dockerfile",
"context": ".."
},
// specify run arguments
"runArgs": [
"--dns=8.8.8.8" // for some reason DNS doesn't work right unless we explicitly name a DNS server
],
// mount entire sites_v2 directory, so we can access global config and shared DB
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace/${localWorkspaceFolderBasename},type=bind,consistency=cached",
"workspaceFolder": "/workspace/${localWorkspaceFolderBasename}",
// specify extensions that we want
"customizations": {
"vscode": {
"extensions": [
"DEVSENSE.intelli-php-vscode",
"DEVSENSE.phptools-vscode",
"DEVSENSE.profiler-php-vscode",
"DEVSENSE.composer-php-vscode",
"SanderRonde.phpstan-vscode",
"sibiraj-s.vscode-scss-formatter",
"mrmlnc.vscode-scss",
"Gruntfuggly.todo-tree",
"redhat.vscode-yaml",
"oliversturm.fix-json",
"ecmel.vscode-html-css",
"yzhang.markdown-all-in-one",
"DavidAnson.vscode-markdownlint",
"helixquar.randomeverything",
"neilbrayfield.php-docblocker",
"ms-vscode.test-adapter-converter"
]
}
}
}

14
.github/workflows/phpstan.yaml vendored Normal file
View 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
View 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
View file

@ -3,4 +3,5 @@ composer.phar
composer.lock
debug.log
/examples/out/*
!.gitkeep
!.gitkeep
.phpunit.result.cache

View file

@ -1,9 +0,0 @@
language: php
php:
- 7.1
- 7.2
- 7.3
- 7.4
install:
- composer install
script: composer test

View file

@ -1,9 +1,6 @@
# image-transform
[![Build Status](https://travis-ci.org/jobyone/image-transform.svg?branch=main)](https://travis-ci.org/jobyone/image-transform)
[![Coverage Status](https://coveralls.io/repos/github/jobyone/image-transform/badge.svg?branch=main)](https://coveralls.io/github/jobyone/image-transform?branch=main)
A tightly-focused library for performing a very limited set of simple image transformations. This library's purpose is to eschew the standard kitchen sink approach to PHP image libraries in favor of high performance, wide driver support, and a dead simple API.
A tightly-focused library for performing a very limited set of simple image transformations. This library's purpose is to eschew the standard kitchen sink approach to PHP image libraries in favor of high performance, wider driver support, and a dead simple API.
## Current state
@ -11,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 | Overlay | Grayscale | Colorize |
| :--------- | :----: | :----: | :----: | :--: | :-----: | :-------: | :------: |
| 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
@ -25,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
@ -57,7 +52,7 @@ More complex, and also lesser used effects/stages that may or may not make it in
#### Order of operations
In the name of simplicity and ease of use, the effective order of operations will always be as reflected above:
In the name of simplicity, ease of use, and performance, the effective order of operations will always and only be:
1. Orientation
2. Resizing and cropping

View file

@ -15,23 +15,22 @@
}
},
"suggest": {
"ext-imagick": "to use the Imagick implementation",
"ext-imagick": "to use the ImageMagick implementation",
"ext-gmagick": "to use the Gmagick implementation"
},
"require": {
"php": ">=7.1",
"php": ">=8.1",
"ext-gd": "*"
},
"require-dev": {
"atoum/atoum": "^3.4",
"atoum/stubs": "^2.6"
"ext-gmagick": "*",
"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"
}
}

View file

@ -1,12 +1,12 @@
<?php
use ByJoby\ImageTransform\Drivers\ImagickCLIDriver;
use ByJoby\ImageTransform\Drivers\MagickCliDriver;
use ByJoby\ImageTransform\Sizers\Cover;
include __DIR__ . '/../vendor/autoload.php';
// first step is instantiate a Driver, in this case ImagickCLI
$driver = new ImagickCLIDriver();
$driver = new MagickCliDriver();
// instantiate an Image object using a source file and Sizer
// in this example we're covering a 200x500 box

4
phpstan.neon Normal file
View file

@ -0,0 +1,4 @@
parameters:
level: max
paths:
- src

31
phpunit.xml Normal file
View 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>

26
src/DefaultDriver.php Normal file
View file

@ -0,0 +1,26 @@
<?php
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform;
use ByJoby\ImageTransform\Drivers\GDDriver;
/**
* Stores a default driver to be used when a driver is not set for an Image.
* Defaults to a GD driver.
*/
class DefaultDriver
{
/** @var DriverInterface|null */
protected static $driver;
public static function get(): DriverInterface
{
return static::$driver
?? static::$driver = new GDDriver();
}
public static function set(DriverInterface $driver): void
{
static::$driver = $driver;
}
}

View file

@ -6,6 +6,24 @@ use ByJoby\ImageTransform\Sizers\AbstractSizer;
interface DriverInterface
{
public function image(string $src, AbstractSizer $sizer): Image;
/**
* Save an image with its current settings. If a filename is specified the
* image will be saved to it and null returned, otherwise the image will be
* returned as a string.
*
* @param Image $image
* @param string|null $filename
* @return string|null
*/
public function save(Image $image, ?string $filename = null): ?string;
}
/**
* Set the temp directory in which files should be created if necessary for
* processing images. A directory in the system temp folder will be used by
* default.
*
* @param string $dir
* @return static
*/
public function setTempDir(string $dir): static;
}

View file

@ -1,13 +0,0 @@
<?php
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
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");
}
}
}

View file

@ -0,0 +1,7 @@
<?php
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\Drivers;
abstract class AbstractCliDriver extends AbstractDriver
{
}

View file

@ -5,41 +5,38 @@ 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;
protected $chmod = 0775;
/** @var int */
protected $chmod_dir = 0775;
/** @var int */
protected $chmod_file = 0665;
abstract protected function doSave(Image $image, string $filename);
public function __clone()
{
$this->tempDir = null;
}
public function image(string $src, AbstractSizer $sizer): Image
{
return new Image($src, $this, $sizer);
}
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;
}
protected function setTempDir(string $dir)
public function setTempDir(string $dir): static
{
if (!$this->mkdir($dir)) {
throw new \Exception("Temp directory " . htmlentities($dir) . " doesn't exist or isn't writeable, and couldn't be created.");
}
$this->tempDir = $dir;
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)) {
@ -52,7 +49,7 @@ abstract class AbstractDriver implements DriverInterface
if (is_dir($parent)) {
// check parent permissions
if (!is_writeable($parent)) {
chmod($parent, $this->chmod);
chmod($parent, $this->chmod_dir);
}
if (!is_writeable($parent)) {
return false;
@ -61,7 +58,7 @@ abstract class AbstractDriver implements DriverInterface
if (!mkdir($dir)) {
return false;
}
chmod($dir, $this->chmod);
chmod($dir, $this->chmod_dir);
return is_writeable($dir);
} else {
// parent doesn't exist, so recursive call failed
@ -82,12 +79,18 @@ 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() . '/save.jpg';
$filename = $this->tempDir() . '/' . uniqid() . '.jpg';
$this->doSave($image, $filename);
return file_get_contents($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;
}
}
}
}

View file

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

View file

@ -3,10 +3,12 @@
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, but support is basically universal.
* This driver uses PHP's built-in GD libary. This is by far the slowest driver,
* and its memory use is absolutely atrocious for large images, but support is
* basically universal.
*/
class GDDriver extends AbstractExtensionDriver
{
@ -17,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()
);
@ -63,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 {
@ -82,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));
}
}
}
}

View file

@ -5,20 +5,16 @@ namespace ByJoby\ImageTransform\Drivers;
use ByJoby\ImageTransform\Image;
/**
* This driver uses exec() and command-line ImageMagick utilities to
* transform images. It is likely approaching the limits of how fast this
* library can possibly be. The downside is that it will only run if
* you have exec() enabled, and your server allows it to execute the
* ImageMagick binaries.
* This driver uses exec() and command-line ImageMagick (or Gmagick) utilities
* to transform images. It is likely approaching the limits of how fast this
* library can possibly be. The downside is that it will only run if you have
* exec() enabled, and your server allows it to execute the ImageMagick/Gmagick
* binaries and use them to read and write image files.
*/
class ImagickCLIDriver extends AbstractCLIDriver
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 ImagickCLIDriver 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 = [

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

View file

@ -3,21 +3,31 @@
namespace ByJoby\ImageTransform;
use ByJoby\ImageTransform\Sizers\AbstractSizer;
use ByJoby\ImageTransform\Sizers\Original;
class Image
{
protected $source, $driver;
protected $originalWidth, $originalHeight;
/** @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, DriverInterface $driver, AbstractSizer $sizer)
public function __construct(string $source, AbstractSizer|null $sizer = null)
{
$this->setSource($source);
$this->setSizer($sizer);
$this->driver = clone $driver;
$this->setSizer($sizer ?? new Original());
}
public function source(): string
@ -25,13 +35,14 @@ class Image
return $this->source;
}
public function setSource(string $source)
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));
@ -40,7 +51,13 @@ 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;
}
public function sizer(): AbstractSizer
@ -48,15 +65,17 @@ class Image
return $this->sizer;
}
public function setSizer(AbstractSizer $sizer)
public function setSizer(AbstractSizer $sizer): static
{
$this->sizer = clone $sizer;
$this->sizer->image($this);
$this->sizer->setImage($this);
return $this;
}
public function rotate(int $steps = 1)
public function rotate(int $steps = 1): static
{
$this->rotation = ($this->rotation + $steps) % 4;
return $this;
}
public function rotation(): int
@ -64,22 +83,24 @@ class Image
return $this->rotation;
}
public function flipH()
public function flipH(): static
{
$this->flipH = !$this->flipH;
return $this;
}
public function flipV()
public function flipV(): static
{
$this->flipV = !$this->flipV;
return $this;
}
public function getFlipH()
public function getFlipH(): bool
{
return $this->flipH;
}
public function getFlipV()
public function getFlipV(): bool
{
return $this->flipV;
}
@ -114,8 +135,14 @@ class Image
return $this->originalHeight;
}
public function save(string $file)
public function driver(): DriverInterface
{
$this->driver->save($this, $file);
return $this->driver ?? DefaultDriver::get();
}
}
public function save(string $file): static
{
$this->driver()->save($this, $file);
return $this;
}
}

View file

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

View file

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

View file

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

View file

@ -2,6 +2,9 @@
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\Sizers;
/**
* This sizer does no manipulations and keeps the image the size it originally was.
*/
class Original extends AbstractSizer
{
public function resizeToWidth(): ?int

109
tests/Sizers/CoverTest.php Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 B

BIN
tests/input-200x100.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

BIN
tests/input-200x200.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 834 B

196
tests/test.svg Normal file
View 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

View file

@ -1,173 +0,0 @@
<?php
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\tests\units\Sizers;
use atoum;
use ByJoby\ImageTransform\Sizers\Cover as SizerUnderTest;
use ByJoby\ImageTransform\tests\units\mock\MockDriver;
class Cover extends atoum
{
public function testPortraitWhenImageIsTaller()
{
$this
->given($image = $this->image(__DIR__.'/../100x200.jpg',100,150))
->integer($image->width())->isEqualTo(100)
->integer($image->height())->isEqualTo(150)
->variable($image->sizer()->resizeToWidth())->isEqualTo(100)
->variable($image->sizer()->resizeToHeight())->isEqualTo(200)
->variable($image->sizer()->cropToWidth())->isEqualTo(100)
->variable($image->sizer()->cropToHeight())->isEqualTo(150)
->given($image->rotate())
->integer($image->width())->isEqualTo(100)
->integer($image->height())->isEqualTo(150)
->variable($image->sizer()->resizeToWidth())->isEqualTo(300)
->variable($image->sizer()->resizeToHeight())->isEqualTo(150)
->variable($image->sizer()->cropToWidth())->isEqualTo(100)
->variable($image->sizer()->cropToHeight())->isEqualTo(150)
;
}
public function testPortraitWhenImageIsShorter()
{
$this
->given($image = $this->image(__DIR__.'/../100x200.jpg',50,300))
->integer($image->width())->isEqualTo(50)
->integer($image->height())->isEqualTo(300)
->variable($image->sizer()->resizeToWidth())->isEqualTo(150)
->variable($image->sizer()->resizeToHeight())->isEqualTo(300)
->variable($image->sizer()->cropToWidth())->isEqualTo(50)
->variable($image->sizer()->cropToHeight())->isEqualTo(300)
->given($image->rotate())
->integer($image->width())->isEqualTo(50)
->integer($image->height())->isEqualTo(300)
->variable($image->sizer()->resizeToWidth())->isEqualTo(600)
->variable($image->sizer()->resizeToHeight())->isEqualTo(300)
->variable($image->sizer()->cropToWidth())->isEqualTo(50)
->variable($image->sizer()->cropToHeight())->isEqualTo(300)
;
}
public function testPortraitWithSquareImage()
{
$this
->given($image = $this->image(__DIR__.'/../200x200.jpg',75,300))
->integer($image->width())->isEqualTo(75)
->integer($image->height())->isEqualTo(300)
->variable($image->sizer()->resizeToWidth())->isEqualTo(300)
->variable($image->sizer()->resizeToHeight())->isEqualTo(300)
->variable($image->sizer()->cropToWidth())->isEqualTo(75)
->variable($image->sizer()->cropToHeight())->isEqualTo(300)
;
}
public function testPortraitWithLandscapeImage()
{
$this
->given($image = $this->image(__DIR__.'/../200x100.jpg',100,200))
->integer($image->width())->isEqualTo(100)
->integer($image->height())->isEqualTo(200)
->variable($image->sizer()->resizeToWidth())->isEqualTo(400)
->variable($image->sizer()->resizeToHeight())->isEqualTo(200)
->variable($image->sizer()->cropToWidth())->isEqualTo(100)
->variable($image->sizer()->cropToHeight())->isEqualTo(200)
;
}
public function testLandscapeWhenImageIsTaller()
{
$this
->given($image = $this->image(__DIR__.'/../200x100.jpg',150,100))
->integer($image->width())->isEqualTo(150)
->integer($image->height())->isEqualTo(100)
->variable($image->sizer()->resizeToWidth())->isEqualTo(200)
->variable($image->sizer()->resizeToHeight())->isEqualTo(100)
->variable($image->sizer()->cropToWidth())->isEqualTo(150)
->variable($image->sizer()->cropToHeight())->isEqualTo(100)
;
}
public function testLandscapeWhenImageIsShorter()
{
$this
->given($image = $this->image(__DIR__.'/../200x100.jpg',300,75))
->integer($image->width())->isEqualTo(300)
->integer($image->height())->isEqualTo(75)
->variable($image->sizer()->resizeToWidth())->isEqualTo(300)
->variable($image->sizer()->resizeToHeight())->isEqualTo(150)
->variable($image->sizer()->cropToWidth())->isEqualTo(300)
->variable($image->sizer()->cropToHeight())->isEqualTo(75)
;
}
public function testLandscapeWithSquareImage()
{
$this
->given($image = $this->image(__DIR__.'/../200x200.jpg',300,75))
->integer($image->width())->isEqualTo(300)
->integer($image->height())->isEqualTo(75)
->variable($image->sizer()->resizeToWidth())->isEqualTo(300)
->variable($image->sizer()->resizeToHeight())->isEqualTo(300)
->variable($image->sizer()->cropToWidth())->isEqualTo(300)
->variable($image->sizer()->cropToHeight())->isEqualTo(75)
;
}
public function testLandscapeWithPortraitImage()
{
$this
->given($image = $this->image(__DIR__.'/../100x200.jpg',200,100))
->integer($image->width())->isEqualTo(200)
->integer($image->height())->isEqualTo(100)
->variable($image->sizer()->resizeToWidth())->isEqualTo(200)
->variable($image->sizer()->resizeToHeight())->isEqualTo(400)
->variable($image->sizer()->cropToWidth())->isEqualTo(200)
->variable($image->sizer()->cropToHeight())->isEqualTo(100)
;
}
public function testWithSmallSquareImage()
{
$this
->given($image = $this->image(__DIR__.'/../200x200.jpg',300,300))
->integer($image->width())->isEqualTo(300)
->integer($image->height())->isEqualTo(300)
->variable($image->sizer()->resizeToWidth())->isEqualTo(300)
->variable($image->sizer()->resizeToHeight())->isEqualTo(300)
->variable($image->sizer()->cropToWidth())->isEqualTo(300)
->variable($image->sizer()->cropToHeight())->isEqualTo(300)
;
}
public function testWithSmallLandscapeImage()
{
$this
->given($image = $this->image(__DIR__.'/../200x100.jpg',300,300))
->integer($image->width())->isEqualTo(300)
->integer($image->height())->isEqualTo(300)
->variable($image->sizer()->resizeToWidth())->isEqualTo(600)
->variable($image->sizer()->resizeToHeight())->isEqualTo(300)
->variable($image->sizer()->cropToWidth())->isEqualTo(300)
->variable($image->sizer()->cropToHeight())->isEqualTo(300)
;
}
public function testWithSmallPortraitImage()
{
$this
->given($image = $this->image(__DIR__.'/../100x200.jpg',300,300))
->integer($image->width())->isEqualTo(300)
->integer($image->height())->isEqualTo(300)
->variable($image->sizer()->resizeToWidth())->isEqualTo(300)
->variable($image->sizer()->resizeToHeight())->isEqualTo(600)
->variable($image->sizer()->cropToWidth())->isEqualTo(300)
->variable($image->sizer()->cropToHeight())->isEqualTo(300)
;
}
protected function image(string $path, int $width, int $height)
{
$driver = new MockDriver();
return $driver->image($path, new SizerUnderTest($width, $height));
}
}

View file

@ -1,173 +0,0 @@
<?php
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\tests\units\Sizers;
use atoum;
use ByJoby\ImageTransform\Sizers\Fit as SizerUnderTest;
use ByJoby\ImageTransform\tests\units\mock\MockDriver;
class Fit extends atoum
{
public function testPortraitWhenImageIsTaller()
{
$this
->given($image = $this->image(__DIR__.'/../100x200.jpg',100,150))
->integer($image->width())->isEqualTo(75)
->integer($image->height())->isEqualTo(150)
->variable($image->sizer()->resizeToWidth())->isEqualTo(75)
->variable($image->sizer()->resizeToHeight())->isEqualTo(150)
->variable($image->sizer()->cropToWidth())->isNull()
->variable($image->sizer()->cropToHeight())->isNull()
->given($image->rotate())
->integer($image->width())->isEqualTo(100)
->integer($image->height())->isEqualTo(50)
->variable($image->sizer()->resizeToWidth())->isEqualTo(100)
->variable($image->sizer()->resizeToHeight())->isEqualTo(50)
->variable($image->sizer()->cropToWidth())->isNull()
->variable($image->sizer()->cropToHeight())->isNull()
;
}
public function testPortraitWhenImageIsShorter()
{
$this
->given($image = $this->image(__DIR__.'/../100x200.jpg',50,300))
->integer($image->width())->isEqualTo(50)
->integer($image->height())->isEqualTo(100)
->variable($image->sizer()->resizeToWidth())->isEqualTo(50)
->variable($image->sizer()->resizeToHeight())->isEqualTo(100)
->variable($image->sizer()->cropToWidth())->isNull()
->variable($image->sizer()->cropToHeight())->isNull()
->given($image->rotate())
->integer($image->width())->isEqualTo(50)
->integer($image->height())->isEqualTo(25)
->variable($image->sizer()->resizeToWidth())->isEqualTo(50)
->variable($image->sizer()->resizeToHeight())->isEqualTo(25)
->variable($image->sizer()->cropToWidth())->isNull()
->variable($image->sizer()->cropToHeight())->isNull()
;
}
public function testPortraitWithSquareImage()
{
$this
->given($image = $this->image(__DIR__.'/../200x200.jpg',75,300))
->integer($image->width())->isEqualTo(75)
->integer($image->height())->isEqualTo(75)
->variable($image->sizer()->resizeToWidth())->isEqualTo(75)
->variable($image->sizer()->resizeToHeight())->isEqualTo(75)
->variable($image->sizer()->cropToWidth())->isNull()
->variable($image->sizer()->cropToHeight())->isNull()
;
}
public function testPortraitWithLandscapeImage()
{
$this
->given($image = $this->image(__DIR__.'/../200x100.jpg',100,200))
->integer($image->width())->isEqualTo(100)
->integer($image->height())->isEqualTo(50)
->variable($image->sizer()->resizeToWidth())->isEqualTo(100)
->variable($image->sizer()->resizeToHeight())->isEqualTo(50)
->variable($image->sizer()->cropToWidth())->isNull()
->variable($image->sizer()->cropToHeight())->isNull()
;
}
public function testLandscapeWhenImageIsTaller()
{
$this
->given($image = $this->image(__DIR__.'/../200x100.jpg',150,100))
->integer($image->width())->isEqualTo(150)
->integer($image->height())->isEqualTo(75)
->variable($image->sizer()->resizeToWidth())->isEqualTo(150)
->variable($image->sizer()->resizeToHeight())->isEqualTo(75)
->variable($image->sizer()->cropToWidth())->isNull()
->variable($image->sizer()->cropToHeight())->isNull()
;
}
public function testLandscapeWhenImageIsShorter()
{
$this
->given($image = $this->image(__DIR__.'/../200x100.jpg',300,75))
->integer($image->width())->isEqualTo(150)
->integer($image->height())->isEqualTo(75)
->variable($image->sizer()->resizeToWidth())->isEqualTo(150)
->variable($image->sizer()->resizeToHeight())->isEqualTo(75)
->variable($image->sizer()->cropToWidth())->isNull()
->variable($image->sizer()->cropToHeight())->isNull()
;
}
public function testLandscapeWithSquareImage()
{
$this
->given($image = $this->image(__DIR__.'/../200x200.jpg',300,75))
->integer($image->width())->isEqualTo(75)
->integer($image->height())->isEqualTo(75)
->variable($image->sizer()->resizeToWidth())->isEqualTo(75)
->variable($image->sizer()->resizeToHeight())->isEqualTo(75)
->variable($image->sizer()->cropToWidth())->isNull()
->variable($image->sizer()->cropToHeight())->isNull()
;
}
public function testLandscapeWithPortraitImage()
{
$this
->given($image = $this->image(__DIR__.'/../100x200.jpg',200,100))
->integer($image->width())->isEqualTo(50)
->integer($image->height())->isEqualTo(100)
->variable($image->sizer()->resizeToWidth())->isEqualTo(50)
->variable($image->sizer()->resizeToHeight())->isEqualTo(100)
->variable($image->sizer()->cropToWidth())->isNull()
->variable($image->sizer()->cropToHeight())->isNull()
;
}
public function testWithSmallSquareImage()
{
$this
->given($image = $this->image(__DIR__.'/../200x200.jpg',300,300))
->integer($image->width())->isEqualTo(300)
->integer($image->height())->isEqualTo(300)
->variable($image->sizer()->resizeToWidth())->isEqualTo(300)
->variable($image->sizer()->resizeToHeight())->isEqualTo(300)
->variable($image->sizer()->cropToWidth())->isNull()
->variable($image->sizer()->cropToHeight())->isNull()
;
}
public function testWithSmallLandscapeImage()
{
$this
->given($image = $this->image(__DIR__.'/../200x100.jpg',300,300))
->integer($image->width())->isEqualTo(300)
->integer($image->height())->isEqualTo(150)
->variable($image->sizer()->resizeToWidth())->isEqualTo(300)
->variable($image->sizer()->resizeToHeight())->isEqualTo(150)
->variable($image->sizer()->cropToWidth())->isNull()
->variable($image->sizer()->cropToHeight())->isNull()
;
}
public function testWithSmallPortraitImage()
{
$this
->given($image = $this->image(__DIR__.'/../100x200.jpg',300,300))
->integer($image->width())->isEqualTo(150)
->integer($image->height())->isEqualTo(300)
->variable($image->sizer()->resizeToWidth())->isEqualTo(150)
->variable($image->sizer()->resizeToHeight())->isEqualTo(300)
->variable($image->sizer()->cropToWidth())->isNull()
->variable($image->sizer()->cropToHeight())->isNull()
;
}
protected function image(string $path, int $width, int $height)
{
$driver = new MockDriver();
return $driver->image($path, new SizerUnderTest($width, $height));
}
}

View file

@ -1,36 +0,0 @@
<?php
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\tests\units\Sizers;
use atoum;
use ByJoby\ImageTransform\Sizers\Original as SizerUnderTest;
use ByJoby\ImageTransform\tests\units\mock\MockDriver;
class Original extends atoum
{
public function testRotation()
{
$this
->given($image = $this->image(__DIR__.'/../100x200.jpg'))
->integer($image->width())->isEqualTo(100)
->integer($image->height())->isEqualTo(200)
->variable($image->sizer()->resizeToWidth())->isNull()
->variable($image->sizer()->resizeToHeight())->isNull()
->variable($image->sizer()->cropToWidth())->isNull()
->variable($image->sizer()->cropToHeight())->isNull()
->given($image->rotate())
->integer($image->width())->isEqualTo(200)
->integer($image->height())->isEqualTo(100)
->variable($image->sizer()->resizeToWidth())->isNull()
->variable($image->sizer()->resizeToHeight())->isNull()
->variable($image->sizer()->cropToWidth())->isNull()
->variable($image->sizer()->cropToHeight())->isNull()
;
}
protected function image(string $path)
{
$driver = new MockDriver();
return $driver->image($path, new SizerUnderTest());
}
}

View file

@ -1,14 +0,0 @@
<?php
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\tests\units\mock;
use ByJoby\ImageTransform\Drivers\AbstractDriver;
use ByJoby\ImageTransform\Image;
class MockDriver extends AbstractDriver
{
protected function doSave(Image $image, string $filename)
{
//does nothing
}
}