From d6392c974a96efaa1ab4842c093644bdd131ab4b Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Mon, 24 Apr 2017 21:26:09 -0500 Subject: [PATCH 01/13] Begun flushing out the new Image classes. --- application/Controllers/Checks.php | 5 + system/Config/AutoloadConfig.php | 138 +++++++------- system/Images/Exceptions.php | 3 + system/Images/Handlers/BaseHandler.php | 129 +++++++++++++ system/Images/Image.php | 237 ++++++++++++++++++++++++ system/Images/ImageHandlerInterface.php | 122 ++++++++++++ 6 files changed, 565 insertions(+), 69 deletions(-) create mode 100644 system/Images/Exceptions.php create mode 100644 system/Images/Handlers/BaseHandler.php create mode 100644 system/Images/Image.php create mode 100644 system/Images/ImageHandlerInterface.php diff --git a/application/Controllers/Checks.php b/application/Controllers/Checks.php index 5135805a8e9a..4078a6d76da3 100644 --- a/application/Controllers/Checks.php +++ b/application/Controllers/Checks.php @@ -146,5 +146,10 @@ public function catch() echo $body; } + public function redirect() + { + redirect('/checks/model'); + } + } diff --git a/system/Config/AutoloadConfig.php b/system/Config/AutoloadConfig.php index 89633948eb59..90ad76e44b02 100644 --- a/system/Config/AutoloadConfig.php +++ b/system/Config/AutoloadConfig.php @@ -111,76 +111,76 @@ public function __construct() * ]; */ $this->classmap = [ - 'CodeIgniter\CodeIgniter' => BASEPATH.'CodeIgniter.php', - 'CodeIgniter\CLI\CLI' => BASEPATH.'CLI/CLI.php', - 'CodeIgniter\Loader' => BASEPATH.'Loader.php', - 'CodeIgniter\Cache\CacheFactory' => BASEPATH.'Cache/CacheFactory.php', - 'CodeIgniter\Cache\CacheInterface' => BASEPATH.'Cache/CacheInterface.php', - 'CodeIgniter\Cache\Handlers\DummyHandler' => BASEPATH.'Cache/Handlers/DummyHandler.php', - 'CodeIgniter\Cache\Handlers\FileHandler' => BASEPATH.'Cache/Handlers/FileHandler.php', - 'CodeIgniter\Cache\Handlers\MemcachedHandler' => BASEPATH.'Cache/Handlers/MemcachedHandler.php', - 'CodeIgniter\Cache\Handlers\PredisHandler' => BASEPATH.'Cache/Handlers/PredisHandler.php', - 'CodeIgniter\Cache\Handlers\RedisHandler' => BASEPATH.'Cache/Handlers/RedisHandler.php', - 'CodeIgniter\Cache\Handlers\WincacheHandler' => BASEPATH.'Cache/Handlers/WincacheHandler.php', - 'CodeIgniter\Controller' => BASEPATH.'Controller.php', - 'CodeIgniter\Config\AutoloadConfig' => BASEPATH.'Config/Autoload.php', - 'CodeIgniter\Config\BaseConfig' => BASEPATH.'Config/BaseConfig.php', - 'CodeIgniter\Config\Database' => BASEPATH.'Config/Database.php', - 'CodeIgniter\Config\Database\Connection' => BASEPATH.'Config/Database/Connection.php', - 'CodeIgniter\Config\Database\Connection\MySQLi' => BASEPATH.'Config/Database/Connection/MySQLi.php', - 'CodeIgniter\Config\DotEnv' => BASEPATH.'Config/DotEnv.php', - 'CodeIgniter\Database\BaseBuilder' => BASEPATH.'Database/BaseBuilder.php', - 'CodeIgniter\Database\BaseConnection' => BASEPATH.'Database/BaseConnection.php', - 'CodeIgniter\Database\BaseResult' => BASEPATH.'Database/BaseResult.php', - 'CodeIgniter\Database\Config' => BASEPATH.'Database/Config.php', - 'CodeIgniter\Database\ConnectionInterface' => BASEPATH.'Database/ConnectionInterface.php', - 'CodeIgniter\Database\Database' => BASEPATH.'Database/Database.php', - 'CodeIgniter\Database\Query' => BASEPATH.'Database/Query.php', - 'CodeIgniter\Database\QueryInterface' => BASEPATH.'Database/QueryInterface.php', - 'CodeIgniter\Database\ResultInterface' => BASEPATH.'Database/ResultInterface.php', - 'CodeIgniter\Database\Migration' => BASEPATH.'Database/Migration.php', - 'CodeIgniter\Database\MigrationRunner' => BASEPATH.'Database/MigrationRunner.php', - 'CodeIgniter\Debug\Exceptions' => BASEPATH.'Debug/Exceptions.php', - 'CodeIgniter\Debug\Timer' => BASEPATH.'Debug/Timer.php', - 'CodeIgniter\Debug\Iterator' => BASEPATH.'Debug/Iterator.php', - 'CodeIgniter\Events\Events' => BASEPATH.'Events/Events.php', - 'CodeIgniter\HTTP\CLIRequest' => BASEPATH.'HTTP/CLIRequest.php', - 'CodeIgniter\HTTP\ContentSecurityPolicy' => BASEPATH.'HTTP/ContentSecurityPolicy.php', - 'CodeIgniter\HTTP\CURLRequest' => BASEPATH.'HTTP/CURLRequest.php', - 'CodeIgniter\HTTP\IncomingRequest' => BASEPATH.'HTTP/IncomingRequest.php', - 'CodeIgniter\HTTP\Message' => BASEPATH.'HTTP/Message.php', - 'CodeIgniter\HTTP\Negotiate' => BASEPATH.'HTTP/Negotiate.php', - 'CodeIgniter\HTTP\Request' => BASEPATH.'HTTP/Request.php', - 'CodeIgniter\HTTP\RequestInterface' => BASEPATH.'HTTP/RequestInterface.php', - 'CodeIgniter\HTTP\Response' => BASEPATH.'HTTP/Response.php', - 'CodeIgniter\HTTP\ResponseInterface' => BASEPATH.'HTTP/ResponseInterface.php', - 'CodeIgniter\HTTP\URI' => BASEPATH.'HTTP/URI.php', - 'CodeIgniter\Log\Logger' => BASEPATH.'Log/Logger.php', - 'Psr\Log\LoggerAwareInterface' => BASEPATH.'ThirdParty/PSR/Log/LoggerAwareInterface.php', - 'Psr\Log\LoggerAwareTrait' => BASEPATH.'ThirdParty/PSR/Log/LoggerAwareTrait.php', - 'Psr\Log\LoggerInterface' => BASEPATH.'ThirdParty/PSR/Log/LoggerInterface.php', - 'Psr\Log\LogLevel' => BASEPATH.'ThirdParty/PSR/Log/LogLevel.php', - 'CodeIgniter\Log\Handlers\BaseHandler' => BASEPATH.'Log/Handlers/BaseHandler.php', - 'CodeIgniter\Log\Handlers\ChromeLoggerHandler' => BASEPATH.'Log/Handlers/ChromeLoggerHandler.php', - 'CodeIgniter\Log\Handlers\FileHandler' => BASEPATH.'Log/Handlers/FileHandler.php', - 'CodeIgniter\Log\Handlers\HandlerInterface' => BASEPATH.'Log/Handlers/HandlerInterface.php', - 'CodeIgniter\Router\RouteCollection' => BASEPATH.'Router/RouteCollection.php', - 'CodeIgniter\Router\RouteCollectionInterface' => BASEPATH.'Router/RouteCollectionInterface.php', - 'CodeIgniter\Router\Router' => BASEPATH.'Router/Router.php', - 'CodeIgniter\Router\RouterInterface' => BASEPATH.'Router/RouterInterface.php', - 'CodeIgniter\Security\Security' => BASEPATH.'Security/Security.php', - 'CodeIgniter\Session\Session' => BASEPATH.'Session/Session.php', - 'CodeIgniter\Session\SessionInterface' => BASEPATH.'Session/SessionInterface.php', - 'CodeIgniter\Session\Handlers\BaseHandler' => BASEPATH.'Session/Handlers/BaseHandler.php', - 'CodeIgniter\Session\Handlers\FileHandler' => BASEPATH.'Session/Handlers/FileHandler.php', - 'CodeIgniter\Session\Handlers\MemcachedHandler' => BASEPATH.'Session/Handlers/MemcachedHandler.php', - 'CodeIgniter\Session\Handlers\RedisHandler' => BASEPATH.'Session/Handlers/RedisHandler.php', - 'CodeIgniter\View\RendererInterface' => BASEPATH.'View/RendererInterface.php', - 'CodeIgniter\View\View' => BASEPATH.'View/View.php', - 'CodeIgniter\View\Parser' => BASEPATH.'View/Parser.php', - 'CodeIgniter\View\Cell' => BASEPATH.'View/Cell.php', +// 'CodeIgniter\CodeIgniter' => BASEPATH.'CodeIgniter.php', +// 'CodeIgniter\CLI\CLI' => BASEPATH.'CLI/CLI.php', +// 'CodeIgniter\Loader' => BASEPATH.'Loader.php', +// 'CodeIgniter\Cache\CacheFactory' => BASEPATH.'Cache/CacheFactory.php', +// 'CodeIgniter\Cache\CacheInterface' => BASEPATH.'Cache/CacheInterface.php', +// 'CodeIgniter\Cache\Handlers\DummyHandler' => BASEPATH.'Cache/Handlers/DummyHandler.php', +// 'CodeIgniter\Cache\Handlers\FileHandler' => BASEPATH.'Cache/Handlers/FileHandler.php', +// 'CodeIgniter\Cache\Handlers\MemcachedHandler' => BASEPATH.'Cache/Handlers/MemcachedHandler.php', +// 'CodeIgniter\Cache\Handlers\PredisHandler' => BASEPATH.'Cache/Handlers/PredisHandler.php', +// 'CodeIgniter\Cache\Handlers\RedisHandler' => BASEPATH.'Cache/Handlers/RedisHandler.php', +// 'CodeIgniter\Cache\Handlers\WincacheHandler' => BASEPATH.'Cache/Handlers/WincacheHandler.php', +// 'CodeIgniter\Controller' => BASEPATH.'Controller.php', +// 'CodeIgniter\Config\AutoloadConfig' => BASEPATH.'Config/Autoload.php', +// 'CodeIgniter\Config\BaseConfig' => BASEPATH.'Config/BaseConfig.php', +// 'CodeIgniter\Config\Database' => BASEPATH.'Config/Database.php', +// 'CodeIgniter\Config\Database\Connection' => BASEPATH.'Config/Database/Connection.php', +// 'CodeIgniter\Config\Database\Connection\MySQLi' => BASEPATH.'Config/Database/Connection/MySQLi.php', +// 'CodeIgniter\Config\DotEnv' => BASEPATH.'Config/DotEnv.php', +// 'CodeIgniter\Database\BaseBuilder' => BASEPATH.'Database/BaseBuilder.php', +// 'CodeIgniter\Database\BaseConnection' => BASEPATH.'Database/BaseConnection.php', +// 'CodeIgniter\Database\BaseResult' => BASEPATH.'Database/BaseResult.php', +// 'CodeIgniter\Database\Config' => BASEPATH.'Database/Config.php', +// 'CodeIgniter\Database\ConnectionInterface' => BASEPATH.'Database/ConnectionInterface.php', +// 'CodeIgniter\Database\Database' => BASEPATH.'Database/Database.php', +// 'CodeIgniter\Database\Query' => BASEPATH.'Database/Query.php', +// 'CodeIgniter\Database\QueryInterface' => BASEPATH.'Database/QueryInterface.php', +// 'CodeIgniter\Database\ResultInterface' => BASEPATH.'Database/ResultInterface.php', +// 'CodeIgniter\Database\Migration' => BASEPATH.'Database/Migration.php', +// 'CodeIgniter\Database\MigrationRunner' => BASEPATH.'Database/MigrationRunner.php', +// 'CodeIgniter\Debug\Exceptions' => BASEPATH.'Debug/Exceptions.php', +// 'CodeIgniter\Debug\Timer' => BASEPATH.'Debug/Timer.php', +// 'CodeIgniter\Debug\Iterator' => BASEPATH.'Debug/Iterator.php', +// 'CodeIgniter\Events\Events' => BASEPATH.'Events/Events.php', +// 'CodeIgniter\HTTP\CLIRequest' => BASEPATH.'HTTP/CLIRequest.php', +// 'CodeIgniter\HTTP\ContentSecurityPolicy' => BASEPATH.'HTTP/ContentSecurityPolicy.php', +// 'CodeIgniter\HTTP\CURLRequest' => BASEPATH.'HTTP/CURLRequest.php', +// 'CodeIgniter\HTTP\IncomingRequest' => BASEPATH.'HTTP/IncomingRequest.php', +// 'CodeIgniter\HTTP\Message' => BASEPATH.'HTTP/Message.php', +// 'CodeIgniter\HTTP\Negotiate' => BASEPATH.'HTTP/Negotiate.php', +// 'CodeIgniter\HTTP\Request' => BASEPATH.'HTTP/Request.php', +// 'CodeIgniter\HTTP\RequestInterface' => BASEPATH.'HTTP/RequestInterface.php', +// 'CodeIgniter\HTTP\Response' => BASEPATH.'HTTP/Response.php', +// 'CodeIgniter\HTTP\ResponseInterface' => BASEPATH.'HTTP/ResponseInterface.php', +// 'CodeIgniter\HTTP\URI' => BASEPATH.'HTTP/URI.php', +// 'CodeIgniter\Log\Logger' => BASEPATH.'Log/Logger.php', +// 'Psr\Log\LoggerAwareInterface' => BASEPATH.'ThirdParty/PSR/Log/LoggerAwareInterface.php', +// 'Psr\Log\LoggerAwareTrait' => BASEPATH.'ThirdParty/PSR/Log/LoggerAwareTrait.php', +// 'Psr\Log\LoggerInterface' => BASEPATH.'ThirdParty/PSR/Log/LoggerInterface.php', +// 'Psr\Log\LogLevel' => BASEPATH.'ThirdParty/PSR/Log/LogLevel.php', +// 'CodeIgniter\Log\Handlers\BaseHandler' => BASEPATH.'Log/Handlers/BaseHandler.php', +// 'CodeIgniter\Log\Handlers\ChromeLoggerHandler' => BASEPATH.'Log/Handlers/ChromeLoggerHandler.php', +// 'CodeIgniter\Log\Handlers\FileHandler' => BASEPATH.'Log/Handlers/FileHandler.php', +// 'CodeIgniter\Log\Handlers\HandlerInterface' => BASEPATH.'Log/Handlers/HandlerInterface.php', +// 'CodeIgniter\Router\RouteCollection' => BASEPATH.'Router/RouteCollection.php', +// 'CodeIgniter\Router\RouteCollectionInterface' => BASEPATH.'Router/RouteCollectionInterface.php', +// 'CodeIgniter\Router\Router' => BASEPATH.'Router/Router.php', +// 'CodeIgniter\Router\RouterInterface' => BASEPATH.'Router/RouterInterface.php', +// 'CodeIgniter\Security\Security' => BASEPATH.'Security/Security.php', +// 'CodeIgniter\Session\Session' => BASEPATH.'Session/Session.php', +// 'CodeIgniter\Session\SessionInterface' => BASEPATH.'Session/SessionInterface.php', +// 'CodeIgniter\Session\Handlers\BaseHandler' => BASEPATH.'Session/Handlers/BaseHandler.php', +// 'CodeIgniter\Session\Handlers\FileHandler' => BASEPATH.'Session/Handlers/FileHandler.php', +// 'CodeIgniter\Session\Handlers\MemcachedHandler' => BASEPATH.'Session/Handlers/MemcachedHandler.php', +// 'CodeIgniter\Session\Handlers\RedisHandler' => BASEPATH.'Session/Handlers/RedisHandler.php', +// 'CodeIgniter\View\RendererInterface' => BASEPATH.'View/RendererInterface.php', +// 'CodeIgniter\View\View' => BASEPATH.'View/View.php', +// 'CodeIgniter\View\Parser' => BASEPATH.'View/Parser.php', +// 'CodeIgniter\View\Cell' => BASEPATH.'View/Cell.php', 'Zend\Escaper\Escaper' => BASEPATH.'ThirdParty/ZendEscaper/Escaper.php', - 'CodeIgniter\Log\TestLogger' => BASEPATH.'../tests/_support/Log/TestLogger.php', +// 'CodeIgniter\Log\TestLogger' => BASEPATH.'../tests/_support/Log/TestLogger.php', 'CIDatabaseTestCase' => BASEPATH.'../tests/_support/CIDatabaseTestCase.php' ]; } diff --git a/system/Images/Exceptions.php b/system/Images/Exceptions.php new file mode 100644 index 000000000000..3aec8a1b1b17 --- /dev/null +++ b/system/Images/Exceptions.php @@ -0,0 +1,3 @@ +handler = $handler; + + return $this; + } + + //-------------------------------------------------------------------- + + public function save(): bool + { + + } + + //-------------------------------------------------------------------- + + public function copy(string $target, int $perms=0644) + { + + } + + //-------------------------------------------------------------------- + + /** + * Returns a boolean flag whether any errors were encountered. + * + * @return bool + */ + public function hasErrors(): bool + { + return ! empty($this->errors); + } + + //-------------------------------------------------------------------- + + /** + * Returns all error messages that were encountered during processing. + * + * @return array + */ + public function getErrors(): array + { + return $this->errors ?? []; + } + + //-------------------------------------------------------------------- + + /** + * Resize the image + * + * @param int $width + * @param int $height + * @param bool $maintainRatio If true, will get the closest match possible while keeping aspect ratio true. + * + * @return $this + */ + public function resize(int $width, int $height, bool $maintainRatio = false) + { + try { + $this->handler->resize($width, $height, $maintainRatio); + } + catch (ImageException $e) + { + $this->errors[] = $e->getMessage(); + } + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Crops the image to the desired height and width. If one of the height/width values + * is not provided, that value will be set the appropriate value based on offsets and + * image dimensions. + * + * @param int|null $width + * @param int|null $height + * @param int|null $x X-axis coord to start cropping from the left of image + * @param int|null $y Y-axis coord to start cropping from the top of image + * + * @return $this + */ + public function crop(int $width = null, int $height = null, int $x = null, int $y = null) + { + try { + $this->handler->crop($width, $height, $x, $y); + } + catch (ImageException $e) + { + $this->errors[] = $e->getMessage(); + } + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Rotates the image on the current canvas. + * + * @param float $angle + * + * @return mixed + */ + public function rotate(float $angle) + { + try { + $this->handler->rotate($angle); + } + catch (ImageException $e) + { + $this->errors[] = $e->getMessage(); + } + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * @return $this + */ + public function watermark() + { + + } + + //-------------------------------------------------------------------- + + /** + * Reads the EXIF information from the image and modifies the orientation + * so that displays correctly in the browser. + * + * @return $this + */ + public function reorient(): bool + { + try { + $this->handler->reorient(); + } + catch (ImageException $e) + { + $this->errors[] = $e->getMessage(); + } + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Retrieve the EXIF information from the image, if possible. Returns + * an array of the information, or null if nothing can be found. + * + * @param string|null $key If specified, will only return this piece of EXIF data. + * + * @return mixed + */ + public function getEXIF(string $key = null) + { + try { + $this->handler->getEXIF($key); + } + catch (ImageException $e) + { + $this->errors[] = $e->getMessage(); + } + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Combine cropping and resizing into a single command. + * + * Supported positions: + * - top-left + * - top + * - top-right + * - left + * - center + * - right + * - bottom-left + * - bottom + * - bottom-right + * + * @param int $width + * @param int $height + * @param string $position + * + * @return $this + */ + public function fit(int $width, int $height, string $position) + { + try { + $this->handler->fit($width, $height, $position); + } + catch (ImageException $e) + { + $this->errors[] = $e->getMessage(); + } + + return $this; + } + + //-------------------------------------------------------------------- + +} diff --git a/system/Images/ImageHandlerInterface.php b/system/Images/ImageHandlerInterface.php new file mode 100644 index 000000000000..6e9c96666eeb --- /dev/null +++ b/system/Images/ImageHandlerInterface.php @@ -0,0 +1,122 @@ + Date: Wed, 26 Apr 2017 23:53:28 -0500 Subject: [PATCH 02/13] Refactoring things a bit. GD driver moving along nicely. --- application/Config/Images.php | 33 +++ application/Controllers/Checks.php | 9 + system/Config/Services.php | 27 ++ system/Images/Handlers/BaseHandler.php | 245 +++++++++++++++-- system/Images/Handlers/GDHandler.php | 333 ++++++++++++++++++++++++ system/Images/Image.php | 247 +++++------------- system/Images/ImageHandlerInterface.php | 26 -- system/Language/en/Images.php | 21 ++ tests/_support/ci-logo.png | Bin 0 -> 7760 bytes tests/system/Images/GDHandlerTest.php | 19 ++ tests/system/Images/ImageTest.php | 56 ++++ 11 files changed, 790 insertions(+), 226 deletions(-) create mode 100644 application/Config/Images.php create mode 100644 system/Images/Handlers/GDHandler.php create mode 100644 system/Language/en/Images.php create mode 100644 tests/_support/ci-logo.png create mode 100644 tests/system/Images/GDHandlerTest.php create mode 100644 tests/system/Images/ImageTest.php diff --git a/application/Config/Images.php b/application/Config/Images.php new file mode 100644 index 000000000000..995c92203070 --- /dev/null +++ b/application/Config/Images.php @@ -0,0 +1,33 @@ + \CodeIgniter\Images\Handlers\GDHandler::class, + 'imagick' => \CodeIgniter\Images\Handlers\ImageMagickHandler::class, + 'gm' => \CodeIgniter\Images\Handlers\GraphicsMagickHandler::class, + 'pbm' => \CodeIgniter\Images\Handlers\NetPBMHandler::class, + ]; +} diff --git a/application/Controllers/Checks.php b/application/Controllers/Checks.php index 4078a6d76da3..3b03a10ac9ea 100644 --- a/application/Controllers/Checks.php +++ b/application/Controllers/Checks.php @@ -151,5 +151,14 @@ public function redirect() redirect('/checks/model'); } + public function image() + { + $images = Services::image('gd') + ->withFile("/Users/kilishan/Documents/BobHeader.jpg") + ->crop(200, 75, 20, 0, false) + ->save('/Users/kilishan/temp.jpg', 100); + + ddd($images); + } } diff --git a/system/Config/Services.php b/system/Config/Services.php index 43024dd79f07..ccc9e317e3d3 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -208,6 +208,33 @@ public static function filters($config = null, $getShared = true) //-------------------------------------------------------------------- + /** + * Acts as a factory for ImageHandler classes and returns an instance + * of the handler. Used like Services::image()->withFile($path)->rotate(90)->save(); + */ + public static function image(string $handler=null, $config = null, $getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('image', $handler, $config); + } + + if (empty($config)) + { + $config = new \Config\Images(); + } + + $handler = is_null($handler) + ? $config->defaultHandler + : $handler; + + $class = $config->handlers[$handler]; + + return new $class($config); + } + + //-------------------------------------------------------------------- + /** * The Iterator class provides a simple way of looping over a function * and timing the results and memory usage. Used when debugging and diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index e038d1d76380..078274ddd3bf 100644 --- a/system/Images/Handlers/BaseHandler.php +++ b/system/Images/Handlers/BaseHandler.php @@ -1,17 +1,130 @@ -config = $config; + } + + //-------------------------------------------------------------------- + + /** + * Sets another image for this handler to work on. + * Keeps us from needing to continually instantiate the handler. + * + * @param string $path + * + * @return $this + */ + public function withFile(string $path) + { + $this->image = new Image($path, true); + + $this->image->getProperties(); + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Returns the image instance. + * + * @return \CodeIgniter\Images\Image + */ + public function getFile() + { + return $this->image; + } + + //-------------------------------------------------------------------- + + /** + * Returns a boolean flag whether any errors were encountered. + * + * @return bool + */ + public function hasErrors(): bool + { + return ! empty($this->errors); + } + + //-------------------------------------------------------------------- + + /** + * Returns all error messages that were encountered during processing. + * + * @return array + */ + public function getErrors(): array + { + return $this->errors ?? []; + } + + //-------------------------------------------------------------------- + /** * Resize the image * * @param int $width * @param int $height - * @param bool $maintainRation If true, will get the closest match possible while keeping aspect ratio true. + * @param bool $maintainRation If true, will get the closest match possible while keeping aspect ratio true. + * + * @return bool|\CodeIgniter\Images\Handlers\GDHandler */ - public abstract function resize(int $width, int $height, bool $maintainRatio = false); + public function resize(int $width, int $height, bool $maintainRatio = false, string $masterDim = 'auto') + { + // If the target width/height match the source, then we have nothing to do here. + if ($this->image->origWidth === $width && $this->image->origHeight === $height) + { + return true; + } + + $this->width = $width; + $this->height = $height; + + if ($maintainRatio) + { + $this->masterDim = $masterDim; + $this->reproportion(); + } + + return $this->process('resize'); + } //-------------------------------------------------------------------- @@ -22,12 +135,28 @@ public abstract function resize(int $width, int $height, bool $maintainRatio = f * * @param int|null $width * @param int|null $height - * @param int|null $x X-axis coord to start cropping from the left of image - * @param int|null $y Y-axis coord to start cropping from the top of image + * @param int|null $x X-axis coord to start cropping from the left of image + * @param int|null $y Y-axis coord to start cropping from the top of image + * @param bool $maintainRatio + * @param string $masterDim * * @return mixed */ - public abstract function crop(int $width = null, int $height = null, int $x = null, int $y = null); + public function crop(int $width = null, int $height = null, int $x = null, int $y = null, bool $maintainRatio = false, string $masterDim = 'auto') + { + $this->width = $width; + $this->height = $height; + $this->xAxis = $x; + $this->yAxis = $y; + + if ($maintainRatio) + { + $this->masterDim = $masterDim; + $this->reproportion(); + } + + return $this->process('crop'); + } //-------------------------------------------------------------------- @@ -63,7 +192,7 @@ public abstract function reorient(): bool; * Retrieve the EXIF information from the image, if possible. Returns * an array of the information, or null if nothing can be found. * - * @param string|null $key If specified, will only return this piece of EXIF data. + * @param string|null $key If specified, will only return this piece of EXIF data. * * @return mixed */ @@ -96,34 +225,112 @@ public abstract function fit(int $width, int $height, string $position): bool; //-------------------------------------------------------------------- /** - * Allows any option to be easily set. We don't mind doing it - * this way here, since the Handler is not something the user - * will be directly interfacing with. + * Get the version of the image library in use. + * + * @return string + */ + public abstract function getVersion(); + + //-------------------------------------------------------------------- + + /** + * Saves any changes that have been made to file. + * + * Example: + * $image->resize(100, 200, true) + * ->save($target); * - * @param string $key - * @param null $value + * @param string $target + * @param int $quality * * @return mixed */ - public function setOption(string $key, $value = null) - { + public abstract function save(string $target = null, int $quality = 90); + + //-------------------------------------------------------------------- + + /** + * Does the driver-specific processing of the image. + * + * @param string $action + * + * @return mixed + */ + protected abstract function process(string $action); + //-------------------------------------------------------------------- + + /** + * Provide access to the Image class' methods if they don't exist + * on the handler itself. + * + * @param string $name + * @param array $args + */ + public function __call(string $name, array $args = []) + { + if (method_exists($this->image, $name)) + { + return $this->image->$name(...$args); + } } //-------------------------------------------------------------------- /** - * Allows multiple options to be set at once through an array of - * key value pairs, where the keys must be Handler properties. + * Re-proportion Image Width/Height * - * @param array $options + * When creating thumbs, the desired width/height + * can end up warping the image due to an incorrect + * ratio between the full-sized image and the thumb. * - * @return mixed + * This function lets us re-proportion the width/height + * if users choose to maintain the aspect ratio when resizing. + * + * @return void */ - public function setOptions(array $options) + protected function reproportion() { + if (($this->width === 0 && $this->height === 0) OR $this->image->origWidth === 0 OR $this->image->origHeight === 0 + OR (! ctype_digit((string)$this->width) && ! ctype_digit((string)$this->height)) + OR ! ctype_digit((string)$this->image->origWidth) OR ! ctype_digit((string)$this->image->origHeight) + ) + { + return; + } + + // Sanitize + $this->width = (int)$this->width; + $this->height = (int)$this->height; + if ($this->masterDim !== 'width' && $this->masterDim !== 'height') + { + if ($this->width > 0 && $this->height > 0) + { + $this->masterDim = ((($this->image->origHeight / $this->image->origWidth)-($this->height / $this->width)) < 0) + ? 'width' : 'height'; + } + else + { + $this->masterDim = ($this->height === 0) ? 'width' : 'height'; + } + } + elseif (($this->masterDim === 'width' && $this->width === 0) OR ($this->masterDim === 'height' && $this->height === 0) + ) + { + return; + } + + if ($this->masterDim === 'width') + { + $this->height = (int)ceil($this->width*$this->image->origHeight/$this->image->origWidth); + } + else + { + $this->width = (int)ceil($this->image->origWidth*$this->height/$this->image->origHeight); + } } //-------------------------------------------------------------------- + } diff --git a/system/Images/Handlers/GDHandler.php b/system/Images/Handlers/GDHandler.php new file mode 100644 index 000000000000..4eec8d0fc195 --- /dev/null +++ b/system/Images/Handlers/GDHandler.php @@ -0,0 +1,333 @@ +image->origWidth; + $origHeight = $this->image->origHeight; + + if ($action == 'crop') + { + // Reassign the source width/height if cropping + $origWidth = $this->width; + $origHeight = $this->height; + } + + // Create the image handle + if (! ($src = $this->createImage())) + { + return false; + } + + if (function_exists('imagecreatetruecolor')) + { + $create = 'imagecreatetruecolor'; + $copy = 'imagecopyresampled'; + } + else + { + $create = 'imagecreate'; + $copy = 'imagecopyresized'; + } + + $dest = $create($this->width, $this->height); + + if ($this->image->imageType === IMAGETYPE_PNG) // png we can actually preserve transparency + { + imagealphablending($dest, false); + imagesavealpha($dest, true); + } + + $copy($dest, $src, 0, 0, $this->xAxis, $this->yAxis, $this->width, $this->height, $origWidth, $origHeight); + + $this->resource = $dest; + imagedestroy($src); + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Saves any changes that have been made to file. If no new filename is + * provided, the existing image is overwritten, otherwise a copy of the + * file is made at $target. + * + * Example: + * $image->resize(100, 200, true) + * ->save(); + * + * @param string|null $target + * @param int $quality + * + * @return bool + */ + public function save(string $target = null, int $quality=90) + { + $target = empty($target) + ? $this->image->getPathname() + : $target; + + switch ($this->image->imageType) + { + case IMAGETYPE_GIF: + if (! function_exists('imagegif')) + { + $this->errors[] = lang('images.unsupportedImagecreate'); + $this->errors[] = lang('images.gifNotSupported'); + + return false; + } + + if (! @imagegif($this->resource, $target)) + { + $this->errors[] = lang('images.saveFailed'); + + return false; + } + break; + case IMAGETYPE_JPEG: + if (! function_exists('imagejpeg')) + { + $this->errors[] = lang('images.unsupportedImagecreate'); + $this->errors[] = lang('images.jpgNotSupported'); + + return false; + } + + if (! @imagejpeg($this->resource, $target, $quality)) + { + $this->errors[] = lang('images.saveFailed'); + + return false; + } + break; + case IMAGETYPE_PNG: + if (! function_exists('imagepng')) + { + $this->errors[] = lang('images.unsupportedImagecreate'); + $this->errors[] = lang('images.pngNotSupported'); + + return false; + } + + if (! @imagepng($this->resource, $target)) + { + $this->errors[] = lang('images.saveFailed'); + + return false; + } + break; + default: + $this->errors[] = lang('images.unsupportedImagecreate'); + + return false; + break; + } + + imagedestroy($this->resource); + + chmod($target, $this->filePermissions); + + return true; + } + + //-------------------------------------------------------------------- + + /** + * Create Image Resource + * + * This simply creates an image resource handle + * based on the type of image being processed + * + * @param string + * @param string + * + * @return resource|bool + */ + protected function createImage($path = '', $imageType = '') + { + if ($this->resource !== null) + { + return clone($this->resource); + } + + if ($path === '') + { + $path = $this->image->getPathname(); + } + + if ($imageType === '') + { + $imageType = $this->image->imageType; + } + + switch ($imageType) + { + case IMAGETYPE_GIF: + if (! function_exists('imagecreatefromgif')) + { + $this->errors[] = lang('images.gifNotSupported'); + + return false; + } + + return imagecreatefromgif($path); + case IMAGETYPE_JPEG: + if (! function_exists('imagecreatefromjpeg')) + { + $this->errors[] = lang('images.jpgNotSupported'); + + return false; + } + + return imagecreatefromjpeg($path); + case IMAGETYPE_PNG: + if (! function_exists('imagecreatefrompng')) + { + $this->errors[] = lang('images.pngNotSupported'); + + return false; + } + + return imagecreatefrompng($path); + default: + $this->errors[] = lang('images.unsupportedImagecreate'); + + return false; + } + } + + //-------------------------------------------------------------------- + +} diff --git a/system/Images/Image.php b/system/Images/Image.php index 014917cf88a3..0592940a719f 100644 --- a/system/Images/Image.php +++ b/system/Images/Image.php @@ -2,234 +2,119 @@ use CodeIgniter\Files\File; -class Image extends File { - - /** - * @var \CodeIgniter\Images\ImageHandlerInterface - */ - protected $handler; - +class Image extends File +{ /** - * Stores any errors that were encountered. + * The original image width in pixels. * - * @var array + * @var */ - protected $errors = []; - - //-------------------------------------------------------------------- - + public $origWidth; /** - * Sets the Image processign handler that should be used. - * - * @param \CodeIgniter\Images\ImageHandlerInterface $handler + * The original image height in pixels. * - * @return $this + * @var */ - public function setHandler(ImageHandlerInterface $handler) - { - $this->handler = $handler; - - return $this; - } - - //-------------------------------------------------------------------- - - public function save(): bool - { - - } - - //-------------------------------------------------------------------- - - public function copy(string $target, int $perms=0644) - { - - } - - //-------------------------------------------------------------------- - + public $origHeight; /** - * Returns a boolean flag whether any errors were encountered. + * The image type constant. + * @see http://php.net/manual/en/image.constants.php * - * @return bool + * @var int */ - public function hasErrors(): bool - { - return ! empty($this->errors); - } - - //-------------------------------------------------------------------- + public $imageType; /** - * Returns all error messages that were encountered during processing. + * attributes string with size info: + * 'height="100" width="200"' * - * @return array + * @var string */ - public function getErrors(): array - { - return $this->errors ?? []; - } - - //-------------------------------------------------------------------- + public $sizeStr; /** - * Resize the image - * - * @param int $width - * @param int $height - * @param bool $maintainRatio If true, will get the closest match possible while keeping aspect ratio true. + * The image's mime type, i.e. image/jpeg * - * @return $this + * @var string */ - public function resize(int $width, int $height, bool $maintainRatio = false) - { - try { - $this->handler->resize($width, $height, $maintainRatio); - } - catch (ImageException $e) - { - $this->errors[] = $e->getMessage(); - } - - return $this; - } - - //-------------------------------------------------------------------- + public $mime; /** - * Crops the image to the desired height and width. If one of the height/width values - * is not provided, that value will be set the appropriate value based on offsets and - * image dimensions. + * Makes a copy of itself to the new location. If no filename is provided + * it will use the existing filename. * - * @param int|null $width - * @param int|null $height - * @param int|null $x X-axis coord to start cropping from the left of image - * @param int|null $y Y-axis coord to start cropping from the top of image + * @param string $targetPath The directory to store the file in + * @param string|null $targetName The new name of the copied file. + * @param int $perms File permissions to be applied after copy. * - * @return $this + * @return bool */ - public function crop(int $width = null, int $height = null, int $x = null, int $y = null) + public function copy(string $targetPath, string $targetName = null, int $perms = 0644) { - try { - $this->handler->crop($width, $height, $x, $y); - } - catch (ImageException $e) - { - $this->errors[] = $e->getMessage(); - } + $targetPath = rtrim($targetPath, '/ ').'/'; - return $this; - } - - //-------------------------------------------------------------------- + $targetName = is_null($targetName) + ? $this->getFilename() + : $targetName; - /** - * Rotates the image on the current canvas. - * - * @param float $angle - * - * @return mixed - */ - public function rotate(float $angle) - { - try { - $this->handler->rotate($angle); - } - catch (ImageException $e) + if (empty($targetName)) { - $this->errors[] = $e->getMessage(); + throw new ImageException('Invalid file name.'); } - return $this; - } - - //-------------------------------------------------------------------- - - /** - * @return $this - */ - public function watermark() - { - - } - - //-------------------------------------------------------------------- - - /** - * Reads the EXIF information from the image and modifies the orientation - * so that displays correctly in the browser. - * - * @return $this - */ - public function reorient(): bool - { - try { - $this->handler->reorient(); - } - catch (ImageException $e) + if (! is_dir($targetPath)) { - $this->errors[] = $e->getMessage(); + mkdir($targetName, 0755, true); } - return $this; - } - - //-------------------------------------------------------------------- - - /** - * Retrieve the EXIF information from the image, if possible. Returns - * an array of the information, or null if nothing can be found. - * - * @param string|null $key If specified, will only return this piece of EXIF data. - * - * @return mixed - */ - public function getEXIF(string $key = null) - { - try { - $this->handler->getEXIF($key); - } - catch (ImageException $e) + if (! copy($this->getPathname(),"{$targetPath}{$targetName}")) { - $this->errors[] = $e->getMessage(); + throw new ImageException('Unable to copy image to new destination.'); } - return $this; + chmod("{$targetPath}/{$targetName}", $perms); + + return true; } //-------------------------------------------------------------------- /** - * Combine cropping and resizing into a single command. + * Get image properties * - * Supported positions: - * - top-left - * - top - * - top-right - * - left - * - center - * - right - * - bottom-left - * - bottom - * - bottom-right + * A helper function that gets info about the file * - * @param int $width - * @param int $height - * @param string $position + * @param string + * @param bool * - * @return $this + * @return mixed */ - public function fit(int $width, int $height, string $position) + public function getProperties($return = false) { - try { - $this->handler->fit($width, $height, $position); - } - catch (ImageException $e) + $path = $this->getPathname(); + + $vals = getimagesize($path); + $types = [1 => 'gif', 2 => 'jpeg', 3 => 'png']; + $mime = (isset($types[$vals[2]])) ? 'image/'.$types[$vals[2]] : 'image/jpg'; + + if ($return === true) { - $this->errors[] = $e->getMessage(); + return [ + 'width' => $vals[0], + 'height' => $vals[1], + 'image_type' => $vals[2], + 'size_str' => $vals[3], + 'mime_type' => $mime, + ]; } - return $this; + $this->origWidth = $vals[0]; + $this->origHeight = $vals[1]; + $this->imageType = $vals[2]; + $this->sizeStr = $vals[3]; + $this->mime = $mime; + + return true; } //-------------------------------------------------------------------- diff --git a/system/Images/ImageHandlerInterface.php b/system/Images/ImageHandlerInterface.php index 6e9c96666eeb..e9a690a949fd 100644 --- a/system/Images/ImageHandlerInterface.php +++ b/system/Images/ImageHandlerInterface.php @@ -93,30 +93,4 @@ public function fit(int $width, int $height, string $position): bool; //-------------------------------------------------------------------- - /** - * Allows any option to be easily set. We don't mind doing it - * this way here, since the Handler is not something the user - * will be directly interfacing with. - * - * @param string $key - * @param null $value - * - * @return mixed - */ - public function setOption(string $key, $value = null); - - //-------------------------------------------------------------------- - - /** - * Allows multiple options to be set at once through an array of - * key value pairs, where the keys must be Handler properties. - * - * @param array $options - * - * @return mixed - */ - public function setOptions(array $options); - - //-------------------------------------------------------------------- - } diff --git a/system/Language/en/Images.php b/system/Language/en/Images.php new file mode 100644 index 000000000000..0b11ceb5672c --- /dev/null +++ b/system/Language/en/Images.php @@ -0,0 +1,21 @@ + 'You must specify a source image in your preferences.', + 'gdRequired' => 'The GD image library is required to use this feature.', + 'gdRequiredForProps' => 'Your server must support the GD image library in order to determine the image properties.', + 'gifNotSupported' => 'GIF images are often not supported due to licensing restrictions. You may have to use JPG or PNG images instead.', + 'jpgNotSupported' => 'JPG images are not supported.', + 'pngNotSupported' => 'PNG images are not supported.', + 'unsupportedImagecreate' => 'Your server does not support the GD function required to process this type of image.', + 'jpgOrPngRequired' => 'The image resize protocol specified in your preferences only works with JPEG or PNG image types.', + 'copyError' => 'An error was encountered while attempting to replace the file. Please make sure your file directory is writable.', + 'rotateUnsupported' => 'Image rotation does not appear to be supported by your server.', + 'libPathInvalid' => 'The path to your image library is not correct. Please set the correct path in your image preferences.', + 'imageProcessFailed' => 'Image processing failed. Please verify that your server supports the chosen protocol and that the path to your image library is correct.', + 'rotationAngleRequired' => 'An angle of rotation is required to rotate the image.', + 'invalidPath' => 'The path to the image is not correct.', + 'copyFailed' => 'The image copy routine failed.', + 'missingFont' => 'Unable to find a font to use.', + 'saveFailed' => 'Unable to save the image. Please make sure the image and file directory are writable.', +]; diff --git a/tests/_support/ci-logo.png b/tests/_support/ci-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a61f163022b70ae20a247aaebe0ef4aa0cb533f1 GIT binary patch literal 7760 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 + + $this->assertTrue(is_array($image->getProperties(true))); + } + + public function () + { + + } + +} diff --git a/tests/system/Images/ImageTest.php b/tests/system/Images/ImageTest.php new file mode 100644 index 000000000000..bb8f76a3542c --- /dev/null +++ b/tests/system/Images/ImageTest.php @@ -0,0 +1,56 @@ +path); + + $this->assertEquals('ci-logo.png', $image->getFilename()); + $this->assertEquals(ROOTPATH.$this->path, $image->getPathname()); + $this->assertEquals(ROOTPATH.'tests/_support', $image->getPath()); + $this->assertEquals('ci-logo.png', $image->getBasename()); + } + + + public function testGetProperties() + { + $image = new Image(ROOTPATH.$this->path); + + $expected = [ + 'width' => 155, + 'height' => 200, + 'image_type' => IMAGETYPE_PNG, + 'size_str' => 'width="155" height="200"', + 'mime_type' => "image/png", + ]; + + $this->assertEquals($expected, $image->getProperties(true)); + } + + + public function testCanCopyDefaultName() + { + $image = new Image(ROOTPATH.$this->path); + + $image->copy(WRITEPATH); + + $this->assertFileExists(WRITEPATH.'ci-logo.png'); + + unlink(WRITEPATH.'ci-logo.png'); + } + + public function testCanCopyNewName() + { + $image = new Image(ROOTPATH.$this->path); + + $image->copy(WRITEPATH, 'new-logo.png'); + + $this->assertFileExists(WRITEPATH.'new-logo.png'); + + unlink(WRITEPATH.'new-logo.png'); + } + +} From 4efe901d7313568cc8dc78dc7f604ddaba99589d Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Thu, 4 May 2017 00:04:01 -0500 Subject: [PATCH 03/13] Crop, Reorient, Fit and more. Still have an issue with Fit when a height is given. --- application/Controllers/Checks.php | 10 +- system/Images/Handlers/BaseHandler.php | 305 +++++++++++++++++++++--- system/Images/Handlers/GDHandler.php | 184 ++++++++------ system/Images/ImageHandlerInterface.php | 6 +- system/Language/en/Images.php | 34 +-- 5 files changed, 409 insertions(+), 130 deletions(-) diff --git a/application/Controllers/Checks.php b/application/Controllers/Checks.php index 3b03a10ac9ea..ba6c1bccf4c2 100644 --- a/application/Controllers/Checks.php +++ b/application/Controllers/Checks.php @@ -153,11 +153,19 @@ public function redirect() public function image() { +// $images = Services::image('gd') +// ->withFile("/Users/kilishan/Documents/BobHeader.jpg") +// ->resize(500, 100, true) +// ->crop(200, 75, 20, 0, false) +// ->rotate(90) +// ->save('/Users/kilishan/temp.jpg', 100); + $images = Services::image('gd') ->withFile("/Users/kilishan/Documents/BobHeader.jpg") - ->crop(200, 75, 20, 0, false) + ->fit(500, 100, 'center') ->save('/Users/kilishan/temp.jpg', 100); + ddd($images); } diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index 078274ddd3bf..7f3b472a5e5c 100644 --- a/system/Images/Handlers/BaseHandler.php +++ b/system/Images/Handlers/BaseHandler.php @@ -1,19 +1,13 @@ errors); - } - - //-------------------------------------------------------------------- - - /** - * Returns all error messages that were encountered during processing. - * - * @return array - */ - public function getErrors(): array - { - return $this->errors ?? []; + return $this->resource; } //-------------------------------------------------------------------- @@ -104,14 +95,14 @@ public function getErrors(): array * @param int $height * @param bool $maintainRation If true, will get the closest match possible while keeping aspect ratio true. * - * @return bool|\CodeIgniter\Images\Handlers\GDHandler + * @return \CodeIgniter\Images\Handlers\BaseHandler */ public function resize(int $width, int $height, bool $maintainRatio = false, string $masterDim = 'auto') { // If the target width/height match the source, then we have nothing to do here. if ($this->image->origWidth === $width && $this->image->origHeight === $height) { - return true; + return $this; } $this->width = $width; @@ -144,10 +135,10 @@ public function resize(int $width, int $height, bool $maintainRatio = false, str */ public function crop(int $width = null, int $height = null, int $x = null, int $y = null, bool $maintainRatio = false, string $masterDim = 'auto') { - $this->width = $width; + $this->width = $width; $this->height = $height; - $this->xAxis = $x; - $this->yAxis = $y; + $this->xAxis = $x; + $this->yAxis = $y; if ($maintainRatio) { @@ -167,7 +158,77 @@ public function crop(int $width = null, int $height = null, int $x = null, int $ * * @return mixed */ - public abstract function rotate(float $angle); + public function rotate(float $angle) + { + // Allowed rotation values + $degs = [90, 180, 270]; + + if ($angle === '' || ! in_array($angle, $degs)) + { + throw new ImageException(lang('images.rotationAngleRequired')); + } + + // Reassign the width and height + if ($angle === 90 OR $angle === 270) + { + $this->width = $this->image->origHeight; + $this->height = $this->image->origWidth; + } + else + { + $this->width = $this->image->origWidth; + $this->height = $this->image->origHeight; + } + + // Call the Handler-specific version. + $this->_rotate($angle); + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Handler-specific method to handle rotating an image in 90 degree increments. + * + * @param int $angle + * + * @return mixed + */ + protected abstract function _rotate(int $angle); + + //-------------------------------------------------------------------- + + /** + * Flips an image either horizontally or vertically. + * + * @param string $dir Either 'vertical' or 'horizontal' + * + * @return $this + */ + public function flip(string $dir) + { + $dir = strtolower($dir); + + if ($dir !== 'vertical' && $dir !== 'horizontal') + { + throw new ImageException(lang('images.invalidDirection')); + } + + return $this->_flip($dir); + } + + //-------------------------------------------------------------------- + + /** + * Handler-specific method to handle flipping an image along its + * horizontal or vertical axis. + * + * @param string $direction + * + * @return mixed + */ + protected abstract function _flip(string $direction); //-------------------------------------------------------------------- @@ -180,11 +241,45 @@ public abstract function watermark(); /** * Reads the EXIF information from the image and modifies the orientation - * so that displays correctly in the browser. + * so that displays correctly in the browser. This is especially an issue + * with images taken by smartphones who always store the image up-right, + * but set the orientation flag to display it correctly. * - * @return bool + * @param bool $silent If true, will ignore exceptions when PHP doesn't support EXIF. + * + * @return $this */ - public abstract function reorient(): bool; + public function reorient(bool $silent = false) + { + $orientation = $this->getEXIF('Orientation', $silent); + + switch ($orientation) + { + case 2: + return $this->flip('horizontal'); + break; + case 3: + return $this->rotate(180); + break; + case 4: + return $this->rotate(180)->flip('horizontal'); + break; + case 5: + return $this->rotate(270)->flip('horizontal'); + break; + case 6: + return $this->rotate(270); + break; + case 7: + return $this->rotate(90)->flip('horizontal'); + break; + case 8: + return $this->rotate(90); + break; + default: + return $this; + } + } //-------------------------------------------------------------------- @@ -194,9 +289,30 @@ public abstract function reorient(): bool; * * @param string|null $key If specified, will only return this piece of EXIF data. * + * @param bool $silent If true, will not throw our own exceptions. + * * @return mixed */ - public abstract function getEXIF(string $key = null); + public function getEXIF(string $key = null, bool $silent = false) + { + if (! function_exists('exif_read_data')) + { + if ($silent) return null; + + throw new ImageException(lang('images.exifNotSupported')); + } + + $exif = exif_read_data($this->image->getPathname()); + + if (! is_null($key) && is_array($exif)) + { + $exif = array_key_exists($key, $exif) + ? $exif[$key] + : false; + } + + return $exif; + } //-------------------------------------------------------------------- @@ -220,7 +336,126 @@ public abstract function getEXIF(string $key = null); * * @return bool */ - public abstract function fit(int $width, int $height, string $position): bool; + public function fit(int $width, int $height=null, string $position = 'center') + { + $origWidth = $this->image->origWidth; + $origHeight = $this->image->origHeight; + + list($cropWidth, $cropHeight) = $this->calcAspectRatio($width, $height, $origWidth, $origHeight); + + if (is_null($height)) + { + $height = ceil(($width / $cropWidth) * $cropHeight); + } + + list($x, $y) = $this->calcCropCoords($width, $height, $origWidth, $origHeight, $position); +// var_dump($cropWidth.' + '.$cropHeight); +// dd($x.' + '. $y); + return $this->crop($cropWidth, $cropHeight, $x, $y) + ->resize($width, $height); + } + + //-------------------------------------------------------------------- + + /** + * + * + * @param $width + * @param null $height + * @param $origWidth + * @param $origHeight + * + * @return array + */ + protected function calcAspectRatio($width, $height = null, $origWidth, $origHeight): array + { + // If $height is null, then we have it easy. + // Calc based on full image size and be done. + if (is_null($height)) + { + $height = ($width / $origWidth) * $origHeight; + + return [$width, (int)$height]; + } + + $xRatio = $width / $origWidth; + $yRatio = $height / $origHeight; + + if ($xRatio > $yRatio) + { + return [ + (int)($origWidth * $yRatio), + (int)$height + ]; + } + + return [ + (int)$width, + (int)($origHeight * $xRatio) + ]; + } + + //-------------------------------------------------------------------- + + /** + * Based on the position, will determine the correct x/y coords to + * crop the desired portion from the image. + * + * @param $width + * @param $height + * @param $origWidth + * @param $origHeight + * @param $position + * + * @return array + */ + protected function calcCropCoords($width, $height, $origWidth, $origHeight, $position): array + { + $position = strtolower($position); + $x = $y = 0; + + switch ($position) + { + case 'top-left': + $x = 0; + $y = 0; + break; + case 'top': + $x = floor(($origWidth - $width) / 2); + $y = 0; + break; + case 'top-right': + $x = $origWidth - $width; + $y = 0; + break; + case 'left': + $x = 0; + $y = floor(($origHeight - $height) / 2); + break; + case 'center': + $x = floor(($origWidth - $width) / 2); + $y = floor(($origHeight - $height) / 2); + break; + case 'right': + $x = ($origWidth - $width); + $y = floor(($origHeight - $height) / 2); + break; + case 'bottom-left': + $x = 0; + $y = $origHeight - $height; + break; + case 'bottom': + $x = floor(($origWidth - $width) / 2); + $y = $origHeight - $height; + break; + case 'bottom-right': + $x = ($origWidth - $width); + $y = $origHeight - $height; + break; + } + + return [$x, $y]; + } //-------------------------------------------------------------------- @@ -307,7 +542,7 @@ protected function reproportion() { if ($this->width > 0 && $this->height > 0) { - $this->masterDim = ((($this->image->origHeight / $this->image->origWidth)-($this->height / $this->width)) < 0) + $this->masterDim = ((($this->image->origHeight/$this->image->origWidth)-($this->height/$this->width)) < 0) ? 'width' : 'height'; } else diff --git a/system/Images/Handlers/GDHandler.php b/system/Images/Handlers/GDHandler.php index 4eec8d0fc195..a2a45046d54a 100644 --- a/system/Images/Handlers/GDHandler.php +++ b/system/Images/Handlers/GDHandler.php @@ -21,21 +21,104 @@ public function __construct($config = null) { throw new ImageException('GD Extension is not loaded.'); } + } + + //-------------------------------------------------------------------- + + /** + * Handles the rotation of an image resource. + * Doesn't save the image, but replaces the current resource. + * + * @param int $angle + * + * @return bool + */ + protected function _rotate(int $angle) + { + // Create the image handle + if (! ($srcImg = $this->createImage())) + { + return false; + } + + // Set the background color + // This won't work with transparent PNG files so we are + // going to have to figure out how to determine the color + // of the alpha channel in a future release. + + $white = imagecolorallocate($srcImg, 255, 255, 255); + + // Rotate it! + $destImg = imagerotate($this->resource, $angle, $white); + + // Kill the file handles + imagedestroy($srcImg); + $this->resource = $destImg; + + return true; } //-------------------------------------------------------------------- /** - * Rotates the image on the current canvas. + * Flips an image along it's vertical or horizontal axis. * - * @param float $angle + * @param string $direction * - * @return mixed + * @return $this */ - public function rotate(float $angle) + public function _flip(string $direction) { + $srcImg = $this->createImage(); + + $width = $this->image->origWidth; + $height = $this->image->origHeight; + + if ($direction === 'horizontal') + { + for ($i = 0; $i < $height; $i++) + { + $left = 0; + $right = $width-1; + + while ($left < $right) + { + $cl = imagecolorat($srcImg, $left, $i); + $cr = imagecolorat($srcImg, $right, $i); + + imagesetpixel($srcImg, $left, $i, $cr); + imagesetpixel($srcImg, $right, $i, $cl); + + $left++; + $right--; + } + } + } + else + { + for ($i = 0; $i < $width; $i++) + { + $top = 0; + $bottom = $height-1; + + while ($top < $bottom) + { + $ct = imagecolorat($srcImg, $i, $top); + $cb = imagecolorat($srcImg, $i, $bottom); + + imagesetpixel($srcImg, $i, $top, $cb); + imagesetpixel($srcImg, $i, $bottom, $ct); + $top++; + $bottom--; + } + } + } + + $this->resource = $srcImg; + + return $this; } //-------------------------------------------------------------------- @@ -78,33 +161,6 @@ public function getEXIF(string $key = null) //-------------------------------------------------------------------- - /** - * Combine cropping and resizing into a single command. - * - * Supported positions: - * - top-left - * - top - * - top-right - * - left - * - center - * - right - * - bottom-left - * - bottom - * - bottom-right - * - * @param int $width - * @param int $height - * @param string $position - * - * @return bool - */ - public function fit(int $width, int $height, string $position): bool - { - - } - - //-------------------------------------------------------------------- - /** * Get GD version * @@ -139,15 +195,18 @@ protected function process(string $action) if ($action == 'crop') { // Reassign the source width/height if cropping - $origWidth = $this->width; + $origWidth = $this->width; $origHeight = $this->height; + + // Modify the "original" width/height to the new + // values so that methods that come after have the + // correct size to work with. + $this->image->origHeight = $this->height; + $this->image->origWidth = $this->width; } // Create the image handle - if (! ($src = $this->createImage())) - { - return false; - } + $src = $this->createImage(); if (function_exists('imagecreatetruecolor')) { @@ -170,8 +229,8 @@ protected function process(string $action) $copy($dest, $src, 0, 0, $this->xAxis, $this->yAxis, $this->width, $this->height, $origWidth, $origHeight); - $this->resource = $dest; imagedestroy($src); + $this->resource = $dest; return $this; } @@ -192,7 +251,7 @@ protected function process(string $action) * * @return bool */ - public function save(string $target = null, int $quality=90) + public function save(string $target = null, int $quality = 90) { $target = empty($target) ? $this->image->getPathname() @@ -203,55 +262,38 @@ public function save(string $target = null, int $quality=90) case IMAGETYPE_GIF: if (! function_exists('imagegif')) { - $this->errors[] = lang('images.unsupportedImagecreate'); - $this->errors[] = lang('images.gifNotSupported'); - - return false; + throw new ImageException(lang('images.unsupportedImagecreate').' '.lang('images.gifNotSupported')); } if (! @imagegif($this->resource, $target)) { - $this->errors[] = lang('images.saveFailed'); - - return false; + throw new ImageException(lang('images.saveFailed')); } break; case IMAGETYPE_JPEG: if (! function_exists('imagejpeg')) { - $this->errors[] = lang('images.unsupportedImagecreate'); - $this->errors[] = lang('images.jpgNotSupported'); - - return false; + throw new ImageException(lang('images.unsupportedImagecreate').' '.lang('images.jpgNotSupported')); } if (! @imagejpeg($this->resource, $target, $quality)) { - $this->errors[] = lang('images.saveFailed'); - - return false; + throw new ImageException(lang('images.saveFailed')); } break; case IMAGETYPE_PNG: if (! function_exists('imagepng')) { - $this->errors[] = lang('images.unsupportedImagecreate'); - $this->errors[] = lang('images.pngNotSupported'); - - return false; + throw new ImageException(lang('images.unsupportedImagecreate').' '.lang('images.pngNotSupported')); } if (! @imagepng($this->resource, $target)) { - $this->errors[] = lang('images.saveFailed'); - - return false; + throw new ImageException(lang('images.saveFailed')); } break; default: - $this->errors[] = lang('images.unsupportedImagecreate'); - - return false; + throw new ImageException(lang('images.unsupportedImagecreate')); break; } @@ -279,7 +321,7 @@ protected function createImage($path = '', $imageType = '') { if ($this->resource !== null) { - return clone($this->resource); + return $this->resource; } if ($path === '') @@ -297,34 +339,26 @@ protected function createImage($path = '', $imageType = '') case IMAGETYPE_GIF: if (! function_exists('imagecreatefromgif')) { - $this->errors[] = lang('images.gifNotSupported'); - - return false; + throw new ImageException(lang('images.gifNotSupported')); } return imagecreatefromgif($path); case IMAGETYPE_JPEG: if (! function_exists('imagecreatefromjpeg')) { - $this->errors[] = lang('images.jpgNotSupported'); - - return false; + throw new ImageException(lang('images.jpgNotSupported')); } return imagecreatefromjpeg($path); case IMAGETYPE_PNG: if (! function_exists('imagecreatefrompng')) { - $this->errors[] = lang('images.pngNotSupported'); - - return false; + throw new ImageException(lang('images.pngNotSupported')); } return imagecreatefrompng($path); default: - $this->errors[] = lang('images.unsupportedImagecreate'); - - return false; + throw new ImageException(lang('images.unsupportedImagecreate')); } } diff --git a/system/Images/ImageHandlerInterface.php b/system/Images/ImageHandlerInterface.php index e9a690a949fd..7eb75cb1327c 100644 --- a/system/Images/ImageHandlerInterface.php +++ b/system/Images/ImageHandlerInterface.php @@ -51,9 +51,9 @@ public function watermark(); * Reads the EXIF information from the image and modifies the orientation * so that displays correctly in the browser. * - * @return bool + * @return $this */ - public function reorient(): bool; + public function reorient(); //-------------------------------------------------------------------- @@ -89,7 +89,7 @@ public function getEXIF(string $key = null); * * @return bool */ - public function fit(int $width, int $height, string $position): bool; + public function fit(int $width, int $height, string $position); //-------------------------------------------------------------------- diff --git a/system/Language/en/Images.php b/system/Language/en/Images.php index 0b11ceb5672c..75eb42702ac5 100644 --- a/system/Language/en/Images.php +++ b/system/Language/en/Images.php @@ -1,21 +1,23 @@ 'You must specify a source image in your preferences.', - 'gdRequired' => 'The GD image library is required to use this feature.', - 'gdRequiredForProps' => 'Your server must support the GD image library in order to determine the image properties.', - 'gifNotSupported' => 'GIF images are often not supported due to licensing restrictions. You may have to use JPG or PNG images instead.', - 'jpgNotSupported' => 'JPG images are not supported.', - 'pngNotSupported' => 'PNG images are not supported.', + 'sourceImageRequired' => 'You must specify a source image in your preferences.', + 'gdRequired' => 'The GD image library is required to use this feature.', + 'gdRequiredForProps' => 'Your server must support the GD image library in order to determine the image properties.', + 'gifNotSupported' => 'GIF images are often not supported due to licensing restrictions. You may have to use JPG or PNG images instead.', + 'jpgNotSupported' => 'JPG images are not supported.', + 'pngNotSupported' => 'PNG images are not supported.', 'unsupportedImagecreate' => 'Your server does not support the GD function required to process this type of image.', - 'jpgOrPngRequired' => 'The image resize protocol specified in your preferences only works with JPEG or PNG image types.', - 'copyError' => 'An error was encountered while attempting to replace the file. Please make sure your file directory is writable.', - 'rotateUnsupported' => 'Image rotation does not appear to be supported by your server.', - 'libPathInvalid' => 'The path to your image library is not correct. Please set the correct path in your image preferences.', - 'imageProcessFailed' => 'Image processing failed. Please verify that your server supports the chosen protocol and that the path to your image library is correct.', - 'rotationAngleRequired' => 'An angle of rotation is required to rotate the image.', - 'invalidPath' => 'The path to the image is not correct.', - 'copyFailed' => 'The image copy routine failed.', - 'missingFont' => 'Unable to find a font to use.', - 'saveFailed' => 'Unable to save the image. Please make sure the image and file directory are writable.', + 'jpgOrPngRequired' => 'The image resize protocol specified in your preferences only works with JPEG or PNG image types.', + 'copyError' => 'An error was encountered while attempting to replace the file. Please make sure your file directory is writable.', + 'rotateUnsupported' => 'Image rotation does not appear to be supported by your server.', + 'libPathInvalid' => 'The path to your image library is not correct. Please set the correct path in your image preferences.', + 'imageProcessFailed' => 'Image processing failed. Please verify that your server supports the chosen protocol and that the path to your image library is correct.', + 'rotationAngleRequired' => 'An angle of rotation is required to rotate the image.', + 'invalidPath' => 'The path to the image is not correct.', + 'copyFailed' => 'The image copy routine failed.', + 'missingFont' => 'Unable to find a font to use.', + 'saveFailed' => 'Unable to save the image. Please make sure the image and file directory are writable.', + 'invalidDirection' => 'Flip direction can be only `vertical` or `horizontal`.', + 'exifNotSupported' => 'Reading EXIF data is not supported by this PHP installation.', ]; From dcd01f13d0f7adb92d3d1a6f0f0fef9e5825e59a Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Thu, 4 May 2017 22:59:25 -0500 Subject: [PATCH 04/13] Small tweak --- application/Controllers/Checks.php | 2 +- system/Images/Handlers/BaseHandler.php | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/application/Controllers/Checks.php b/application/Controllers/Checks.php index ba6c1bccf4c2..7c8751eacf66 100644 --- a/application/Controllers/Checks.php +++ b/application/Controllers/Checks.php @@ -162,7 +162,7 @@ public function image() $images = Services::image('gd') ->withFile("/Users/kilishan/Documents/BobHeader.jpg") - ->fit(500, 100, 'center') + ->fit(500, 100, 'bottom-left') ->save('/Users/kilishan/temp.jpg', 100); diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index 7f3b472a5e5c..f7aac4819fe7 100644 --- a/system/Images/Handlers/BaseHandler.php +++ b/system/Images/Handlers/BaseHandler.php @@ -146,7 +146,12 @@ public function crop(int $width = null, int $height = null, int $x = null, int $ $this->reproportion(); } - return $this->process('crop'); + $result = $this->process('crop'); + + $this->xAxis = null; + $this->yAxis = null; + + return $result; } //-------------------------------------------------------------------- @@ -349,8 +354,7 @@ public function fit(int $width, int $height=null, string $position = 'center') } list($x, $y) = $this->calcCropCoords($width, $height, $origWidth, $origHeight, $position); -// var_dump($cropWidth.' + '.$cropHeight); -// dd($x.' + '. $y); + return $this->crop($cropWidth, $cropHeight, $x, $y) ->resize($width, $height); } @@ -385,12 +389,12 @@ protected function calcAspectRatio($width, $height = null, $origWidth, $origHeig { return [ (int)($origWidth * $yRatio), - (int)$height + (int)($origHeight * $yRatio) ]; } return [ - (int)$width, + (int)($origWidth * $xRatio), (int)($origHeight * $xRatio) ]; } From 08dee709926c26682a2ef40fb4727e92661e82c5 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Thu, 4 May 2017 23:54:54 -0500 Subject: [PATCH 05/13] Basic text overlay for GD. --- application/Controllers/Checks.php | 3 + system/Images/Handlers/BaseHandler.php | 194 +++++++++++++++++++++---- system/Images/Handlers/GDHandler.php | 33 +++++ 3 files changed, 199 insertions(+), 31 deletions(-) diff --git a/application/Controllers/Checks.php b/application/Controllers/Checks.php index 7c8751eacf66..fd1856571d2f 100644 --- a/application/Controllers/Checks.php +++ b/application/Controllers/Checks.php @@ -163,6 +163,9 @@ public function image() $images = Services::image('gd') ->withFile("/Users/kilishan/Documents/BobHeader.jpg") ->fit(500, 100, 'bottom-left') + ->text('Bob is Back!', [ + 'fontSize' => 30, + ]) ->save('/Users/kilishan/temp.jpg', 100); diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index f7aac4819fe7..a9cb1179b858 100644 --- a/system/Images/Handlers/BaseHandler.php +++ b/system/Images/Handlers/BaseHandler.php @@ -27,6 +27,26 @@ abstract class BaseHandler implements ImageHandlerInterface protected $yAxis = 0; protected $masterDim = 'auto'; + /** + * Default options for text watermarking. + * + * @var array + */ + protected $textDefaults = [ + 'fontPath' => null, + 'fontSize' => 16, + 'color' => 'ffffff', + 'opacity' => 1.0, + 'vAlign' => 'bottom', + 'hAlign' => 'center', + 'vOffset' => 0, + 'hOffset' => 0, + 'padding' => 0, + 'withShadow' => false, + 'shadowColor' => '993300', + 'shadowOffset' => 3, + ]; + /** * Temporary image used by the different engines. * @@ -238,9 +258,115 @@ protected abstract function _flip(string $direction); //-------------------------------------------------------------------- /** - * @return mixed + * Overlays a string of text over the image. + * + * @return $this */ - public abstract function watermark(); + public function text(string $text, array $options = []) + { + $options = array_merge($this->textDefaults, $options); + $options['color'] = trim($options['color'], '# '); + $options['shadowColor'] = trim($options['shadowColor'], '# '); + + if (! empty($options['fontPath']) && ! file_exists($options['fontPath'])) + { + throw new ImageException(lang('images.missingFont')); + } + + // Reverse the vertical offset + // When the image is positioned at the bottom + // we don't want the vertical offset to push it + // further down. We want the reverse, so we'll + // invert the offset. Note: The horizontal + // offset flips itself automatically + + if ($options['vAlign'] === 'bottom') + { + $options['vOffset'] = $options['vOffset'] * -1; + } + + if ($options['hAlign'] === 'right') + { + $options['hOffset'] = $options['hOffset'] * -1; + } + + // Set font width and height + // These are calculated differently depending on + // whether we are using the true type font or not + if (! empty($options['fontPath'])) + { + if (function_exists('imagettfbbox')) + { + $temp = imagettfbbox($options['fontSize'], 0, $options['fontPath'], $text); + $temp = $temp[2] - $temp[0]; + + $fontwidth = $temp / strlen($text); + } + else + { + $fontwidth = $options['fontSize'] - ($options['fontSize'] / 4); + } + + $fontheight = $options['fontSize']; + $options['vOffset'] += $options['fontSize']; + } + else + { + $fontwidth = imagefontwidth($options['fontSize']); + $fontheight = imagefontheight($options['fontSize']); + } + + $options['fontheight'] = $fontheight; + $options['fontwidth'] = $fontwidth; + + // Set base X and Y axis values + $xAxis = $options['hOffset'] + $options['padding']; + $yAxis = $options['vOffset'] + $options['padding']; + + // Set vertical alignment + if ($options['vAlign'] === 'middle') + { + $yAxis += ($this->image->origHeight / 2) + ($fontheight / 2); + } + elseif ($options['vAlign'] === 'bottom') + { + $yAxis += $this->image->origHeight - $fontheight - $options['shadowOffset'] - ($fontheight / 2); + } + + // Set horizontal alignment + if ($options['hAlign'] === 'right') + { + $xAxis += $this->image->origWidth - ($fontwidth * strlen($text)) - $options['shadowOffset']; + } + elseif ($options['hAlign'] === 'center') + { + $xAxis += floor(($this->image->origWidth - ($fontwidth * strlen($text))) / 2); + } + + if ($options['withShadow']) + { + // Offset from text + $options['xShadow'] = $xAxis + $options['shadowOffset']; + $options['yShadow'] = $yAxis + $options['shadowOffset']; + + $this->textOverlay($text, $options, true); + } + + $this->textOverlay($text, $options, false); + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Handler-specific method for overlaying text on an image. + * + * @param string $text + * @param array $options + * @param bool $isShadow Whether we are drawing the dropshadow or actual text + */ + protected abstract function textOverlay(string $text, array $options = [], bool $isShadow=false); //-------------------------------------------------------------------- @@ -250,7 +376,7 @@ public abstract function watermark(); * with images taken by smartphones who always store the image up-right, * but set the orientation flag to display it correctly. * - * @param bool $silent If true, will ignore exceptions when PHP doesn't support EXIF. + * @param bool $silent If true, will ignore exceptions when PHP doesn't support EXIF. * * @return $this */ @@ -267,16 +393,19 @@ public function reorient(bool $silent = false) return $this->rotate(180); break; case 4: - return $this->rotate(180)->flip('horizontal'); + return $this->rotate(180) + ->flip('horizontal'); break; case 5: - return $this->rotate(270)->flip('horizontal'); + return $this->rotate(270) + ->flip('horizontal'); break; case 6: return $this->rotate(270); break; case 7: - return $this->rotate(90)->flip('horizontal'); + return $this->rotate(90) + ->flip('horizontal'); break; case 8: return $this->rotate(90); @@ -292,9 +421,9 @@ public function reorient(bool $silent = false) * Retrieve the EXIF information from the image, if possible. Returns * an array of the information, or null if nothing can be found. * - * @param string|null $key If specified, will only return this piece of EXIF data. + * @param string|null $key If specified, will only return this piece of EXIF data. * - * @param bool $silent If true, will not throw our own exceptions. + * @param bool $silent If true, will not throw our own exceptions. * * @return mixed */ @@ -302,7 +431,10 @@ public function getEXIF(string $key = null, bool $silent = false) { if (! function_exists('exif_read_data')) { - if ($silent) return null; + if ($silent) + { + return null; + } throw new ImageException(lang('images.exifNotSupported')); } @@ -341,7 +473,7 @@ public function getEXIF(string $key = null, bool $silent = false) * * @return bool */ - public function fit(int $width, int $height=null, string $position = 'center') + public function fit(int $width, int $height = null, string $position = 'center') { $origWidth = $this->image->origWidth; $origHeight = $this->image->origHeight; @@ -350,7 +482,7 @@ public function fit(int $width, int $height=null, string $position = 'center') if (is_null($height)) { - $height = ceil(($width / $cropWidth) * $cropHeight); + $height = ceil(($width/$cropWidth)*$cropHeight); } list($x, $y) = $this->calcCropCoords($width, $height, $origWidth, $origHeight, $position); @@ -377,25 +509,25 @@ protected function calcAspectRatio($width, $height = null, $origWidth, $origHeig // Calc based on full image size and be done. if (is_null($height)) { - $height = ($width / $origWidth) * $origHeight; + $height = ($width/$origWidth)*$origHeight; return [$width, (int)$height]; } - $xRatio = $width / $origWidth; - $yRatio = $height / $origHeight; + $xRatio = $width/$origWidth; + $yRatio = $height/$origHeight; if ($xRatio > $yRatio) { return [ - (int)($origWidth * $yRatio), - (int)($origHeight * $yRatio) + (int)($origWidth*$yRatio), + (int)($origHeight*$yRatio), ]; } return [ - (int)($origWidth * $xRatio), - (int)($origHeight * $xRatio) + (int)($origWidth*$xRatio), + (int)($origHeight*$xRatio), ]; } @@ -416,7 +548,7 @@ protected function calcAspectRatio($width, $height = null, $origWidth, $origHeig protected function calcCropCoords($width, $height, $origWidth, $origHeight, $position): array { $position = strtolower($position); - $x = $y = 0; + $x = $y = 0; switch ($position) { @@ -425,36 +557,36 @@ protected function calcCropCoords($width, $height, $origWidth, $origHeight, $pos $y = 0; break; case 'top': - $x = floor(($origWidth - $width) / 2); + $x = floor(($origWidth-$width)/2); $y = 0; break; case 'top-right': - $x = $origWidth - $width; + $x = $origWidth-$width; $y = 0; break; case 'left': $x = 0; - $y = floor(($origHeight - $height) / 2); + $y = floor(($origHeight-$height)/2); break; case 'center': - $x = floor(($origWidth - $width) / 2); - $y = floor(($origHeight - $height) / 2); + $x = floor(($origWidth-$width)/2); + $y = floor(($origHeight-$height)/2); break; case 'right': - $x = ($origWidth - $width); - $y = floor(($origHeight - $height) / 2); + $x = ($origWidth-$width); + $y = floor(($origHeight-$height)/2); break; case 'bottom-left': $x = 0; - $y = $origHeight - $height; + $y = $origHeight-$height; break; case 'bottom': - $x = floor(($origWidth - $width) / 2); - $y = $origHeight - $height; + $x = floor(($origWidth-$width)/2); + $y = $origHeight-$height; break; case 'bottom-right': - $x = ($origWidth - $width); - $y = $origHeight - $height; + $x = ($origWidth-$width); + $y = $origHeight-$height; break; } diff --git a/system/Images/Handlers/GDHandler.php b/system/Images/Handlers/GDHandler.php index a2a45046d54a..de0f0b8e2b8e 100644 --- a/system/Images/Handlers/GDHandler.php +++ b/system/Images/Handlers/GDHandler.php @@ -364,4 +364,37 @@ protected function createImage($path = '', $imageType = '') //-------------------------------------------------------------------- + /** + * Handler-specific method for overlaying text on an image. + * + * @param string $text + * @param array $options + * @param bool $isShadow Whether we are drawing the dropshadow or actual text + */ + protected function textOverlay(string $text, array $options = [], bool $isShadow=false) + { + /* Set RGB values for shadow + * + * Get the rest of the string and split it into 2-length + * hex values: + */ + $color = $isShadow ? $options['color'] : $options['color']; + $color = str_split(substr($color, 0, 6), 2); + $color = imagecolorclosest($this->resource, hexdec($color[0]), hexdec($color[1]), hexdec($color[2])); + + $xAxis = $isShadow ? $options['xShadow'] : $options['hOffset']; + $yAxis = $isShadow ? $options['yShadow'] : $options['vOffset']; + + // Add the shadow to the source image + if (! empty($options['fontPath'])) + { + imagettftext($this->resource, $options['fontSize'], 0, $xAxis, $yAxis, $color, $options['fontPath'], $text); + } + else + { + imagestring($this->resource, $options['fontSize'], $xAxis, $yAxis, $text, $color); + } + } + + //-------------------------------------------------------------------- } From aa8f4d9da41ea27fbdae76280bcfb133f4509051 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Tue, 9 May 2017 00:21:25 -0500 Subject: [PATCH 06/13] Getting text overlay working correctly with GD. --- application/Controllers/Checks.php | 9 +++++++-- system/Images/Handlers/BaseHandler.php | 11 +++++++---- system/Images/Handlers/GDHandler.php | 16 +++++++++++----- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/application/Controllers/Checks.php b/application/Controllers/Checks.php index fd1856571d2f..9303e81efa63 100644 --- a/application/Controllers/Checks.php +++ b/application/Controllers/Checks.php @@ -162,9 +162,14 @@ public function image() $images = Services::image('gd') ->withFile("/Users/kilishan/Documents/BobHeader.jpg") - ->fit(500, 100, 'bottom-left') +// ->fit(500, 100, 'bottom-left') ->text('Bob is Back!', [ - 'fontSize' => 30, + 'fontPath' => '/Users/kilishan/Downloads/Calibri.ttf', + 'fontSize' => 40, + 'padding' => 0, +// 'opacity' => 0.2, + 'vAlign' => 'top', + 'hAlign' => 'right' ]) ->save('/Users/kilishan/temp.jpg', 100); diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index a9cb1179b858..278ae9ec9dbb 100644 --- a/system/Images/Handlers/BaseHandler.php +++ b/system/Images/Handlers/BaseHandler.php @@ -308,7 +308,6 @@ public function text(string $text, array $options = []) } $fontheight = $options['fontSize']; - $options['vOffset'] += $options['fontSize']; } else { @@ -326,23 +325,27 @@ public function text(string $text, array $options = []) // Set vertical alignment if ($options['vAlign'] === 'middle') { - $yAxis += ($this->image->origHeight / 2) + ($fontheight / 2); + // Don't apply padding when you're in the middle of the image. + $yAxis += ($this->image->origHeight / 2) + ($fontheight / 2) - $options['padding']; } elseif ($options['vAlign'] === 'bottom') { - $yAxis += $this->image->origHeight - $fontheight - $options['shadowOffset'] - ($fontheight / 2); + $yAxis = ($this->image->origHeight - $fontheight - $options['shadowOffset'] - ($fontheight / 2)) - $yAxis; } // Set horizontal alignment if ($options['hAlign'] === 'right') { - $xAxis += $this->image->origWidth - ($fontwidth * strlen($text)) - $options['shadowOffset']; + $xAxis += ($this->image->origWidth - ($fontwidth * strlen($text)) - $options['shadowOffset']) - (2 * $options['padding']); } elseif ($options['hAlign'] === 'center') { $xAxis += floor(($this->image->origWidth - ($fontwidth * strlen($text))) / 2); } + $options['xAxis'] = $xAxis; + $options['yAxis'] = $yAxis; + if ($options['withShadow']) { // Offset from text diff --git a/system/Images/Handlers/GDHandler.php b/system/Images/Handlers/GDHandler.php index de0f0b8e2b8e..9dabff49d6dc 100644 --- a/system/Images/Handlers/GDHandler.php +++ b/system/Images/Handlers/GDHandler.php @@ -373,27 +373,33 @@ protected function createImage($path = '', $imageType = '') */ protected function textOverlay(string $text, array $options = [], bool $isShadow=false) { + $src = $this->createImage(); + /* Set RGB values for shadow * * Get the rest of the string and split it into 2-length * hex values: */ + $opacity = (int)($options['opacity'] / 127); $color = $isShadow ? $options['color'] : $options['color']; $color = str_split(substr($color, 0, 6), 2); - $color = imagecolorclosest($this->resource, hexdec($color[0]), hexdec($color[1]), hexdec($color[2])); + $color = imagecolorclosestalpha($src, hexdec($color[0]), hexdec($color[1]), hexdec($color[2]), $opacity); - $xAxis = $isShadow ? $options['xShadow'] : $options['hOffset']; - $yAxis = $isShadow ? $options['yShadow'] : $options['vOffset']; + $xAxis = $isShadow ? $options['xShadow'] : $options['xAxis']; + $yAxis = $isShadow ? $options['yShadow'] : $options['yAxis']; // Add the shadow to the source image if (! empty($options['fontPath'])) { - imagettftext($this->resource, $options['fontSize'], 0, $xAxis, $yAxis, $color, $options['fontPath'], $text); + // We have to add fontheight because imagettftext locates the bottom left corner, not top-left corner. + imagettftext($src, $options['fontSize'], 0, $xAxis, $yAxis + $options['fontheight'], $color, $options['fontPath'], $text); } else { - imagestring($this->resource, $options['fontSize'], $xAxis, $yAxis, $text, $color); + imagestring($src, $options['fontSize'], $xAxis, $yAxis, $text, $color); } + + $this->resource = $src; } //-------------------------------------------------------------------- From 14f9a972d34f126cce726ef468676befdb0321a6 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Tue, 9 May 2017 00:33:07 -0500 Subject: [PATCH 07/13] Corrected shadow color and getting opacity working on GD --- application/Controllers/Checks.php | 7 ++++--- system/Images/Handlers/BaseHandler.php | 2 +- system/Images/Handlers/GDHandler.php | 8 ++++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/application/Controllers/Checks.php b/application/Controllers/Checks.php index 9303e81efa63..e417f7ff796b 100644 --- a/application/Controllers/Checks.php +++ b/application/Controllers/Checks.php @@ -162,14 +162,15 @@ public function image() $images = Services::image('gd') ->withFile("/Users/kilishan/Documents/BobHeader.jpg") -// ->fit(500, 100, 'bottom-left') + ->fit(500, 100, 'bottom-left') ->text('Bob is Back!', [ 'fontPath' => '/Users/kilishan/Downloads/Calibri.ttf', 'fontSize' => 40, 'padding' => 0, -// 'opacity' => 0.2, + 'opacity' => 0.5, 'vAlign' => 'top', - 'hAlign' => 'right' + 'hAlign' => 'right', + 'withShadow' => true, ]) ->save('/Users/kilishan/temp.jpg', 100); diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index 278ae9ec9dbb..29e3cd79b7c6 100644 --- a/system/Images/Handlers/BaseHandler.php +++ b/system/Images/Handlers/BaseHandler.php @@ -43,7 +43,7 @@ abstract class BaseHandler implements ImageHandlerInterface 'hOffset' => 0, 'padding' => 0, 'withShadow' => false, - 'shadowColor' => '993300', + 'shadowColor' => '000000', 'shadowOffset' => 3, ]; diff --git a/system/Images/Handlers/GDHandler.php b/system/Images/Handlers/GDHandler.php index 9dabff49d6dc..ad87fa8dcf19 100644 --- a/system/Images/Handlers/GDHandler.php +++ b/system/Images/Handlers/GDHandler.php @@ -380,8 +380,12 @@ protected function textOverlay(string $text, array $options = [], bool $isShadow * Get the rest of the string and split it into 2-length * hex values: */ - $opacity = (int)($options['opacity'] / 127); - $color = $isShadow ? $options['color'] : $options['color']; + $opacity = ($options['opacity'] * 127); + + // Allow opacity to be applied to the text + imagealphablending($src, true); + + $color = $isShadow ? $options['shadowColor'] : $options['color']; $color = str_split(substr($color, 0, 6), 2); $color = imagecolorclosestalpha($src, hexdec($color[0]), hexdec($color[1]), hexdec($color[2]), $opacity); From ea65419a843a168770587e68313ef8177f18735c Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Thu, 11 May 2017 23:16:32 -0500 Subject: [PATCH 08/13] Getting a start on ImageMagick conversion. --- application/Config/Images.php | 3 +- application/Controllers/Checks.php | 31 ++-- system/Common.php | 47 +++++ system/Config/AutoloadConfig.php | 2 +- .../ImageException.php} | 2 +- system/Images/Handlers/BaseHandler.php | 4 +- system/Images/Handlers/GDHandler.php | 38 ---- system/Images/Handlers/ImageMagickHandler.php | 165 ++++++++++++++++++ system/Images/ImageHandlerInterface.php | 7 - 9 files changed, 232 insertions(+), 67 deletions(-) rename system/Images/{Exceptions.php => Exceptions/ImageException.php} (51%) create mode 100644 system/Images/Handlers/ImageMagickHandler.php diff --git a/application/Config/Images.php b/application/Config/Images.php index 995c92203070..f20f25ade0f1 100644 --- a/application/Config/Images.php +++ b/application/Config/Images.php @@ -17,7 +17,7 @@ class Images extends BaseConfig * * @var string */ - public $libraryPath; + public $libraryPath = '/usr/local/bin/convert'; /** * The available handler classes. @@ -28,6 +28,5 @@ class Images extends BaseConfig 'gd' => \CodeIgniter\Images\Handlers\GDHandler::class, 'imagick' => \CodeIgniter\Images\Handlers\ImageMagickHandler::class, 'gm' => \CodeIgniter\Images\Handlers\GraphicsMagickHandler::class, - 'pbm' => \CodeIgniter\Images\Handlers\NetPBMHandler::class, ]; } diff --git a/application/Controllers/Checks.php b/application/Controllers/Checks.php index e417f7ff796b..db477ba9b750 100644 --- a/application/Controllers/Checks.php +++ b/application/Controllers/Checks.php @@ -153,26 +153,27 @@ public function redirect() public function image() { -// $images = Services::image('gd') + $images = Services::image('imagick') + ->getVersion(); // ->withFile("/Users/kilishan/Documents/BobHeader.jpg") // ->resize(500, 100, true) // ->crop(200, 75, 20, 0, false) // ->rotate(90) -// ->save('/Users/kilishan/temp.jpg', 100); +// ->save('/Users/kilishan/temp.jpg'); - $images = Services::image('gd') - ->withFile("/Users/kilishan/Documents/BobHeader.jpg") - ->fit(500, 100, 'bottom-left') - ->text('Bob is Back!', [ - 'fontPath' => '/Users/kilishan/Downloads/Calibri.ttf', - 'fontSize' => 40, - 'padding' => 0, - 'opacity' => 0.5, - 'vAlign' => 'top', - 'hAlign' => 'right', - 'withShadow' => true, - ]) - ->save('/Users/kilishan/temp.jpg', 100); +// $images = Services::image('imagick') +// ->withFile("/Users/kilishan/Documents/BobHeader.jpg") +// ->fit(500, 100, 'bottom-left') +// ->text('Bob is Back!', [ +// 'fontPath' => '/Users/kilishan/Downloads/Calibri.ttf', +// 'fontSize' => 40, +// 'padding' => 0, +// 'opacity' => 0.5, +// 'vAlign' => 'top', +// 'hAlign' => 'right', +// 'withShadow' => true, +// ]) +// ->save('/Users/kilishan/temp.jpg', 100); ddd($images); diff --git a/system/Common.php b/system/Common.php index c934b1d1a361..1148e3aa5471 100644 --- a/system/Common.php +++ b/system/Common.php @@ -860,3 +860,50 @@ function slash_item($item) } } //-------------------------------------------------------------------- + +if ( ! function_exists('function_usable')) +{ + /** + * Function usable + * + * Executes a function_exists() check, and if the Suhosin PHP + * extension is loaded - checks whether the function that is + * checked might be disabled in there as well. + * + * This is useful as function_exists() will return FALSE for + * functions disabled via the *disable_functions* php.ini + * setting, but not for *suhosin.executor.func.blacklist* and + * *suhosin.executor.disable_eval*. These settings will just + * terminate script execution if a disabled function is executed. + * + * The above described behavior turned out to be a bug in Suhosin, + * but even though a fix was commited for 0.9.34 on 2012-02-12, + * that version is yet to be released. This function will therefore + * be just temporary, but would probably be kept for a few years. + * + * @link http://www.hardened-php.net/suhosin/ + * @param string $function_name Function to check for + * @return bool TRUE if the function exists and is safe to call, + * FALSE otherwise. + */ + function function_usable($function_name) + { + static $_suhosin_func_blacklist; + + if (function_exists($function_name)) + { + if ( ! isset($_suhosin_func_blacklist)) + { + $_suhosin_func_blacklist = extension_loaded('suhosin') + ? explode(',', trim(ini_get('suhosin.executor.func.blacklist'))) + : array(); + } + + return ! in_array($function_name, $_suhosin_func_blacklist, TRUE); + } + + return FALSE; + } +} + +//-------------------------------------------------------------------- diff --git a/system/Config/AutoloadConfig.php b/system/Config/AutoloadConfig.php index 90ad76e44b02..b658b24f9677 100644 --- a/system/Config/AutoloadConfig.php +++ b/system/Config/AutoloadConfig.php @@ -140,7 +140,7 @@ public function __construct() // 'CodeIgniter\Database\ResultInterface' => BASEPATH.'Database/ResultInterface.php', // 'CodeIgniter\Database\Migration' => BASEPATH.'Database/Migration.php', // 'CodeIgniter\Database\MigrationRunner' => BASEPATH.'Database/MigrationRunner.php', -// 'CodeIgniter\Debug\Exceptions' => BASEPATH.'Debug/Exceptions.php', +// 'CodeIgniter\Debug\Exceptions' => BASEPATH.'Debug/ImageException.php', // 'CodeIgniter\Debug\Timer' => BASEPATH.'Debug/Timer.php', // 'CodeIgniter\Debug\Iterator' => BASEPATH.'Debug/Iterator.php', // 'CodeIgniter\Events\Events' => BASEPATH.'Events/Events.php', diff --git a/system/Images/Exceptions.php b/system/Images/Exceptions/ImageException.php similarity index 51% rename from system/Images/Exceptions.php rename to system/Images/Exceptions/ImageException.php index 3aec8a1b1b17..9615192b7dcd 100644 --- a/system/Images/Exceptions.php +++ b/system/Images/Exceptions/ImageException.php @@ -1,3 +1,3 @@ -process('-version'); + + // The first line has the version in it... + preg_match('/(ImageMagick\s[\S]+)/', $result[0], $matches); + + return str_replace('ImageMagick ', '', $matches[0]); + } + + //-------------------------------------------------------------------- + + /** + * Handles all of the grunt work of resizing, etc. + * + * @param string $action + * + * @return $this|bool + */ + protected function process(string $action) + { + // Do we have a vaild library path? + if (empty($this->config->libraryPath)) + { + throw new ImageException(lang('images.libPathInvalid')); + } + + if ( ! preg_match('/convert$/i', $this->config->libraryPath)) + { + $this->config->libraryPath = rtrim($this->config->libraryPath, '/').'/convert'; + } + + $cmd = $this->config->libraryPath.' '.$action; + + $retval = 1; + // exec() might be disabled + if (function_usable('exec')) + { + @exec($cmd, $output, $retval); + } + + // Did it work? + if ($retval > 0) + { + throw new ImageException(lang('imageProcessFailed')); + } + + return $output; + } + + //-------------------------------------------------------------------- + + /** + * Saves any changes that have been made to file. If no new filename is + * provided, the existing image is overwritten, otherwise a copy of the + * file is made at $target. + * + * Example: + * $image->resize(100, 200, true) + * ->save(); + * + * @param string|null $target + * @param int $quality + * + * @return bool + */ + public function save(string $target = null, int $quality = 90) + { + + } + + //-------------------------------------------------------------------- + + /** + * Create Image Resource + * + * This simply creates an image resource handle + * based on the type of image being processed + * + * @param string + * @param string + * + * @return resource|bool + */ + protected function createImage($path = '', $imageType = '') + { + + } + + //-------------------------------------------------------------------- + + /** + * Handler-specific method for overlaying text on an image. + * + * @param string $text + * @param array $options + * @param bool $isShadow Whether we are drawing the dropshadow or actual text + */ + protected function textOverlay(string $text, array $options = [], bool $isShadow=false) + { + + } + + //-------------------------------------------------------------------- +} diff --git a/system/Images/ImageHandlerInterface.php b/system/Images/ImageHandlerInterface.php index 7eb75cb1327c..b2fbc90c3fa9 100644 --- a/system/Images/ImageHandlerInterface.php +++ b/system/Images/ImageHandlerInterface.php @@ -40,13 +40,6 @@ public function rotate(float $angle); //-------------------------------------------------------------------- - /** - * @return mixed - */ - public function watermark(); - - //-------------------------------------------------------------------- - /** * Reads the EXIF information from the image and modifies the orientation * so that displays correctly in the browser. From 86fd69e4d0e7bc7affd663bdb4b348a1b75f50cb Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sun, 14 May 2017 23:31:27 -0500 Subject: [PATCH 09/13] Most of the ImageMagick driver is in place. Just text overlay left --- system/Images/Handlers/BaseHandler.php | 14 +-- system/Images/Handlers/GDHandler.php | 26 ++++- system/Images/Handlers/ImageMagickHandler.php | 104 ++++++++++++++++-- system/Images/Image.php | 1 + 4 files changed, 129 insertions(+), 16 deletions(-) diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index 82a5f4ef7c81..a534c711e3de 100644 --- a/system/Images/Handlers/BaseHandler.php +++ b/system/Images/Handlers/BaseHandler.php @@ -1,8 +1,8 @@ reproportion(); } - return $this->process('resize'); + return $this->_resize($maintainRatio); } //-------------------------------------------------------------------- @@ -164,7 +164,7 @@ public function crop(int $width = null, int $height = null, int $x = null, int $ $this->reproportion(); } - $result = $this->process('crop'); + $result = $this->_crop(); $this->xAxis = null; $this->yAxis = null; @@ -663,9 +663,9 @@ public function __call(string $name, array $args = []) */ protected function reproportion() { - if (($this->width === 0 && $this->height === 0) OR $this->image->origWidth === 0 OR $this->image->origHeight === 0 - OR (! ctype_digit((string)$this->width) && ! ctype_digit((string)$this->height)) - OR ! ctype_digit((string)$this->image->origWidth) OR ! ctype_digit((string)$this->image->origHeight) + if (($this->width === 0 && $this->height === 0) || $this->image->origWidth === 0 || $this->image->origHeight === 0 + || (! ctype_digit((string)$this->width) && ! ctype_digit((string)$this->height)) + || ! ctype_digit((string)$this->image->origWidth) || ! ctype_digit((string)$this->image->origHeight) ) { return; @@ -687,7 +687,7 @@ protected function reproportion() $this->masterDim = ($this->height === 0) ? 'width' : 'height'; } } - elseif (($this->masterDim === 'width' && $this->width === 0) OR ($this->masterDim === 'height' && $this->height === 0) + elseif (($this->masterDim === 'width' && $this->width === 0) || ($this->masterDim === 'height' && $this->height === 0) ) { return; diff --git a/system/Images/Handlers/GDHandler.php b/system/Images/Handlers/GDHandler.php index 9cbc76095b88..305f487bec0f 100644 --- a/system/Images/Handlers/GDHandler.php +++ b/system/Images/Handlers/GDHandler.php @@ -1,6 +1,6 @@ process('resize'); + } + + //-------------------------------------------------------------------- + + /** + * Crops the image. + * + * @return bool|\CodeIgniter\Images\Handlers\GDHandler + */ + public function _crops() + { + return $this->process('crop'); + } + + //-------------------------------------------------------------------- + /** * Handles all of the grunt work of resizing, etc. * diff --git a/system/Images/Handlers/ImageMagickHandler.php b/system/Images/Handlers/ImageMagickHandler.php index 4f597b0dc227..e9edba866f80 100644 --- a/system/Images/Handlers/ImageMagickHandler.php +++ b/system/Images/Handlers/ImageMagickHandler.php @@ -24,17 +24,64 @@ class ImageMagickHandler extends BaseHandler //-------------------------------------------------------------------- + /** + * Handles the actual resizing of the image. + */ + public function _resize(bool $maintainRatio = false) + { + $source = ! empty($this->resource) ? $this->resource : $this->image->getPathname(); + $destination = $this->getResourcePath(); + + $action = $maintainRatio === true + ? ' -resize '.$this->width.'x'.$this->height.' "'.$source.'" "'.$destination.'"' + : ' -resize '.$this->width.'x'.$this->height.'\! "'.$source.'" "'.$destination.'"'; + + $this->process($action); + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Crops the image. + * + * @return bool|\CodeIgniter\Images\Handlers\ImageMagickHandler + */ + public function _crop() + { + $source = ! empty($this->resource) ? $this->resource : $this->image->getPathname(); + $destination = $this->getResourcePath(); + + $action = ' -crop '.$this->width.'x'.$this->height.'+'.$this->xAxis.'+'.$this->yAxis.' "'.$source.'" "'.$destination .'"'; + + $this->process($action); + + return $this; + } + + //-------------------------------------------------------------------- + /** * Handles the rotation of an image resource. * Doesn't save the image, but replaces the current resource. * * @param int $angle * - * @return bool + * @return $this */ protected function _rotate(int $angle) { + $angle = '-rotate '.$angle; + + $source = ! empty($this->resource) ? $this->resource : $this->image->getPathname(); + $destination = $this->getResourcePath(); + + $action = ' '.$angle.' "'.$source.'" "'.$destination.'"'; + $this->process($action); + + return $this; } //-------------------------------------------------------------------- @@ -48,7 +95,16 @@ protected function _rotate(int $angle) */ public function _flip(string $direction) { + $angle = $direction == 'horizontal' ? '-flop' : '-flip'; + + $source = ! empty($this->resource) ? $this->resource : $this->image->getPathname(); + $destination = $this->getResourcePath(); + + $action = ' '.$angle.' "'.$source.'" "'.$destination.'"'; + $this->process($action); + + return $this; } //-------------------------------------------------------------------- @@ -77,7 +133,7 @@ public function getVersion() * * @return $this|bool */ - protected function process(string $action) + protected function process(string $action, int $quality = 100) { // Do we have a vaild library path? if (empty($this->config->libraryPath)) @@ -90,7 +146,7 @@ protected function process(string $action) $this->config->libraryPath = rtrim($this->config->libraryPath, '/').'/convert'; } - $cmd = $this->config->libraryPath.' '.$action; + $cmd = $this->config->libraryPath.' -quality '.$quality.' '.$action; $retval = 1; // exec() might be disabled @@ -126,25 +182,57 @@ protected function process(string $action) */ public function save(string $target = null, int $quality = 90) { + $target = empty($target) + ? $this->image + : $target; + // If no new resource has been created, then we're + // simply copy the existing one. + if (empty($this->resource)) + { + $name = basename($target); + $path = pathinfo($target, PATHINFO_DIRNAME); + + return $this->image->copy($path, $name); + } + + // Copy the file through ImageMagick so that it has + // a chance to convert file format. + $action = '"'.$this->resource.'" "'.$target.'"'; + + $result = $this->process($action, $quality); + + unlink($this->resource); + + return $result; } //-------------------------------------------------------------------- /** - * Create Image Resource + * Get Image Resource * * This simply creates an image resource handle - * based on the type of image being processed + * based on the type of image being processed. + * Since ImageMagick is used on the cli, we need to + * ensure we have a temporary file on the server + * that we can use. * - * @param string - * @param string + * To ensure we can use all features, like transparency, + * during the process, we'll use a PNG as the temp file type. * * @return resource|bool */ - protected function createImage($path = '', $imageType = '') + protected function getResourcePath() { + if (! is_null($this->resource)) + { + return $this->resource; + } + + $this->resource = WRITEPATH.'cache/'.time().'_'.bin2hex(random_bytes(10)).'.png'; + return $this->resource; } //-------------------------------------------------------------------- diff --git a/system/Images/Image.php b/system/Images/Image.php index 0592940a719f..7f070e21e4ce 100644 --- a/system/Images/Image.php +++ b/system/Images/Image.php @@ -1,6 +1,7 @@ Date: Mon, 15 May 2017 23:44:23 -0500 Subject: [PATCH 10/13] Good progress with text overlay for ImageMagick. Needs shadows and padding to work. --- system/Images/Handlers/BaseHandler.php | 94 ++---------------- system/Images/Handlers/GDHandler.php | 89 +++++++++++++++++ system/Images/Handlers/ImageMagickHandler.php | 98 ++++++++++++++++++- 3 files changed, 190 insertions(+), 91 deletions(-) diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index a534c711e3de..73ec153bb740 100644 --- a/system/Images/Handlers/BaseHandler.php +++ b/system/Images/Handlers/BaseHandler.php @@ -266,94 +266,12 @@ public function text(string $text, array $options = []) $options['color'] = trim($options['color'], '# '); $options['shadowColor'] = trim($options['shadowColor'], '# '); - if (! empty($options['fontPath']) && ! file_exists($options['fontPath'])) - { - throw new ImageException(lang('images.missingFont')); - } - - // Reverse the vertical offset - // When the image is positioned at the bottom - // we don't want the vertical offset to push it - // further down. We want the reverse, so we'll - // invert the offset. Note: The horizontal - // offset flips itself automatically - - if ($options['vAlign'] === 'bottom') - { - $options['vOffset'] = $options['vOffset'] * -1; - } - - if ($options['hAlign'] === 'right') - { - $options['hOffset'] = $options['hOffset'] * -1; - } - - // Set font width and height - // These are calculated differently depending on - // whether we are using the true type font or not - if (! empty($options['fontPath'])) - { - if (function_exists('imagettfbbox')) - { - $temp = imagettfbbox($options['fontSize'], 0, $options['fontPath'], $text); - $temp = $temp[2] - $temp[0]; - - $fontwidth = $temp / strlen($text); - } - else - { - $fontwidth = $options['fontSize'] - ($options['fontSize'] / 4); - } - - $fontheight = $options['fontSize']; - } - else - { - $fontwidth = imagefontwidth($options['fontSize']); - $fontheight = imagefontheight($options['fontSize']); - } - - $options['fontheight'] = $fontheight; - $options['fontwidth'] = $fontwidth; - - // Set base X and Y axis values - $xAxis = $options['hOffset'] + $options['padding']; - $yAxis = $options['vOffset'] + $options['padding']; - - // Set vertical alignment - if ($options['vAlign'] === 'middle') - { - // Don't apply padding when you're in the middle of the image. - $yAxis += ($this->image->origHeight / 2) + ($fontheight / 2) - $options['padding']; - } - elseif ($options['vAlign'] === 'bottom') - { - $yAxis = ($this->image->origHeight - $fontheight - $options['shadowOffset'] - ($fontheight / 2)) - $yAxis; - } - - // Set horizontal alignment - if ($options['hAlign'] === 'right') - { - $xAxis += ($this->image->origWidth - ($fontwidth * strlen($text)) - $options['shadowOffset']) - (2 * $options['padding']); - } - elseif ($options['hAlign'] === 'center') - { - $xAxis += floor(($this->image->origWidth - ($fontwidth * strlen($text))) / 2); - } - - $options['xAxis'] = $xAxis; - $options['yAxis'] = $yAxis; - - if ($options['withShadow']) - { - // Offset from text - $options['xShadow'] = $xAxis + $options['shadowOffset']; - $options['yShadow'] = $yAxis + $options['shadowOffset']; - - $this->textOverlay($text, $options, true); - } +// if (! empty($options['fontPath']) && ! file_exists($options['fontPath'])) +// { +// throw new ImageException(lang('images.missingFont')); +// } - $this->textOverlay($text, $options, false); + $this->_text($text, $options); return $this; } @@ -367,7 +285,7 @@ public function text(string $text, array $options = []) * @param array $options * @param bool $isShadow Whether we are drawing the dropshadow or actual text */ - protected abstract function textOverlay(string $text, array $options = [], bool $isShadow=false); + protected abstract function _text(string $text, array $options = []); //-------------------------------------------------------------------- diff --git a/system/Images/Handlers/GDHandler.php b/system/Images/Handlers/GDHandler.php index 305f487bec0f..96449812040e 100644 --- a/system/Images/Handlers/GDHandler.php +++ b/system/Images/Handlers/GDHandler.php @@ -350,6 +350,95 @@ protected function createImage($path = '', $imageType = '') //-------------------------------------------------------------------- + protected function _text(string $text, array $options = []) + { + // Reverse the vertical offset + // When the image is positioned at the bottom + // we don't want the vertical offset to push it + // further down. We want the reverse, so we'll + // invert the offset. Note: The horizontal + // offset flips itself automatically + + if ($options['vAlign'] === 'bottom') + { + $options['vOffset'] = $options['vOffset'] * -1; + } + + if ($options['hAlign'] === 'right') + { + $options['hOffset'] = $options['hOffset'] * -1; + } + + // Set font width and height + // These are calculated differently depending on + // whether we are using the true type font or not + if (! empty($options['fontPath'])) + { + if (function_exists('imagettfbbox')) + { + $temp = imagettfbbox($options['fontSize'], 0, $options['fontPath'], $text); + $temp = $temp[2] - $temp[0]; + + $fontwidth = $temp / strlen($text); + } + else + { + $fontwidth = $options['fontSize'] - ($options['fontSize'] / 4); + } + + $fontheight = $options['fontSize']; + } + else + { + $fontwidth = imagefontwidth($options['fontSize']); + $fontheight = imagefontheight($options['fontSize']); + } + + $options['fontheight'] = $fontheight; + $options['fontwidth'] = $fontwidth; + + // Set base X and Y axis values + $xAxis = $options['hOffset'] + $options['padding']; + $yAxis = $options['vOffset'] + $options['padding']; + + // Set vertical alignment + if ($options['vAlign'] === 'middle') + { + // Don't apply padding when you're in the middle of the image. + $yAxis += ($this->image->origHeight / 2) + ($fontheight / 2) - $options['padding']; + } + elseif ($options['vAlign'] === 'bottom') + { + $yAxis = ($this->image->origHeight - $fontheight - $options['shadowOffset'] - ($fontheight / 2)) - $yAxis; + } + + // Set horizontal alignment + if ($options['hAlign'] === 'right') + { + $xAxis += ($this->image->origWidth - ($fontwidth * strlen($text)) - $options['shadowOffset']) - (2 * $options['padding']); + } + elseif ($options['hAlign'] === 'center') + { + $xAxis += floor(($this->image->origWidth - ($fontwidth * strlen($text))) / 2); + } + + $options['xAxis'] = $xAxis; + $options['yAxis'] = $yAxis; + + if ($options['withShadow']) + { + // Offset from text + $options['xShadow'] = $xAxis + $options['shadowOffset']; + $options['yShadow'] = $yAxis + $options['shadowOffset']; + + $this->textOverlay($text, $options, true); + } + + $this->textOverlay($text, $options, false); + } + + //-------------------------------------------------------------------- + /** * Handler-specific method for overlaying text on an image. * diff --git a/system/Images/Handlers/ImageMagickHandler.php b/system/Images/Handlers/ImageMagickHandler.php index e9edba866f80..1fa173958f03 100644 --- a/system/Images/Handlers/ImageMagickHandler.php +++ b/system/Images/Handlers/ImageMagickHandler.php @@ -154,7 +154,8 @@ protected function process(string $action, int $quality = 100) { @exec($cmd, $output, $retval); } - + var_dump($cmd); +dd($output); // Did it work? if ($retval > 0) { @@ -242,11 +243,102 @@ protected function getResourcePath() * * @param string $text * @param array $options - * @param bool $isShadow Whether we are drawing the dropshadow or actual text */ - protected function textOverlay(string $text, array $options = [], bool $isShadow=false) + protected function _text(string $text, array $options = []) { + $cmd = ''; + + // Reverse the vertical offset + // When the image is positioned at the bottom + // we don't want the vertical offset to push it + // further down. We want the reverse, so we'll + // invert the offset. Note: The horizontal + // offset flips itself automatically + if ($options['vAlign'] === 'bottom') + { + $options['vOffset'] = $options['vOffset'] * -1; + } + + if ($options['hAlign'] === 'right') + { + $options['hOffset'] = $options['hOffset'] * -1; + } + + // Font + if (! empty($options['fontPath'])) + { + $cmd .= " -font '{$options['fontPath']}'"; + } + + if (isset($options['hAlign']) && isseT($options['vAlign'])) + { + switch ($options['hAlign']) + { + case 'left': + $xAxis = $options['hOffset'] + $options['padding']; + $yAxis = $options['vOffset'] + $options['padding']; + $gravity = $options['vAlign'] == 'top' + ? 'NorthWest' + : 'West'; + if ($options['vAlign'] == 'bottom') { + $gravity = 'SouthWest'; + $yAxis = $options['vOffset'] - $options['padding']; + } + break; + case 'center': + $xAxis = $options['hOffset'] + $options['padding']; + $yAxis = $options['vOffset'] + $options['padding']; + $gravity = $options['vAlign'] == 'top' + ? 'North' + : 'Center'; + if ($options['vAlign'] == 'bottom') + { + $yAxis = $options['vOffset'] - $options['padding']; + $gravity = 'South'; + } + break; + case 'right': + $xAxis = $options['hOffset'] - $options['padding']; + $yAxis = $options['vOffset'] + $options['padding']; + $gravity = $options['vAlign'] == 'top' + ? 'NorthEast' + : 'East'; + if ($options['vAlign'] == 'bottom') { + $gravity = 'SouthEast'; + $yAxis = $options['vOffset'] - $options['padding']; + } + break; + } + + $xAxis = $xAxis >= 0 ? '+'.$xAxis : $xAxis; + $yAxis = $yAxis >= 0 ? '+'.$yAxis : $yAxis; + + $cmd .= " -gravity {$gravity} -geometry {$xAxis}{$yAxis}"; + } + + // Color + if (isset($options['color'])) + { + list($r, $g, $b) = sscanf("#{$options['color']}", "#%02x%02x%02x"); + + $cmd .= " -fill 'rgba({$r},{$g},{$b},{$options['opacity']})'"; + } + + // Font Size - use points.... + if (isset($options['fontSize'])) + { + $cmd .= " -pointsize {$options['fontSize']}"; + } + + // Text + $cmd .= " -annotate 0 '{$text}'"; + + $source = ! empty($this->resource) ? $this->resource : $this->image->getPathname(); + $destination = $this->getResourcePath(); + + $cmd = " '{$source}' {$cmd} '{$destination}'"; + $this->process($cmd); } //-------------------------------------------------------------------- From 95fa1fe7c74941677ad47a46e8c93af5989c155b Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Wed, 24 May 2017 22:47:11 -0500 Subject: [PATCH 11/13] Fix issue with ImageMagick not returning version --- system/Images/Handlers/ImageMagickHandler.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/system/Images/Handlers/ImageMagickHandler.php b/system/Images/Handlers/ImageMagickHandler.php index 1fa173958f03..5932cd92a213 100644 --- a/system/Images/Handlers/ImageMagickHandler.php +++ b/system/Images/Handlers/ImageMagickHandler.php @@ -146,7 +146,10 @@ protected function process(string $action, int $quality = 100) $this->config->libraryPath = rtrim($this->config->libraryPath, '/').'/convert'; } - $cmd = $this->config->libraryPath.' -quality '.$quality.' '.$action; + $cmd = $this->config->libraryPath; + $cmd .= $action == '-version' + ? ' '.$action + : ' -quality '.$quality.' '.$action; $retval = 1; // exec() might be disabled @@ -154,8 +157,7 @@ protected function process(string $action, int $quality = 100) { @exec($cmd, $output, $retval); } - var_dump($cmd); -dd($output); + // Did it work? if ($retval > 0) { From 0b704c3c0b0020e9cc566c757de6859ee163c29d Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Thu, 25 May 2017 22:52:57 -0500 Subject: [PATCH 12/13] Latest image docs --- application/Config/Images.php | 1 - application/Controllers/Checks.php | 7 + system/Images/Handlers/BaseHandler.php | 19 +- user_guide_src/source/libraries/images.rst | 278 +++++++++++++++++++++ user_guide_src/source/libraries/index.rst | 1 + 5 files changed, 299 insertions(+), 7 deletions(-) create mode 100644 user_guide_src/source/libraries/images.rst diff --git a/application/Config/Images.php b/application/Config/Images.php index f20f25ade0f1..c819ba29c205 100644 --- a/application/Config/Images.php +++ b/application/Config/Images.php @@ -27,6 +27,5 @@ class Images extends BaseConfig public $handlers = [ 'gd' => \CodeIgniter\Images\Handlers\GDHandler::class, 'imagick' => \CodeIgniter\Images\Handlers\ImageMagickHandler::class, - 'gm' => \CodeIgniter\Images\Handlers\GraphicsMagickHandler::class, ]; } diff --git a/application/Controllers/Checks.php b/application/Controllers/Checks.php index db477ba9b750..3b59087181a2 100644 --- a/application/Controllers/Checks.php +++ b/application/Controllers/Checks.php @@ -153,6 +153,13 @@ public function redirect() public function image() { + $info = Services::image('imagick') + ->withFile("/Users/kilishan/Documents/BobHeader.jpg") + ->getFile() + ->getProperties(true); + + dd($info); + $images = Services::image('imagick') ->getVersion(); // ->withFile("/Users/kilishan/Documents/BobHeader.jpg") diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index 73ec153bb740..b0c4c5430896 100644 --- a/system/Images/Handlers/BaseHandler.php +++ b/system/Images/Handlers/BaseHandler.php @@ -13,7 +13,7 @@ abstract class BaseHandler implements ImageHandlerInterface /** * The image/file instance - * + *d * @var \CodeIgniter\Images\Image */ protected $image; @@ -258,6 +258,18 @@ protected abstract function _flip(string $direction); /** * Overlays a string of text over the image. * + * Valid options: + * + * - color Text Color (hex number) + * - shadowColor Color of the shadow (hex number) + * - hAlign Horizontal alignment: left, center, right + * - vAlign Vertical alignment: top, middle, bottom + * - hOffset + * - vOffset + * - fontPath + * - fontSize + * - shadowOffset + * * @return $this */ public function text(string $text, array $options = []) @@ -266,11 +278,6 @@ public function text(string $text, array $options = []) $options['color'] = trim($options['color'], '# '); $options['shadowColor'] = trim($options['shadowColor'], '# '); -// if (! empty($options['fontPath']) && ! file_exists($options['fontPath'])) -// { -// throw new ImageException(lang('images.missingFont')); -// } - $this->_text($text, $options); return $this; diff --git a/user_guide_src/source/libraries/images.rst b/user_guide_src/source/libraries/images.rst new file mode 100644 index 000000000000..903c46348411 --- /dev/null +++ b/user_guide_src/source/libraries/images.rst @@ -0,0 +1,278 @@ +######################## +Image Manipulation Class +######################## + +CodeIgniter's Image Manipulation class lets you perform the following +actions: + +- Image Resizing +- Thumbnail Creation +- Image Cropping +- Image Rotating +- Image Watermarking + +The following image libraries are supported: GD/GD2, and ImageMagick. + +.. contents:: +:local: + +********************** +Initializing the Class +********************** + +Like most other classes in CodeIgniter, the image class is initialized +in your controller by calling the Services class:: + + $image = Config\Services::image(); + +You can pass the alias for the image library you wish to use into the +Service function:: + + $image = Config\Services::image('imagick'); + +The available Handlers are as follows: + +- gd The GD/GD2 image library +- imagick The ImageMagick library. + +If using the ImageMagick library, you must set the path to the library on your +server in **application/Config/Images.php**. + +.. note:: The ImageMagick handler does NOT require the imagick extension to be + loaded on the server. As long as your script can access the library + and can run ``exec()`` on the server, it should work. + +Processing an Image +=================== + +Regardless of the type of processing you would like to perform +(resizing, cropping, rotation, or watermarking), the general process is +identical. You will set some preferences corresponding to the action you +intend to perform, then call one of the available processing functions. +For example, to create an image thumbnail you'll do this:: + + $image = Config\Services::image() + ->withFile('/path/to/image/mypic.jpg') + ->fit(100, 100, 'center') + ->save('/path/to/image/mypic_thumb.jpg'); + +The above code tells the library to look for an image +called *mypic.jpg* located in the source_image folder, then create a +new image from it that is 100 x 100pixels using the GD2 image_library, +and save it to a new file (the thumb). Since it is using the fit() method, +it will attempt to find the best portion of the image to crop based on the +desired aspect ratio, and then crop and resize the result. + +An image can be processed through as many of the available methods as +needed before saving. The original image is left untouched, and a new image +is used and passed through each method, applying the results on top of the +previous results:: + + $image = Config\Services::image() + ->withFile('/path/to/image/mypic.jpg') + ->reorient() + ->rotate(90) + ->crop(100, 100, 0, 0) + ->save('/path/to/image/mypic_thumb.jpg'); + +This example would take the same image and first fix any mobile phone orientation issues, +rotate the image by 90 degress, and then crop the result into a 100x100 pixel image, +starting at the top left corner. The result would be saved as the thumbnail. + +.. note:: In order for the image class to be allowed to do any + processing, the folder containing the image files must have write + permissions. + +.. note:: Image processing can require a considerable amount of server + memory for some operations. If you are experiencing out of memory errors + while processing images you may need to limit their maximum size, and/or + adjust PHP memory limits. + +Processing Methods +================== + +There are six available processing methods: + +- $image->crop() +- $image->fit() +- $image->flip() +- $image->resize() +- $image->rotate() +- $image->text() + + +These methods return the class instance so they can be chained together, as shown above. +If they fail they will throw a ``CodeIgniter\Images\ImageException`` that contains +the error message. A good practice is to catch the exceptions, showing an +error upon failure, like this:: + + try { + $image = Config\Services::image() + ->withFile('/path/to/image/mypic.jpg') + ->fit(100, 100, 'center') + ->save('/path/to/image/mypic_thumb.jpg'); + } + catch (CodeIgniter\Images\ImageException $e) + { + echo $e->getMessage(); + } + +.. note:: You can optionally specify the HTML formatting to be applied to + the errors, by submitting the opening/closing tags in the function, + like this:: + + $this->image_lib->display_errors('

', '

'); + + +Cropping Images +--------------- + +Images can be cropped so that only a portion of the original image remains. This is often used when creating +thumbnail images that should match a certain size/aspect ratio. This is handled with the ``crop()`` method:: + + crop(int $width = null, int $height = null, int $x = null, int $y = null, bool $maintainRatio = false, string $masterDim = 'auto') + +- **$width** is the desired width of the resulting image, in pixels. +- **$height** is the desired height of the resulting image, in pixels. +- **$x** is the number of pixels from the left side of the image to start cropping. +- **$y** is the number of pixels from the top of the image to start cropping. +- **$maintainRatio** will, if true, adjust the final dimensions as needed to maintain the image's original aspect ratio. +- **$masterDim** specifies which dimension should be left untouched when $maintainRatio is true. Values can be: 'width', 'height', or 'auto'. + +To take a 50x50 pixel square out of the center of an image, you would need to first calculate the appropriate x and y +offset values:: + + $info = Services::image('imagick') + ->withFile('/path/to/image/mypic.jpg') + ->getFile() + ->getProperties(true); + + $xOffset = ($info['width'] / 2) - 25; + $yOffset = ($info['height'] / 2) - 25; + + Services::image('imagick') + ->withFile('/path/to/image/mypic.jpg') + ->crop(50, 50, $xOffset, $yOffset) + ->save('path/to/new/image.jpg'); + +Fitting Images +-------------- + +The ``fit()`` method aims to help simplify cropping a portion of an image in a "smart" way, by doing the following steps: + +- Determine the correct portion of the original image to crop in order to maintain the desired aspect ratio. +- Crop the original image. +- Resize to the final dimensions. + +:: + + fit(int $width, int $height = null, string $position = 'center') + +- **$width** is the desired final width of the image. +- **$height** is the desired final height of the image. +- **$position** determines the portion of the image to crop out. Allowed positions: 'top-left', 'top', 'top-right', 'left', 'center', 'right', 'bottom-left', 'bottom', 'bottom-right'. + +This provides a much simpler way to crop that will always maintain the aspect ratio:: + + Services::image('imagick') + ->withFile('/path/to/image/mypic.jpg') + ->fit(100, 150, 'left') + ->save('path/to/new/image.jpg'); + +Flipping Images +--------------- + +Images can be flipped along either their horizontal or vertical axis:: + + flip(string $dir) + +- **$dir** specifies the axis to flip along. Can be either 'vertical' or 'horizontal'. + +:: + + Services::image('imagick') + ->withFile('/path/to/image/mypic.jpg') + ->flip('horizontal') + ->save('path/to/new/image.jpg'); + +Resizing Images +--------------- + +Images can be resized to fit any dimension you require with the resize() method:: + + resize(int $width, int $height, bool $maintainRatio = false, string $masterDim = 'auto') + +- **$width** is the desired width of the new image in pixels +- **$height** is the desired height of the new image in pixels +- **$maintainRatio** determines whether the image is stretched to fit the new dimensions, or the original aspect ratio is maintained. +- **$masterDim** specifies which axis should have its dimension honored when maintaining ratio. Either 'width', 'height'. + +When resizing images you can choose whether to maintain the ratio of the original image, or stretch/squash the new +image to fit the desired dimensions. If $maintainRatio is true, the dimension specified by $masterDim will stay the same, +while the other dimension will be altered to match the original image's aspect ratio. + +:: + + Services::image('imagick') + ->withFile('/path/to/image/mypic.jpg') + ->resize(200, 100, true, 'height') + ->save('path/to/new/image.jpg'); + + +Rotating Images +--------------- + +The rotate() method allows you to rotate an image in 90 degree increments:: + + rotate(float $angle) + +- **$angle** is the number of degrees to rotate. One of '90', '180', '270'. + +.. note:: While the $angle parameter accepts a float, it will convert it to an integer during the process. + If the value is any other than the three values listed above, it will throw a CodeIgniter\Images\ImageException. + + +Adding a Text Watermark +----------------------- + +You can overlay a text watermark onto the image very simply with the text() method. This is useful for placing copyright +notices, photogropher names, or simply marking the images as a preview so they won't be used in other people's final +products. + +:: + + text(string $text, array $options = []) + +The first parameter is the string of text that you wish to display. The second parameter is an array of options +that allow you to specify how the text should be displayed:: + + Services::image('imagick') + ->withFile('/path/to/image/mypic.jpg') + ->text('Copyright 2017 My Photo Co', [ + 'color' => '#fff', + 'opacity' => 0.5, + 'withShadow' => true, + 'hAlign' => 'center', + 'vAlign' => 'bottom', + 'fontSize' => 20 + ]) + ->save('path/to/new/image.jpg'); + +The possible options that are recognized are as follows: + +- color Text Color (hex number), i.e. #ff0000 +- opacity A number between 0 and 1 that represents the opacity of the text. +- withShadow Boolean value whether to display a shadow or not. +- shadowColor Color of the shadow (hex number) +- shadowOffset How many pixels to offset the shadow. Applies to both the vertical and horizontal values. +- hAlign Horizontal alignment: left, center, right +- vAlign Vertical alignment: top, middle, bottom +- hOffset Additional offset on the x axis, in pixels +- vOffset Additional offset on the y axis, in pixels +- fontPath The full server path to the TTF font you wish to use. System font will be used if none is given. +- fontSize The font size to use. When using the GD handler with the system font, valid values are between 1-5. + +.. note:: The ImageMagick driver does not recognize full server path for fontPath. Instead, simply provide the + name of one of the installed system fonts that you wish to use, i.e. Calibri. + + diff --git a/user_guide_src/source/libraries/index.rst b/user_guide_src/source/libraries/index.rst index 3574314ef1a1..2f0915e827d9 100644 --- a/user_guide_src/source/libraries/index.rst +++ b/user_guide_src/source/libraries/index.rst @@ -12,6 +12,7 @@ Library Reference content_negotiation localization curlrequest + images incomingrequest message pagination From 04de7b89dcf91ae2e8e5b50c1b463d63f8682a08 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Thu, 25 May 2017 22:56:58 -0500 Subject: [PATCH 13/13] Will come back to image tests --- tests/system/Images/GDHandlerTest.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/system/Images/GDHandlerTest.php b/tests/system/Images/GDHandlerTest.php index 379f4a20f3cd..1ad55c07746e 100644 --- a/tests/system/Images/GDHandlerTest.php +++ b/tests/system/Images/GDHandlerTest.php @@ -11,9 +11,4 @@ public function testCanReachImageMethods() $this->assertTrue(is_array($image->getProperties(true))); } - public function () - { - - } - }