Rev 1 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download
<?php namespace ProcessWire;/*** ImageSizer Engine Module (Abstract)** 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/** @property bool $autoRotation* @property bool $upscaling* @property bool $interlace* @property array|string|bool $cropping* @property int $quality* @property string $sharpening* @property float $defaultGamma* @property float $scale* @property int $rotate* @property string $flip* @property bool $useUSM* @property int $enginePriority Priority for use among other ImageSizerEngine modules (0=disabled, 1=first, 2=second, 3=and so on)* @property bool $webpAdd* @property int $webpQuality* @property bool|null $webpResult* @property bool|null $webpOnly**/abstract class ImageSizerEngine extends WireData implements Module, ConfigurableModule {/*** Filename to be resized** @var string**/protected $filename;/*** Temporary filename as used by the resize() method** @var string**/protected $tmpFile;/*** Extension of filename** @var string**/protected $extension;/*** Type of image**/protected $imageType = null;/*** Image quality setting, 1..100** @var int**/protected $quality = 90;/*** WebP Image quality setting, 1..100** @var int**/protected $webpQuality = 90;/*** Also create a WebP Image with this variation?** @var bool**/protected $webpAdd = false;/*** Only create the webp file?** @var bool**/protected $webpOnly = false;/*** webp result (null=not known or not applicable)** @var bool|null**/protected $webpResult = null;/*** Image interlace setting, false or true** @var bool**/protected $interlace = false;/*** Information about the image (width/height)** @var array**/protected $image = array('width' => 0,'height' => 0);/*** Allow images to be upscaled / enlarged?** @var bool**/protected $upscaling = true;/*** Directions that cropping may gravitate towards** Beyond those included below, TRUE represents center and FALSE represents no cropping.**/static protected $croppingValues = array('nw' => 'northwest','n' => 'north','ne' => 'northeast','w' => 'west','e' => 'east','sw' => 'southwest','s' => 'south','se' => 'southeast',);/*** Allow images to be cropped to achieve necessary dimension? If so, what direction?** Possible values: northwest, north, northeast, west, center, east, southwest, south, southeast* or TRUE to crop to center, or FALSE to disable cropping.* Or array where index 0 is % or px from left, and index 1 is % or px from top. Percent is assumed if* values are number strings that end with %. Pixels are assumed of values are just integers.* Default is: TRUE** @var bool|array**/protected $cropping = true;/*** This can be populated on a per image basis. It provides cropping first and then resizing, the opposite of the* default behavior** It needs an array with 4 params: x y w h for the cropping rectangle** Default is: null** @var null|array**/protected $cropExtra = null;/*** Was the given image modified?** @var bool**/protected $modified = false;/*** enable auto_rotation according to EXIF-Orientation-Flag** @var bool**/protected $autoRotation = true;/*** default sharpening mode: [ none | soft | medium | strong ]** @var string**/protected $sharpening = 'soft';/*** Degrees to rotate: -270, -180, -90, 90, 180, 270** @var int**/protected $rotate = 0;/*** Flip image: Specify 'v' for vertical or 'h' for horizontal** @var string**/protected $flip = '';/*** default gamma correction: 0.5 - 4.0 | -1 to disable gammacorrection, default = 2.0** can be overridden by setting it to $config->imageSizerOptions['defaultGamma']* or passing it along with image options array**/protected $defaultGamma = 2.2;/*** @var bool**/protected $useUSM = false;/*** Other options for 3rd party use** @var array**/protected $options = array();/*** Options allowed for sharpening** @var array**/static protected $sharpeningValues = array(0 => 'none', // none1 => 'soft',2 => 'medium',3 => 'strong');/*** List of valid option Names from config.php (@horst)** @var array**/protected $optionNames = array('autoRotation','upscaling','cropping','interlace','quality','webpQuality','webpAdd','sharpening','defaultGamma','scale','rotate','flip','useUSM',);/*** Supported image types (@teppo)** @var array**/protected $supportedImageTypes = array('gif' => IMAGETYPE_GIF,'jpg' => IMAGETYPE_JPEG,'jpeg' => IMAGETYPE_JPEG,'png' => IMAGETYPE_PNG,);/*** Indicates how much an image should be sharpened** @var int**/protected $usmValue = 100;/*** Result of iptcparse(), if available** @var mixed**/protected $iptcRaw = null;/*** List of valid IPTC tags (@horst)** @var array**/protected $validIptcTags = array('005', '007', '010', '012', '015', '020', '022', '025', '030', '035', '037', '038', '040', '045', '047', '050', '055', '060','062', '063', '065', '070', '075', '080', '085', '090', '092', '095', '100', '101', '103', '105', '110', '115', '116', '118','120', '121', '122', '130', '131', '135', '150', '199', '209', '210', '211', '212', '213', '214', '215', '216', '217');/*** Information about the image from getimagesize (width, height, imagetype, channels, etc.)** @var array|null**/protected $info = null;/*** HiDPI scale value (2.0 = hidpi, 1.0 = normal)** @var float**/protected $scale = 1.0;/*** the resize wrapper method sets this to the width of the given source image** @var integer**/protected $fullWidth;/*** the resize wrapper method sets this to the height of the given source image** @var integer**/protected $fullHeight;/*** the resize wrapper method sets this to the width of the target image* therefore it also may calculate it in regard of hiDpi** @var integer**/protected $finalWidth;/*** the resize wrapper method sets this to the height of the target image* therefor it also may calculate it in regard of hiDpi** @var integer**/protected $finalHeight;/*** Data received from the setConfigData() method** @var array**/protected $moduleConfigData = array();/*** Collection of Imagefile and -format Informations from ImageInspector** @var null|array**/protected $inspectionResult = null;/******************************************************************************************************/public function __construct() {$this->set('enginePriority', 1);parent::__construct();}/*** Prepare the ImageSizer (this should be the first method you call)** This is used as a replacement for __construct() since modules can't have required arguments* to their constructor.** @param string $filename* @param array $options* @param null|array $inspectionResult**/public function prepare($filename, $options = array(), $inspectionResult = null) {// ensures the resize doesn't timeout the request (with at least 30 seconds)$this->setTimeLimit();// when invoked from Pageimage, $inspectionResult holds the InfoCollection from ImageInspector, otherwise NULL// if it is invoked manually and its value is NULL, the method loadImageInfo() will fetch the infos$this->inspectionResult = $inspectionResult;// filling all options with global custom values from config.php$options = array_merge($this->wire('config')->imageSizerOptions, $options);$this->setOptions($options);$this->loadImageInfo($filename, false);}/************************************************************************************************** ABSTRACT AND TEMPLATE METHODS**//*** Is this ImageSizer class ready only means: does the server / system provide all Requirements!** @param string $action Optional type of action supported.* @return bool**/abstract public function supported($action = 'imageformat');/*** Process the image resize** Processing is as follows:* 1. first do a check if the given image(type) can be processed, if not do an early return false* 2. than (try) to process all required steps, if one failes, return false* 3. if all is successful, finally return true** @param string $srcFilename Source file* @param string $dstFilename Destination file* @param int $fullWidth Current width* @param int $fullHeight Current height* @param int $finalWidth Requested final width* @param int $finalHeight Requested final height* @return bool True if successful, false if not* @throws WireException**/abstract protected function processResize($srcFilename, $dstFilename, $fullWidth, $fullHeight, $finalWidth, $finalHeight);/*** Process rotate of an image** @param string $srcFilename* @param string $dstFilename* @param int $degrees Clockwise degrees, i.e. 90, 180, 270, -90, -180, -270* @return bool**/protected function processRotate($srcFilename, $dstFilename, $degrees) {if($srcFilename && $dstFilename && $degrees) {}$this->error('rotate not implemented for ' . $this->className());return false;}/*** Process vertical or horizontal flip of an image** @param string $srcFilename* @param string $dstFilename* @param bool $flipVertical True if flip is vertical, false if flip is horizontal* @return bool**/protected function processFlip($srcFilename, $dstFilename, $flipVertical) {if($srcFilename && $dstFilename && $flipVertical) {}$this->error('flip not implemented for ' . $this->className());return false;}/*** Get array of image file extensions this ImageSizerModule can process** @return array of uppercase file extensions, i.e. ['PNG', 'JPG']**/abstract protected function validSourceImageFormats();/*** Get an array of image file extensions this ImageSizerModule can create** @return array of uppercase file extensions, i.e. ['PNG', 'JPG']**/protected function validTargetImageFormats() {return $this->validSourceImageFormats();}/*** Get an array of image file formats this ImageSizerModule can use as source or target** Unless using the $type argument, returned array contains 'source' and 'target' indexes,* each an array of image file types/extensions in uppercase.** @param string $type Specify 'source' or 'target' to get just those formats, or omit to get all.* @return array* @since 3.0.138**/public function getSupportedFormats($type = '') {$a = array('source' => $this->validSourceImageFormats(),'target' => $this->validTargetImageFormats());return $type && isset($a[$type]) ? $a[$type] : $a;}/*** Get array of information about this engine** @return array* @since 3.0.138**/public function getEngineInfo() {$formats = $this->getSupportedFormats();$moduleName = $this->className();$className = $this->className(true);if(is_callable("$className::getModuleInfo")) {$moduleInfo = $className::getModuleInfo();} else {$moduleInfo = $this->wire('modules')->getModuleInfoVerbose($className);}if(!is_array($moduleInfo)) $moduleInfo = array();$info = array('name' => str_replace('ImageSizerEngine', '', $moduleName),'title' => isset($moduleInfo['title']) ? $moduleInfo['title'] : '','class' => $moduleName,'summary' => isset($moduleInfo['summary']) ? $moduleInfo['summary'] : '','author' => isset($moduleInfo['author']) ? $moduleInfo['author'] : '','moduleVersion' => isset($moduleInfo['version']) ? $moduleInfo['version'] : '','libraryVersion' => $this->getLibraryVersion(),'priority' => $this->enginePriority,'sources' => $formats['source'],'targets' => $formats['target'],'quality' => $this->quality,'sharpening' => $this->sharpening,);return $info;}/************************************************************************************************** COMMON IMPLEMENTATION METHODS**//*** Load all image information from ImageInspector (Module)** @param string $filename* @param bool $reloadAll* @throws WireException**/protected function loadImageInfo($filename, $reloadAll = false) {// if the engine is invoked manually, we need to inspect the image firstif(empty($this->inspectionResult) || $reloadAll) {$imageInspector = new ImageInspector($filename);$this->inspectionResult = $imageInspector->inspect($filename, true);}if(null === $this->inspectionResult) throw new WireException("no valid filename passed to image inspector");if(false === $this->inspectionResult) throw new WireException(basename($filename) . " - not a recognized image");$this->filename = $this->inspectionResult['filename'];$this->extension = $this->inspectionResult['extension'];$this->imageType = $this->inspectionResult['imageType'];if(!in_array($this->imageType, $this->supportedImageTypes)) {throw new WireException(basename($filename) . " - not a supported image type");}$this->info = $this->inspectionResult['info'];$this->iptcRaw = $this->inspectionResult['iptcRaw'];// set width & height$this->setImageInfo($this->info['width'], $this->info['height']);}/*** ImageInformation from Image Inspector* in short form or full RawInfoData** @param bool $rawData* @return string|array* @todo this appears to be a duplicate of what's in ImageSizer class?**/protected function getImageInfo($rawData = false) {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 = is_array($this->inspectionResult['info']['trans']) ? '-trans' : '';$trans = $this->inspectionResult['info']['alpha'] ? '-alpha' : $trans;$animated = '';break;}return $type . $indexed . $trans . $animated;}/*** Default IPTC Handling** If we've retrieved IPTC-Metadata from sourcefile, we write it into the variation here but we omit* custom tags for internal use (@horst)** @param string $filename the file we want write the IPTC data to* @param bool $includeCustomTags default is FALSE* @return bool|null**/public function writeBackIPTC($filename, $includeCustomTags = false) {if($this->wire('config')->debug) {// add a timestamp and the name of the image sizer engine to the IPTC tag number 217$entry = $this->className() . '-' . date('Ymd:His');if(!$this->iptcRaw) $this->iptcRaw = array();$this->iptcRaw['2#217'] = array(0 => $entry);}if(!$this->iptcRaw) return null; // the sourceimage doesn't contain IPTC infos$content = iptcembed($this->iptcPrepareData($includeCustomTags), $filename);if(false === $content) return null;$extension = pathinfo($filename, \PATHINFO_EXTENSION);$dest = preg_replace('/\.' . $extension . '$/', '_tmp.' . $extension, $filename);if(strlen($content) == @file_put_contents($dest, $content, \LOCK_EX)) {// on success we replace the file$this->wire('files')->unlink($filename);$this->wire('files')->rename($dest, $filename);$this->wire('files')->chmod($filename);return true;} else {// it was created a temp diskfile but not with all data in itif(file_exists($dest)) @unlink($dest);return false;}}/*** Save the width and height of the image** @param int $width* @param int $height**/protected function setImageInfo($width, $height) {$this->image['width'] = $width;$this->image['height'] = $height;}/*** Return the image width** @return int**/public function getWidth() {return $this->image['width'];}/*** Return the image height** @return int**/public function getHeight() {return $this->image['height'];}/*** Given a target height, return the proportional width for this image** @param int $targetHeight** @return int**/protected function getProportionalWidth($targetHeight) {$img =& $this->image;return ceil(($targetHeight / $img['height']) * $img['width']); // @horst}/*** Given a target width, return the proportional height for this image** @param int $targetWidth** @return int**/protected function getProportionalHeight($targetWidth) {$img =& $this->image;return ceil(($targetWidth / $img['width']) * $img['height']); // @horst}/*** Get an array of the 4 dimensions necessary to perform the resize** Note: Some code used in this method is adapted from code found in comments at php.net for the GD functions** Intended for use by the resize() method** @param int $targetWidth* @param int $targetHeight** @return array**/protected function getResizeDimensions($targetWidth, $targetHeight) {$pWidth = $targetWidth;$pHeight = $targetHeight;$img =& $this->image;if(!$targetHeight) $targetHeight = round(($targetWidth / $img['width']) * $img['height']);if(!$targetWidth) $targetWidth = round(($targetHeight / $img['height']) * $img['width']);$originalTargetWidth = $targetWidth;$originalTargetHeight = $targetHeight;if($img['width'] < $img['height']) {$pHeight = $this->getProportionalHeight($targetWidth);} else {$pWidth = $this->getProportionalWidth($targetHeight);}if($pWidth < $targetWidth) {// if the proportional width is smaller than specified target width$pWidth = $targetWidth;$pHeight = $this->getProportionalHeight($targetWidth);}if($pHeight < $targetHeight) {// if the proportional height is smaller than specified target height$pHeight = $targetHeight;$pWidth = $this->getProportionalWidth($targetHeight);}// rounding issue fix via @horst-n for #191if($targetWidth == $originalTargetWidth && 1 + $targetWidth == $pWidth) $pWidth = $pWidth - 1;if($targetHeight == $originalTargetHeight && 1 + $targetHeight == $pHeight) $pHeight = $pHeight - 1;if(!$this->upscaling && ($img['width'] < $targetWidth || $img['height'] < $targetHeight)) {// via @horst-n PR #118:// upscaling is not allowed and we have one or both dimensions to small,// we scale down the target dimensions to fit within the image dimensions,// with respect to the target dimensions ratio$ratioSource = $img['height'] / $img['width'];$ratioTarget = !$this->cropping ? $ratioSource : $targetHeight / $targetWidth;if($ratioSource >= $ratioTarget) {// ratio is equal or target fits into source$pWidth = $targetWidth = $img['width'];$pHeight = $img['height'];$targetHeight = ceil($pWidth * $ratioTarget);} else {// target doesn't fit into source$pHeight = $targetHeight = $img['height'];$pWidth = $img['width'];$targetWidth = ceil($pHeight / $ratioTarget);}if($this->cropping) {// we have to disable any sharpening method here,// as the source will not be resized, only cropped$this->sharpening = 'none';}}if(!$this->cropping) {// we will make the image smaller so that none of it gets cropped// this means we'll be adjusting either the targetWidth or targetHeight// till we have a suitable dimensionif($pHeight > $originalTargetHeight) {$pHeight = $originalTargetHeight;$pWidth = $this->getProportionalWidth($pHeight);$targetWidth = $pWidth;$targetHeight = $pHeight;}if($pWidth > $originalTargetWidth) {$pWidth = $originalTargetWidth;$pHeight = $this->getProportionalHeight($pWidth);$targetWidth = $pWidth;$targetHeight = $pHeight;}}return array(0 => (int) $pWidth,1 => (int) $pHeight,2 => (int) $targetWidth,3 => (int) $targetHeight);}/*** Was the image modified?** @return bool**/public function isModified() {return $this->modified;}/*** Given an unknown cropping value, return the validated internal representation of it** @param string|bool|array $cropping** @return string|bool|array**/static public function croppingValue($cropping) {if(is_string($cropping)) {$cropping = strtolower($cropping);if(strpos($cropping, ',')) {$cropping = explode(',', $cropping);} else if(strpos($cropping, 'x') && preg_match('/^([pd])(\d+)x(\d+)(z\d+)?/', $cropping, $matches)) {$cropping = array(0 => (int) $matches[2], // x1 => (int) $matches[3] // y);if(isset($matches[4])) {$cropping[2] = (int) ltrim($matches[4], 'z'); // zoom}if($matches[1] == 'p') { // percent$cropping[0] .= '%';$cropping[1] .= '%';}}}if(is_array($cropping)) {if(strpos($cropping[0], '%') !== false) {$v = trim($cropping[0], '%');if(ctype_digit(trim($v, '-'))) $v = (int) $v;$cropping[0] = round(min(100, max(0, $v))) . '%';} else {$cropping[0] = (int) $cropping[0];}if(strpos($cropping[1], '%') !== false) {$v = trim($cropping[1], '%');if(ctype_digit(trim($v, '-'))) $v = (int) $v;$cropping[1] = round(min(100, max(0, $v))) . '%';} else {$cropping[1] = (int) $cropping[1];}if(isset($cropping[2])) { // zoom$cropping[2] = (int) $cropping[2];if($cropping[2] < 2 || $cropping[2] > 99) unset($cropping[2]);}}if($cropping === true) {$cropping = true; // default, crop to center} else if(!$cropping) {$cropping = false;} else if(is_array($cropping)) {// already took care of it above} else if(in_array($cropping, self::$croppingValues)) {$cropping = array_search($cropping, self::$croppingValues);} else if(array_key_exists($cropping, self::$croppingValues)) {// okay} else {$cropping = true; // unknown value or 'center', default to TRUE/center}return $cropping;}/*** Given an unknown cropping value, return the string representation of it** Okay for use in filenames** @param string|bool|array $cropping** @return string**/static public function croppingValueStr($cropping) {$cropping = self::croppingValue($cropping);// crop name if custom center point is specifiedif(is_array($cropping)) {// p = percent, d = pixel dimension, z = zoom$zoom = isset($cropping[2]) ? (int) $cropping[2] : 0;$cropping =(strpos($cropping[0], '%') !== false ? 'p' : 'd') .((int) rtrim($cropping[0], '%')) . 'x' . ((int) rtrim($cropping[1], '%'));if($zoom > 1 && $zoom < 100) $cropping .= "z$zoom";}// if crop is TRUE or FALSE, we don't reflect that in the filename, so make it blankif(is_bool($cropping)) $cropping = '';return $cropping;}/*** Turn on/off cropping and/or set cropping direction** @param bool|string|array $cropping Specify one of: northwest, north, northeast, west, center, east, southwest,* south, southeast. Or a string of: 50%,50% (x and y percentages to crop from) Or an array('50%', '50%') Or to* disable cropping, specify boolean false. To enable cropping with default (center), you may also specify* boolean true.** @return self**/public function setCropping($cropping = true) {$this->cropping = self::croppingValue($cropping);return $this;}/*** Set values for cropExtra rectangle, which enables cropping before resizing** Added by @horst** @param array $value containing 4 params (x y w h) indexed or associative** @return self* @throws WireException when given invalid value**/public function setCropExtra($value) {$this->cropExtra = null;$x = null;$y = null;$w = null;$h = null;if(!is_array($value) || 4 != count($value)) {throw new WireException('Missing or wrong param Array for ImageSizer-cropExtra!');}if(array_keys($value) === range(0, count($value) - 1)) {// we have a zerobased sequential array, we assume this order: x y w hlist($x, $y, $w, $h) = $value;} else {// check for associative arrayforeach(array('x', 'y', 'w', 'h') as $v) {if(isset($value[$v])) $$v = $value[$v];}}foreach(array('x', 'y', 'w', 'h') as $k) {$v = (int) (isset($$k) ? $$k : -1);if(!$v && $k == 'w' && $h > 0) $v = $this->getProportionalWidth((int) $h);if(!$v && $k == 'h' && $w > 0) $v = $this->getProportionalHeight((int) $w);if($v < 0) throw new WireException("Missing or wrong param $k=$v for ImageSizer-cropExtra! " . print_r($value, true));if(('w' == $k || 'h' == $k) && 0 == $v) throw new WireException("Wrong param $k=$v for ImageSizer-cropExtra! " . print_r($value, true));}$this->cropExtra = array($x, $y, $w, $h);return $this;}/*** Set the image quality 1-100, where 100 is highest quality** @param int $n** @return self**/public function setQuality($n) {$this->quality = $this->getIntegerValue($n, 1, 100);return $this;}/*** Set the image quality 1-100 for WebP output, where 100 is highest quality** @param int $n** @return self**/public function setWebpQuality($n) {$this->webpQuality = $this->getIntegerValue($n, 1, 100);return $this;}/*** Set flag to also create a webp file or not** @param bool $webpAdd* @param bool|null $webpOnly* @return self**/public function setWebpAdd($webpAdd, $webpOnly = null) {$this->webpAdd = (bool) $webpAdd;if(is_bool($webpOnly)) $this->webpOnly = $webpOnly;return $this;}/*** Set flag to only create a webp file** @param bool value$* @return self**/public function setWebpOnly($value) {$this->webpOnly = (bool) $value;if($this->webpOnly) $this->webpAdd = true; // webpAdd required for webpOnlyreturn $this;}/*** Given an unknown sharpening value, return the string representation of it** Okay for use in filenames. Method added by @horst** @param string|bool $value* @param bool $short** @return string**/static public function sharpeningValueStr($value, $short = false) {$sharpeningValues = self::$sharpeningValues;if(is_string($value) && in_array(strtolower($value), $sharpeningValues)) {$ret = strtolower($value);} else if(is_int($value) && isset($sharpeningValues[$value])) {$ret = $sharpeningValues[$value];} else if(is_bool($value)) {$ret = $value ? "soft" : "none";} else {// sharpening is unknown, return empty stringreturn '';}if(!$short) return $ret; // return name$flip = array_flip($sharpeningValues);return 's' . $flip[$ret]; // return char s appended with the numbered index}/*** Set sharpening value: blank (for none), soft, medium, or strong** @param mixed $value** @return self* @throws WireException**/public function setSharpening($value) {if(is_string($value) && in_array(strtolower($value), self::$sharpeningValues)) {$ret = strtolower($value);} else if(is_int($value) && isset(self::$sharpeningValues[$value])) {$ret = self::$sharpeningValues[$value];} else if(is_bool($value)) {$ret = $value ? "soft" : "none";} else {throw new WireException("Unknown value for sharpening");}$this->sharpening = $ret;return $this;}/*** Turn on/off auto rotation** @param bool $value Whether to auto-rotate or not (default = true)** @return self**/public function setAutoRotation($value = true) {$this->autoRotation = $this->getBooleanValue($value);return $this;}/*** Turn on/off upscaling** @param bool $value Whether to upscale or not (default = true)** @return self**/public function setUpscaling($value = true) {$this->upscaling = $this->getBooleanValue($value);return $this;}/*** Turn on/off interlace** @param bool $value Whether to upscale or not (default = true)** @return self**/public function setInterlace($value = true) {$this->interlace = $this->getBooleanValue($value);return $this;}/*** Set default gamma value: 0.5 - 4.0 | -1** @param float|int $value 0.5 to 4.0 or -1 to disable** @return self* @throws WireException when given invalid value**/public function setDefaultGamma($value = 2.2) {if($value === -1 || ($value >= 0.5 && $value <= 4.0)) {$this->defaultGamma = $value;} else {throw new WireException('Invalid defaultGamma value - must be 0.5 - 4.0 or -1 to disable gammacorrection');}return $this;}/*** Set a time limit for manipulating one image (default is 30)** If specified time limit is less than PHP's max_execution_time, then PHP's setting will be used instead.** @param int $value 10 to 60 recommended, default is 30** @return self**/public function setTimeLimit($value = 30) {// imagesizer can get invoked from different locations, including those that are inside of loops// like the wire/modules/Inputfield/InputfieldFile/InputfieldFile.module :: ___renderList() method$prevLimit = ini_get('max_execution_time');// if unlimited execution time, no need to introduce oneif(!$prevLimit) return $this;// don't override a previously set high time limit, just start over with it$timeLimit = (int) ($prevLimit > $value ? $prevLimit : $value);// restart time limitset_time_limit($timeLimit);return $this;}/*** Set scale for hidpi (2.0=hidpi, 1.0=normal, or other value if preferred)** @param float $scale** @return self**/public function setScale($scale) {$this->scale = (float) $scale;return $this;}/*** Enable hidpi mode?** Just a shortcut for calling $this->scale()** @param bool $hidpi True or false (default=true)** @return self**/public function setHidpi($hidpi = true) {return $this->setScale($hidpi ? 2.0 : 1.0);}/*** Set rotation degrees** Specify one of: -270, -180, -90, 90, 180, 270** @param $degrees** @return self**/public function setRotate($degrees) {$valid = array(-270, -180, -90, 90, 180, 270);$degrees = (int) $degrees;if(in_array($degrees, $valid)) $this->rotate = $degrees;return $this;}/*** Set flip** Specify one of: 'vertical' or 'horizontal', also accepts* shorter versions like, 'vert', 'horiz', 'v', 'h', etc.** @param $flip** @return self**/public function setFlip($flip) {$flip = strtolower(substr($flip, 0, 1));if($flip == 'v' || $flip == 'h') $this->flip = $flip;return $this;}/*** Toggle on/off the usage of USM algorithm for sharpening** @param bool $value Whether to USM is used or not (default = true)** @return self**/public function setUseUSM($value = true) {$this->useUSM = true === $this->getBooleanValue($value) ? true : false;return $this;}/*** Alternative to the above set* functions where you specify all in an array** @param array $options May contain the following (show with default values):* 'quality' => 90,* 'webpQuality' => 90,* 'cropping' => true,* 'upscaling' => true,* 'autoRotation' => true,* 'sharpening' => 'soft' (none|soft|medium|string)* 'scale' => 1.0 (use 2.0 for hidpi or 1.0 for normal-default)* 'hidpi' => false, (alternative to scale, specify true to enable hidpi)* 'rotate' => 0 (90, 180, 270 or negative versions of those)* 'flip' => '', (vertical|horizontal)** @return self**/public function setOptions(array $options) {foreach($options as $key => $value) {switch($key) {case 'autoRotation':$this->setAutoRotation($value);break;case 'upscaling':$this->setUpscaling($value);break;case 'interlace':$this->setInterlace($value);break;case 'sharpening':$this->setSharpening($value);break;case 'quality':$this->setQuality($value);break;case 'webpQuality':$this->setWebpQuality($value);break;case 'webpAdd':$this->setWebpAdd($value);break;case 'webpOnly':$this->setWebpOnly($value);break;case 'cropping':$this->setCropping($value);break;case 'defaultGamma':$this->setDefaultGamma($value);break;case 'cropExtra':$this->setCropExtra($value);break;case 'scale':$this->setScale($value);break;case 'hidpi':$this->setHidpi($value);break;case 'rotate':$this->setRotate($value);break;case 'flip':$this->setFlip($value);break;case 'useUSM':$this->setUseUSM($value);break;default:// unknown or 3rd party option$this->options[$key] = $value;}}return $this;}/*** Given a value, convert it to a boolean.** Value can be string representations like: 0, 1 off, on, yes, no, y, n, false, true.** @param bool|int|string $value** @return bool**/protected function getBooleanValue($value) {if(in_array(strtolower($value), array('0', 'off', 'false', 'no', 'n', 'none'))) return false;return ((int) $value) > 0;}/*** Get integer value within given range** @param int $n Number to require in given range* @param int $min Minimum allowed number* @param int $max Maximum allowed number* @return int**/protected function getIntegerValue($n, $min, $max) {$n = (int) $n;if($n < $min) {$n = $min;} else if($n > $max) {$n = $max;}return $n;}/*** Return an array of the current options** @return array**/public function getOptions() {$options = array('quality' => $this->quality,'webpQuality' => $this->webpQuality,'webpAdd' => $this->webpAdd,'webpOnly' => $this->webpOnly,'cropping' => $this->cropping,'upscaling' => $this->upscaling,'interlace' => $this->interlace,'autoRotation' => $this->autoRotation,'sharpening' => $this->sharpening,'defaultGamma' => $this->defaultGamma,'cropExtra' => $this->cropExtra,'scale' => $this->scale,'useUSM' => $this->useUSM,);$options = array_merge($this->options, $options);return $options;}/*** Get a property** @param string $key** @return mixed|null**/public function get($key) {$keys = array('filename','extension','imageType','image','modified','supportedImageTypes','info','iptcRaw','validIptcTags','cropExtra','options');if($key === 'webpResult') return $this->webpResult;if($key === 'webpOnly') return $this->webpOnly;if(in_array($key, $keys)) return $this->$key;if(in_array($key, $this->optionNames)) return $this->$key;if(isset($this->options[$key])) return $this->options[$key];return parent::get($key);}/*** Return the filename** @return string**/public function getFilename() {return $this->filename;}/*** Return the file extension** @return string**/public function getExtension() {return $this->extension;}/*** Return the image type constant** @return string**/public function getImageType() {return $this->imageType;}/*** Prepare IPTC data (@horst)** @param bool $includeCustomTags (default=false)** @return string $iptcNew**/protected function iptcPrepareData($includeCustomTags = false) {$customTags = array('213', '214', '215', '216', '217');$iptcNew = '';foreach(array_keys($this->iptcRaw) as $s) {$tag = substr($s, 2);if(!$includeCustomTags && in_array($tag, $customTags)) continue;if(substr($s, 0, 1) == '2' && in_array($tag, $this->validIptcTags) && is_array($this->iptcRaw[$s])) {foreach($this->iptcRaw[$s] as $row) {$iptcNew .= $this->iptcMakeTag(2, $tag, $row);}}}return $iptcNew;}/*** Make IPTC tag (@horst)** @param string $rec* @param string $dat* @param string $val** @return string**/protected function iptcMakeTag($rec, $dat, $val) {$len = strlen($val);if($len < 0x8000) {return @chr(0x1c) . @chr($rec) . @chr($dat) .chr($len >> 8) .chr($len & 0xff) .$val;} else {return chr(0x1c) . chr($rec) . chr($dat) .chr(0x80) . chr(0x04) .chr(($len >> 24) & 0xff) .chr(($len >> 16) & 0xff) .chr(($len >> 8) & 0xff) .chr(($len) & 0xff) .$val;}}/*** Check orientation (@horst)** @param array** @return bool**/protected function checkOrientation(&$correctionArray) {// 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(!isset($this->info['orientation']) || !isset($corrections[strval($this->info['orientation'])])) {return false;}$correctionArray = $corrections[strval($this->info['orientation'])];return true;}/*** Check for alphachannel in PNGs** @return bool**/protected function hasAlphaChannel() {if(!isset($this->info['alpha']) && !isset($this->info['trans'])) return false;if(isset($this->info['alpha']) && $this->info['alpha']) return true;if(isset($this->info['trans']) && $this->info['trans']) return true;return false;}/*** Set whether the image was modified** Public so that other modules/hooks can adjust this property if needed.* Not for general API use** @param bool $modified** @return self**/public function setModified($modified) {$this->modified = $modified ? true : false;return $this;}/*** Get whether the image was modified** @return bool**/public function getModified() {return $this->modified;}/*** Check if cropping is needed, if yes, populate x- and y-position to params $w1 and $h1** Intended for use by the resize() method** @param int $w1 - byReference* @param int $h1 - byReference* @param int $gdWidth* @param int $targetWidth* @param int $gdHeight* @param int $targetHeight**/protected function getCropDimensions(&$w1, &$h1, $gdWidth, $targetWidth, $gdHeight, $targetHeight) {if(is_string($this->cropping)) {// calculate from 8 named cropping pointsswitch($this->cropping) {case 'nw':$w1 = 0;$h1 = 0;break;case 'n':$h1 = 0;break;case 'ne':$w1 = $gdWidth - $targetWidth;$h1 = 0;break;case 'w':$w1 = 0;break;case 'e':$w1 = $gdWidth - $targetWidth;break;case 'sw':$w1 = 0;$h1 = $gdHeight - $targetHeight;break;case 's':$h1 = $gdHeight - $targetHeight;break;case 'se':$w1 = $gdWidth - $targetWidth;$h1 = $gdHeight - $targetHeight;break;default: // center or false, we do nothing}} else if(is_array($this->cropping)) {// calculate from specific percent or pixels from left and top// $this->cropping is an array with the following:// index 0 represents % or pixels from left// index 1 represents % or pixels from top// @interrobang + @u-nikosif(strpos($this->cropping[0], '%') === false) {$pointX = (int) $this->cropping[0];} else {$pointX = $gdWidth * ((int) $this->cropping[0] / 100);}if(strpos($this->cropping[1], '%') === false) {$pointY = (int) $this->cropping[1];} else {$pointY = $gdHeight * ((int) $this->cropping[1] / 100);}/*if(isset($this->cropping[2]) && $this->cropping[2] > 1) {// zoom percent (2-100)$zoom = (int) $this->cropping[2];}*/if($pointX < $targetWidth / 2) {$w1 = 0;} else if($pointX > ($gdWidth - $targetWidth / 2)) {$w1 = $gdWidth - $targetWidth;} else {$w1 = $pointX - $targetWidth / 2;}if($pointY < $targetHeight / 2) {$h1 = 0;} else if($pointY > ($gdHeight - $targetHeight / 2)) {$h1 = $gdHeight - $targetHeight;} else {$h1 = $pointY - $targetHeight / 2;}}}/*************************************************************************************************//*** Resize the image** The resize method does all pre and post-processing for the engines + calls the engine's* processResize() method.** Pre-processing is:* Calculate and set dimensions, create a tempfile.** Post-processing is:* Copy back and delete tempfile, write IPTC if necessary, reload imageinfo, set the modified flag.** @param int $finalWidth* @param int $finalHeight* @return bool**/public function resize($finalWidth, $finalHeight) {// @todo is this call necessary, since ImageSizer.php would have already checked this?if(!$this->supported()) return false;// first prepare dimension settings for the engine(s)$this->finalWidth = $finalWidth;$this->finalHeight = $finalHeight;$this->fullWidth = $this->image['width'];$this->fullHeight = $this->image['height'];if(0 == $this->finalWidth && 0 == $this->finalHeight) return false;if($this->scale !== 1.0) { // adjust for hidpiif($this->finalWidth) $this->finalWidth = ceil($this->finalWidth * $this->scale);if($this->finalHeight) $this->finalHeight = ceil($this->finalHeight * $this->scale);}// create another temp copy so that we have the source unaltered if an engine fails// and we need to start another one$this->tmpFile = $this->filename . '-tmp.' . $this->extension;if(!@copy($this->filename, $this->tmpFile)) return false; // fallback or failed// lets the engine do the resize workif(!$this->processResize($this->filename, $this->tmpFile,$this->fullWidth, $this->fullHeight,$this->finalWidth, $this->finalHeight)) {return false; // fallback or failed}if($this->webpOnly) {$this->wire('files')->unlink($this->tmpFile);} else {// all went well, copy back the temp file,if(!@copy($this->tmpFile, $this->filename)) return false; // fallback or failed$this->wire('files')->chmod($this->filename);// remove the temp file$this->wire('files')->unlink($this->tmpFile);// post processing: IPTC, setModified and reload ImageInfo$this->writeBackIPTC($this->filename, false);}$this->setModified($this->modified);$this->loadImageInfo($this->filename, true);return true;}/*** Just rotate image by number of degrees** @param int $degrees* @param string $dstFilename Optional destination filename. If not present, source will be overwritten.* @return bool True on success, false on fail**/public function rotate($degrees, $dstFilename = '') {$degrees = (int) $degrees;$srcFilename = $this->filename;if(empty($dstFilename)) $dstFilename = $srcFilename;if($degrees > 360) $degrees = 360 - $degrees;if($degrees < -360) $degrees = $degrees - 360;if($degrees == 0 || $degrees == 360 || $degrees == -360) {if($dstFilename != $this->filename) wireCopy($this->filename, $dstFilename);return true;}if($srcFilename == $dstFilename) {// src and dest are the same, so use a temporary file$n = 1;do {$tmpFilename = dirname($dstFilename) . "/.ise$n-" . basename($dstFilename);} while(file_exists($tmpFilename) && $n++);} else {// src and dest are different files$tmpFilename = $dstFilename;}$result = $this->processRotate($srcFilename, $tmpFilename, $degrees);if($result) {// successif($tmpFilename != $dstFilename) {if(is_file($dstFilename)) $this->wire('files')->unlink($dstFilename);$this->wire('files')->rename($tmpFilename, $dstFilename);}$this->wire('files')->chmod($dstFilename);} else {// failif(is_file($tmpFilename)) $this->wire('files')->unlink($tmpFilename);}return $result;}/*** Flip vertically** @param string $dstFilename* @return bool**/public function flipVertical($dstFilename = '') {if(empty($dstFilename)) $dstFilename = $this->filename;return $this->processFlip($this->filename, $dstFilename, 'vertical');}/*** Flip horizontally** @param string $dstFilename* @return bool**/public function flipHorizontal($dstFilename = '') {if(empty($dstFilename)) $dstFilename = $this->filename;return $this->processFlip($this->filename, $dstFilename, 'horizontal');}/*** Flip both vertically and horizontally** @param string $dstFilename* @return bool**/public function flipBoth($dstFilename = '') {if(empty($dstFilename)) $dstFilename = $this->filename;return $this->processFlip($this->filename, $dstFilename, 'both');}/*** Convert image to greyscale** @param string $dstFilename If different from source file* @return bool**/public function convertToGreyscale($dstFilename = '') {if($dstFilename) {}return false;}/*** Convert image to sepia** @param string $dstFilename If different from source file* @param float|int $sepia Sepia value* @return bool**/public function convertToSepia($dstFilename = '', $sepia = 55) {if($dstFilename && $sepia) {}return false;}/*** Get an integer representing the resize method to use** This method calculates all dimensions at first. It is called before any of the main image operations,* but after rotation and crop_before_resize. As result it returns an integer [0|2|4] that indicates which* steps should be processed:** 0 = this is the case if the original size is requested or a greater size but upscaling is set to false* 2 = only resize with aspect ratio* 4 = resize and crop with aspect ratio** @param mixed $gdWidth* @param mixed $gdHeight* @param mixed $targetWidth* @param mixed $targetHeight* @param mixed $x1* @param mixed $y1** @return int 0|2|4**/protected function getResizeMethod(&$gdWidth, &$gdHeight, &$targetWidth, &$targetHeight, &$x1, &$y1) {list($gdWidth, $gdHeight, $targetWidth, $targetHeight) = $this->getResizeDimensions($targetWidth, $targetHeight);$x1 = ($gdWidth / 2) - ($targetWidth / 2);$y1 = ($gdHeight / 2) - ($targetHeight / 2);$this->getCropDimensions($x1, $y1, $gdWidth, $targetWidth, $gdHeight, $targetHeight);$x1 = intval($x1);$y1 = intval($y1);if($gdWidth == $targetWidth && $gdWidth == $this->image['width'] && $gdHeight == $this->image['height'] && $gdHeight == $targetHeight) return 0;if($gdWidth == $targetWidth && $gdHeight == $targetHeight) return 2;return 4;}/*** Helper function to perform a cropExtra / cropBefore cropping** Intended for use by the getFocusZoomCropDimensions() method** @param string $focus (focus point in percent, like: 54.7%)* @param int $sourceDimension (source image width or height)* @param int $cropDimension (target crop-image width or height)* @param int $zoom** @return int $position (crop position x or y in pixel)**/protected function getFocusZoomPosition($focus, $sourceDimension, $cropDimension, $zoom) {$focus = intval($focus); // string with float value and percent char, (needs to be converted to integer)$scale = 1 + (($zoom / 100) * 2);$focusPX = ($sourceDimension / 100 * $focus);$posMinPX = $cropDimension / 2 / $scale;$posMaxPX = $sourceDimension - ($cropDimension / 2);// calculate the position in pixel !if($focusPX >= $posMaxPX) {$posPX = $sourceDimension - $cropDimension;} else if($focusPX <= $posMinPX) {$posPX = 0;} else {$posPX = $focusPX - ($cropDimension / 2);if(0 > $posPX) $posPX = 0;}return $posPX;}/*** Get an array of the 4 dimensions necessary to perform a cropExtra / cropBefore cropping** Intended for use by the resize() method** @param int $zoom* @param int $fullWidth* @param int $fullHeight* @param int $finalWidth* @param int $finalHeight* @return array**/protected function getFocusZoomCropDimensions($zoom, $fullWidth, $fullHeight, $finalWidth, $finalHeight) {// validate & calculate / prepare params$zoom = $zoom <= 70 ? $zoom : 70; // validate / correct the zoom value, it needs to be between 2 and 70$zoom = $zoom >= 2 ? $zoom : 2;// calculate the max crop dimensions$ratioFinal = $finalWidth / $finalHeight; // get the ratio of the requested crop$percentW = $finalWidth / $fullWidth * 100; // calculate percentage of the crop width in regard of the original width$percentH = $finalHeight / $fullHeight * 100; // calculate percentage of the crop height in regard of the original heightif($percentW >= $percentH) { // check wich one is greater$maxW = $fullWidth; // if percentW is greater, maxW becomes the original Width$maxH = $fullWidth / $ratioFinal; // ... and maxH gets calculated via the ratio} else {$maxH = $fullHeight; // if percentH is greater, maxH becomes the original Height$maxW = $fullHeight * $ratioFinal; // ... and maxW gets calculated via the ratio}// calculate the zoomed dimensions$cropW = $maxW - ($maxW * $zoom / 100); // to get the final crop Width and Height, the amount for zoom-in$cropH = $maxH - ($maxH * $zoom / 100); // needs to get stripped out// validate against the minimal dimensionsif(!$this->upscaling) { // if upscaling isn't allowed, we decrease the zoom, so that we get a crop with the min-Dimensionsif($cropW < $finalWidth) {$cropW = $finalWidth;$cropH = $finalWidth / $ratioFinal;}if($cropH < $finalHeight) {$cropH = $finalHeight;$cropW = $finalHeight * $ratioFinal;}}// calculate the crop positions$posX = $this->getFocusZoomPosition($this->cropping[0], $fullWidth, $cropW, $zoom); // calculate the x-position$posY = $this->getFocusZoomPosition($this->cropping[1], $fullHeight, $cropH, $zoom); // calculate the y-positionreturn array(0 => (int) $posX,1 => (int) $posY,2 => (int) $cropW,3 => (int) $cropH);}/*** Get current zoom percentage setting or 0 if not set** Value is determined from the $this->cropping array index 2 and is used only if index 0 and* index 1 are percentages (and indicated as such with a percent sign).** @return int**/protected function getFocusZoomPercent() {// check if we have to proceed a zoomed focal point cropping,// therefore we need index 0 and 1 to be strings with '%' sign included// and index 2 to be an integer between 2 and 70$a = $this->cropping;if(is_array($a) && isset($a[2]) && strpos($a[0], '%') !== false && strpos($a[1], '%') !== false) {$zoom = (int) $a[2];if($zoom < 2) $zoom = 0;if($zoom > 70) $zoom = 70;} else {$zoom = 0;}return $zoom;}/*** Module info: not-autoload** @return bool**/public function isAutoload() {return false;}/*** Module info: not singular** @return bool**/public function isSingular() {return false;}/*** Set module config data for ConfigurableModule interface** @param array $data**/public function setConfigData(array $data) {if(count($data)) $this->moduleConfigData = $data;foreach($data as $key => $value) {if($key == 'sharpening') {$this->setSharpening($value);} else if($key == 'quality') {$this->setQuality($value);} else {$this->set($key, $value);}}}/*** Get module config data** @return array**/public function getConfigData() {return $this->moduleConfigData;}/*** Get library version string** @return string Returns version string or blank string if not applicable/available* @since 3.0.138**/public function getLibraryVersion() {return '';}/*** Module configuration** @param InputfieldWrapper $inputfields**/public function getModuleConfigInputfields(InputfieldWrapper $inputfields) {$f = $this->wire('modules')->get('InputfieldInteger');$f->attr('name', 'enginePriority');$f->label = $this->_('Engine priority');$f->description = $this->_('This determines what order this engine is tried in relation to other ImageSizerEngine modules.');$f->description .= ' ' . $this->_('The lower the number, the more preference you give it.');$f->description .= ' ' . $this->_('If you have other ImageSizerEngine modules installed, make sure no two have the same priority.');$f->attr('value', $this->enginePriority);$f->icon = 'sort-numeric-asc';$inputfields->add($f);$f = $this->wire('modules')->get('InputfieldRadios');$f->attr('name', 'sharpening');$f->label = $this->_('Sharpening');$f->addOption('none', $this->_('None'));$f->addOption('soft', $this->_('Soft'));$f->addOption('medium', $this->_('Medium'));$f->addOption('strong', $this->_('Strong'));$f->optionColumns = 1;$f->attr('value', $this->sharpening);$f->icon = 'image';$inputfields->add($f);$f = $this->wire('modules')->get('InputfieldInteger');$f->attr('name', 'quality');$f->label = $this->_('Quality');$f->description = $this->_('Default quality setting from 1 to 100 where 1 is lowest quality, and 100 is highest.');$f->attr('value', $this->quality);$f->min = 0;$f->max = 100;$f->icon = 'dashboard';$inputfields->add($f);}}