Rev 1 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download
<?php namespace ProcessWire;/*** ProcessWire ImageSizer with Engines for ProcessWire 3.x** ImageSizer handles resizing of a single JPG, GIF, or PNG image using GD2* or another supported and configured engine. (Imagick, ImageMagick, Netpbm)** Code for IPTC, auto rotation and sharpening by Horst Nogajski.* http://nogajski.de/** Other user contributions as noted.** Copyright (C) 2016-2019 by Horst Nogajski and Ryan Cramer* This file licensed under Mozilla Public License v2.0 http://mozilla.org/MPL/2.0/** @method bool resize($targetWidth, $targetHeight = 0)**/class ImageSizer extends Wire {/*** @var ImageSizerEngine|null**/protected $engine = null;/*** Default/fallback image sizer engine name** @var string**/protected $defaultEngineName = 'ImageSizerEngineGD';/*** @var string|null**/protected $filename = null;/*** @var array**/protected $initialOptions = array();/*** Known ImageSizer engine modules (class names) excluding ImageSizerEngineGD (the default)** Cached result of getEngines() call** @var null|array**/static protected $knownEngines = null;/*** Names of engines that failed the supported() checks** @var array**/protected $failedEngineNames = array();/*** Module/class name of engine that must only be used (for cases where you want to force a specific engine)** If the $options array contained a 'forceEngine' property, this contains the value.** @var string**/protected $forceEngineName = '';/*** Whether or not ImageSizer has an engine ready to use** @var bool**/protected $initialized = false;/*** @var null|false|array**/protected $inspectionResult = null;/*** Construct the ImageSizer for a single image** @param string $filename Filename to resize. Omit only if instantiating class for a getEngines() call.* @param array $options Initial options to the engine.**/public function __construct($filename = '', $options = array()) {if(!empty($options)) $this->setOptions($options);if(!empty($filename)) $this->setFilename($filename);}/*** Get array of all available ImageSizer engine names in order of priority** Note that the returned value excludes the default engine (ImageSizerEngineGD).** @param bool $forceReload Specify true only if you want to prevent it from using cached result from previous call.* @return array of module names**/public function getEngines($forceReload = false) {if(!$forceReload && is_array(self::$knownEngines)) return self::$knownEngines;self::$knownEngines = array();$modules = $this->wire('modules');$engines = $modules->findByPrefix('ImageSizerEngine');$numEngines = count($engines);foreach($engines as $moduleName) {if(!$modules->isInstalled($moduleName)) continue;if($numEngines > 1) {$configData = $modules->getConfig($moduleName);$priority = isset($configData['enginePriority']) ? (int) $configData['enginePriority'] : 0;// multiply by 10 to ensure two priority 1 engines don't get mixed up with a priority 2 engine// for instance, two priority 1 engines become 10 and 11, rather than 1 and 2, as a priority 1// engine incremented to 2 could otherwise be confused with a priority 2 engine$priority *= 10;while(isset(self::$knownEngines[$priority])) {$priority++;}} else {$priority = 0;}self::$knownEngines[$priority] = $moduleName;}if(count(self::$knownEngines) > 1) ksort(self::$knownEngines);self::$knownEngines[] = $this->defaultEngineName;return self::$knownEngines;}/*** Get array of information for all ImageSizer engines (or optionally a specific ImageSizer engine)** Returns array of arrays indexed by engine name, each with the following:** - `name` (string): engine name* - `title` (string): engine title* - `class` (string): PHP class name for engine* - `summary` (string): Single sentence summary of the engine* - `author` (string): Authr name (if available)* - `moduleVersion` (string): Version of the module that powers this engine* - `libraryVersion` (string): Version of the library that powers this engine* - `sources` (array): Supported formats for source images it reads (i.e. JPG, JPEG, PNG, PNG24, GIF, GIF87, etc.)* - `targets` (array): Supported formats for target images it creates (i.e. JPG, PNG, PNG24, WEBP, etc.)* - `quality` (int): Current quality setting configured with the engine* - `sharpening` (string): Current sharpening setting configured with the engine* - `priority` (int): Engine priority (lower is higher priority)* - `runOrder` (int): Order ImageSizer will try this engine in relative to others (lower runs first), derived from priority.** @param string $name Specify engine name to get info just for that engine or omit to get info for all engines (default)* @return array Array of arrays indexed by engine name, or if $name specified then just array of info for that engine.* Returns empty array on error.* @since 3.0.138**/public function getEngineInfo($name = '') {$infos = array();$engineNames = $name ? array($name) : $this->getEngines();$prefix = 'ImageSizerEngine';if($name && stripos($name, $prefix) === 0) {$name = str_replace($prefix, '', $name);}foreach($engineNames as $priority => $engineName) {$shortName = str_replace($prefix, '', $engineName);if($name && $shortName !== $name) continue;$engine = $this->getEngine($engineName);if(!$engine) continue;$info = $engine->getEngineInfo();$info['runOrder'] = $priority;$infos[$shortName] = $info;}// if one engine requested reduce array to just that engineif($name) $infos = isset($infos[$name]) ? $infos[$name] : array();return $infos;}/*** Instantiate an ImageSizerEngine** @param string $filename* @param array $options* @param null|array $inspectionResult* @return ImageSizerEngine|null* @throws WireException**/protected function newImageSizerEngine($filename = '', array $options = array(), $inspectionResult = null) {if(empty($filename)) $filename = $this->filename;if(empty($options)) $options = $this->initialOptions;if(empty($inspectionResult)) $inspectionResult = $this->inspectionResult;if(empty($inspectionResult) && $filename && is_readable($filename)) {$imageInspector = new ImageInspector($filename);$this->wire($imageInspector);$inspectionResult = $imageInspector->inspect($filename, true);$this->inspectionResult = $inspectionResult;}$engine = null;$bestFallbackEngine = null; // first engine that was supported but failed webp check$engineNames = $this->getEngines();// find first supported engine, according to knownEngines priorityforeach($engineNames as $engineName) {if($this->forceEngineName && $engineName != $this->forceEngineName) continue;$e = $this->getEngine($engineName);if(!$e) continue;/** @var ImageSizerEngine $e */$e->prepare($filename, $options, $inspectionResult);$supported = $e->supported();if($supported && !empty($options['webpAdd']) && !$e->supported('webp')) {// engine does not support requested webp extra imageif(!$bestFallbackEngine) $bestFallbackEngine = $e;} else if($supported) {// found supported engine$engine = $e;break;}$this->failedEngineNames[$engineName] = $engineName;}if(!$engine) {// no engine foundif($bestFallbackEngine) {// if there is a next best fallback, use it$engine = $bestFallbackEngine;} else {// otherwise fallback to default$engine = $this->newDefaultImageSizerEngine($filename, $options, $inspectionResult);}}return $engine;}/*** Get the default/fallback ImageSizer engine** @param string $filename* @param array $options* @param null|array $inspectionResult* @return ImageSizerEngine|null* @throws WireException**/protected function newDefaultImageSizerEngine($filename = '', $options = array(), $inspectionResult = null) {if(empty($filename)) $filename = $this->filename;if(empty($options)) $options = $this->initialOptions;if(empty($inspectionResult)) $inspectionResult = $this->inspectionResult;if($this->forceEngineName && $this->forceEngineName != $this->defaultEngineName) {throw new WireException("Forced engine $this->forceEngineName is not supported for this operation");}$engineClass = __NAMESPACE__ . "\\$this->defaultEngineName";/** @var ImageSizerEngine $engine */$engine = new $engineClass();$this->wire($engine);$engine->prepare($filename, $options, $inspectionResult);if(!$engine->supported()) $engine = null;return $engine;}/*** Resize the image proportionally to the given width/height** @param int $targetWidth Target width in pixels, or 0 for proportional to height* @param int $targetHeight Target height in pixels, or 0 for proportional to width. Optional-if not specified, 0* is assumed.** @return bool True if the resize was successful, false if not* @throws WireException when not enough memory to load image or missing required data**/public function ___resize($targetWidth, $targetHeight = 0) {$engine = $this->getEngine();$success = $engine->resize($targetWidth, $targetHeight);if(!$success) {// fallback to GD$success = $this->resizeFallback($targetWidth, $targetHeight);}return $success;}/*** GD is the fallback ImageEngine, it gets invoked if there is no other Engine defined,** or if a defined Engine is not available,* or if an invoked Engine failes with the image manipulation.** @param mixed $targetWidth* @param mixed $targetHeight** @return bool**/protected function resizeFallback($targetWidth, $targetHeight = 0) {$engine = $this->newDefaultImageSizerEngine();$success = false;if($engine->supported()) $success = $engine->resize($targetWidth, $targetHeight);return $success;}/*** Set the filename** @param $filename* @return $this**/public function setFilename($filename) {$this->filename = $filename;return $this;}/*** Force the use of a specific engine** @param $engineName Module name of engine you want to force* @return $this**/public function setForceEngine($engineName) {$this->forceEngineName = $engineName;return $this;}/*** Set multiple resize options** @param array $options* @return $this**/public function setOptions(array $options) {if(isset($options['forceEngine'])) {$this->setForceEngine($options['forceEngine']);unset($options['forceEngine']);}$this->initialOptions = array_merge($this->initialOptions, $options);if($this->engine) $this->engine->setOptions($this->initialOptions);return $this;}/*** Set whether a modification was made** @param $modified* @return $this**/public function setModified($modified) {if($this->engine) $this->engine->setModified($modified ? true : false);return $this;}public function setAutoRotation($value = true) { return $this->setOptions(array('autoRotation', $value)); }public function setCropExtra($value) { return $this->setOptions(array('cropExtra', $value)); }public function setCropping($cropping = true) { return $this->setOptions(array('cropping', $cropping)); }public function setDefaultGamma($value = 2.2) { return $this->setOptions(array('defaultGamma', $value)); }public function setFlip($flip) { return $this->setOptions(array('flip', $flip)); }public function setHidpi($hidpi = true) { return $this->setOptions(array('hidpi', $hidpi)); }public function setQuality($n) { return $this->setOptions(array('quality', $n)); }public function setRotate($degrees) { return $this->setOptions(array('rotate', $degrees)); }public function setScale($scale) { return $this->setOptions(array('scale', $scale)); }public function setSharpening($value) { return $this->setOptions(array('sharpening', $value)); }public function setTimeLimit($value = 30) { return $this->setOptions(array('timeLimit', $value)); }public function setUpscaling($value = true) { return $this->setOptions(array('upscaling', $value)); }public function setUseUSM($value = true) { return $this->setOptions(array('useUSM', $value)); }public function getWidth() {$image = $this->getEngine()->get('image');return $image['width'];}public function getHeight() {$image = $this->getEngine()->get('image');return $image['height'];}public function getFilename() { return $this->getEngine()->getFilename(); }public function getExtension() { return $this->getEngine()->getExtension(); }public function getImageType() { return $this->getEngine()->getImageType(); }public function isModified() { return $this->getEngine()->getModified(); }public function getOptions() { return $this->getEngine()->getOptions(); }public function getFailedEngineNames() { return $this->failedEngineNames; }/*** Get the current ImageSizerEngine** @param string $engineName Optionally specify a specific engine name to get a new instance of that engine* When used, returned engine is in an unprepared state (no filename assigned, etc.). Since 3.0.138.* @return ImageSizerEngine|null Returns ImageSizerEngine or null only if requested $engineName is not found.* If no $engineName is specified this method may return an existing instance from a previous call.* @throws WireException**/public function getEngine($engineName = '') {if($engineName) {if($engineName === $this->defaultEngineName) {$engineClass = __NAMESPACE__ . "\\$engineName";$engine = $this->wire(new $engineClass());} else {$engine = $this->wire('modules')->get($engineName);}return $engine;}if($this->engine) return $this->engine;if(empty($this->filename)) {throw new WireException('No file to process: please call setFilename($file) before calling other methods');}$imageInspector = new ImageInspector($this->filename);$this->inspectionResult = $imageInspector->inspect($this->filename, true);$this->engine = $this->newImageSizerEngine($this->filename, $this->initialOptions, $this->inspectionResult);// set the engine, and check if the engine is ready to useif(!$this->engine) {throw new WireException('There seems to be no support for the GD image library on your host?');}return $this->engine;}public function __get($key) { return $this->getEngine()->__get($key); }/*** ImageInformation from Image Inspector in short form or full RawInfoData** @param bool $rawData* @return string|array**/public function getImageInfo($rawData = false) {$this->getEngine();if($rawData) return $this->inspectionResult;$imageType = $this->inspectionResult['info']['imageType'];$type = '';$indexed = '';$trans = '';$animated = '';switch($imageType) {case \IMAGETYPE_JPEG:$type = 'jpg';$indexed = '';$trans = '';$animated = '';break;case \IMAGETYPE_GIF:$type = 'gif';$indexed = '';$trans = $this->inspectionResult['info']['trans'] ? '-trans' : '';$animated = $this->inspectionResult['info']['animated'] ? '-anim' : '';break;case \IMAGETYPE_PNG:$type = 'png';$indexed = 'Indexed' == $this->inspectionResult['info']['colspace'] ? '8' : '24';$trans = $this->inspectionResult['info']['alpha'] ? '-alpha' : '';$trans = is_array($this->inspectionResult['info']['trans']) ? '-trans' : $trans;$animated = '';break;}return $type . $indexed . $trans . $animated;}/*** Given an unknown cropping value, return the validated internal representation of it** Needed for backwards compatibility.** @param string|bool|array $cropping** @return string|bool**/static public function croppingValue($cropping) {return ImageSizerEngine::croppingValue($cropping);}/*** Given an unknown cropping value, return the string representation of it** Okay for use in filenames.* Needed for backwards compatibility.** @param string|bool|array $cropping** @return string**/static public function croppingValueStr($cropping) {return ImageSizerEngine::croppingValueStr($cropping);}/*** Given an unknown sharpening value, return the string representation of it** Okay for use in filenames. Method added by @horst* Needed for backwards compatibility.** @param string|bool $value* @param bool $short** @return string**/static public function sharpeningValueStr($value, $short = false) {return ImageSizerEngine::sharpeningValueStr($value, $short);}/*** Check orientation (@horst)** @param mixed $image Pageimage or filename* @param mixed $correctionArray Null or array by reference* @return bool|null**/static public function imageIsRotated($image, &$correctionArray = null) {if($image instanceof Pageimage) {$filename = $image->filename;} else if(is_readable($image)) {$filename = $image;} else {return null;}// first value is rotation-degree and second value is flip-mode: 0=NONE | 1=HORIZONTAL | 2=VERTICAL$corrections = array('1' => array(0, 0),'2' => array(0, 1),'3' => array(180, 0),'4' => array(0, 2),'5' => array(90, 1), // OLD: 270'6' => array(90, 0), // OLD: 270'7' => array(270, 1), // OLD: 90'8' => array(270, 0) // OLD: 90);if(!function_exists('exif_read_data')) return null;$exif = @exif_read_data($filename, 'IFD0');if(!is_array($exif)|| !isset($exif['Orientation'])|| !in_array(strval($exif['Orientation']), array_keys($corrections))) {return null;}$correctionArray = $corrections[strval($exif['Orientation'])];return $correctionArray[0] > 0;}/*** Check if GIF-image is animated or not (@horst)** @param mixed $image Pageimage or filename* @return bool|null**/static public function imageIsAnimatedGif($image) {if($image instanceof Pageimage) {$filename = $image->filename;} elseif(is_readable($image)) {$filename = $image;} else {return null;}$info = getimagesize($filename);if(\IMAGETYPE_GIF != $info[2]) return false;if(ImageSizerEngineGD::checkMemoryForImage(array($info[0], $info[1]))) {return (bool) preg_match('/\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)/s', file_get_contents($filename));}// we have not enough free memory to load the complete image at once, so we do it in chunksif(!($fh = @fopen($filename, 'rb'))) {return null;}$count = 0;while(!feof($fh) && $count < 2) {$chunk = fread($fh, 1024 * 100); //read 100kb at a time$count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', $chunk, $matches);}fclose($fh);return $count > 1;}/*** Possibility to clean IPTC data, also for original images (@horst)** @param mixed $image Pageimage or filename** @return mixed|null|bool**/static public function imageResetIPTC($image) {$wire = null;if($image instanceof Pageimage) {$wire = $image;$filename = $image->filename;} else if(is_readable($image)) {$filename = $image;} else {return null;}$sizer = new ImageSizerEngineGD();if($wire) $wire->wire($sizer);$result = false !== $sizer->writeBackIPTC($filename) ? true : false;return $result;}/*** Rotate image by given degrees** @param int $degrees* @return bool**/public function rotate($degrees) {return $this->getEngine()->rotate($degrees);}/*** Flip image vertically** @return bool**/public function flipVertical() {return $this->getEngine()->flipVertical();}/*** Flip image horizontally** @return bool**/public function flipHorizontal() {return $this->getEngine()->flipHorizontal();}/*** Flip both vertically and horizontally** @return bool**/public function flipBoth() {return $this->getEngine()->flipBoth();}/*** Convert image to greyscale (black and white)** @return bool**/public function convertToGreyscale() {return $this->getEngine()->convertToGreyscale();}/*** Convert image to sepia tone** @param int $sepia Sepia amount* @return bool**/public function convertToSepia($sepia = 55) {return $this->getEngine()->convertToSepia('', $sepia);}}