some fundamentals are actually working

This commit is contained in:
Joby Elliott 2020-10-01 16:07:33 -06:00
parent 67b4bd1209
commit 5cac6c5f64
15 changed files with 316 additions and 81 deletions

2
.gitignore vendored
View file

@ -2,3 +2,5 @@ composer.phar
/vendor/ /vendor/
composer.lock composer.lock
debug.log debug.log
/examples/out/*
!.gitkeep

View file

@ -3,13 +3,23 @@
[![Build Status](https://travis-ci.org/jobyone/image-transform.svg?branch=main)](https://travis-ci.org/jobyone/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) [![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, wide driver support, and a dead simple API.
## Roadmap ## Current state
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. 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.
### Current progress
| Driver | Rotate | Mirror | Resize | Crop | Overlay | Grayscale | Colorize |
| :--------- | :----: | :----: | :----: | :--: | :-----: | :-------: | :------: |
| GD | X | X | X | X | | | |
| Imagick | | | | | | | |
| Gmagick | | | | | | | |
| ImagickCLI | X | X | X | X | | | |
## Roadmap
### Drivers ### Drivers
A 1.0 release will not be made until the following drivers are available and solidly tested: A 1.0 release will not be made until the following drivers are available and solidly tested:
@ -53,10 +63,3 @@ In the name of simplicity and ease of use, the effective order of operations wil
2. Resizing and cropping 2. Resizing and cropping
3. Color effects 3. Color effects
4. Content-changing 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

28
examples/imagick-cli.php Normal file
View file

@ -0,0 +1,28 @@
<?php
use ByJoby\ImageTransform\Drivers\ImagickCLIDriver;
use ByJoby\ImageTransform\Sizers\Cover;
include __DIR__ . '/../vendor/autoload.php';
// first step is instantiate a Driver, in this case ImagickCLI
$driver = new ImagickCLIDriver();
// instantiate an Image object using a source file and Sizer
// in this example we're covering in a 200x500 box
$image = $driver->image(
'example-portrait.jpg',
new Cover(200, 500)
);
// currently the only operation supported are 90 degree rotation and flipping
$image->rotate(2); //rotates by two 90 degree chunks, so 180
$image->flipH(); //flips horizontally, use flipV to flip vertically
// use save() to build the image and save it to a file
$image->save('out/example-2.jpg');
// display the generated file in the browser
echo '<img src="out/example-2.jpg">';
var_dump(memory_get_peak_usage());

0
examples/out/.gitkeep Normal file
View file

View file

@ -1,30 +1,28 @@
<?php <?php
use ByJoby\ImageTransform\Drivers\ImagickCLIDriver; use ByJoby\ImageTransform\Drivers\GDDriver;
use ByJoby\ImageTransform\Sizers\Cover;
use ByJoby\ImageTransform\Sizers\Crop;
use ByJoby\ImageTransform\Sizers\Fit; use ByJoby\ImageTransform\Sizers\Fit;
use ByJoby\ImageTransform\Sizers\Original;
include __DIR__ . '/../vendor/autoload.php'; include __DIR__ . '/../vendor/autoload.php';
$driver = new ImagickCLIDriver(); // first step is instantiate a Driver, in this case GD
// $sizer = new Fit(1000, 500); $driver = new GDDriver();
// $sizer = new Original();
$sizer = new Crop(500,400);
// instantiate an Image object using a source file and Sizer
// in this example we're fitting in a 200x500 box
$image = $driver->image( $image = $driver->image(
'example-portrait.jpg', 'example-portrait.jpg',
$sizer new Fit(200, 500)
); );
// $image->rotate();
var_dump( // currently the only operation supported are 90 degree rotation and flipping
'crop: width: '.$image->sizer()->cropToWidth(), $image->rotate(2); //rotates by two 90 degree chunks, so 180
'crop: height: '.$image->sizer()->cropToHeight(), $image->flipH(); //flips horizontally, use flipV to flip vertically
'resize: width: '.$image->sizer()->resizeToWidth(),
'resize: height: '.$image->sizer()->resizeToHeight(), // use save() to build the image and save it to a file
'final: width: '.$image->width(), $image->save('out/example-1.jpg');
'final: height: '.$image->height(),
$image // display the generated file in the browser
); echo '<img src="out/example-1.jpg">';
var_dump(memory_get_peak_usage());

View file

@ -7,4 +7,5 @@ use ByJoby\ImageTransform\Sizers\AbstractSizer;
interface DriverInterface interface DriverInterface
{ {
public function image(string $src, AbstractSizer $sizer): Image; public function image(string $src, AbstractSizer $sizer): Image;
public function save(Image $image, string $filename);
} }

View file

@ -4,18 +4,18 @@ namespace ByJoby\ImageTransform\Drivers;
abstract class AbstractCLIDriver extends AbstractDriver abstract class AbstractCLIDriver extends AbstractDriver
{ {
protected $executablePath = ''; protected $executablePath;
public function __construct() public function __construct(string $executablePath = null)
{ {
if (!function_exists('exec')) { if (!function_exists('exec')) {
throw new \Exception("CLI drivers can't be used with the current configuration because exec is disabled"); throw new \Exception("CLI drivers can't be used with the current configuration because exec is disabled");
} }
parent::__construct(); $this->executablePath = $executablePath;
} }
public function executablePath($name) public function executablePath()
{ {
return $this->executablePath.$name; return $this->executablePath ?? static::DEFAULT_EXECUTABLE;
} }
} }

View file

@ -8,17 +8,14 @@ use ByJoby\ImageTransform\Sizers\AbstractSizer;
abstract class AbstractDriver implements DriverInterface abstract class AbstractDriver implements DriverInterface
{ {
protected $tmpDir = null; protected $tempDir = null;
protected $chmod = 0775; protected $chmod = 0775;
public function __construct() abstract protected function doSave(Image $image, string $filename);
{
$this->__clone();
}
public function __clone() public function __clone()
{ {
$this->setTempDir(sys_get_temp_dir() . '/byjoby_image-transform/' . uniqid("",true)); $this->tempDir = null;
} }
public function image(string $src, AbstractSizer $sizer): Image public function image(string $src, AbstractSizer $sizer): Image
@ -26,12 +23,20 @@ abstract class AbstractDriver implements DriverInterface
return new Image($src, $this, $sizer); return new Image($src, $this, $sizer);
} }
public function setTempDir(string $dir) public function tempDir(): string
{
if (!$this->tempDir) {
$this->setTempDir(sys_get_temp_dir() . '/byjoby_image-transform/' . uniqid("", true));
}
return $this->tempDir;
}
protected function setTempDir(string $dir)
{ {
if (!$this->mkdir($dir)) { if (!$this->mkdir($dir)) {
throw new \Exception("Temp directory " . htmlentities($dir) . " doesn't exist or isn't writeable, and couldn't be created."); throw new \Exception("Temp directory " . htmlentities($dir) . " doesn't exist or isn't writeable, and couldn't be created.");
} }
$this->tmpDir = $dir; $this->tempDir = $dir;
} }
protected function mkdir(string $dir) protected function mkdir(string $dir)
@ -63,4 +68,19 @@ abstract class AbstractDriver implements DriverInterface
return false; return false;
} }
} }
public function save(Image $image, string $filename)
{
if (is_file($filename)) {
if (!is_writeable($filename)) {
throw new \Exception("Can't save image because file already exists and is not writeable: " . htmlentities($filename));
}
} else {
if (!$this->mkdir(dirname($filename))) {
throw new \Exception("Can't save image because parent directory isn't writeable or couldn't be created: " . htmlentities($filename));
}
touch($filename);
}
$this->doSave($image, realpath($filename));
}
} }

View file

@ -0,0 +1,25 @@
<?php
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\Drivers;
use ByJoby\ImageTransform\Image;
abstract class AbstractExtensionDriver extends AbstractDriver
{
abstract protected function getImageObject(Image $image);
abstract protected function doResize($object, Image $image);
abstract protected function doCrop($object, Image $image);
abstract protected function doFlip($object, Image $image);
abstract protected function doRotation($object, Image $image);
abstract protected function saveImageObject($object, string $filename);
public function doSave(Image $image, string $filename)
{
$object = $this->getImageObject($image);
$object = $this->doRotation($object, $image);
$object = $this->doResize($object, $image);
$object = $this->doCrop($object, $image);
$object = $this->doFlip($object, $image);
$this->saveImageObject($object, $filename);
}
}

View file

@ -2,9 +2,127 @@
/* image-transform | https://github.com/jobyone/image-transform | MIT License */ /* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\Drivers; namespace ByJoby\ImageTransform\Drivers;
use ByJoby\ImageTransform\Image;
/** /**
* This driver uses PHP's built-in GD libary. This is by far the slowest * This driver uses PHP's built-in GD libary. This is by far the slowest
* driver, but support is basically universal. * driver, but support is basically universal.
*/ */
class GDDriver extends AbstractDriver class GDDriver extends AbstractExtensionDriver
{} {
public function __construct()
{
if (!extension_loaded('gd')) {
throw new \Exception("GD driver can't be used because the extension is not loaded");
}
}
protected function getImageObject(Image $image)
{
$source = $image->source();
$extension = strtolower(preg_replace('/^.+\./', '', $source));
switch ($extension) {
case 'bmp':
return imagecreatefrombmp($source);
case 'gif':
return imagecreatefromgif($source);
case 'jpg':
return imagecreatefromjpeg($source);
case 'jpeg':
return imagecreatefromjpeg($source);
case 'png':
return imagecreatefrompng($source);
case 'wbmp':
return imagecreatefromwbmp($source);
case 'webp':
return imagecreatefromwebp($source);
case 'xbm':
return imagecreatefromxbm($source);
default:
throw new \Exception("Unsupported input type: " . htmlentities($source));
}
}
protected function doResize($object, Image $image)
{
$sizer = $image->sizer();
if ($sizer->resizeToHeight() && $sizer->resizeToWidth()) {
// sizer is calling for a resize
$new = imagecreatetruecolor($sizer->resizeToWidth(), $sizer->resizeToHeight());
imagecopyresampled(
$new, $object,
0, 0,//dst x/y
0, 0,
$sizer->resizeToWidth(), $sizer->resizeToHeight(),
$sizer->originalWidth(), $sizer->originalHeight()
);
return $new;
} else {
// sizer isn't calling for a resize, return object unchanged
return $object;
}
}
protected function doCrop($object, Image $image)
{
$sizer = $image->sizer();
if ($sizer->cropToHeight() && $sizer->cropToWidth()) {
// sizer is calling for a crop
$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()
);
return $new;
} else {
// sizer isn't calling for a resize, return object unchanged
return $object;
}
}
protected function doFlip($object, Image $image)
{
if ($image->getFlipH()) {
imageflip($object,IMG_FLIP_HORIZONTAL);
}
if ($image->getFlipV()) {
imageflip($object,IMG_FLIP_VERTICAL);
}
return $object;
}
protected function doRotation($object, Image $image)
{
if ($rotationAmount = 360 - $image->rotation() * 90) {
return imagerotate($object, $rotationAmount, 0);
}
return $object;
}
protected function saveImageObject($object, string $filename)
{
$extension = strtolower(preg_replace('/^.+\./', '', $filename));
switch ($extension) {
case 'bmp':
return imagebmp($object, $filename);
case 'gif':
return imagegif($object, $filename);
case 'jpg':
return imagejpeg($object, $filename);
case 'jpeg':
return imagejpeg($object, $filename);
case 'png':
return imagepng($object, $filename);
case 'wbmp':
return imagewbmp($object, $filename);
case 'webp':
return imagewebp($object, $filename);
case 'xbm':
return imagexbm($object, $filename);
default:
throw new \Exception("Unsupported output type: " . htmlentities($filename));
}
}
}

View file

@ -1,9 +0,0 @@
<?php
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\Drivers;
/**
* This driver uses the Gmagick extension
*/
class GmagickDriver extends AbstractDriver
{}

View file

@ -2,12 +2,49 @@
/* image-transform | https://github.com/jobyone/image-transform | MIT License */ /* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\Drivers; namespace ByJoby\ImageTransform\Drivers;
use ByJoby\ImageTransform\Image;
/** /**
* This driver is designed to use exec() and your OS's binary Imagick * This driver uses exec() and command-line ImageMagick utilities to
* installation to do all of its image transforms. This will likely be * transform images. It is likely approaching the limits of how fast this
* the highest-performance driver of all in most situations, but does * library can possibly be. The downside is that it will only run if
* require that you have CLI Imagick installed, and that your server allows * you have exec() enabled, and your server allows it to execute the
* PHP's exec() function to use it. * ImageMagick binaries.
*/ */
class ImagickCLIDriver extends AbstractCLIDriver class ImagickCLIDriver extends AbstractCLIDriver
{} {
const DEFAULT_EXECUTABLE = 'magick';
protected function doSave(Image $image, string $filename)
{
// basics of command
$command = [
$this->executablePath(),
'"' . $image->source() . '"',
];
// rotation
if ($image->rotation()) {
$command[] = '-rotate ' . ($image->rotation() * 90);
}
// flip/flop
if ($image->getFlipH()) {
$command[] = '-flop';
}
if ($image->getFlipV()) {
$command[] = '-flip';
}
// resizing/cropping
$sizer = $image->sizer();
if ($sizer->resizeToWidth() || $sizer->cropToWidth()) {
$command[] = '-resize '.$sizer->resizeToWidth().'x'.$sizer->resizeToHeight();
}
if ($sizer->cropToWidth() && $sizer->cropToHeight()) {
$command[] = '-gravity center';
$command[] = '-extent '.$sizer->cropToWidth().'x'.$sizer->cropToHeight();
}
// output file
$command[] = '"' . $filename . '"';
var_dump($command);
exec(implode(' ', $command));
}
}

View file

@ -1,9 +0,0 @@
<?php
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\Drivers;
/**
* This class uses the ImageMagick extension
*/
class ImagickDriver extends AbstractDriver
{}

View file

@ -15,12 +15,17 @@ class Image
public function __construct(string $source, DriverInterface $driver, AbstractSizer $sizer) public function __construct(string $source, DriverInterface $driver, AbstractSizer $sizer)
{ {
$this->source($source); $this->setSource($source);
$this->sizer($sizer); $this->setSizer($sizer);
$this->driver = clone $driver; $this->driver = clone $driver;
} }
public function source(string $source) public function source(): string
{
return $this->source;
}
public function setSource(string $source)
{ {
// set source // set source
$this->source = realpath($source); $this->source = realpath($source);
@ -38,15 +43,17 @@ class Image
list($this->originalWidth, $this->originalHeight) = getimagesize($this->source); list($this->originalWidth, $this->originalHeight) = getimagesize($this->source);
} }
public function sizer(AbstractSizer $sizer = null): AbstractSizer public function sizer(): AbstractSizer
{ {
if ($sizer) {
$this->sizer = clone $sizer;
$this->sizer->image($this);
}
return $this->sizer; return $this->sizer;
} }
public function setSizer(AbstractSizer $sizer)
{
$this->sizer = clone $sizer;
$this->sizer->image($this);
}
public function rotate(int $steps = 1) public function rotate(int $steps = 1)
{ {
$this->rotation = ($this->rotation + $steps) % 4; $this->rotation = ($this->rotation + $steps) % 4;
@ -60,17 +67,21 @@ class Image
public function flipH() public function flipH()
{ {
$this->flipH = !$this->flipH; $this->flipH = !$this->flipH;
if ($this->flipH && $this->flipV) {
$this->flipH = $this->flipV = false;
}
} }
public function flipV() public function flipV()
{ {
$this->flipV = !$this->flipV; $this->flipV = !$this->flipV;
if ($this->flipH && $this->flipV) { }
$this->flipH = $this->flipV = false;
} public function getFlipH()
{
return $this->flipH;
}
public function getFlipV()
{
return $this->flipV;
} }
public function width(): int public function width(): int
@ -102,4 +113,9 @@ class Image
{ {
return $this->originalHeight; return $this->originalHeight;
} }
public function save(string $file)
{
$this->driver->save($this, $file);
}
} }

View file

@ -3,7 +3,12 @@
namespace ByJoby\ImageTransform\tests\units\mock; namespace ByJoby\ImageTransform\tests\units\mock;
use ByJoby\ImageTransform\Drivers\AbstractDriver; use ByJoby\ImageTransform\Drivers\AbstractDriver;
use ByJoby\ImageTransform\Image;
class MockDriver extends AbstractDriver class MockDriver extends AbstractDriver
{ {
protected function doSave(Image $image, string $filename)
{
//does nothing
}
} }