big overhaul of structure, stubs of tests

This commit is contained in:
Joby Elliott 2020-10-01 12:05:10 -06:00
parent e95b1d7413
commit fa8607b276
20 changed files with 623 additions and 143 deletions

View file

@ -4,7 +4,7 @@ A tightly-focused library for performing a very limited set of simple image tran
## Roadmap
This library is under active development, and until a 1.0 release is made you should expect it to potentially change its API and functionality. Possibly **drastically**. That said, I *do* have use of this thing for work, so I'm probably going to be working pretty dang hard on it, and hope to have a stable release by about November of 2020.
This library is under active development, and until a 1.0 release is made you should expect it to potentially be broken, and unexpectedly and dramatically change its API and functionality. That said, I *do* have use of this thing for work, so I'm probably going to be working pretty dang hard on it, and hope to have a stable release by about November of 2020.
### Drivers
@ -19,22 +19,40 @@ A 1.0 release will not be made until the following drivers are available and sol
A 1.0 release will be made available once the following transforms are available and solidly tested across all drivers. These are basically what I see as the bare minimum for such a library to be useful.
* rotate (in 90 degree increments)
* mirror-h
* mirror-v
* max-width
* max-height
* fit (basically an alias for max-width and max-height)
* cover (scale to cover a box, then crop excess)
* crop (crop toward the center to a given size)
* cover-crop (convenience transform, combining cover and crop, useful for thumbnails)
* orientation
* rotate (in 90 degree increments)
* mirror-h
* mirror-v
* size
* fit (basically an alias for max-width and max-height)
* cover (scale to cover a box, then crop excess)
* crop (crop toward the center to a given size)
* cover-crop (convenience transform, combining cover and crop, useful for thumbnails)
The following transforms are also on my mind as possibilities, but may or may not make it into 1.0:
More complex, and also lesser used effects/stages that may or may not make it into 1.0
* grayscale
* colorize (needs to function consistently though)
* overlay (i.e. for watermarking)
* blur
* hue
* saturation
* brightness
* color effects
* grayscale
* colorize
* content effects
* overlay
* blur
* hue
* saturation
* brightness
#### Order of operations
In the name of simplicity and ease of use, the effective order of operations will always be as reflected above:
1. Orientation
2. Resizing and cropping
3. Color effects
4. Content-changing effects
This is only the effective order of operations, because to improve performance the *actual* order of operations, if applicable, will be:
1. Resizing and cropping
2. Orientation
3. Color effects
4. Content-changing effects

View file

@ -21,5 +21,14 @@
"require": {
"php": ">=7.1",
"ext-gd": "*"
},
"require-dev": {
"atoum/atoum": "^3.4",
"atoum/stubs": "^2.6"
},
"autoload-dev": {
"psr-4": {
"ByJoby\\ImageTransform\\tests\\": "tests/"
}
}
}

139
composer.lock generated
View file

@ -4,17 +4,148 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6eb3038135c5de665d7b86ae7ecb3427",
"content-hash": "5972ee7f683ce7e0b61ef2140ae044e9",
"packages": [],
"packages-dev": [],
"packages-dev": [
{
"name": "atoum/atoum",
"version": "3.4.2",
"source": {
"type": "git",
"url": "https://github.com/atoum/atoum.git",
"reference": "e90606b89e62c5c18c5d02596078edf55f35b3c3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/atoum/atoum/zipball/e90606b89e62c5c18c5d02596078edf55f35b3c3",
"reference": "e90606b89e62c5c18c5d02596078edf55f35b3c3",
"shasum": ""
},
"require": {
"ext-hash": "*",
"ext-json": "*",
"ext-tokenizer": "*",
"ext-xml": "*",
"php": "^5.6.0 || ^7.0.0 <7.5.0"
},
"replace": {
"mageekguy/atoum": "*"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2"
},
"suggest": {
"atoum/stubs": "Provides IDE support (like autocompletion) for atoum",
"ext-mbstring": "Provides support for UTF-8 strings",
"ext-xdebug": "Provides code coverage report (>= 2.3)"
},
"bin": [
"bin/atoum"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"classmap": [
"classes/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Frédéric Hardy",
"email": "frederic.hardy@atoum.org",
"homepage": "http://blog.mageekbox.net"
},
{
"name": "François Dussert",
"email": "francois.dussert@atoum.org"
},
{
"name": "Gérald Croes",
"email": "gerald.croes@atoum.org"
},
{
"name": "Julien Bianchi",
"email": "julien.bianchi@atoum.org"
},
{
"name": "Ludovic Fleury",
"email": "ludovic.fleury@atoum.org"
}
],
"description": "Simple modern and intuitive unit testing framework for PHP 5.3+",
"homepage": "http://www.atoum.org",
"keywords": [
"TDD",
"atoum",
"test",
"unit testing"
],
"time": "2020-03-04T10:29:09+00:00"
},
{
"name": "atoum/stubs",
"version": "2.6.0",
"source": {
"type": "git",
"url": "https://github.com/atoum/stubs.git",
"reference": "df8b73b0358de7283ecba91d8f4a9683f583993d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/atoum/stubs/zipball/df8b73b0358de7283ecba91d8f4a9683f583993d",
"reference": "df8b73b0358de7283ecba91d8f4a9683f583993d",
"shasum": ""
},
"suggest": {
"atoum/atoum": "Include atoum in your projet dependencies"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Julien Bianchi",
"email": "julien.bianchi@atoum.org"
},
{
"name": "Ludovic Fleury",
"email": "ludovic.fleury@atoum.org"
}
],
"description": "Stubs for atoum, the simple modern and intuitive unit testing framework for PHP 5.3+",
"homepage": "http://www.atoum.org",
"keywords": [
"TDD",
"atoum",
"test",
"unit testing"
],
"time": "2018-01-29T22:41:37+00:00"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=7.1",
"ext-gd": "*"
},
"platform-dev": [],
"plugin-api-version": "1.1.0"
"platform-dev": []
}

2
debug.log Normal file
View file

@ -0,0 +1,2 @@
[1001/084735.845:ERROR:registration_protocol_win.cc(103)] CreateFile: The system cannot find the file specified. (0x2)
[1001/084736.163:ERROR:registration_protocol_win.cc(103)] CreateFile: The system cannot find the file specified. (0x2)

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 KiB

30
examples/usage.php Normal file
View file

@ -0,0 +1,30 @@
<?php
use ByJoby\ImageTransform\Drivers\ImagickCLIDriver;
use ByJoby\ImageTransform\Sizers\Cover;
use ByJoby\ImageTransform\Sizers\Crop;
use ByJoby\ImageTransform\Sizers\Fit;
use ByJoby\ImageTransform\Sizers\Original;
include __DIR__ . '/../vendor/autoload.php';
$driver = new ImagickCLIDriver();
// $sizer = new Fit(1000, 500);
// $sizer = new Original();
$sizer = new Crop(500,400);
$image = $driver->image(
'example-portrait.jpg',
$sizer
);
// $image->rotate();
var_dump(
'crop: width: '.$image->sizer()->cropToWidth(),
'crop: height: '.$image->sizer()->cropToHeight(),
'resize: width: '.$image->sizer()->resizeToWidth(),
'resize: height: '.$image->sizer()->resizeToHeight(),
'final: width: '.$image->width(),
'final: height: '.$image->height(),
$image
);

View file

@ -2,9 +2,9 @@
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform;
use ByJoby\ImageTransform\Sizers\AbstractSizer;
interface DriverInterface
{
public function source(string $source);
public function originalWidth(): int;
public function originalHeight(): int;
public function image(string $src, AbstractSizer $sizer): Image;
}

View file

@ -0,0 +1,21 @@
<?php
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\Drivers;
abstract class AbstractCLIDriver extends AbstractDriver
{
protected $executablePath = '';
public function __construct()
{
if (!function_exists('exec')) {
throw new \Exception("CLI drivers can't be used with the current configuration because exec is disabled");
}
parent::__construct();
}
public function executablePath($name)
{
return $this->executablePath.$name;
}
}

View file

@ -3,23 +3,64 @@
namespace ByJoby\ImageTransform\Drivers;
use ByJoby\ImageTransform\DriverInterface;
use ByJoby\ImageTransform\Image;
use ByJoby\ImageTransform\Sizers\AbstractSizer;
abstract class AbstractDriver implements DriverInterface
{
protected $source;
protected $tmpDir = null;
protected $chmod = 0775;
public function source(string $source)
public function __construct()
{
// set source
$this->source = $source;
// validate file
if (!is_file($this->source)) {
throw new \Exception("Image file doesn't exist: " . htmlentities($this->source));
$this->__clone();
}
public function __clone()
{
$this->setTempDir(sys_get_temp_dir() . '/byjoby_image-transform/' . uniqid("",true));
}
public function image(string $src, AbstractSizer $sizer): Image
{
return new Image($src, $this, $sizer);
}
public function setTempDir(string $dir)
{
if (!$this->mkdir($dir)) {
throw new \Exception("Temp directory " . htmlentities($dir) . " doesn't exist or isn't writeable, and couldn't be created.");
}
if (!exif_imagetype($this->source)) {
throw new \Exception("Invalid image file: " . htmlentities($this->source));
$this->tmpDir = $dir;
}
protected function mkdir(string $dir)
{
// return true if dir exists and is writeable
if (is_dir($dir) && is_writeable($dir)) {
return true;
}
// recursively ensure parent directory exists
$parent = dirname($dir);
$this->mkdir($parent);
// try to create this directory if it doesn't exist
if (is_dir($parent)) {
// check parent permissions
if (!is_writeable($parent)) {
chmod($parent, $this->chmod);
}
if (!is_writeable($parent)) {
return false;
}
// create this directory
if (!mkdir($dir)) {
return false;
}
chmod($dir, $this->chmod);
return is_writeable($dir);
} else {
// parent doesn't exist, so recursive call failed
return false;
}
// get height/width
list($this->originalWidth, $this->originalHeight) = getimagesize($this->source);
}
}

View file

@ -9,5 +9,5 @@ namespace ByJoby\ImageTransform\Drivers;
* require that you have CLI Imagick installed, and that your server allows
* PHP's exec() function to use it.
*/
class ImagickCLIDriver extends AbstractDriver
class ImagickCLIDriver extends AbstractCLIDriver
{}

View file

@ -2,30 +2,104 @@
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform;
use ByJoby\ImageTransform\Sizers\AbstractSizer;
class Image
{
protected $src, $driver;
protected $transforms = [];
protected $source, $driver;
protected $originalWidth, $originalHeight;
protected $rotation = 0;
protected $flipH = false;
protected $flipV = false;
protected $sizer = null;
public function __construct(string $src, DriverInterface $driver)
public function __construct(string $source, DriverInterface $driver, AbstractSizer $sizer)
{
$this->src = $src;
$this->source($source);
$this->sizer($sizer);
$this->driver = clone $driver;
$this->driver->source($src);
}
public function transform(TransformInterface $transform)
public function source(string $source)
{
$this->transforms[] = $transform;
// set source
$this->source = realpath($source);
if (!$this->source) {
throw new \Exception("Source image not found: " . htmlentities($source));
}
// validate file
if (!is_file($this->source)) {
throw new \Exception("Image file doesn't exist: " . htmlentities($this->source));
}
if (!exif_imagetype($this->source)) {
throw new \Exception("Invalid image file: " . htmlentities($this->source));
}
// get height/width
list($this->originalWidth, $this->originalHeight) = getimagesize($this->source);
}
public function sizer(AbstractSizer $sizer = null): AbstractSizer
{
if ($sizer) {
$this->sizer = clone $sizer;
$this->sizer->image($this);
}
return $this->sizer;
}
public function rotate(int $steps = 1)
{
$this->rotation = ($this->rotation + $steps) % 4;
}
public function rotation(): int
{
return $this->rotation;
}
public function flipH()
{
$this->flipH = !$this->flipH;
if ($this->flipH && $this->flipV) {
$this->flipH = $this->flipV = false;
}
}
public function flipV()
{
$this->flipV = !$this->flipV;
if ($this->flipH && $this->flipV) {
$this->flipH = $this->flipV = false;
}
}
public function width(): int
{
return $this->sizer->width();
}
public function height(): int
{
return $this->sizer->height();
}
public function ratio(): float
{
return $this->width() / $this->height();
}
public function originalRatio(): float
{
return $this->originalWidth() / $this->originalHeight();
}
public function originalWidth(): int
{
return $this->driver->originalWidth();
return $this->originalWidth;
}
public function originalHeight(): int
{
return $this->driver->originalHeight();
return $this->originalHeight;
}
}

View file

@ -0,0 +1,53 @@
<?php
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\Sizers;
use ByJoby\ImageTransform\Image;
abstract class AbstractSizer
{
protected $image;
abstract public function width(): int;
abstract public function height(): int;
abstract public function cropToWidth(): ?int;
abstract public function cropToHeight(): ?int;
public function resizeToWidth(): ?int
{
return $this->width();
}
public function resizeToHeight(): ?int
{
return $this->height();
}
public function originalWidth(): int
{
if ($this->image->rotation() % 2) {
return $this->image->originalHeight();
}else {
return $this->image->originalWidth();
}
}
public function originalHeight(): int
{
if ($this->image->rotation() % 2) {
return $this->image->originalWidth();
}else {
return $this->image->originalHeight();
}
}
public function originalRatio(): float
{
return $this->originalWidth()/$this->originalHeight();
}
public function image(Image $image)
{
$this->image = $image;
}
}

54
src/Sizers/Cover.php Normal file
View file

@ -0,0 +1,54 @@
<?php
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\Sizers;
class Cover extends AbstractSizer
{
protected $width, $height;
public function __construct(int $width, int $height)
{
$this->width = $width;
$this->height = $height;
}
protected function targetRatio(): float
{
return $this->width / $this->height;
}
protected function calculateSize(): array
{
if ($this->targetRatio() < $this->originalRatio()) {
$height = $this->height;
$width = round($height * $this->originalRatio());
} else {
$width = $this->width;
$height = round($width / $this->originalRatio());
}
return [
'height' => $height,
'width' => $width,
];
}
public function width(): int
{
return $this->calculateSize()['width'];
}
public function height(): int
{
return $this->calculateSize()['height'];
}
public function cropToWidth(): ?int
{
return $this->width;
}
public function cropToHeight(): ?int
{
return $this->height;
}
}

44
src/Sizers/Crop.php Normal file
View file

@ -0,0 +1,44 @@
<?php
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\Sizers;
class Crop extends AbstractSizer
{
protected $width, $height;
public function __construct(int $width, int $height)
{
$this->width = $width;
$this->height = $height;
}
public function width(): int
{
return $this->width;
}
public function height(): int
{
return $this->height;
}
public function resizeToHeight(): ?int
{
return null;
}
public function resizeToWidth(): ?int
{
return null;
}
public function cropToWidth(): ?int
{
return $this->width;
}
public function cropToHeight(): ?int
{
return $this->height;
}
}

54
src/Sizers/Fit.php Normal file
View file

@ -0,0 +1,54 @@
<?php
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\Sizers;
class Fit extends AbstractSizer
{
protected $width, $height;
public function __construct(int $width, int $height)
{
$this->width = $width;
$this->height = $height;
}
public function cropToWidth(): ?int
{
return null;
}
public function cropToHeight(): ?int
{
return null;
}
protected function targetRatio(): float
{
return $this->width / $this->height;
}
protected function calculateSize(): array
{
if ($this->targetRatio() > $this->originalRatio()) {
$height = $this->height;
$width = round($height * $this->originalRatio());
} else {
$width = $this->width;
$height = round($width / $this->originalRatio());
}
return [
'height' => $height,
'width' => $width,
];
}
public function width(): int
{
return $this->calculateSize()['width'];
}
public function height(): int
{
return $this->calculateSize()['height'];
}
}

36
src/Sizers/Original.php Normal file
View file

@ -0,0 +1,36 @@
<?php
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\Sizers;
class Original extends AbstractSizer
{
public function resizeToWidth(): ?int
{
return null;
}
public function resizeToHeight(): ?int
{
return null;
}
public function cropToWidth(): ?int
{
return null;
}
public function cropToHeight(): ?int
{
return null;
}
public function width(): int
{
return $this->originalWidth();
}
public function height(): int
{
return $this->originalHeight();
}
}

View file

@ -1,52 +0,0 @@
<?php
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform;
interface TransformInterface
{
/**
* Whether this transformer changes the contents of the image. Used when
* optimizing transformation order.
*/
const CHANGES_CONTENT = false;
/**
* Whether this transformer changes the size of the image. Used when
* optimizing transformation order.
*/
const CHANGES_SIZE = false;
/**
* Whether this transformer changes the colors of the image. Used when
* optimizing transformation order.
*/
const CHANGES_COLOR = false;
/**
* Whether or not applying this transform multiple times will always
* produce the same result. Used with optimizing transformation order.
*/
const TRANSFORM_STABLE = true;
/**
* Optionally return an array of other transforms that perform the
* operations necessary to perform this single transform.
*
* If this method returns anything, the Image object will add the
* Transformers returned here, and this object will be discarded.
*
* This allows new Transformers to be constructed by composing
* existing ones.
*
* @return array
*/
public function chain(): array;
/**
* Return whether or not this transformation will impact the given
* Image. Should return true if it's possible, as it will be
* skipped if it returns false.
* @return bool
*/
public function willTransform(Image $image): bool;
}

View file

@ -1,19 +0,0 @@
<?php
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\Drivers;
use ByJoby\ImageTransform\Image;
use ByJoby\ImageTransform\TransformInterface;
abstract class AbstractTransform implements TransformInterface
{
public function chain(): array
{
return [];
}
public function willTransform(Image $image): bool
{
return true;
}
}

View file

@ -1,26 +0,0 @@
<?php
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\Drivers;
use ByJoby\ImageTransform\Image;
class MaxWidth extends AbstractTransform
{
const CHANGES_SIZE = true;
protected $size;
public function __construct(int $size)
{
$this->size = $size;
}
public function willTransform(Image $image): bool
{
if ($image->originalWidth() <= $this->size) {
return false;
} else {
return true;
}
}
}

View file

@ -0,0 +1,10 @@
<?php
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\tests\units\Sizers;
use atoum;
class Cover extends atoum
{
}