diff --git a/.gitignore b/.gitignore
index 43b913f..b9c83f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,5 @@ composer.phar
/vendor/
composer.lock
debug.log
+/examples/out/*
+!.gitkeep
\ No newline at end of file
diff --git a/README.md b/README.md
index 69c8c71..07d0a68 100644
--- a/README.md
+++ b/README.md
@@ -3,13 +3,23 @@
[![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.
-## 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.
+### Current progress
+
+| Driver | Rotate | Mirror | Resize | Crop | Overlay | Grayscale | Colorize |
+| :--------- | :----: | :----: | :----: | :--: | :-----: | :-------: | :------: |
+| GD | X | X | X | X | | | |
+| Imagick | | | | | | | |
+| Gmagick | | | | | | | |
+| ImagickCLI | X | X | X | X | | | |
+
+## Roadmap
+
### Drivers
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
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
diff --git a/examples/imagick-cli.php b/examples/imagick-cli.php
new file mode 100644
index 0000000..500f7de
--- /dev/null
+++ b/examples/imagick-cli.php
@@ -0,0 +1,28 @@
+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 '';
+
+var_dump(memory_get_peak_usage());
diff --git a/examples/out/.gitkeep b/examples/out/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/examples/usage.php b/examples/usage.php
index 2a9a839..51a10e8 100644
--- a/examples/usage.php
+++ b/examples/usage.php
@@ -1,30 +1,28 @@
image(
'example-portrait.jpg',
- $sizer
+ new Fit(200, 500)
);
-// $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
-);
+// 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-1.jpg');
+
+// display the generated file in the browser
+echo '';
+
+var_dump(memory_get_peak_usage());
diff --git a/src/DriverInterface.php b/src/DriverInterface.php
index 6317ad7..c3f555c 100644
--- a/src/DriverInterface.php
+++ b/src/DriverInterface.php
@@ -7,4 +7,5 @@ use ByJoby\ImageTransform\Sizers\AbstractSizer;
interface DriverInterface
{
public function image(string $src, AbstractSizer $sizer): Image;
+ public function save(Image $image, string $filename);
}
diff --git a/src/Drivers/AbstractCLIDriver.php b/src/Drivers/AbstractCLIDriver.php
index a493134..b89798f 100644
--- a/src/Drivers/AbstractCLIDriver.php
+++ b/src/Drivers/AbstractCLIDriver.php
@@ -4,18 +4,18 @@ namespace ByJoby\ImageTransform\Drivers;
abstract class AbstractCLIDriver extends AbstractDriver
{
- protected $executablePath = '';
+ protected $executablePath;
- public function __construct()
+ public function __construct(string $executablePath = null)
{
if (!function_exists('exec')) {
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;
}
}
diff --git a/src/Drivers/AbstractDriver.php b/src/Drivers/AbstractDriver.php
index c6640a9..20df9a8 100644
--- a/src/Drivers/AbstractDriver.php
+++ b/src/Drivers/AbstractDriver.php
@@ -8,17 +8,14 @@ use ByJoby\ImageTransform\Sizers\AbstractSizer;
abstract class AbstractDriver implements DriverInterface
{
- protected $tmpDir = null;
+ protected $tempDir = null;
protected $chmod = 0775;
- public function __construct()
- {
- $this->__clone();
- }
+ abstract protected function doSave(Image $image, string $filename);
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
@@ -26,12 +23,20 @@ abstract class AbstractDriver implements DriverInterface
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)) {
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)
@@ -63,4 +68,19 @@ abstract class AbstractDriver implements DriverInterface
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));
+ }
}
diff --git a/src/Drivers/AbstractExtensionDriver.php b/src/Drivers/AbstractExtensionDriver.php
new file mode 100644
index 0000000..b454259
--- /dev/null
+++ b/src/Drivers/AbstractExtensionDriver.php
@@ -0,0 +1,25 @@
+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);
+ }
+}
diff --git a/src/Drivers/GDDriver.php b/src/Drivers/GDDriver.php
index ded729f..14e53e7 100644
--- a/src/Drivers/GDDriver.php
+++ b/src/Drivers/GDDriver.php
@@ -2,9 +2,127 @@
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
namespace ByJoby\ImageTransform\Drivers;
+use ByJoby\ImageTransform\Image;
+
/**
* This driver uses PHP's built-in GD libary. This is by far the slowest
* 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));
+ }
+ }
+}
diff --git a/src/Drivers/GmagickDriver.php b/src/Drivers/GmagickDriver.php
deleted file mode 100644
index 254745c..0000000
--- a/src/Drivers/GmagickDriver.php
+++ /dev/null
@@ -1,9 +0,0 @@
-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));
+ }
+}
diff --git a/src/Drivers/ImagickDriver.php b/src/Drivers/ImagickDriver.php
deleted file mode 100644
index b039049..0000000
--- a/src/Drivers/ImagickDriver.php
+++ /dev/null
@@ -1,9 +0,0 @@
-source($source);
- $this->sizer($sizer);
+ $this->setSource($source);
+ $this->setSizer($sizer);
$this->driver = clone $driver;
}
- public function source(string $source)
+ public function source(): string
+ {
+ return $this->source;
+ }
+
+ public function setSource(string $source)
{
// set source
$this->source = realpath($source);
@@ -38,15 +43,17 @@ class Image
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;
}
+ public function setSizer(AbstractSizer $sizer)
+ {
+ $this->sizer = clone $sizer;
+ $this->sizer->image($this);
+ }
+
public function rotate(int $steps = 1)
{
$this->rotation = ($this->rotation + $steps) % 4;
@@ -60,17 +67,21 @@ class Image
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 getFlipH()
+ {
+ return $this->flipH;
+ }
+
+ public function getFlipV()
+ {
+ return $this->flipV;
}
public function width(): int
@@ -102,4 +113,9 @@ class Image
{
return $this->originalHeight;
}
+
+ public function save(string $file)
+ {
+ $this->driver->save($this, $file);
+ }
}
diff --git a/tests/units/mock/MockDriver.php b/tests/units/mock/MockDriver.php
index 7883a42..fb55806 100644
--- a/tests/units/mock/MockDriver.php
+++ b/tests/units/mock/MockDriver.php
@@ -3,7 +3,12 @@
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
+ }
}