Blame | Last modification | View Log | Download
<?php namespace ProcessWire;/*** ImageSizer Engine IMagick by Horst** @todo some class properties need phpdoc**/class ImageSizerEngineIMagick extends ImageSizerEngine {public static function getModuleInfo() {return array('title' => 'IMagick Image Sizer','version' => 3,'summary' => "Upgrades image manipulations to use PHP's ImageMagick library when possible.",'author' => 'Horst Nogajski','autoload' => false,'singular' => false,);}/*** The (main) IMagick bitimage handler for regular image variations, (JPEG PNG)** @var \IMagick|null**/protected $im = null;/*** The (optionally) IMagick bitimage handler for additional WebP copies** @var \IMagick|null**/protected $imWebp = null;/*** Static cache of formats and whether or not supported, as used by the supportsFormat() method (RJC)** @var array of ['FORMAT' => true|false]**/static protected $formatSupport = array();// @todo the following need phpdocprotected $workspaceColorspace;protected $imageFormat;protected $imageColorspace;protected $imageMetadata;protected $imageDepth;protected $imageGamma;/*** @var bool**/protected $hasICC;/*** @var bool**/protected $hasIPTC;/*** @var bool**/protected $hasEXIF;/*** @var bool**/protected $hasXMP;/*** Class constructor**/public function __construct() {// set a lower default quality of 80, which is more like 90 in GD$this->setQuality(80);parent::__construct();}/*** Class destructor**/public function __destruct() {$this->release();}/*** Release resources used by IMagick**/protected function release() {if(is_object($this->im)) {$this->im->clear();$this->im->destroy();}if(is_object($this->imWebp)) {$this->imWebp->clear();$this->imWebp->destroy();}}/*** Get valid image source formats** @return array**/protected function validSourceImageFormats() {// 2019/06/07: “PNG8” removed because some versions of ImageMagick have some bug, may be able to add back laterreturn array('JPG', 'JPEG', 'PNG24', 'PNG', 'GIF', 'GIF87');//return array(// 'PNG', 'PNG8', 'PNG24',// 'JPG', 'JPEG',// 'GIF', 'GIF87'//);}/*** Get valid target image formats** @return array**/protected function validTargetImageFormats() {$formats = $this->validSourceImageFormats();if($this->supportsFormat('WEBP')) $formats[] = 'WEBP';return $formats;}/*** Get library version string** @return string Returns version string or blank string if not applicable/available* @since 3.0.138**/public function getLibraryVersion() {$a = \Imagick::getVersion();return "$a[versionString] n=$a[versionNumber]";}/*** Is the given image format supported by this IMagick for source and target? (RJC)** @param string $format String like png, jpg, jpg, png8, png24, png24-trans, png24-alpha, etc.* @return bool**/public function supportsFormat($format) {if(strpos($format, '-')) list($format,) = explode('-', $format);$format = strtoupper($format);if(isset(self::$formatSupport[$format])) return self::$formatSupport[$format];try {$im = new \IMagick();$formats = $im->queryformats($format);$supported = count($formats) > 0;} catch(\Exception $e) {$supported = false;}self::$formatSupport[$format] = $supported;return $supported;}/*** Is IMagick supported? Is the current image(sub)format supported?** @param string $action* @return bool**/public function supported($action = 'imageformat') {// first we check parts that are mandatory for all $actionsif(!class_exists("\\IMagick")) return false;// and if it passes the mandatory requirements, we check particularly aspects here$supported = false;if($action === 'imageformat') {// compare current imagefile infos fetched from ImageInspector$requested = $this->getImageInfo(false);$supported = $this->supportsFormat($requested);} else if($action === 'webp') {$supported = $this->supportsFormat('WEBP');} else if($action === 'install') {$supported = true;}return $supported;}/*** 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**/protected function processResize($srcFilename, $dstFilename, $fullWidth, $fullHeight, $finalWidth, $finalHeight) {$this->setTimeLimit(120);// start image magick$this->im = new \IMagick();// set the working colorspace: COLORSPACE_RGB or COLORSPACE_SRGB ( whats about COLORSPACE_GRAY ??)$this->workspaceColorspace = \Imagick::COLORSPACE_SRGB;$this->im->setColorspace($this->workspaceColorspace);if(!$this->im->readImage($srcFilename)) { // actually we get a filecopy from origFilename to destFilename from PageImage$this->release();return false;}// check validity against image magickif(!$this->im->valid()) {$this->release();throw new WireException(sprintf($this->_("loaded file '%s' is not a valid image"), basename($srcFilename)));}// get image format$this->imageFormat = strtoupper($this->im->getImageFormat());// only for JPEGs and 24bit PNGsif(!in_array($this->imageFormat, $this->validSourceImageFormats())) {$this->release();return false;}// check validity against PW (this does not seem to be reachable due to above code, so commented it out —Ryan)// if(!in_array($this->imageFormat, $this->validSourceImageFormats())) {// $this->release();// throw new WireException(sprintf($this->_("loaded file '%s' is not in the list of valid images"), basename($dstFilename)));// }// check and retrieve different image parts and information: ICC, Colorspace, Colordepth, Metadata, etc$this->imageColorspace = $this->im->getImageColorspace();$this->workspaceColorspace = \Imagick::COLORSPACE_GRAY == $this->imageColorspace ? \Imagick::COLORSPACE_GRAY : $this->workspaceColorspace;$this->im->setColorspace($this->workspaceColorspace);$this->imageMetadata = $this->im->getImageProfiles('*');if(!is_array($this->imageMetadata)) $this->imageMetadata = array();$this->hasICC = array_key_exists('icc', $this->imageMetadata);$this->hasIPTC = array_key_exists('iptc', $this->imageMetadata);$this->hasEXIF = array_key_exists('exif', $this->imageMetadata);$this->hasXMP = array_key_exists('xmp', $this->imageMetadata);$this->imageType = $this->im->getImageType();$this->imageDepth = $this->im->getImageDepth();$this->imageGamma = $this->im->getImageGamma();if(0 == $this->imageGamma) {// we seem to running on a IMagick version that lacks some features,// at least the 'getImageGamma()', therefor we asume a Gamma of 2.2 here$this->imageGamma = 0.454545;}// remove not wanted / needed Metadata = this is the same behave as processed via GD-libforeach(array_keys($this->imageMetadata) as $k) {#if('icc'==$k) continue; // we keep embedded icc profiles#if('iptc' == $k) continue; // we keep embedded iptc data#if('exif'==$k && $this->data['keepEXIF']) continue; // we have to keep exif data too#if('xmp'==$k && $this->data['keepXMP']) continue; // we have to keep xmp data too$this->im->profileImage("$k", null); // remove this one}$this->im->setImageDepth(16);$resetGamma = false;if($this->imageGamma && $this->imageGamma != 1) {$resetGamma = $this->im->gammaImage($this->imageGamma);}$orientations = null;if($this->autoRotation !== true) {$needRotation = false;} else if($this->checkOrientation($orientations) && (!empty($orientations[0]) || !empty($orientations[1]))) {$needRotation = true;} else {$needRotation = false;}if($this->rotate || $needRotation) { // @horstif($this->rotate) {$degrees = $this->rotate;} else if((is_float($orientations[0]) || is_int($orientations[0])) && $orientations[0] > -361 && $orientations[0] < 361) {$degrees = $orientations[0];} else {$degrees = false;}if($degrees !== false && !in_array($degrees, array(-360, 0, 360))) {$this->im->rotateImage(new \IMagickPixel('none'), $degrees);if(abs($degrees) == 90 || abs($degrees) == 270) {// we have to swap width & height now!$tmp = array($this->getWidth(), $this->getHeight());$this->setImageInfo($tmp[1], $tmp[0]);}}}if($this->flip || $needRotation) {$vertical = null;if($this->flip) {$vertical = $this->flip == 'v';} else if($orientations[1] > 0) {$vertical = $orientations[1] == 2;}if(!is_null($vertical)) {$res = $vertical ? $this->im->flipImage() : $this->im->flopImage();if(!$res) {$this->release();return false;}}}$zoom = $this->getFocusZoomPercent();if($zoom > 1) {// we need to configure a cropExtra call to respect the zoom factor$this->cropExtra = $this->getFocusZoomCropDimensions($zoom, $fullWidth, $fullHeight, $finalWidth, $finalHeight);$this->cropping = false;}if(is_array($this->cropExtra) && 4 == count($this->cropExtra)) { // crop before resizelist($cropX, $cropY, $cropWidth, $cropHeight) = $this->cropExtra;#list($x, $y, $w, $h) = $this->cropExtra;if(!$this->im->cropImage($cropWidth, $cropHeight, $cropX, $cropY)) {$this->release();return false;}$this->im->setImagePage(0, 0, 0, 0); //remove the canvas$this->setImageInfo($this->im->getImageWidth(), $this->im->getImageHeight());}$bgX = $bgY = 0;$bgWidth = $fullWidth;$bgHeight = $fullHeight;$resizemethod = $this->getResizeMethod($bgWidth, $bgHeight, $finalWidth, $finalHeight, $bgX, $bgY);if(0 == $resizemethod) {$this->sharpening = 'none'; // no need for sharpening because we use original copy without scaling// oh, do we need to save with more compression for JPEGs ??#return true;} else if(2 == $resizemethod) { // 2 = resize with aspect ratioif(!$this->im->resizeImage($finalWidth, $finalHeight, \Imagick::FILTER_LANCZOS, 1)) {$this->release();return false;}$this->setImageInfo($this->im->getImageWidth(), $this->im->getImageHeight());} else if(4 == $resizemethod) { // 4 = resize and crop from center with aspect ratioif(!$this->im->resizeImage($bgWidth, $bgHeight, \Imagick::FILTER_LANCZOS, 1)) {$this->release();return false;}$this->setImageInfo($this->im->getImageWidth(), $this->im->getImageHeight());if(!$this->im->cropImage($finalWidth, $finalHeight, $bgX, $bgY)) {$this->release();return false;}$this->im->setImagePage(0, 0, 0, 0); //remove the canvas$this->setImageInfo($this->im->getImageWidth(), $this->im->getImageHeight());}if($this->sharpening && $this->sharpening != 'none') {$this->imSharpen($this->sharpening);}// optionally apply interlace bit to the final image. This will result in progressive JPEGsif($this->interlace && in_array(strtoupper($this->imageFormat), array('JPG', 'JPEG'))) {$this->im->setInterlaceScheme(\Imagick::INTERLACE_JPEG);}if(isset($resetGamma) && $this->imageGamma && $this->imageGamma != 1) {$this->im->gammaImage(1 / $this->imageGamma);}$this->im->setImageDepth(($this->imageDepth > 8 ? 8 : $this->imageDepth));// determine whether webp should be created as well (or on its own)$webpOnly = $this->webpOnly && $this->supported('webp');$webpAdd = $webpOnly || ($this->webpAdd && $this->supported('webp'));if($webpOnly) {// only a webp file will be created$this->imWebp = $this->im;} else {if($webpAdd) $this->imWebp = clone $this->im; // make a copy before compressions take effect$this->im->setImageFormat($this->imageFormat);$this->im->setImageType($this->imageType);if(in_array(strtoupper($this->imageFormat), array('JPG', 'JPEG'))) {$this->im->setImageCompression(\Imagick::COMPRESSION_JPEG);$this->im->setImageCompressionQuality($this->quality);} else if(in_array(strtoupper($this->imageFormat), array('PNG', 'PNG8', 'PNG24'))) {$this->im->setImageCompression(\Imagick::COMPRESSION_ZIP);$this->im->setImageCompressionQuality($this->quality);} else {$this->im->setImageCompression(\Imagick::COMPRESSION_UNDEFINED);$this->im->setImageCompressionQuality($this->quality);}// write to fileif(file_exists($dstFilename)) $this->wire('files')->unlink($dstFilename);@clearstatcache(dirname($dstFilename));##if(!$this->im->writeImage($this->destFilename)) {// We use this approach for saving so that it behaves the same like core ImageSizer with images that// have a wrong extension in their filename. When using writeImage() it automatically corrects the// mimetype to match the fileextension, <- we want to avoid this!if(!file_put_contents($dstFilename, $this->im)) {$this->release();return false;}}// set modified flag and delete optional webp dependency file$this->modified = true;$return = true;// if there is a corresponding webp file present, remove it$pathinfo = pathinfo($srcFilename);$webpFilename = $pathinfo['dirname'] . '/' . $pathinfo['filename'] . '.webp';if(file_exists($webpFilename)) $this->wire('files')->unlink($webpFilename);// optionally create a WebP dependency fileif($webpAdd) {// prepare for webp output$this->imWebp->setImageFormat('webp');$this->imWebp->setImageCompressionQuality($this->webpQuality);$this->imWebp->setOption('webp:method', '6');//$this->imWebp->setOption('webp:lossless', 'true'); // is this useful?//$this->imWebp->setImageAlphaChannel(imagick::ALPHACHANNEL_ACTIVATE); // is this useful?//$this->imWebp->setBackgroundColor(new ImagickPixel('transparent')); // is this useful?// save to file$return = $this->imWebp->writeImage($webpFilename);}// release and return to event-object$this->release();return $return;}/*** 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) {$success = false;$imagick = $this->getImagick($srcFilename);if($imagick->rotateImage(new \ImagickPixel('#00000000'), $degrees)) {$success = $this->processSave($imagick, $dstFilename);}return $success;}/*** Process vertical or horizontal flip of an image** @param string $srcFilename* @param string $dstFilename* @param string $flipType Specify vertical, horizontal or both* @return bool**/protected function processFlip($srcFilename, $dstFilename, $flipType) {$imagick = $this->getImagick($srcFilename);if($flipType == 'vertical') {$success = $imagick->flipImage();} else if($flipType == 'horizontal') {$success = $imagick->flopImage();} else {$success = $imagick->flipImage() && $imagick->flopImage();}if($success) $success = $this->processSave($imagick, $dstFilename);return $success;}/*** Reduce dimensions of image by half (using Imagick minifyImage method)** @param string $dstFilename If different from filename specified by setFilename()* @return bool**/public function reduceByHalf($dstFilename = '') {$imagick = $this->getImagick($this->filename);$success = $imagick->minifyImage();if($success) $success = $this->processSave($imagick, $dstFilename);return $success;}/*** Convert image to greyscale** @param string $dstFilename* @return bool**/public function convertToGreyscale($dstFilename = '') {$imagick = $this->getImagick($this->filename);$success = $imagick->transformImageColorspace(\imagick::COLORSPACE_GRAY);if($success) $success = $this->processSave($imagick, $dstFilename);return $success;}/*** Convert image to sepia** @param string $dstFilename* @param float|int $sepia Sepia threshold* @return bool**/public function convertToSepia($dstFilename = '', $sepia = 55) {$sepia += 35;$imagick = $this->getImagick($this->filename);$success = $imagick->sepiaToneImage((float) $sepia);if($success) $success = $this->processSave($imagick, $dstFilename);return $success;}/*** Save action image to file** @param \IMagick $imagick* @param string $dstFilename* @return bool**/protected function processSave(\IMagick $imagick, $dstFilename) {if(empty($dstFilename)) $dstFilename = $this->filename;$ext = strtolower(pathinfo($dstFilename, PATHINFO_EXTENSION));if(in_array($ext, array('jpg', 'jpeg'))) {if($this->interlace) {$imagick->setInterlaceScheme(\Imagick::INTERLACE_JPEG);}}$imagick->setImageCompressionQuality($this->quality);$fp = fopen($dstFilename, 'wb');if($fp === false) return false;$success = $imagick->writeImageFile($fp);fclose($fp);return $success;}/*** Get instance of Imagick** @param string $filename Optional filename to read* @return \Imagick* @throws WireException**/public function getImagick($filename = '') {$imagick = new \Imagick();if($filename) {if(!$imagick->readImage($filename)) {throw new WireException("Imagick unable to load file: " . basename($filename));}}return $imagick;}/*** Sharpen the image** @param string $mode May be none|string|medium|soft* @return bool**/protected function imSharpen($mode) {if('none' == $mode) return true;$mp = intval($this->finalHeight * $this->finalWidth);if($mp > 1440000) {switch($mode) {case 'strong':$m = array(0, 0.5, 4.6, 0.03);break;case 'medium':$m = array(0, 0.5, 3.0, 0.04);break;case 'soft':default:$m = array(0, 0.5, 2.3, 0.07);break;}} else if($mp > 480000) {switch($mode) {case 'strong':$m = array(0, 0.5, 3.0, 0.04);break;case 'medium':$m = array(0, 0.5, 2.3, 0.06);break;case 'soft':default:$m = array(0, 0.5, 1.8, 0.08);break;}} else {switch($mode) {case 'strong':$m = array(0, 0.5, 2.0, 0.06);break;case 'medium':$m = array(0, 0.5, 1.7, 0.08);break;case 'soft':default:$m = array(0, 0.5, 1.2, 0.1);break;}}$this->im->unsharpMaskImage($m[0], $m[1], $m[2], $m[3]);$this->modified = true;return true;}/*** Module install** @throws WireException**/public function ___install() {if(!$this->supported('install')) {throw new WireException("This module requires that you have PHP's IMagick (Image Magick) extension installed");}}}