diff --git a/application/Config/Images.php b/application/Config/Images.php new file mode 100644 index 000000000000..c819ba29c205 --- /dev/null +++ b/application/Config/Images.php @@ -0,0 +1,31 @@ + \CodeIgniter\Images\Handlers\GDHandler::class, + 'imagick' => \CodeIgniter\Images\Handlers\ImageMagickHandler::class, + ]; +} diff --git a/application/Controllers/Checks.php b/application/Controllers/Checks.php index 5135805a8e9a..3b59087181a2 100644 --- a/application/Controllers/Checks.php +++ b/application/Controllers/Checks.php @@ -146,5 +146,44 @@ public function catch() echo $body; } + public function redirect() + { + redirect('/checks/model'); + } + + 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") +// ->resize(500, 100, true) +// ->crop(200, 75, 20, 0, false) +// ->rotate(90) +// ->save('/Users/kilishan/temp.jpg'); + +// $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 da0158ae2770..de17b4f3ddab 100644 --- a/system/Common.php +++ b/system/Common.php @@ -861,3 +861,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 8cd032fc97ef..ebc8f994a188 100644 --- a/system/Config/AutoloadConfig.php +++ b/system/Config/AutoloadConfig.php @@ -110,76 +110,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/ImageException.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/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/Exceptions/ImageException.php b/system/Images/Exceptions/ImageException.php new file mode 100644 index 000000000000..9615192b7dcd --- /dev/null +++ b/system/Images/Exceptions/ImageException.php @@ -0,0 +1,3 @@ + null, + 'fontSize' => 16, + 'color' => 'ffffff', + 'opacity' => 1.0, + 'vAlign' => 'bottom', + 'hAlign' => 'center', + 'vOffset' => 0, + 'hOffset' => 0, + 'padding' => 0, + 'withShadow' => false, + 'shadowColor' => '000000', + 'shadowOffset' => 3, + ]; + + /** + * Temporary image used by the different engines. + * + * @var Resource + */ + protected $resource; + + //-------------------------------------------------------------------- + + public function __construct($config = null) + { + $this->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 the temporary image used during the image processing. + * Good for extending the system or doing things this library + * is not intended to do. + * + * @return Resource + */ + public function getResource() + { + return $this->resource; + } + + //-------------------------------------------------------------------- + + /** + * 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. + * + * @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 $this; + } + + $this->width = $width; + $this->height = $height; + + if ($maintainRatio) + { + $this->masterDim = $masterDim; + $this->reproportion(); + } + + return $this->_resize($maintainRatio); + } + + //-------------------------------------------------------------------- + + /** + * 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 + * @param bool $maintainRatio + * @param string $masterDim + * + * @return mixed + */ + 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(); + } + + $result = $this->_crop(); + + $this->xAxis = null; + $this->yAxis = null; + + return $result; + } + + //-------------------------------------------------------------------- + + /** + * Rotates the image on the current canvas. + * + * @param float $angle + * + * @return mixed + */ + 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); + + //-------------------------------------------------------------------- + + /** + * 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 = []) + { + $options = array_merge($this->textDefaults, $options); + $options['color'] = trim($options['color'], '# '); + $options['shadowColor'] = trim($options['shadowColor'], '# '); + + $this->_text($text, $options); + + 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 _text(string $text, array $options = []); + + //-------------------------------------------------------------------- + + /** + * Reads the EXIF information from the image and modifies the orientation + * 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. + * + * @param bool $silent If true, will ignore exceptions when PHP doesn't support EXIF. + * + * @return $this + */ + 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; + } + } + + //-------------------------------------------------------------------- + + /** + * 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 bool $silent If true, will not throw our own exceptions. + * + * @return mixed + */ + 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; + } + + //-------------------------------------------------------------------- + + /** + * 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 = 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); + + 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)($origHeight*$yRatio), + ]; + } + + return [ + (int)($origWidth*$xRatio), + (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]; + } + + //-------------------------------------------------------------------- + + /** + * 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 $target + * @param int $quality + * + * @return mixed + */ + 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); + } + } + + //-------------------------------------------------------------------- + + /** + * Re-proportion Image Width/Height + * + * 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. + * + * This function lets us re-proportion the width/height + * if users choose to maintain the aspect ratio when resizing. + * + * @return void + */ + protected function reproportion() + { + 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; + } + + // 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) || ($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..96449812040e --- /dev/null +++ b/system/Images/Handlers/GDHandler.php @@ -0,0 +1,485 @@ +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; + } + + //-------------------------------------------------------------------- + + /** + * Flips an image along it's vertical or horizontal axis. + * + * @param string $direction + * + * @return $this + */ + 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; + } + + //-------------------------------------------------------------------- + + /** + * Get GD version + * + * @return mixed + */ + public function getVersion() + { + if (function_exists('gd_info')) + { + $gd_version = @gd_info(); + + return preg_replace('/\D/', '', $gd_version['GD Version']); + } + + return false; + } + + //-------------------------------------------------------------------- + + /** + * Resizes the image. + * + * @return bool|\CodeIgniter\Images\Handlers\GDHandler + */ + public function _resize() + { + return $this->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. + * + * @param string $action + * + * @return $this|bool + */ + protected function process(string $action) + { + $origWidth = $this->image->origWidth; + $origHeight = $this->image->origHeight; + + if ($action == 'crop') + { + // Reassign the source width/height if cropping + $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 + $src = $this->createImage(); + + 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); + + imagedestroy($src); + $this->resource = $dest; + + 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')) + { + throw new ImageException(lang('images.unsupportedImagecreate').' '.lang('images.gifNotSupported')); + } + + if (! @imagegif($this->resource, $target)) + { + throw new ImageException(lang('images.saveFailed')); + } + break; + case IMAGETYPE_JPEG: + if (! function_exists('imagejpeg')) + { + throw new ImageException(lang('images.unsupportedImagecreate').' '.lang('images.jpgNotSupported')); + } + + if (! @imagejpeg($this->resource, $target, $quality)) + { + throw new ImageException(lang('images.saveFailed')); + } + break; + case IMAGETYPE_PNG: + if (! function_exists('imagepng')) + { + throw new ImageException(lang('images.unsupportedImagecreate').' '.lang('images.pngNotSupported')); + } + + if (! @imagepng($this->resource, $target)) + { + throw new ImageException(lang('images.saveFailed')); + } + break; + default: + throw new ImageException(lang('images.unsupportedImagecreate')); + 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 $this->resource; + } + + if ($path === '') + { + $path = $this->image->getPathname(); + } + + if ($imageType === '') + { + $imageType = $this->image->imageType; + } + + switch ($imageType) + { + case IMAGETYPE_GIF: + if (! function_exists('imagecreatefromgif')) + { + throw new ImageException(lang('images.gifNotSupported')); + } + + return imagecreatefromgif($path); + case IMAGETYPE_JPEG: + if (! function_exists('imagecreatefromjpeg')) + { + throw new ImageException(lang('images.jpgNotSupported')); + } + + return imagecreatefromjpeg($path); + case IMAGETYPE_PNG: + if (! function_exists('imagecreatefrompng')) + { + throw new ImageException(lang('images.pngNotSupported')); + } + + return imagecreatefrompng($path); + default: + throw new ImageException(lang('images.unsupportedImagecreate')); + } + } + + //-------------------------------------------------------------------- + + 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. + * + * @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) + { + $src = $this->createImage(); + + /* Set RGB values for shadow + * + * Get the rest of the string and split it into 2-length + * hex values: + */ + $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); + + $xAxis = $isShadow ? $options['xShadow'] : $options['xAxis']; + $yAxis = $isShadow ? $options['yShadow'] : $options['yAxis']; + + // Add the shadow to the source image + if (! empty($options['fontPath'])) + { + // 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($src, $options['fontSize'], $xAxis, $yAxis, $text, $color); + } + + $this->resource = $src; + } + + //-------------------------------------------------------------------- +} diff --git a/system/Images/Handlers/ImageMagickHandler.php b/system/Images/Handlers/ImageMagickHandler.php new file mode 100644 index 000000000000..5932cd92a213 --- /dev/null +++ b/system/Images/Handlers/ImageMagickHandler.php @@ -0,0 +1,347 @@ +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 $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; + } + + //-------------------------------------------------------------------- + + /** + * Flips an image along it's vertical or horizontal axis. + * + * @param string $direction + * + * @return $this + */ + 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; + } + + //-------------------------------------------------------------------- + + /** + * Get GD version + * + * @return mixed + */ + public function getVersion() + { + $result = $this->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, int $quality = 100) + { + // 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; + $cmd .= $action == '-version' + ? ' '.$action + : ' -quality '.$quality.' '.$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) + { + $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; + } + + //-------------------------------------------------------------------- + + /** + * Get Image Resource + * + * This simply creates an image resource handle + * 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. + * + * 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 getResourcePath() + { + if (! is_null($this->resource)) + { + return $this->resource; + } + + $this->resource = WRITEPATH.'cache/'.time().'_'.bin2hex(random_bytes(10)).'.png'; + + return $this->resource; + } + + //-------------------------------------------------------------------- + + /** + * Handler-specific method for overlaying text on an image. + * + * @param string $text + * @param array $options + */ + 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); + } + + //-------------------------------------------------------------------- +} diff --git a/system/Images/Image.php b/system/Images/Image.php new file mode 100644 index 000000000000..7f070e21e4ce --- /dev/null +++ b/system/Images/Image.php @@ -0,0 +1,123 @@ +getFilename() + : $targetName; + + if (empty($targetName)) + { + throw new ImageException('Invalid file name.'); + } + + if (! is_dir($targetPath)) + { + mkdir($targetName, 0755, true); + } + + if (! copy($this->getPathname(),"{$targetPath}{$targetName}")) + { + throw new ImageException('Unable to copy image to new destination.'); + } + + chmod("{$targetPath}/{$targetName}", $perms); + + return true; + } + + //-------------------------------------------------------------------- + + /** + * Get image properties + * + * A helper function that gets info about the file + * + * @param string + * @param bool + * + * @return mixed + */ + public function getProperties($return = false) + { + $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) + { + return [ + 'width' => $vals[0], + 'height' => $vals[1], + 'image_type' => $vals[2], + 'size_str' => $vals[3], + 'mime_type' => $mime, + ]; + } + + $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 new file mode 100644 index 000000000000..b2fbc90c3fa9 --- /dev/null +++ b/system/Images/ImageHandlerInterface.php @@ -0,0 +1,89 @@ + '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.', + 'invalidDirection' => 'Flip direction can be only `vertical` or `horizontal`.', + 'exifNotSupported' => 'Reading EXIF data is not supported by this PHP installation.', +]; diff --git a/tests/_support/ci-logo.png b/tests/_support/ci-logo.png new file mode 100644 index 000000000000..a61f163022b7 Binary files /dev/null and b/tests/_support/ci-logo.png differ diff --git a/tests/system/Images/GDHandlerTest.php b/tests/system/Images/GDHandlerTest.php new file mode 100644 index 000000000000..1ad55c07746e --- /dev/null +++ b/tests/system/Images/GDHandlerTest.php @@ -0,0 +1,14 @@ +path); + + $this->assertTrue(is_array($image->getProperties(true))); + } + +} 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'); + } + +} 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