diff --git a/wire/config.php b/wire/config.php index fbe1cfe9..857649d8 100644 --- a/wire/config.php +++ b/wire/config.php @@ -634,6 +634,8 @@ $config->imageSizerOptions = array( 'quality' => 90, // quality: 1-100 where higher is better but bigger 'hidpiQuality' => 60, // Same as above quality setting, but specific to hidpi images 'defaultGamma' => 2.0, // defaultGamma: 0.5 to 4.0 or -1 to disable gamma correction (default=2.0) + 'webpAdd' => false, // set this to true, if the imagesizer engines should create a Webp copy with every (new) image variation + 'webpQuality' => 90, // webpQuality: 1-100 where higher is better but bigger ); /** diff --git a/wire/core/ImageSizerEngine.php b/wire/core/ImageSizerEngine.php index 6268044f..d2a670dc 100755 --- a/wire/core/ImageSizerEngine.php +++ b/wire/core/ImageSizerEngine.php @@ -10,6 +10,8 @@ * @property bool $upscaling * @property bool $interlace * @property array|string|bool $cropping + * @property bool $webpAdd + * @property int $webpQuality * @property int $quality * @property string $sharpening * @property float $defaultGamma @@ -60,6 +62,22 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable */ 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; + /** * Image interlace setting, false or true * @@ -219,6 +237,8 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable 'cropping', 'interlace', 'quality', + 'webpQuality', + 'webpAdd', 'sharpening', 'defaultGamma', 'scale', @@ -819,7 +839,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * disable cropping, specify boolean false. To enable cropping with default (center), you may also specify * boolean true. * - * @return $this + * @return self * */ public function setCropping($cropping = true) { @@ -834,7 +854,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param array $value containing 4 params (x y w h) indexed or associative * - * @return $this + * @return self * @throws WireException when given invalid value * */ @@ -878,14 +898,43 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param int $n * - * @return $this + * @return self * */ public function setQuality($n) { $n = (int) $n; if($n < 1) $n = 1; - if($n > 100) $n = 100; - $this->quality = (int) $n; + else if($n > 100) $n = 100; + $this->quality = $n; + 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) { + $n = (int) $n; + if($n < 1) $n = 1; + else if($n > 100) $n = 100; + $this->webpQuality = $n; + return $this; + } + + /** + * Set flag to also create a webp file or not + * + * @param bool $value + * + * @return self + * + */ + public function setWebpAdd($value) { + $this->webpAdd = (bool) $value; return $this; } @@ -928,7 +977,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param mixed $value * - * @return $this + * @return self * @throws WireException * */ @@ -957,7 +1006,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param bool $value Whether to auto-rotate or not (default = true) * - * @return $this + * @return self * */ public function setAutoRotation($value = true) { @@ -970,7 +1019,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param bool $value Whether to upscale or not (default = true) * - * @return $this + * @return self * */ public function setUpscaling($value = true) { @@ -983,7 +1032,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param bool $value Whether to upscale or not (default = true) * - * @return $this + * @return self * */ public function setInterlace($value = true) { @@ -996,7 +1045,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param float|int $value 0.5 to 4.0 or -1 to disable * - * @return $this + * @return self * @throws WireException when given invalid value * */ @@ -1016,7 +1065,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param int $value 10 to 60 recommended, default is 30 * - * @return $this + * @return self * */ public function setTimeLimit($value = 30) { @@ -1042,7 +1091,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param float $scale * - * @return $this + * @return self * */ public function setScale($scale) { @@ -1057,7 +1106,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param bool $hidpi True or false (default=true) * - * @return $this + * @return self * */ public function setHidpi($hidpi = true) { @@ -1071,7 +1120,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param $degrees * - * @return $this + * @return self * */ public function setRotate($degrees) { @@ -1089,7 +1138,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param $flip * - * @return $this + * @return self * */ public function setFlip($flip) { @@ -1103,7 +1152,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param bool $value Whether to USM is used or not (default = true) * - * @return $this + * @return self * */ public function setUseUSM($value = true) { @@ -1116,6 +1165,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param array $options May contain the following (show with default values): * 'quality' => 90, + * 'webpQuality' => 90, * 'cropping' => true, * 'upscaling' => true, * 'autoRotation' => true, @@ -1125,7 +1175,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * 'rotate' => 0 (90, 180, 270 or negative versions of those) * 'flip' => '', (vertical|horizontal) * - * @return $this + * @return self * */ public function setOptions(array $options) { @@ -1148,6 +1198,12 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable case 'quality': $this->setQuality($value); break; + case 'webpQuality': + $this->setWebpQuality($value); + break; + case 'webpAdd': + $this->setWebpAdd($value); + break; case 'cropping': $this->setCropping($value); break; @@ -1207,6 +1263,8 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable $options = array( 'quality' => $this->quality, + 'webpQuality' => $this->webpQuality, + 'webpAdd' => $this->webpAdd, 'cropping' => $this->cropping, 'upscaling' => $this->upscaling, 'interlace' => $this->interlace, @@ -1387,7 +1445,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param bool $modified * - * @return $this + * @return self * */ public function setModified($modified) { diff --git a/wire/core/ImageSizerEngineGD.php b/wire/core/ImageSizerEngineGD.php index 4da72c6a..97bad910 100755 --- a/wire/core/ImageSizerEngineGD.php +++ b/wire/core/ImageSizerEngineGD.php @@ -57,6 +57,7 @@ class ImageSizerEngineGD extends ImageSizerEngine { // and if it passes the mandatory requirements, we check particularly aspects here switch($action) { + case 'imageformat': // compare current imagefile infos fetched from ImageInspector $requested = $this->getImageInfo(false); @@ -69,7 +70,16 @@ class ImageSizerEngineGD extends ImageSizerEngine { return true; } break; - + + case 'webp': + if(!isset($this->wire('config')->webpSupportGD)) { + // only call it once + $gd = gd_info(); + $this->wire('config')->webpSupportGD = isset($gd['WebP Support']) ? $gd['WebP Support'] : false; + } + return $this->wire('config')->webpSupportGD; + break; + case 'install': /* $gd = gd_info(); @@ -77,6 +87,7 @@ class ImageSizerEngineGD extends ImageSizerEngine { $png = isset($gd['PNG Support']) ? $gd['PNG Support'] : false; $gif = isset($gd['GIF Read Support']) && isset($gd['GIF Create Support']) ? $gd['GIF Create Support'] : false; $freetype = isset($gd['FreeType Support']) ? $gd['FreeType Support'] : false; + $webp = isset($gd['WebP Support']) ? $gd['WebP Support'] : false; $this->config->gdReady = true; */ return true; @@ -322,32 +333,74 @@ class ImageSizerEngineGD extends ImageSizerEngine { } } - // optionally apply interlace bit to the final image. - // this will result in progressive JPEGs - if($this->interlace && \IMAGETYPE_JPEG == $this->imageType) { - if(0 == imageinterlace($thumb, 1)) { - // log that setting the interlace bit has failed ? - // ... - } - } - - // write to file + // write to file(s) + if(file_exists($dstFilename)) $this->wire('files')->unlink($dstFilename); $result = false; switch($this->imageType) { + case \IMAGETYPE_GIF: // correct gamma from linearized 1.0 back to 2.0 $this->gammaCorrection($thumb, false); - $result = imagegif($thumb, $dstFilename); + + // If only a WebP file is required + if($this->webpOnly) { + $result = $this->imSaveWebP($thumb, $srcFilename, $this->webpQuality); + + } else { + // optionally save an additional WebP file + if($this->webpAdd) { + $resultWebp = $this->imSaveWebP($thumb, $srcFilename, $this->webpQuality); + } + + // save the final GIF image file + $result = imagegif($thumb, $dstFilename); + } break; + case \IMAGETYPE_PNG: + // optionally correct gamma from linearized 1.0 back to 2.0 if(!$this->hasAlphaChannel()) $this->gammaCorrection($thumb, false); - // always use highest compression level for PNG (9) per @horst - $result = imagepng($thumb, $dstFilename, 9); + + // If only a WebP file is required + if($this->webpOnly) { + $result = $this->imSaveWebP($thumb, $srcFilename, $this->webpQuality); + + } else { + // optionally save an additional WebP file + if($this->webpAdd) { + $resultWebp = $this->imSaveWebP($thumb, $srcFilename, $this->webpQuality); + } + + // save the final PNG image file and always use highest compression level (9) per @horst + $result = imagepng($thumb, $dstFilename, 9); + } break; + case \IMAGETYPE_JPEG: // correct gamma from linearized 1.0 back to 2.0 $this->gammaCorrection($thumb, false); - $result = imagejpeg($thumb, $dstFilename, $this->quality); + + // If only a WebP file is required + if($this->webpOnly) { + $result = $this->imSaveWebP($thumb, $srcFilename, $this->webpQuality); + + } else { + // optionally save an additional WebP file + if($this->webpAdd) { + $resultWebp = $this->imSaveWebP($thumb, $srcFilename, $this->webpQuality); + } + + // optionally apply interlace bit to the final image. this will result in progressive JPEGs + if($this->interlace) { + if(0 == imageinterlace($thumb, 1)) { + // log that setting the interlace bit has failed ? + // ... + } + } + + // save the final JPEG image file + $result = imagejpeg($thumb, $dstFilename, $this->quality); + } break; } @@ -357,6 +410,26 @@ class ImageSizerEngineGD extends ImageSizerEngine { return $result; } + + /** + * Create WebP image (@horst) + * Is requested by image options: ["webpAdd" => true] OR ["webpOnly" => true] + * + * @param resource $im + * @param string $dstFilename + * @param int $quality + * + * @return boolean true | false + * + */ + protected function imSaveWebP($im, $filename, $quality = 90) { + if(!function_exists('imagewebp')) return false; + $path_parts = pathinfo($filename); + $webpFilename = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.webp'; + if(file_exists($webpFilename)) $this->wire('files')->unlink($webpFilename); + return imagewebp($im, $webpFilename, $quality); + } + /** * Rotate image (@horst) * diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index bc3c5d16..82baba46 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -40,6 +40,11 @@ * @property-read string $suffixStr String of file suffix(es) separated by comma. * @property-read string $alt Convenient alias for the 'description' property, unless overridden (since 3.0.125). * @property-read string $src Convenient alias for the 'url' property, unless overridden (since 3.0.125). + * @property-read string $urlWebp The url property of an optional WebP-dependency file (since 3.0.132). + * @property-read string $srcWebp Convenient alias for the 'urlWebp' property (since 3.0.132). + * @property-read string $webpUrl Convenient alias for the 'urlWebp' property (since 3.0.132). + * @property-read string $webpSrc Convenient alias for the 'urlWebp' property (since 3.0.132). + * @property-read bool $hasWebp Does exist an optional WebP-dependency file for this image variation? (since 3.0.132) * * Properties inherited from Pagefile * ================================== @@ -175,6 +180,45 @@ class Pageimage extends Pagefile { } } + /** + * Return the web accessible URL to this image files webP dependency, also if no webp copy exists! + * + * @return string + * + */ + public function webpUrl() { + $path_parts = pathinfo($this->url); + $webpUrl = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.webp'; + return $webpUrl; + } + + /** + * Return the filesystem path to this image files webP dependency + * + * @return string + * + */ + public function webpFilename() { + if(!$this->hasWebp()) { + return ''; + } + $path_parts = pathinfo($this->filename); + $webpFilename = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.webp'; + return $webpFilename; + } + + /** + * Return if this image file has a webP dependency file + * + * @return boolean + * + */ + public function hasWebp() { + $path_parts = pathinfo($this->filename()); + $webpFilename = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.webp'; + return is_readable($webpFilename); + } + /** * Returns the full disk path to the image file * @@ -391,6 +435,18 @@ class Pageimage extends Pagefile { $value = parent::get('src'); if($value === null) $value = $this->url(); break; + case 'hasWebp': + $value = $this->hasWebp(); + break; + case 'webpUrl': + case 'webpSrc': + case 'urlWebp': + case 'srcWebp': + $value = $this->webpUrl(); + break; + case 'webpFilename': + $value = $this->webpFilename(); + break; default: $value = parent::get($key); } @@ -578,7 +634,7 @@ class Pageimage extends Pagefile { * - Or you may specify type `int` containing "quality" value. * - Or you may specify type `bool` containing "upscaling" value. * @return Pageimage Returns a new Pageimage object that is a variation of the original. - * If the specified dimensions/options are the same as the original, then the original then the original will be returned. + * If the specified dimensions/options are the same as the original, then the original will be returned. * */ public function size($width, $height, $options = array()) { @@ -635,6 +691,8 @@ class Pageimage extends Pagefile { 'sharpening' => 'soft', 'quality' => 90, 'hidpiQuality' => 40, + 'webpQuality' => 90, + 'webpAdd' => false, 'suffix' => array(), // can be array of suffixes or string of 1 suffix 'forceNew' => false, // force it to create new image even if already exists 'hidpi' => false, @@ -736,16 +794,32 @@ class Pageimage extends Pagefile { $filenameUnvalidated = ''; $exists = file_exists($filenameFinal); + $path_parts = pathinfo($filenameFinal); + $filenameFinalWebp = $this->pagefiles->path() . $path_parts['filename'] . '.webp'; + // force new creation if requested webp copy doesn't exist, (regardless if regular variation exists or not) + if($options['webpAdd'] && !file_exists($filenameFinalWebp)) { + $options['forceNew'] = true; + } + // create a new resize if it doesn't already exist or forceNew option is set if(!$exists && !file_exists($this->filename())) { // no original file exists to create variation from $this->error = "Original image does not exist to create size variation"; } else if(!$exists || $options['forceNew']) { + // filenameUnvalidated is temporary filename used for resize - $filenameUnvalidated = $this->pagefiles->page->filesManager()->getTempPath() . $basename; + $tempDir = $this->pagefiles->page->filesManager()->getTempPath(); + $filenameUnvalidated = $tempDir . $basename; + $path_parts = pathinfo($filenameUnvalidated); + $filenameUnvalidatedWebp = $tempDir . $path_parts['filename'] . '.webp'; + if($exists && $options['forceNew']) $this->wire('files')->unlink($filenameFinal, true); + if(file_exists($filenameFinalWebp) && $options['forceNew']) $this->wire('files')->unlink($filenameFinalWebp, true); + if(file_exists($filenameUnvalidated)) $this->wire('files')->unlink($filenameUnvalidated, true); + if(file_exists($filenameUnvalidatedWebp)) $this->wire('files')->unlink($filenameUnvalidatedWebp, true); + if(@copy($this->filename(), $filenameUnvalidated)) { try { @@ -756,7 +830,13 @@ class Pageimage extends Pagefile { /** @var ImageSizerEngine $engine */ $engine = $sizer->getEngine(); - + + /* if the current engine installation does not support webp, modify the options param */ + if(isset($options['webpAdd']) && $options['webpAdd'] && !$engine->supported('webp')) { + $options['webpAdd'] = false; + $engine->setOptions($options); + } + // allow for ImageSizerEngine module settings for quality and sharpening to override system defaults // when they are not specified as an option to this resize() method $engineConfigData = $engine->getConfigData(); @@ -773,6 +853,9 @@ class Pageimage extends Pagefile { if($sizer->resize($width, $height) && @rename($filenameUnvalidated, $filenameFinal)) { $this->wire('files')->chmod($filenameFinal); + if($options['webpAdd'] && file_exists(($filenameUnvalidatedWebp)) && @rename($filenameUnvalidatedWebp, $filenameFinalWebp)) { + $this->wire('files')->chmod($filenameFinalWebp); + } } else { $this->error = "ImageSizer::resize($width, $height) failed for $filenameUnvalidated"; } @@ -805,6 +888,8 @@ class Pageimage extends Pagefile { // error condition: unlink copied file if(is_file($filenameFinal)) $this->wire('files')->unlink($filenameFinal, true); if($filenameUnvalidated && is_file($filenameUnvalidated)) $this->wire('files')->unlink($filenameUnvalidated); + if(is_file($filenameFinalWebp)) $this->wire('files')->unlink($filenameFinalWebp, true); + if(is_file($filenameUnvalidatedWebp)) $this->wire('files')->unlink($filenameUnvalidatedWebp, true); // we also tell PW about it for logging and/or admin purposes $this->error($this->error); @@ -1582,6 +1667,17 @@ class Pageimage extends Pagefile { $success = $files->unlink($filename, true); } if($success) $deletedFiles[] = $filename; + + // Also remove WebP variation, if there exist one + $path_parts = pathinfo($filename); + $webp = dirname($filename) . '/' . $path_parts['filename'] . '.webp'; + if(!is_file($webp)) continue; + if($options['dryRun']) { + $success = true; + } else { + $success = $files->unlink($webp, true); + } + if($success) $deletedFiles[] = $webp; } if(!$options['dryRun']) $this->variations = null; @@ -1810,6 +1906,194 @@ class Pageimage extends Pagefile { } } + + /** + * Get verbose DebugInfo, optionally with individual options array, @horst + * (without invoking the magic debug) + * + * @param array $options The individual options you also passes with your image variation creation + * @param bool $returnType 'string'|'array'|'object', default is 'string' and returns markup or plain text + * @return array|object|string + * + */ + public function getDebugInfo($options = array(), $returnType = 'string') { + static $depth = 0; + $depth++; + + // fetch imagesizer, some infos and some options + $oSizer = new ImageSizer($this->filename, $options); + $osInfo = $oSizer->getImageInfo(true); + $finalOptions = $oSizer->getOptions(); + + // build some info parts and fetch some from parent (pagefile) + $thumb = array('thumb' => ""); + $original = $this->original ? array('original' => $this->original->basename, 'basename' => $this->basename) : array('original' => '{SELF}', 'basename' => $this->basename); + $parent = array('files' => array_merge( + $original, + parent::__debugInfo(), + array( + 'suffix' => isset($finalOptions['suffix']) ? $finalOptions['suffix'] : '', + 'extension' => $osInfo['extension'] + ) + )); + // rearange parts + unset($parent['files']['filesize']); + $parent['files']['filesize'] = filesize($this->filename); + + // VARIATIONS + if($depth < 2) { + $variationArray = array(); + $variations = $this->getVariations(array('info' => true, 'verbose' => false)); + foreach($variations as $name) $variationArray[] = $name; + } + $depth--; + unset($variations, $name); + + // start collecting the $info + $info = array_merge($thumb, $parent, + array('variations' => $variationArray), + array('imageinfo' => array( + 'imageType' => $osInfo['info']['imageType'], + 'mime' => $osInfo['info']['mime'], + 'width' => $this->width(), + 'height' => $this->height(), + 'focus' => $this->hasFocus ? $this->focusStr : NULL, + 'description' => $parent['files']['description'], + 'tags' => $parent['files']['tags'], + )) + ); + unset($info['files']['tags'], $info['files']['description']); + + // beautify the output, remove unnecessary items + if(isset($info['files']['filedata']) && isset($info['files']['filedata']['focus'])) unset($info['files']['filedata']['focus']); + if(empty($info['files']['filedata'])) unset($info['files']['filedata']); + unset($osInfo['info']['mime'], $osInfo['info']['imageType']); + + // add the rest from osInfo to the final $info array + foreach($osInfo['info'] as $k => $v) $info['imageinfo'][$k] = $v; + $info['imageinfo']['iptcRaw'] = $osInfo['iptcRaw']; + unset($osInfo, $thumb, $original, $parent); + + // WEBP + $webp = array('webp_copy' => array( + 'hasWebp' => $this->hasWebp(), + 'webpUrl' => (!$this->hasWebp() ? NULL : $this->webpUrl), + 'webpQuality' => (!isset($finalOptions['webpQuality']) ? NULL : $finalOptions['webpQuality']), + 'filesize' => (!$this->hasWebp() ? 0 : filesize($this->webpFilename())), + 'savings' => (!$this->hasWebp() ? 0 : intval($info['files']['filesize'] - filesize($this->webpFilename()))), + 'savings_percent' => (!$this->hasWebp() ? 0 : 100 - intval(filesize($this->webpFilename()) / ($info['files']['filesize'] / 100))), + )); + + // ENGINES + $a = []; + $modules = $this->wire('modules'); + $engines = array_merge($oSizer->getEngines(), array('ImageSizerEngineGD')); + foreach($engines as $moduleName) { + $configData = $modules->getModuleConfigData($moduleName); + $priority = isset($configData['enginePriority']) ? (int) $configData['enginePriority'] : 0; + $a[$moduleName] = "priority {$priority}"; + } + asort($a, SORT_STRING); + $enginesArray = array( + 'neededEngineSupport' => strtoupper($oSizer->getImageInfo()), + 'installedEngines' => $a, + 'selectedEngine' => $oSizer->getEngine()->className, + 'engineWebpSupport' => $oSizer->getEngine()->supported('webp') + ); + unset($a, $moduleName, $configData, $engines, $priority, $modules, $oSizer); + + // merge all into $info + $info = array_merge($info, $webp, + array('engines' => $enginesArray), + // OPTIONS + array('options_hierarchy' => array( + 'imageSizerOptions' => $this->wire('config')->imageSizerOptions, + 'individualOptions' => $options, + 'finalOptions' => $finalOptions + ) + ) + ); + unset($variationArray, $webp, $enginesArray, $options, $finalOptions); + + // If not in browser environment, remove the thumb image + if(!isset($_SERVER['HTTP_HOST'])) unset($info['thumb']); + + if('array' == $returnType) { + // return as array + return $info; + } else if('object' == $returnType) { + // return as object + $object = new \stdClass(); + foreach($info as $group => $array) { + $object->$group = new \stdClass(); + if('thumb' == $group) { + $object->$group = $array; + continue; + } + $this->array_to_object($array, $object->$group); + } + return $object; + } + + // make a beautyfied var_dump + $tmp = $info; + $info = array(); + foreach($tmp as $group => $array) { + $info[mb_strtoupper($group)] = $array; + } + unset($tmp, $group, $array); + ob_start(); + var_dump($info); + $content = ob_get_contents(); + ob_end_clean(); + $m = 0; + preg_match_all('#^(.*)=>#mU', $content, $stack); + $lines = $stack[1]; + $indents = array_map('strlen', $lines); + if($indents) $m = max($indents) + 1; + $content = preg_replace_callback( + '#^(.*)=>\\n\s+(\S)#Um', + function($match) use ($m) { + return $match[1] . str_repeat(' ', ($m - strlen($match[1]) > 1 ? $m - strlen($match[1]) : 1)) . $match[2]; + }, + $content + ); + $content = preg_replace('#^((\s*).*){$#m', "\\1\n\\2{", $content); + $content = str_replace(array('
', '
'), '', $content); + if(isset($_SERVER['HTTP_HOST'])) { + // build output for HTML + $return = "
{$content}
"; + } else { + // output for Console + $return = $content; + } + + return $return; + } + + /** + * Helper method that converts a multidim array to a multidim object for the getDebugInfo method + * + * @param array $array the input array + * @param object $object the initial object, gets passed recursive by reference through all loops + * @param bool $multidim set this to true to avoid multidimensional object + * @return object the final multidim object + * + */ + private function array_to_object($array, &$object, $multidim = true) { + foreach($array as $key => $value) { + if($multidim && is_array($value)) { + $object->$key = new \stdClass(); + $this->array_to_object($value, $object->$key, false); + } else { + $object->$key = $value; + } + } + return $object; + } + + + /** * Debug info * diff --git a/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module b/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module index ee8fc37e..9cecb6a0 100755 --- a/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module +++ b/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module @@ -11,7 +11,7 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { public static function getModuleInfo() { return array( 'title' => 'IMagick Image Sizer', - 'version' => 2, + 'version' => 3, 'summary' => "Upgrades image manipulations to use PHP's ImageMagick library when possible.", 'author' => 'Horst Nogajski', 'autoload' => false, @@ -20,10 +20,20 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { } /** + * 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; // @todo the following need phpdoc protected $workspaceColorspace; @@ -80,9 +90,14 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { * */ protected function release() { - if(!is_object($this->im)) return; - $this->im->clear(); - $this->im->destroy(); + if(is_object($this->im)) { + $this->im->clear(); + $this->im->destroy(); + } + if(is_object($this->imWebp)) { + $this->imWebp->clear(); + $this->imWebp->destroy(); + } } /** @@ -146,7 +161,22 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { return false; } break; - + + case 'webp': + if(!isset($this->wire('config')->webpSupportIM)) { + // only call it once + ob_start(); + phpinfo(INFO_MODULES); + $dump = ob_get_clean(); + $list = array(); + if(preg_match('#ImageMagick supported formats (.*?)#msi', $dump, $matches) && isset($matches[1])) { + $list = explode(',', str_replace(' ', '', mb_strtolower($matches[1]))); + } + $this->wire('config')->webpSupportIM = in_array('webp', $list); + } + return $this->wire('config')->webpSupportIM; + break; + case 'install': return true; @@ -176,7 +206,7 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { protected function processResize($srcFilename, $dstFilename, $fullWidth, $fullHeight, $finalWidth, $finalHeight) { $this->setTimeLimit(120); - + // start image magick $this->im = new \IMagick(); @@ -352,8 +382,12 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { } $this->im->setImageDepth(($this->imageDepth > 8 ? 8 : $this->imageDepth)); + + // prepare to save file(s) + if($this->webpAdd) { + $this->imWebp = clone $this->im; // make a copy before compressions take effect + } - // prepare to save file $this->im->setImageFormat($this->imageFormat); $this->im->setImageType($this->imageType); if(in_array(strtoupper($this->imageFormat), array('JPG', 'JPEG'))) { @@ -367,8 +401,8 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { $this->im->setImageCompressionQuality($this->quality); } - // save to file - $this->wire('files')->unlink($dstFilename); + // write to file + if(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 @@ -378,11 +412,30 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { $this->release(); return false; } + + // set modified flag and delete optional webp dependency file + $this->modified = true; + $return = true; + $path_parts = pathinfo($srcFilename); + $webpFilename = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.webp'; + if(file_exists($webpFilename)) $this->wire('files')->unlink($webpFilename); + + // optionally create a WebP dependency file + if($this->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(); - $this->modified = true; - return true; + return $return; } /**