From c1814c8e6b6535cc195b149895dd940f2f9fc856 Mon Sep 17 00:00:00 2001 From: horst-n Date: Wed, 24 Apr 2019 21:28:33 +0200 Subject: [PATCH 01/33] add option webpQuality --- wire/core/ImageSizerEngine.php | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/wire/core/ImageSizerEngine.php b/wire/core/ImageSizerEngine.php index 6268044f..8f2edb91 100755 --- a/wire/core/ImageSizerEngine.php +++ b/wire/core/ImageSizerEngine.php @@ -11,6 +11,7 @@ * @property bool $interlace * @property array|string|bool $cropping * @property int $quality + * @property int $webpQuality * @property string $sharpening * @property float $defaultGamma * @property float $scale @@ -60,6 +61,14 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable */ protected $quality = 90; + /** + * WebP Image quality setting, 1..100 + * + * @var int + * + */ + protected $webpQuality = 90; + /** * Image interlace setting, false or true * @@ -219,6 +228,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable 'cropping', 'interlace', 'quality', + 'webpQuality', 'sharpening', 'defaultGamma', 'scale', @@ -889,6 +899,22 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable return $this; } + /** + * Set the image quality 1-100 for WebP output, where 100 is highest quality + * + * @param int $n + * + * @return $this + * + */ + public function setWebpQuality($n) { + $n = (int) $n; + if($n < 1) $n = 1; + if($n > 100) $n = 100; + $this->webpQuality = (int) $n; + return $this; + } + /** * Given an unknown sharpening value, return the string representation of it * @@ -1116,6 +1142,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, @@ -1148,6 +1175,9 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable case 'quality': $this->setQuality($value); break; + case 'webpQuality': + $this->setWebpQuality($value); + break; case 'cropping': $this->setCropping($value); break; @@ -1207,6 +1237,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable $options = array( 'quality' => $this->quality, + 'webpQuality' => $this->webpQuality, 'cropping' => $this->cropping, 'upscaling' => $this->upscaling, 'interlace' => $this->interlace, From bbe13583c5a3b7600fd483c0d6d5472b8f3a12f1 Mon Sep 17 00:00:00 2001 From: horst-n Date: Wed, 24 Apr 2019 21:29:09 +0200 Subject: [PATCH 02/33] add webp support to GD --- wire/core/ImageSizerEngineGD.php | 87 +++++++++++++++++++++++++++----- 1 file changed, 73 insertions(+), 14 deletions(-) diff --git a/wire/core/ImageSizerEngineGD.php b/wire/core/ImageSizerEngineGD.php index 4da72c6a..467f3d60 100755 --- a/wire/core/ImageSizerEngineGD.php +++ b/wire/core/ImageSizerEngineGD.php @@ -322,32 +322,73 @@ 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) $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 +398,24 @@ 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; + $newBasename = str_replace(array('.jpg', '.jpeg', '.png', '.gif'), '.webp', basename($filename)); + return imagewebp($im, dirname($filename) . '/' . $newBasename, $quality); + } + /** * Rotate image (@horst) * From 63018ab1cd3a0e6d98fa234cf1ef7df16b1b86eb Mon Sep 17 00:00:00 2001 From: horst-n Date: Wed, 24 Apr 2019 21:32:05 +0200 Subject: [PATCH 03/33] add support for webp added option "webpAdd" and "webpQuality" that allows to create a webp file as sidecar file when creating a JPEG, GIF or PNG file. --- wire/core/Pageimage.php | 44 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index bc3c5d16..5d0883f6 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -635,6 +635,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, @@ -733,9 +735,18 @@ class Pageimage extends Pagefile { // i.e. myfile.100x100.jpg or myfile.100x100nw-suffix1-suffix2.jpg $basename .= '.' . $nameWidth . 'x' . $nameHeight . $crop . $suffixStr . "." . $this->ext(); $filenameFinal = $this->pagefiles->path() . $basename; + $filenameFinalWebp = $this->pagefiles->path() . str_replace(array('.jpg', '.jpeg', '.png', '.gif'), '.webp', $basename); $filenameUnvalidated = ''; $exists = file_exists($filenameFinal); + +//my_var_dump([ +// $defaultOptions, +// $requestOptions, +// $options, +//]); + + // 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 @@ -743,9 +754,25 @@ class Pageimage extends Pagefile { } 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; + $filenameUnvalidatedWebp = $tempDir . str_replace(array('.jpg', '.jpeg', '.png', '.gif'), '.webp', $basename); + 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); + +//my_var_dump([ +// $options['webpAdd'], +// $basename, +// $filenameFinal, +// $filenameUnvalidated, +// $filenameFinalWebp, +// $filenameUnvalidatedWebp +//]); + if(@copy($this->filename(), $filenameUnvalidated)) { try { @@ -773,6 +800,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 +835,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 +1614,16 @@ class Pageimage extends Pagefile { $success = $files->unlink($filename, true); } if($success) $deletedFiles[] = $filename; + + // Also remove WebP variation, if there exist one + $webp = dirname($filename) . '/' . str_replace(array('.jpg', '.jpeg', '.png', '.gif'), '.webp', basename($filename)); + 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; From 7f132638d09ba233469f01ac184f9e8fb1eb6f99 Mon Sep 17 00:00:00 2001 From: horst-n Date: Wed, 24 Apr 2019 23:23:05 +0200 Subject: [PATCH 04/33] added properties for webp support with the property $image->hasWebp we can detect, for example in a template file, if an image variation has a dependant webp file, so we can render conditional markup with srcset or picture elements. And urlWebp and srcWebp for returning the URL. --- wire/core/Pageimage.php | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index 5d0883f6..4a0b64f2 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -175,6 +175,33 @@ class Pageimage extends Pagefile { } } + /** + * Return the web accessible URL to this image files webP dependency + * + * @return string + * + */ + public function urlWebp() { + if(!$this->hasWebp()) { + return '#'; // @Ryan: what should be returned for none existing webp variations here? + } + $path_parts = pathinfo($this->url); + $webpUrl = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.webp'; + return $webpUrl; + } + + /** + * 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 +418,13 @@ class Pageimage extends Pagefile { $value = parent::get('src'); if($value === null) $value = $this->url(); break; + case 'hasWebp': + $value = $this->hasWebp(); + break; + case 'urlWebp': + case 'srcWebp': + $value = $this->urlWebp(); + break; default: $value = parent::get($key); } From a5a101a456cab49a64fd919ba1b1ac3dc27a5bfb Mon Sep 17 00:00:00 2001 From: horst-n Date: Wed, 24 Apr 2019 23:40:16 +0200 Subject: [PATCH 05/33] documentation for webp properties --- wire/core/Pageimage.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index 4a0b64f2..be43aaf0 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -40,6 +40,9 @@ * @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 bool $hasWebp Does exist an optional WebP-dependency file for this image variation? (since 3.0.132) * * Properties inherited from Pagefile * ================================== From f3f4e427b02a8043ecca0899b9aaddc0570e00af Mon Sep 17 00:00:00 2001 From: horst-n Date: Thu, 25 Apr 2019 00:21:09 +0200 Subject: [PATCH 06/33] changed to more robust filename creation --- wire/core/ImageSizerEngineGD.php | 5 +- .../ImageSizerEngineIMagick.module | 75 +++++++++++++++++-- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/wire/core/ImageSizerEngineGD.php b/wire/core/ImageSizerEngineGD.php index 467f3d60..157d7042 100755 --- a/wire/core/ImageSizerEngineGD.php +++ b/wire/core/ImageSizerEngineGD.php @@ -412,8 +412,9 @@ class ImageSizerEngineGD extends ImageSizerEngine { */ protected function imSaveWebP($im, $filename, $quality = 90) { if(!function_exists('imagewebp')) return false; - $newBasename = str_replace(array('.jpg', '.jpeg', '.png', '.gif'), '.webp', basename($filename)); - return imagewebp($im, dirname($filename) . '/' . $newBasename, $quality); + $path_parts = pathinfo($filename); + $webpFilename = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.webp'; + return imagewebp($im, $webpFilename, $quality); } /** diff --git a/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module b/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module index ee8fc37e..53c0b06c 100755 --- a/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module +++ b/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module @@ -24,6 +24,7 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { * */ protected $im = null; + protected $imWebp = null; // @todo the following need phpdoc protected $workspaceColorspace; @@ -80,9 +81,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(); + } } /** @@ -176,7 +182,8 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { protected function processResize($srcFilename, $dstFilename, $fullWidth, $fullHeight, $finalWidth, $finalHeight) { $this->setTimeLimit(120); - +my_var_dump([$srcFilename, $dstFilename, $fullWidth, $fullHeight, $finalWidth, $finalHeight]); +die('RIP'); // start image magick $this->im = new \IMagick(); @@ -353,7 +360,15 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { $this->im->setImageDepth(($this->imageDepth > 8 ? 8 : $this->imageDepth)); - // prepare to save file + + + + + // prepare to save file(s) + if($this->webpAdd) { + $this->imWebp = clone $this->im; + } + $this->im->setImageFormat($this->imageFormat); $this->im->setImageType($this->imageType); if(in_array(strtoupper($this->imageFormat), array('JPG', 'JPEG'))) { @@ -367,6 +382,37 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { $this->im->setImageCompressionQuality($this->quality); } + // write to file(s) + $result = false; + switch($this->imageType) { + + case \IMAGETYPE_GIF: + // optionally save an additional WebP file + + // save the final GIF image file + #$result = imagegif($thumb, $dstFilename); + break; + + case \IMAGETYPE_PNG: + // 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: + // optionally save an additional WebP file + if($this->webpAdd) { + #$resultWebp = $this->imSaveWebP($thumb, $srcFilename, $this->webpQuality); + } + + // save the final JPEG image file + #$result = imagejpeg($thumb, $dstFilename, $this->quality); + break; + } // save to file $this->wire('files')->unlink($dstFilename); @clearstatcache(dirname($dstFilename)); @@ -378,7 +424,24 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { $this->release(); return false; } - + // optionally create a WebP dependency file + if($this->webpAdd) { + $path_parts = pathinfo($dstFilename); + $webpFilename = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.webp'; + // 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'); + // save to file + $this->wire('files')->unlink($webpFilename); + @clearstatcache(dirname($webpFilename)); + if(!file_put_contents($webpFilename, $this->imWebp)) { + $this->release(); + return false; + } + } + // release and return to event-object $this->release(); $this->modified = true; From b531275a6edc5949ec4367d37adc4afabe82ca96 Mon Sep 17 00:00:00 2001 From: horst-n Date: Thu, 25 Apr 2019 00:41:14 +0200 Subject: [PATCH 07/33] make more robust filename creation and remove debug code --- wire/core/Pageimage.php | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index be43aaf0..02cab7f8 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -772,17 +772,11 @@ class Pageimage extends Pagefile { // i.e. myfile.100x100.jpg or myfile.100x100nw-suffix1-suffix2.jpg $basename .= '.' . $nameWidth . 'x' . $nameHeight . $crop . $suffixStr . "." . $this->ext(); $filenameFinal = $this->pagefiles->path() . $basename; - $filenameFinalWebp = $this->pagefiles->path() . str_replace(array('.jpg', '.jpeg', '.png', '.gif'), '.webp', $basename); + $path_parts = pathinfo($filenameFinal); + $filenameFinalWebp = $this->pagefiles->path() . $path_parts['filename'] . '.webp'; $filenameUnvalidated = ''; $exists = file_exists($filenameFinal); - -//my_var_dump([ -// $defaultOptions, -// $requestOptions, -// $options, -//]); - // create a new resize if it doesn't already exist or forceNew option is set if(!$exists && !file_exists($this->filename())) { @@ -793,22 +787,14 @@ class Pageimage extends Pagefile { // filenameUnvalidated is temporary filename used for resize $tempDir = $this->pagefiles->page->filesManager()->getTempPath(); $filenameUnvalidated = $tempDir . $basename; - $filenameUnvalidatedWebp = $tempDir . str_replace(array('.jpg', '.jpeg', '.png', '.gif'), '.webp', $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); - -//my_var_dump([ -// $options['webpAdd'], -// $basename, -// $filenameFinal, -// $filenameUnvalidated, -// $filenameFinalWebp, -// $filenameUnvalidatedWebp -//]); if(@copy($this->filename(), $filenameUnvalidated)) { try { From 8a63886da13fd5be8da6e68a66a6b3155232edd6 Mon Sep 17 00:00:00 2001 From: horst-n Date: Thu, 25 Apr 2019 00:41:47 +0200 Subject: [PATCH 08/33] remove debug code --- .../ImageSizerEngineIMagick.module | 38 +------------------ 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module b/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module index 53c0b06c..df52d75a 100755 --- a/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module +++ b/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module @@ -182,8 +182,7 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { protected function processResize($srcFilename, $dstFilename, $fullWidth, $fullHeight, $finalWidth, $finalHeight) { $this->setTimeLimit(120); -my_var_dump([$srcFilename, $dstFilename, $fullWidth, $fullHeight, $finalWidth, $finalHeight]); -die('RIP'); + // start image magick $this->im = new \IMagick(); @@ -359,16 +358,12 @@ die('RIP'); } $this->im->setImageDepth(($this->imageDepth > 8 ? 8 : $this->imageDepth)); - - - // prepare to save file(s) if($this->webpAdd) { $this->imWebp = clone $this->im; } - $this->im->setImageFormat($this->imageFormat); $this->im->setImageType($this->imageType); if(in_array(strtoupper($this->imageFormat), array('JPG', 'JPEG'))) { @@ -383,37 +378,6 @@ die('RIP'); } // write to file(s) - $result = false; - switch($this->imageType) { - - case \IMAGETYPE_GIF: - // optionally save an additional WebP file - - // save the final GIF image file - #$result = imagegif($thumb, $dstFilename); - break; - - case \IMAGETYPE_PNG: - // 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: - // optionally save an additional WebP file - if($this->webpAdd) { - #$resultWebp = $this->imSaveWebP($thumb, $srcFilename, $this->webpQuality); - } - - // save the final JPEG image file - #$result = imagejpeg($thumb, $dstFilename, $this->quality); - break; - } - // save to file $this->wire('files')->unlink($dstFilename); @clearstatcache(dirname($dstFilename)); ##if(!$this->im->writeImage($this->destFilename)) { From 4e534e03153084f4c72e51a347a557592415bdc2 Mon Sep 17 00:00:00 2001 From: horst-n Date: Thu, 25 Apr 2019 08:59:58 +0200 Subject: [PATCH 09/33] add property and method for webpAdd --- wire/core/ImageSizerEngine.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/wire/core/ImageSizerEngine.php b/wire/core/ImageSizerEngine.php index 8f2edb91..cf625c2b 100755 --- a/wire/core/ImageSizerEngine.php +++ b/wire/core/ImageSizerEngine.php @@ -69,6 +69,14 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable */ protected $webpQuality = 90; + /** + * Also create a WebP Image with this variation? + * + * @var bool + * + */ + protected $webpAdd = false; + /** * Image interlace setting, false or true * @@ -915,6 +923,19 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable return $this; } + /** + * Set flag to also create a webp file or not + * + * @param bool $value + * + * @return $this + * + */ + public function setWebpAdd($value) { + $this->webpAdd = (bool) $value; + return $this; + } + /** * Given an unknown sharpening value, return the string representation of it * @@ -1178,6 +1199,9 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable case 'webpQuality': $this->setWebpQuality($value); break; + case 'webpAdd': + $this->setWebpAdd($value); + break; case 'cropping': $this->setCropping($value); break; From 35b9a403e13d027833efbc38a28f8cc775e24103 Mon Sep 17 00:00:00 2001 From: horst-n Date: Thu, 25 Apr 2019 11:01:13 +0200 Subject: [PATCH 10/33] rearranged code for webP saving --- .../ImageSizerEngineIMagick.module | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module b/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module index df52d75a..7332fd3d 100755 --- a/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module +++ b/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module @@ -359,11 +359,7 @@ 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; - } $this->im->setImageFormat($this->imageFormat); $this->im->setImageType($this->imageType); if(in_array(strtoupper($this->imageFormat), array('JPG', 'JPEG'))) { @@ -377,8 +373,8 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { $this->im->setImageCompressionQuality($this->quality); } - // write to file(s) - $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 @@ -388,28 +384,31 @@ 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) { - $path_parts = pathinfo($dstFilename); - $webpFilename = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.webp'; - // 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'); - // save to file - $this->wire('files')->unlink($webpFilename); - @clearstatcache(dirname($webpFilename)); - if(!file_put_contents($webpFilename, $this->imWebp)) { - $this->release(); - return false; + $this->imWebp = new \IMagick(); + if($this->imWebp->readImage($dstFilename)) { + // 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'); + // save to file + $return = $this->imWebp->writeImage($webpFilename); } } - + // release and return to event-object $this->release(); - $this->modified = true; - return true; + return $return; } /** From 232f66dedf4d54a5a849a31fc08dedfb9e97b27d Mon Sep 17 00:00:00 2001 From: horst-n Date: Thu, 25 Apr 2019 11:01:31 +0200 Subject: [PATCH 11/33] minor changes --- wire/core/ImageSizerEngineGD.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wire/core/ImageSizerEngineGD.php b/wire/core/ImageSizerEngineGD.php index 157d7042..74f47d29 100755 --- a/wire/core/ImageSizerEngineGD.php +++ b/wire/core/ImageSizerEngineGD.php @@ -323,6 +323,7 @@ class ImageSizerEngineGD extends ImageSizerEngine { } // write to file(s) + if(file_exists($dstFilename)) $this->wire('files')->unlink($dstFilename); $result = false; switch($this->imageType) { @@ -414,6 +415,7 @@ class ImageSizerEngineGD extends ImageSizerEngine { 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); } From 023a672e26ceabd8add0ae3f83ff7c02d743483e Mon Sep 17 00:00:00 2001 From: horst-n Date: Thu, 25 Apr 2019 11:01:57 +0200 Subject: [PATCH 12/33] Update Pageimage.php --- wire/core/Pageimage.php | 1 - 1 file changed, 1 deletion(-) diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index 02cab7f8..2535f668 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -777,7 +777,6 @@ class Pageimage extends Pagefile { $filenameUnvalidated = ''; $exists = file_exists($filenameFinal); - // 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 From 04135462c83647b406606416d727dcd8146c7f93 Mon Sep 17 00:00:00 2001 From: horst-n Date: Fri, 26 Apr 2019 18:11:04 +0200 Subject: [PATCH 13/33] refactored webp creation and compression --- .../ImageSizerEngineIMagick.module | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module b/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module index 7332fd3d..2f743e00 100755 --- a/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module +++ b/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module @@ -360,6 +360,10 @@ 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 + } + $this->im->setImageFormat($this->imageFormat); $this->im->setImageType($this->imageType); if(in_array(strtoupper($this->imageFormat), array('JPG', 'JPEG'))) { @@ -394,16 +398,13 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { // optionally create a WebP dependency file if($this->webpAdd) { - $this->imWebp = new \IMagick(); - if($this->imWebp->readImage($dstFilename)) { - // 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'); - // save to file - $return = $this->imWebp->writeImage($webpFilename); - } + // 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'); + // save to file + $return = $this->imWebp->writeImage($webpFilename); } // release and return to event-object From 9f01172a4b48cb7ad7f0189d9396f99ad0a1ff12 Mon Sep 17 00:00:00 2001 From: horst-n Date: Fri, 26 Apr 2019 18:32:23 +0200 Subject: [PATCH 14/33] added 2 aliases beginning with webp, as we also use two new options beginning with webp. Maybe this is better to avoid confusion. webpAdd, webpQuality, webpUrl, webpSrc --- wire/core/Pageimage.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index 2535f668..65a0d41e 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -42,6 +42,8 @@ * @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 @@ -426,6 +428,8 @@ class Pageimage extends Pagefile { break; case 'urlWebp': case 'srcWebp': + case 'webpUrl': + case 'webpSrc': $value = $this->urlWebp(); break; default: From a6aa39d75d5c42c637e66f4a090a6b1b34174894 Mon Sep 17 00:00:00 2001 From: horst-n Date: Fri, 26 Apr 2019 19:01:05 +0200 Subject: [PATCH 15/33] added webp params to imagesizerOptions --- wire/config.php | 2 ++ 1 file changed, 2 insertions(+) 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 ); /** From ebe7a5d80f7efc62fce6089ad0ded5a64a0f3d94 Mon Sep 17 00:00:00 2001 From: horst-n Date: Sat, 27 Apr 2019 00:30:58 +0200 Subject: [PATCH 16/33] added a verbose debug info method --- wire/core/Pageimage.php | 82 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index 65a0d41e..4da0ef6a 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -1878,6 +1878,88 @@ class Pageimage extends Pagefile { } } + + /** + * Get verbose DebugInfo, optionally with individual options array, @horst + * (without invoking the magic debug) + * + * @return mixed: array | string + * + */ + public function getDebugInfo($options = array(), $returnAsString = false) { + static $depth = 0; + $depth++; + $info = $this->original ? array('original' => $this->original->basename, 'basename' => $this->basename) : array('original' => $this->basename, 'basename' => $this->basename); + $info = array_merge($info, parent::__debugInfo()); + $fs = $info['filesize']; + unset($info['filesize']); + $is = new ImageSizer($this->filename, $options); + $ii = $is->getImageInfo(true); + $info['filesize'] = $fs; + $info['imageType'] = $ii['info']['imageType']; + $info['mime'] = $ii['info']['mime']; + unset($ii['info']['mime'], $ii['info']['imageType']); + $info['extension'] = $ii['extension']; + $info['webp'] = array(); + if($this->hasWebp()) { + $info['webp']['hasWebp'] = true; + $info['webp']['webpUrl'] = $this->webpUrl; + $info['webp']['webpFilesize'] = filesize(dirname($this->filename) . '/' . pathinfo($this->filename, \PATHINFO_FILENAME) . '.webp'); + } else { + $info['webp']['hasWebp'] = false; + } + $info['width'] = $this->width(); + $info['height'] = $this->height(); + $info['suffix'] = $this->suffixStr; + $info['focus'] = $this->hasFocus ? $this->focusStr : NULL; + if(isset($info['filedata']) && isset($info['filedata']['focus'])) unset($info['filedata']['focus']); + if(empty($info['filedata'])) unset($info['filedata']); + foreach($ii['info'] as $k => $v) $info[$k] = $v; + $info['iptcRaw'] = $ii['iptcRaw']; + if($depth < 2) { + $info['variations'] = array(); + $variations = $this->getVariations(array('info' => true, 'verbose' => false)); + foreach($variations as $name) { + $info['variations'][] = $name; + } + if(empty($info['variations'])) unset($info['variations']); + } + $depth--; + $info['neededEngineSupport'] = $is->getImageInfo(); + $info['installedEngines'] = array_merge($is->getEngines(), array('ImageSizerEngineGD')); + $info['selectedEngine'] = $is->getEngine(); + $info['individualOptions'] = $options; + if(!$returnAsString) { + return $info; + } + 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'])) { + $return = "
{$content}
"; + } else { + $return = $content; + } + return $return; + } + + + /** * Debug info * From 8da54040d41c0fb93d22e67f7b6ecd82e9612bd6 Mon Sep 17 00:00:00 2001 From: horst-n Date: Sat, 27 Apr 2019 00:36:42 +0200 Subject: [PATCH 17/33] added a verbose debug method that can be called manually on every imagevariation and with individual options array. --- wire/core/Pageimage.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index 4da0ef6a..b89b3e3c 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -1942,12 +1942,12 @@ class Pageimage extends Pagefile { $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 - ); + '#^(.*)=>\\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'])) { From ec2c2cf4e0b784a886f1deff7841aaf3809478b1 Mon Sep 17 00:00:00 2001 From: horst-n Date: Sat, 27 Apr 2019 10:27:07 +0200 Subject: [PATCH 18/33] cleaning minor things and whitespaces and added "Options Hierarchy" to the getDebugInfo method --- wire/core/Pageimage.php | 48 +++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index b89b3e3c..50b4d619 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -186,13 +186,13 @@ class Pageimage extends Pagefile { * @return string * */ - public function urlWebp() { - if(!$this->hasWebp()) { - return '#'; // @Ryan: what should be returned for none existing webp variations here? - } - $path_parts = pathinfo($this->url); - $webpUrl = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.webp'; - return $webpUrl; + public function webpUrl() { + if(!$this->hasWebp()) { + return '#'; // @Ryan: what should be returned for none existing webp variations here? + } + $path_parts = pathinfo($this->url); + $webpUrl = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.webp'; + return $webpUrl; } /** @@ -202,9 +202,9 @@ class Pageimage extends Pagefile { * */ public function hasWebp() { - $path_parts = pathinfo($this->filename()); - $webpFilename = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.webp'; - return is_readable($webpFilename); + $path_parts = pathinfo($this->filename()); + $webpFilename = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.webp'; + return is_readable($webpFilename); } /** @@ -426,11 +426,11 @@ class Pageimage extends Pagefile { case 'hasWebp': $value = $this->hasWebp(); break; - case 'urlWebp': - case 'srcWebp': case 'webpUrl': case 'webpSrc': - $value = $this->urlWebp(); + case 'urlWebp': + case 'srcWebp': + $value = $this->webpUrl(); break; default: $value = parent::get($key); @@ -790,8 +790,8 @@ class Pageimage extends Pagefile { // filenameUnvalidated is temporary filename used for resize $tempDir = $this->pagefiles->page->filesManager()->getTempPath(); $filenameUnvalidated = $tempDir . $basename; - $path_parts = pathinfo($filenameUnvalidated); - $filenameUnvalidatedWebp = $tempDir . $path_parts['filename'] . '.webp'; + $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); @@ -1642,8 +1642,9 @@ class Pageimage extends Pagefile { if($success) $deletedFiles[] = $filename; // Also remove WebP variation, if there exist one - $webp = dirname($filename) . '/' . str_replace(array('.jpg', '.jpeg', '.png', '.gif'), '.webp', basename($filename)); - if(!is_file($webp)) continue; + $path_parts = pathinfo($filenameFinal); + $webp = dirname($filename) . '/' . $path_parts['filename'] . '.webp'; + if(!is_file($webp)) continue; if($options['dryRun']) { $success = true; } else { @@ -1928,10 +1929,16 @@ class Pageimage extends Pagefile { $info['neededEngineSupport'] = $is->getImageInfo(); $info['installedEngines'] = array_merge($is->getEngines(), array('ImageSizerEngineGD')); $info['selectedEngine'] = $is->getEngine(); - $info['individualOptions'] = $options; + $info['Options Hierarchy'] = array(); + $info['Options Hierarchy']['imageSizerOptions'] = $this->wire('config')->imageSizerOptions; + $info['Options Hierarchy']['individualOptions'] = $options; + $info['Options Hierarchy']['finalOptions'] = $is->getOptions(); + if(!$returnAsString) { return $info; } + + // make a beautyfied var_dump ob_start(); var_dump($info); $content = ob_get_contents(); @@ -1951,10 +1958,13 @@ class Pageimage extends Pagefile { $content = preg_replace('#^((\s*).*){$#m', "\\1\n\\2{", $content); $content = str_replace(array('
', '
'), '', $content); if(isset($_SERVER['HTTP_HOST'])) { - $return = "
{$content}
"; + // build output for HTML + $return = "
{$content}
"; } else { + // output for Console $return = $content; } + return $return; } From a4431829d9756c1eec5649c24ff689b3db27d3e2 Mon Sep 17 00:00:00 2001 From: horst-n Date: Sat, 27 Apr 2019 10:37:10 +0200 Subject: [PATCH 19/33] added method and property webpFilename --- wire/core/Pageimage.php | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index 50b4d619..2a5b3a41 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -188,13 +188,28 @@ class Pageimage extends Pagefile { */ public function webpUrl() { if(!$this->hasWebp()) { - return '#'; // @Ryan: what should be returned for none existing webp variations here? + return '#'; // @Ryan: what should be returned for none existing webp variations here? } $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 * @@ -432,6 +447,9 @@ class Pageimage extends Pagefile { case 'srcWebp': $value = $this->webpUrl(); break; + case 'webpFilename': + $value = $this->webpFilename(); + break; default: $value = parent::get($key); } @@ -1905,7 +1923,7 @@ class Pageimage extends Pagefile { if($this->hasWebp()) { $info['webp']['hasWebp'] = true; $info['webp']['webpUrl'] = $this->webpUrl; - $info['webp']['webpFilesize'] = filesize(dirname($this->filename) . '/' . pathinfo($this->filename, \PATHINFO_FILENAME) . '.webp'); + $info['webp']['webpFilesize'] = filesize($this->webpFilename()); } else { $info['webp']['hasWebp'] = false; } From 1b96368aa2bb13a8fc45d048ffdbfe3c7b61c7ab Mon Sep 17 00:00:00 2001 From: horst-n Date: Sat, 27 Apr 2019 10:46:39 +0200 Subject: [PATCH 20/33] cleaning and beautyfying --- wire/core/ImageSizerEngine.php | 1 + wire/core/ImageSizerEngineGD.php | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/wire/core/ImageSizerEngine.php b/wire/core/ImageSizerEngine.php index cf625c2b..80d1c253 100755 --- a/wire/core/ImageSizerEngine.php +++ b/wire/core/ImageSizerEngine.php @@ -1262,6 +1262,7 @@ 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, diff --git a/wire/core/ImageSizerEngineGD.php b/wire/core/ImageSizerEngineGD.php index 74f47d29..f58bb97c 100755 --- a/wire/core/ImageSizerEngineGD.php +++ b/wire/core/ImageSizerEngineGD.php @@ -413,8 +413,8 @@ class ImageSizerEngineGD extends ImageSizerEngine { */ 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'; + $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); } From 32afdbdf52c550ee1de1910cabd43a137eaa0cdc Mon Sep 17 00:00:00 2001 From: horst-n Date: Sat, 27 Apr 2019 11:55:50 +0200 Subject: [PATCH 21/33] cleaning up a bit --- wire/core/ImageSizerEngine.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wire/core/ImageSizerEngine.php b/wire/core/ImageSizerEngine.php index 80d1c253..04f0b79a 100755 --- a/wire/core/ImageSizerEngine.php +++ b/wire/core/ImageSizerEngine.php @@ -10,8 +10,9 @@ * @property bool $upscaling * @property bool $interlace * @property array|string|bool $cropping - * @property int $quality + * @property bool $webpAdd * @property int $webpQuality + * @property int $quality * @property string $sharpening * @property float $defaultGamma * @property float $scale @@ -237,6 +238,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable 'interlace', 'quality', 'webpQuality', + 'webpAdd', 'sharpening', 'defaultGamma', 'scale', From ba6803f4257889ff538698cefb8712d667f66a3a Mon Sep 17 00:00:00 2001 From: horst-n Date: Sat, 27 Apr 2019 13:21:07 +0200 Subject: [PATCH 22/33] refactored getDebugInfo() method --- wire/core/Pageimage.php | 87 ++++++++++++++++++++++++++--------------- 1 file changed, 55 insertions(+), 32 deletions(-) diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index 2a5b3a41..a5558c1e 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -1905,55 +1905,78 @@ class Pageimage extends Pagefile { * @return mixed: array | string * */ - public function getDebugInfo($options = array(), $returnAsString = false) { + public function getDebugInfo($options = array(), $returnAsString = true) { static $depth = 0; $depth++; + + $thumb = ""; $info = $this->original ? array('original' => $this->original->basename, 'basename' => $this->basename) : array('original' => $this->basename, 'basename' => $this->basename); - $info = array_merge($info, parent::__debugInfo()); - $fs = $info['filesize']; + $info = array_merge(array('thumb' => $thumb), $info, parent::__debugInfo()); unset($info['filesize']); - $is = new ImageSizer($this->filename, $options); - $ii = $is->getImageInfo(true); - $info['filesize'] = $fs; - $info['imageType'] = $ii['info']['imageType']; - $info['mime'] = $ii['info']['mime']; - unset($ii['info']['mime'], $ii['info']['imageType']); - $info['extension'] = $ii['extension']; - $info['webp'] = array(); - if($this->hasWebp()) { - $info['webp']['hasWebp'] = true; - $info['webp']['webpUrl'] = $this->webpUrl; - $info['webp']['webpFilesize'] = filesize($this->webpFilename()); - } else { - $info['webp']['hasWebp'] = false; - } + if(!isset($_SERVER['HTTP_HOST'])) unset($info['thumb']); + + $oSizer = new ImageSizer($this->filename, $options); + $osInfo = $oSizer->getImageInfo(true); + $finalOptions = $oSizer->getOptions(); + + $info['suffix'] = $finalOptions['suffix']; + $info['extension'] = $osInfo['extension']; + $info['imageType'] = $osInfo['info']['imageType']; + $info['mime'] = $osInfo['info']['mime']; + unset($osInfo['info']['mime'], $osInfo['info']['imageType']); $info['width'] = $this->width(); $info['height'] = $this->height(); - $info['suffix'] = $this->suffixStr; + $info['filesize'] = filesize($this->filename); $info['focus'] = $this->hasFocus ? $this->focusStr : NULL; if(isset($info['filedata']) && isset($info['filedata']['focus'])) unset($info['filedata']['focus']); if(empty($info['filedata'])) unset($info['filedata']); - foreach($ii['info'] as $k => $v) $info[$k] = $v; - $info['iptcRaw'] = $ii['iptcRaw']; + foreach($osInfo['info'] as $k => $v) $info[$k] = $v; + $info['iptcRaw'] = $osInfo['iptcRaw']; + + // WEBP + $info['WEBP COPY'] = array(); + if($this->hasWebp()) { + $info['WEBP COPY']['hasWebp'] = true; + $info['WEBP COPY']['webpUrl'] = $this->webpUrl; + if(isset($finalOptions['webpQuality'])) $info['WEBP COPY']['webpQuality'] = $finalOptions['webpQuality']; + $info['WEBP COPY']['filesize'] = filesize($this->webpFilename()); + $info['WEBP COPY']['savings in percent'] = 100 - intval($info['WEBP COPY']['filesize'] / ($info['filesize'] / 100)); + } else { + $info['WEBP COPY']['hasWebp'] = false; + } + // VARIATIONS if($depth < 2) { - $info['variations'] = array(); + $info['VARIATIONS'] = array(); $variations = $this->getVariations(array('info' => true, 'verbose' => false)); foreach($variations as $name) { - $info['variations'][] = $name; + $info['VARIATIONS'][] = $name; } - if(empty($info['variations'])) unset($info['variations']); + #if(empty($info['VARIATIONS'])) unset($info['variations']); } $depth--; - $info['neededEngineSupport'] = $is->getImageInfo(); - $info['installedEngines'] = array_merge($is->getEngines(), array('ImageSizerEngineGD')); - $info['selectedEngine'] = $is->getEngine(); - $info['Options Hierarchy'] = array(); - $info['Options Hierarchy']['imageSizerOptions'] = $this->wire('config')->imageSizerOptions; - $info['Options Hierarchy']['individualOptions'] = $options; - $info['Options Hierarchy']['finalOptions'] = $is->getOptions(); + // ENGINES + $info['ENGINE(S)'] = array(); + $info['ENGINE(S)']['neededEngineSupport'] = strtoupper($oSizer->getImageInfo()); + $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); + $info['ENGINE(S)']['installedEngines'] = $a; + unset($a, $moduleName, $configData, $engines, $priority, $engines, $modules); + $info['ENGINE(S)']['selectedEngine'] = $oSizer->getEngine()->className; + // OPTIONS + $info['OPTIONS HIERARCHY'] = array(); + $info['OPTIONS HIERARCHY']['imageSizerOptions'] = $this->wire('config')->imageSizerOptions; + $info['OPTIONS HIERARCHY']['individualOptions'] = $options; + $info['OPTIONS HIERARCHY']['finalOptions'] = $finalOptions; if(!$returnAsString) { - return $info; + return $info; // return as array } // make a beautyfied var_dump From e6f4fbcb408352a18202559292e4c4a18475c99f Mon Sep 17 00:00:00 2001 From: horst-n Date: Sat, 27 Apr 2019 23:12:37 +0200 Subject: [PATCH 23/33] minor changes and cleaning phpdoc --- wire/core/ImageSizerEngine.php | 44 +++++++++++++++++----------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/wire/core/ImageSizerEngine.php b/wire/core/ImageSizerEngine.php index 04f0b79a..d2a670dc 100755 --- a/wire/core/ImageSizerEngine.php +++ b/wire/core/ImageSizerEngine.php @@ -839,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) { @@ -854,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 * */ @@ -898,14 +898,14 @@ 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; } @@ -914,14 +914,14 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param int $n * - * @return $this + * @return self * */ public function setWebpQuality($n) { $n = (int) $n; if($n < 1) $n = 1; - if($n > 100) $n = 100; - $this->webpQuality = (int) $n; + else if($n > 100) $n = 100; + $this->webpQuality = $n; return $this; } @@ -930,7 +930,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param bool $value * - * @return $this + * @return self * */ public function setWebpAdd($value) { @@ -977,7 +977,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param mixed $value * - * @return $this + * @return self * @throws WireException * */ @@ -1006,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) { @@ -1019,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) { @@ -1032,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) { @@ -1045,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 * */ @@ -1065,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) { @@ -1091,7 +1091,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param float $scale * - * @return $this + * @return self * */ public function setScale($scale) { @@ -1106,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) { @@ -1120,7 +1120,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param $degrees * - * @return $this + * @return self * */ public function setRotate($degrees) { @@ -1138,7 +1138,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param $flip * - * @return $this + * @return self * */ public function setFlip($flip) { @@ -1152,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) { @@ -1175,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) { @@ -1445,7 +1445,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * @param bool $modified * - * @return $this + * @return self * */ public function setModified($modified) { From d062bc4345cc87791a3b9fd43137b0b1de78790d Mon Sep 17 00:00:00 2001 From: horst-n Date: Sat, 27 Apr 2019 23:13:26 +0200 Subject: [PATCH 24/33] added to phpdocs --- .../ImageSizerEngineIMagick.module | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module b/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module index 2f743e00..dcc4a5b5 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,19 @@ 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 From 53c414aa60b9e23d79eb90a0010d8ce832a1a6e8 Mon Sep 17 00:00:00 2001 From: horst-n Date: Sat, 27 Apr 2019 23:15:44 +0200 Subject: [PATCH 25/33] reworked the getDebugInfo method, trying to make the code more readable. But the main goal for me is, that the output is best readable! :) --- wire/core/Pageimage.php | 112 ++++++++++++++++++++++++---------------- 1 file changed, 67 insertions(+), 45 deletions(-) diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index a5558c1e..4e7fa8cf 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -637,7 +637,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()) { @@ -1902,61 +1902,67 @@ class Pageimage extends Pagefile { * Get verbose DebugInfo, optionally with individual options array, @horst * (without invoking the magic debug) * - * @return mixed: array | string + * @param array $options The individual options you also passes with your image variation creation + * @param bool $returnAsString default is true and returns markup or plain text, false returns multidim array + * @return array|string * */ public function getDebugInfo($options = array(), $returnAsString = true) { static $depth = 0; $depth++; - - $thumb = ""; - $info = $this->original ? array('original' => $this->original->basename, 'basename' => $this->basename) : array('original' => $this->basename, 'basename' => $this->basename); - $info = array_merge(array('thumb' => $thumb), $info, parent::__debugInfo()); - unset($info['filesize']); - if(!isset($_SERVER['HTTP_HOST'])) unset($info['thumb']); - + + // fetch imagesizer, some infos and some options $oSizer = new ImageSizer($this->filename, $options); $osInfo = $oSizer->getImageInfo(true); $finalOptions = $oSizer->getOptions(); - $info['suffix'] = $finalOptions['suffix']; - $info['extension'] = $osInfo['extension']; - $info['imageType'] = $osInfo['info']['imageType']; - $info['mime'] = $osInfo['info']['mime']; - unset($osInfo['info']['mime'], $osInfo['info']['imageType']); - $info['width'] = $this->width(); - $info['height'] = $this->height(); - $info['filesize'] = filesize($this->filename); - $info['focus'] = $this->hasFocus ? $this->focusStr : NULL; + // 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 = parent::__debugInfo(); + unset($parent['filesize']); + + // start collecting the $info + $info = array_merge($thumb, $original, $parent, array( + 'suffix' => $finalOptions['suffix'], + 'extension' => $osInfo['extension'], + 'imageType' => $osInfo['info']['imageType'], + 'mime' => $osInfo['info']['mime'], + 'width' => $this->width(), + 'height' => $this->height(), + 'filesize' => filesize($this->filename), + 'focus' => $this->hasFocus ? $this->focusStr : NULL, + )); + + // beautify the output, remove unnecessary items if(isset($info['filedata']) && isset($info['filedata']['focus'])) unset($info['filedata']['focus']); if(empty($info['filedata'])) unset($info['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[$k] = $v; $info['iptcRaw'] = $osInfo['iptcRaw']; + unset($osInfo, $thumb, $original, $parent); // WEBP - $info['WEBP COPY'] = array(); - if($this->hasWebp()) { - $info['WEBP COPY']['hasWebp'] = true; - $info['WEBP COPY']['webpUrl'] = $this->webpUrl; - if(isset($finalOptions['webpQuality'])) $info['WEBP COPY']['webpQuality'] = $finalOptions['webpQuality']; - $info['WEBP COPY']['filesize'] = filesize($this->webpFilename()); - $info['WEBP COPY']['savings in percent'] = 100 - intval($info['WEBP COPY']['filesize'] / ($info['filesize'] / 100)); - } else { - $info['WEBP COPY']['hasWebp'] = false; - } + $webp = array('WEBP COPY' => (!$this->hasWebp() ? array('hasWebp' => false) : array( + 'hasWebp' => true, + 'webpUrl' => $this->webpUrl, + 'webpQuality' => isset($finalOptions['webpQuality']) ? $finalOptions['webpQuality'] : NULL, + 'filesize' => filesize($this->webpFilename()), + 'savings in percent' => 100 - intval(filesize($this->webpFilename()) / ($info['filesize'] / 100)) + ))); + // VARIATIONS if($depth < 2) { - $info['VARIATIONS'] = array(); + $variationArray = array(); $variations = $this->getVariations(array('info' => true, 'verbose' => false)); - foreach($variations as $name) { - $info['VARIATIONS'][] = $name; - } - #if(empty($info['VARIATIONS'])) unset($info['variations']); + foreach($variations as $name) $variationArray[] = $name; } $depth--; + unset($variations, $name); + // ENGINES - $info['ENGINE(S)'] = array(); - $info['ENGINE(S)']['neededEngineSupport'] = strtoupper($oSizer->getImageInfo()); $a = []; $modules = $this->wire('modules'); $engines = array_merge($oSizer->getEngines(), array('ImageSizerEngineGD')); @@ -1966,17 +1972,33 @@ class Pageimage extends Pagefile { $a[$moduleName] = "priority {$priority}"; } asort($a, SORT_STRING); - $info['ENGINE(S)']['installedEngines'] = $a; - unset($a, $moduleName, $configData, $engines, $priority, $engines, $modules); - $info['ENGINE(S)']['selectedEngine'] = $oSizer->getEngine()->className; - // OPTIONS - $info['OPTIONS HIERARCHY'] = array(); - $info['OPTIONS HIERARCHY']['imageSizerOptions'] = $this->wire('config')->imageSizerOptions; - $info['OPTIONS HIERARCHY']['individualOptions'] = $options; - $info['OPTIONS HIERARCHY']['finalOptions'] = $finalOptions; + $enginesArray = array( + 'neededEngineSupport' => strtoupper($oSizer->getImageInfo()), + 'installedEngines' => $a, + 'selectedEngine' => $oSizer->getEngine()->className + ); + unset($a, $moduleName, $configData, $engines, $priority, $modules, $oSizer); + // merge all into $info + $info = array_merge($info, $webp, + array('VARIATIONS' => $variationArray), + array('ENGINE(S)' => $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']); + + // return as array if(!$returnAsString) { - return $info; // return as array + return $info; } // make a beautyfied var_dump @@ -2000,7 +2022,7 @@ class Pageimage extends Pagefile { $content = str_replace(array('
', '
'), '', $content); if(isset($_SERVER['HTTP_HOST'])) { // build output for HTML - $return = "
{$content}
"; + $return = "
{$content}
"; } else { // output for Console $return = $content; From 540ae4f2d9634afe330d4506a4064835ad2b395a Mon Sep 17 00:00:00 2001 From: horst-n Date: Mon, 29 Apr 2019 16:51:57 +0200 Subject: [PATCH 26/33] rearanged and grouped the output of getDebugInfo --- wire/core/Pageimage.php | 83 +++++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index 4e7fa8cf..f92377c0 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -1917,42 +1917,20 @@ class Pageimage extends Pagefile { $finalOptions = $oSizer->getOptions(); // build some info parts and fetch some from parent (pagefile) - $thumb = array('thumb' => ""); + $thumb = array('THUMB' => ""); $original = $this->original ? array('original' => $this->original->basename, 'basename' => $this->basename) : array('original' => '{SELF}', 'basename' => $this->basename); - $parent = parent::__debugInfo(); - unset($parent['filesize']); - - // start collecting the $info - $info = array_merge($thumb, $original, $parent, array( - 'suffix' => $finalOptions['suffix'], - 'extension' => $osInfo['extension'], - 'imageType' => $osInfo['info']['imageType'], - 'mime' => $osInfo['info']['mime'], - 'width' => $this->width(), - 'height' => $this->height(), - 'filesize' => filesize($this->filename), - 'focus' => $this->hasFocus ? $this->focusStr : NULL, + $parent = array('FILES' => array_merge( + $original, + parent::__debugInfo(), + array( + 'suffix' => $finalOptions['suffix'], + 'extension' => $osInfo['extension'] + ) )); - - // beautify the output, remove unnecessary items - if(isset($info['filedata']) && isset($info['filedata']['focus'])) unset($info['filedata']['focus']); - if(empty($info['filedata'])) unset($info['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[$k] = $v; - $info['iptcRaw'] = $osInfo['iptcRaw']; - unset($osInfo, $thumb, $original, $parent); - - // WEBP - $webp = array('WEBP COPY' => (!$this->hasWebp() ? array('hasWebp' => false) : array( - 'hasWebp' => true, - 'webpUrl' => $this->webpUrl, - 'webpQuality' => isset($finalOptions['webpQuality']) ? $finalOptions['webpQuality'] : NULL, - 'filesize' => filesize($this->webpFilename()), - 'savings in percent' => 100 - intval(filesize($this->webpFilename()) / ($info['filesize'] / 100)) - ))); - + // rearange parts + unset($parent['FILES']['filesize']); + $parent['FILES']['filesize'] = filesize($this->filename); + // VARIATIONS if($depth < 2) { $variationArray = array(); @@ -1962,6 +1940,40 @@ class Pageimage extends Pagefile { $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' => (!$this->hasWebp() ? array('hasWebp' => false) : array( + 'hasWebp' => true, + 'webpUrl' => $this->webpUrl, + 'webpQuality' => isset($finalOptions['webpQuality']) ? $finalOptions['webpQuality'] : NULL, + 'filesize' => filesize($this->webpFilename()), + 'savings in percent' => 100 - intval(filesize($this->webpFilename()) / ($info['FILES']['filesize'] / 100)) + ))); + // ENGINES $a = []; $modules = $this->wire('modules'); @@ -1981,7 +1993,6 @@ class Pageimage extends Pagefile { // merge all into $info $info = array_merge($info, $webp, - array('VARIATIONS' => $variationArray), array('ENGINE(S)' => $enginesArray), // OPTIONS array('OPTIONS HIERARCHY' => array( @@ -1994,7 +2005,7 @@ class Pageimage extends Pagefile { unset($variationArray, $webp, $enginesArray, $options, $finalOptions); // If not in browser environment, remove the thumb image - if(!isset($_SERVER['HTTP_HOST'])) unset($info['thumb']); + if(!isset($_SERVER['HTTP_HOST'])) unset($info['THUMB']); // return as array if(!$returnAsString) { From e8e2594d3e2ecdb578fefa49c6c29a5d40bf2ee3 Mon Sep 17 00:00:00 2001 From: horst-n Date: Mon, 29 Apr 2019 17:30:06 +0200 Subject: [PATCH 27/33] force new creation for condition where the regular variation exists and should not be recreated, but where a webp copy, that do not exist, is requested too. --- wire/core/Pageimage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index f92377c0..f3da00e3 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -804,7 +804,7 @@ class Pageimage extends Pagefile { // no original file exists to create variation from $this->error = "Original image does not exist to create size variation"; - } else if(!$exists || $options['forceNew']) { + } else if(!$exists || $options['forceNew'] || ($options['webpAdd'] && !$this->hasWebp())) { // filenameUnvalidated is temporary filename used for resize $tempDir = $this->pagefiles->page->filesManager()->getTempPath(); $filenameUnvalidated = $tempDir . $basename; From b46bdfea94ccb27d99289dc9e371848008f4195c Mon Sep 17 00:00:00 2001 From: horst-n Date: Mon, 29 Apr 2019 19:29:53 +0200 Subject: [PATCH 28/33] updated font-family with more monospace fonts --- wire/core/Pageimage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index f3da00e3..fcf16c48 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -2033,7 +2033,7 @@ class Pageimage extends Pagefile { $content = str_replace(array('
', '
'), '', $content); if(isset($_SERVER['HTTP_HOST'])) { // build output for HTML - $return = "
{$content}
"; + $return = "
{$content}
"; } else { // output for Console $return = $content; From fa917b30e0667ae03c3d765df5b59d91a503392c Mon Sep 17 00:00:00 2001 From: horst-n Date: Thu, 2 May 2019 10:37:08 +0200 Subject: [PATCH 29/33] fixed a bug with internal checking if a webp copy exists or not. Using $this->hasWebp() within the resize method doesn't work. Changed to test for file_exists of the temporary webp copy. --- wire/core/Pageimage.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index fcf16c48..3338dace 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -794,17 +794,23 @@ class Pageimage extends Pagefile { // i.e. myfile.100x100.jpg or myfile.100x100nw-suffix1-suffix2.jpg $basename .= '.' . $nameWidth . 'x' . $nameHeight . $crop . $suffixStr . "." . $this->ext(); $filenameFinal = $this->pagefiles->path() . $basename; - $path_parts = pathinfo($filenameFinal); - $filenameFinalWebp = $this->pagefiles->path() . $path_parts['filename'] . '.webp'; $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'] || ($options['webpAdd'] && !$this->hasWebp())) { + } else if(!$exists || $options['forceNew']) { + // filenameUnvalidated is temporary filename used for resize $tempDir = $this->pagefiles->page->filesManager()->getTempPath(); $filenameUnvalidated = $tempDir . $basename; @@ -1660,7 +1666,7 @@ class Pageimage extends Pagefile { if($success) $deletedFiles[] = $filename; // Also remove WebP variation, if there exist one - $path_parts = pathinfo($filenameFinal); + $path_parts = pathinfo($filename); $webp = dirname($filename) . '/' . $path_parts['filename'] . '.webp'; if(!is_file($webp)) continue; if($options['dryRun']) { From d361c6c11e1db62bbd2bfb5d03ae7c9d0b5d1503 Mon Sep 17 00:00:00 2001 From: horst-n Date: Fri, 3 May 2019 13:54:04 +0200 Subject: [PATCH 30/33] added a webp support check for engines see: https://processwire.com/talk/topic/14236-webp-support/page/3/?tab=comments#comment-185020 --- wire/core/ImageSizerEngineGD.php | 13 +++++++++++- wire/core/Pageimage.php | 13 +++++++++--- .../ImageSizerEngineIMagick.module | 21 +++++++++++++++++-- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/wire/core/ImageSizerEngineGD.php b/wire/core/ImageSizerEngineGD.php index f58bb97c..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; diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index 3338dace..81a954e6 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -833,7 +833,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(); @@ -1929,7 +1935,7 @@ class Pageimage extends Pagefile { $original, parent::__debugInfo(), array( - 'suffix' => $finalOptions['suffix'], + 'suffix' => isset($finalOptions['suffix']) ? $finalOptions['suffix'] : '', 'extension' => $osInfo['extension'] ) )); @@ -1993,7 +1999,8 @@ class Pageimage extends Pagefile { $enginesArray = array( 'neededEngineSupport' => strtoupper($oSizer->getImageInfo()), 'installedEngines' => $a, - 'selectedEngine' => $oSizer->getEngine()->className + 'selectedEngine' => $oSizer->getEngine()->className, + 'engineWebpSupport' => $oSizer->getEngine()->supported('webp') ); unset($a, $moduleName, $configData, $engines, $priority, $modules, $oSizer); diff --git a/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module b/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module index dcc4a5b5..9cecb6a0 100755 --- a/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module +++ b/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module @@ -161,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; @@ -411,7 +426,9 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { $this->imWebp->setImageFormat('webp'); $this->imWebp->setImageCompressionQuality($this->webpQuality); $this->imWebp->setOption('webp:method', '6'); - #$this->imWebp->setOption('webp:lossless', 'true'); + //$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); } From 88d4b11db40fd7ffce4ab0d1e62096e64d095c73 Mon Sep 17 00:00:00 2001 From: horst-n Date: Sat, 4 May 2019 11:21:58 +0200 Subject: [PATCH 31/33] added object as output format to the getDebugInfo I figured out that this is useful to have for (automated) testing and debugging too. It is much easier to get a value for conditional coding via an object: ``` $dbgInfo = $image->getDebugInfo($options, 'object'); if($dbgInfo->engines->selectedEngine == .... ``` --- wire/core/Pageimage.php | 110 +++++++++++++++++++++++++++------------- 1 file changed, 75 insertions(+), 35 deletions(-) diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index 81a954e6..a0601854 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -1915,11 +1915,11 @@ class Pageimage extends Pagefile { * (without invoking the magic debug) * * @param array $options The individual options you also passes with your image variation creation - * @param bool $returnAsString default is true and returns markup or plain text, false returns multidim array - * @return array|string + * @param bool $returnType 'string'|'array'|'object', default is 'string' and returns markup or plain text + * @return array|object|string * */ - public function getDebugInfo($options = array(), $returnAsString = true) { + public function getDebugInfo($options = array(), $returnType = 'string') { static $depth = 0; $depth++; @@ -1927,11 +1927,11 @@ class Pageimage extends Pagefile { $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' => ""); + $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( + $parent = array('files' => array_merge( $original, parent::__debugInfo(), array( @@ -1940,8 +1940,8 @@ class Pageimage extends Pagefile { ) )); // rearange parts - unset($parent['FILES']['filesize']); - $parent['FILES']['filesize'] = filesize($this->filename); + unset($parent['files']['filesize']); + $parent['files']['filesize'] = filesize($this->filename); // VARIATIONS if($depth < 2) { @@ -1951,41 +1951,42 @@ class Pageimage extends Pagefile { } $depth--; unset($variations, $name); - + // start collecting the $info $info = array_merge($thumb, $parent, - array('VARIATIONS' => $variationArray), - array('IMAGEINFO' => array( + 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'], + 'description' => $parent['files']['description'], + 'tags' => $parent['files']['tags'], )) ); - unset($info['FILES']['tags'], $info['FILES']['description']); - + 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']); + 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']; + 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' => (!$this->hasWebp() ? array('hasWebp' => false) : array( - 'hasWebp' => true, - 'webpUrl' => $this->webpUrl, - 'webpQuality' => isset($finalOptions['webpQuality']) ? $finalOptions['webpQuality'] : NULL, - 'filesize' => filesize($this->webpFilename()), - 'savings in percent' => 100 - intval(filesize($this->webpFilename()) / ($info['FILES']['filesize'] / 100)) - ))); - + $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'); @@ -2003,12 +2004,12 @@ class Pageimage extends Pagefile { 'engineWebpSupport' => $oSizer->getEngine()->supported('webp') ); unset($a, $moduleName, $configData, $engines, $priority, $modules, $oSizer); - + // merge all into $info $info = array_merge($info, $webp, - array('ENGINE(S)' => $enginesArray), + array('engines' => $enginesArray), // OPTIONS - array('OPTIONS HIERARCHY' => array( + array('options_hierarchy' => array( 'imageSizerOptions' => $this->wire('config')->imageSizerOptions, 'individualOptions' => $options, 'finalOptions' => $finalOptions @@ -2018,14 +2019,32 @@ class Pageimage extends Pagefile { unset($variationArray, $webp, $enginesArray, $options, $finalOptions); // If not in browser environment, remove the thumb image - if(!isset($_SERVER['HTTP_HOST'])) unset($info['THUMB']); + if(!isset($_SERVER['HTTP_HOST'])) unset($info['thumb']); - // return as array - if(!$returnAsString) { + 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(); @@ -2054,6 +2073,27 @@ class Pageimage extends Pagefile { 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; + } From 6e580f71e174a81c24ab4b72322105468b1200c9 Mon Sep 17 00:00:00 2001 From: horst-n Date: Sat, 4 May 2019 18:42:11 +0200 Subject: [PATCH 32/33] changed the return of webpUrl() to return the calculated URL, even if there is no webp copy available! --- wire/core/Pageimage.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index a0601854..82baba46 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -181,15 +181,12 @@ class Pageimage extends Pagefile { } /** - * Return the web accessible URL to this image files webP dependency + * Return the web accessible URL to this image files webP dependency, also if no webp copy exists! * * @return string * */ public function webpUrl() { - if(!$this->hasWebp()) { - return '#'; // @Ryan: what should be returned for none existing webp variations here? - } $path_parts = pathinfo($this->url); $webpUrl = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.webp'; return $webpUrl; From 9964473776062ab13460e644ff24e5afb7700d8d Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Thu, 23 May 2019 15:49:06 -0400 Subject: [PATCH 33/33] Updates to @horst-n PR #141 for webp image support --- wire/core/ImageSizerEngine.php | 45 +- wire/core/ImageSizerEngineGD.php | 106 ++-- wire/core/Pagefile.php | 52 +- wire/core/PagefileExtra.php | 228 +++++++++ wire/core/Pageimage.php | 458 ++++++------------ wire/core/PageimageDebugInfo.php | 306 ++++++++++++ .../ImageSizerEngineIMagick.module | 33 +- 7 files changed, 844 insertions(+), 384 deletions(-) create mode 100644 wire/core/PagefileExtra.php create mode 100644 wire/core/PageimageDebugInfo.php diff --git a/wire/core/ImageSizerEngine.php b/wire/core/ImageSizerEngine.php index d2a670dc..de09c4dc 100755 --- a/wire/core/ImageSizerEngine.php +++ b/wire/core/ImageSizerEngine.php @@ -3,15 +3,13 @@ /** * ImageSizer Engine Module (Abstract) * - * Copyright (C) 2016 by Horst Nogajski and Ryan Cramer + * 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 bool $webpAdd - * @property int $webpQuality * @property int $quality * @property string $sharpening * @property float $defaultGamma @@ -20,6 +18,9 @@ * @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 * */ abstract class ImageSizerEngine extends WireData implements Module, ConfigurableModule { @@ -78,6 +79,14 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable */ protected $webpAdd = false; + /** + * webp result (null=not known or not applicable) + * + * @var bool|null + * + */ + protected $webpResult = null; + /** * Image interlace setting, false or true * @@ -902,10 +911,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * */ public function setQuality($n) { - $n = (int) $n; - if($n < 1) $n = 1; - else if($n > 100) $n = 100; - $this->quality = $n; + $this->quality = $this->getIntegerValue($n, 1, 100); return $this; } @@ -918,10 +924,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * */ public function setWebpQuality($n) { - $n = (int) $n; - if($n < 1) $n = 1; - else if($n > 100) $n = 100; - $this->webpQuality = $n; + $this->webpQuality = $this->getIntegerValue($n, 1, 100); return $this; } @@ -1252,6 +1255,25 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable 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 @@ -1305,6 +1327,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable 'options' ); + if($key === 'webpResult') return $this->webpResult; 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]; diff --git a/wire/core/ImageSizerEngineGD.php b/wire/core/ImageSizerEngineGD.php index 97bad910..a2c97425 100755 --- a/wire/core/ImageSizerEngineGD.php +++ b/wire/core/ImageSizerEngineGD.php @@ -8,10 +8,12 @@ * * Other user contributions as noted. * - * Copyright (C) 2016 by Horst Nogajski and Ryan Cramer + * 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/ * * https://processwire.com + * + * @method bool imSaveReady($im, $filename) * */ class ImageSizerEngineGD extends ImageSizerEngine { @@ -34,6 +36,22 @@ class ImageSizerEngineGD extends ImageSizerEngine { */ protected $gammaLinearized; + /** + * webp-only toggle for future use + * + * @var bool + * + */ + protected $webpOnly = false; + + /** + * Webp support available? + * + * @var bool|null + * + */ + static protected $webpSupport = null; + /** * Get formats GD and resize * @@ -72,12 +90,12 @@ class ImageSizerEngineGD extends ImageSizerEngine { break; case 'webp': - if(!isset($this->wire('config')->webpSupportGD)) { + if(self::$webpSupport === null) { // only call it once $gd = gd_info(); - $this->wire('config')->webpSupportGD = isset($gd['WebP Support']) ? $gd['WebP Support'] : false; + self::$webpSupport = isset($gd['WebP Support']) ? $gd['WebP Support'] : false; } - return $this->wire('config')->webpSupportGD; + return self::$webpSupport; break; case 'install': @@ -335,61 +353,29 @@ class ImageSizerEngineGD extends ImageSizerEngine { // write to file(s) if(file_exists($dstFilename)) $this->wire('files')->unlink($dstFilename); - $result = false; + + $result = null; // null=not yet known + switch($this->imageType) { case \IMAGETYPE_GIF: // correct gamma from linearized 1.0 back to 2.0 $this->gammaCorrection($thumb, false); - - // 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); - } + // save the final GIF image file + if($this->imSaveReady($thumb, $srcFilename)) $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); - - // 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); - } + // save the final PNG image file and always use highest compression level (9) per @horst + if($this->imSaveReady($thumb, $srcFilename)) $result = imagepng($thumb, $dstFilename, 9); break; case \IMAGETYPE_JPEG: // correct gamma from linearized 1.0 back to 2.0 $this->gammaCorrection($thumb, false); - - // 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); - } - + if($this->imSaveReady($thumb, $srcFilename)) { // optionally apply interlace bit to the final image. this will result in progressive JPEGs if($this->interlace) { if(0 == imageinterlace($thumb, 1)) { @@ -397,26 +383,46 @@ class ImageSizerEngineGD extends ImageSizerEngine { // ... } } - // save the final JPEG image file $result = imagejpeg($thumb, $dstFilename, $this->quality); } break; + + default: + $result = false; } - + // release the last GD image object if(isset($thumb) && is_resource($thumb)) @imagedestroy($thumb); if(isset($thumb)) $thumb = null; + if($result === null) $result = $this->webpResult; // if webpOnly option used return $result; } + + /** + * Called before saving of image, returns true if save should proceed, false if not + * + * Also Creates a webp file when settings indicate it should. + * + * @param resource $im + * @param string $filename Source filename + * @return bool + * + */ + protected function ___imSaveReady($im, $filename) { + if($this->webpOnly || $this->webpAdd) { + $this->webpResult = $this->imSaveWebP($im, $filename, $this->webpQuality); + } + return $this->webpOnly ? false : true; + } /** * Create WebP image (@horst) * Is requested by image options: ["webpAdd" => true] OR ["webpOnly" => true] * * @param resource $im - * @param string $dstFilename + * @param string $filename * @param int $quality * * @return boolean true | false @@ -582,7 +588,7 @@ class ImageSizerEngineGD extends ImageSizerEngine { * with mode = true it linearizes an image to 1 * with mode = false it set it back to the originating gamma value * - * @param GD -image-resource $image + * @param resource $image * @param bool $mode * */ @@ -817,8 +823,8 @@ class ImageSizerEngineGD extends ImageSizerEngine { * * Intended for use by the resize() method * - * @param GD -resource $im, destination resource needs to be prepared - * @param GD -resource $image, with GIF we need to read from source resource + * @param resource $im, destination resource needs to be prepared + * @param resource $image, with GIF we need to read from source resource * */ protected function prepareImageLayer(&$im, &$image) { diff --git a/wire/core/Pagefile.php b/wire/core/Pagefile.php index 594912ce..7f1c2392 100644 --- a/wire/core/Pagefile.php +++ b/wire/core/Pagefile.php @@ -63,6 +63,12 @@ class Pagefile extends WireData { */ protected $pagefiles; + /** + * @var PagefileExtra[] + * + */ + protected $extras = array(); + /** * Extra file data * @@ -952,8 +958,13 @@ class Pagefile extends WireData { * */ public function unlink() { - if(!strlen($this->basename) || !is_file($this->filename)) return true; - return $this->wire('files')->unlink($this->filename, true); + /** @var WireFileTools $files */ + if(!strlen($this->basename) || !is_file($this->filename)) return true; + $files = $this->wire('files'); + foreach($this->extras() as $extra) { + $extra->unlink(); + } + return $files->unlink($this->filename, true); } /** @@ -968,10 +979,17 @@ class Pagefile extends WireData { * */ public function rename($basename) { + foreach($this->extras() as $extra) { + $extra->filename(); // init + } $basename = $this->pagefiles->cleanBasename($basename, true); if($this->wire('files')->rename($this->filename, $this->pagefiles->path . $basename, true)) { $this->set('basename', $basename); - return $this->basename(); + $basename = $this->basename(); + foreach($this->extras() as $extra) { + $extra->rename(); + } + return $basename; } return false; } @@ -986,8 +1004,13 @@ class Pagefile extends WireData { * */ public function copyToPath($path) { - $result = copy($this->filename, $path . $this->basename()); - if($this->config->chmodFile) chmod($path . $this->basename(), octdec($this->config->chmodFile)); + /** @var WireFileTools $files */ + $files = $this->wire('files'); + $result = $files->copy($this->filename(), $path); + foreach($this->extras() as $extra) { + if(!$extra->exists()) continue; + $files->copy($extra->filename, $path); + } return $result; } @@ -1040,6 +1063,25 @@ class Pagefile extends WireData { return $this->pagefiles->isTemp($this, $set); } + /** + * Get all extras, add an extra, or get an extra + * + * #pw-internal + * + * @param string $name + * @param PagefileExtra $value + * @return PagefileExtra[]|PagefileExtra|null + * @since 3.0.132 + * + */ + public function extras($name = null, PagefileExtra $value = null) { + if($name === null) return $this->extras; + if($value !== null && $value instanceof PagefileExtra) { + $this->extras[$name] = $value; + } + return isset($this->extras[$name]) ? $this->extras[$name] : null; + } + /** * Debug info * diff --git a/wire/core/PagefileExtra.php b/wire/core/PagefileExtra.php new file mode 100644 index 00000000..b319c992 --- /dev/null +++ b/wire/core/PagefileExtra.php @@ -0,0 +1,228 @@ +wire($this); + $this->setPagefile($pagefile); + $this->setExtension($extension); + return parent::__construct(); + } + + /** + * Set Pagefile instance this extra is connected to + * + * @param Pagefile $pagefile + * + */ + public function setPagefile(Pagefile $pagefile) { + $this->pagefile = $pagefile; + } + + /** + * Set extension for this extra + * + * @param $extension + * + */ + public function setExtension($extension) { + $this->extension = $extension; + } + + /** + * Does the extra file currently exist? + * + * @return bool + * + */ + public function exists() { + return is_readable($this->filename()); + } + + /** + * Return the file size in bytes + * + * @return int + * + */ + public function filesize() { + return $this->exists() ? filesize($this->filename()) : 0; + } + + /** + * Return the full server disk path to the extra file, whether it exists or not + * + * @return string + * + */ + public function filename() { + $pathinfo = pathinfo($this->pagefile->filename()); + $filename = $pathinfo['dirname'] . '/' . $pathinfo['filename'] . '.' . $this->extension; + if(empty($this->filenamePrevious)) $this->filenamePrevious = $filename; + return $filename; + } + + /** + * Return just the basename (no path) + * + * @return string + * + */ + public function basename() { + return basename($this->filename()); + } + + /** + * Return the URL to the extra file, creating it if it does not already exist + * + * @return string + * + */ + public function url() { + if(!$this->exists()) $this->create(); + $pathinfo = pathinfo($this->pagefile->url()); + return $pathinfo['dirname'] . '/' . $pathinfo['filename'] . '.' . $this->extension; + } + + /** + * Return the HTTP URL to the extra file + * + * @return string + * + */ + public function httpUrl() { + return str_replace($this->pagefile->url(), $this->url(), $this->pagefile->httpUrl()); + } + + /** + * Unlink/delete the extra file + * + * @return bool + * + */ + public function unlink() { + if(!$this->exists()) return false; + return $this->wire('files')->unlink($this->filename()); + } + + /** + * Rename the extra file to be consistent with Pagefile name + * + * @return bool + * + */ + public function rename() { + if(!$this->exists()) return false; + if(!$this->filenamePrevious) return false; + return $this->wire('files')->rename($this->filenamePrevious, $this->filename()); + } + + /** + * Create the extra file + * + * Must be implemented by a hook or by descending class + * + */ + public function ___create() { } + + /** + * Get property + * + * @param string $key + * @return bool|int|mixed|null|string + * + */ + public function get($key) { + switch($key) { + case 'exists': + $value = $this->exists(); + break; + case 'filesize': + $value = $this->filesize(); + break; + case 'url': + $value = $this->url(); + break; + case 'filename': + case 'pathname': + $value = $this->filename(); + break; + case 'filenamePrevious': + $value = $this->filenamePrevious && $this->filenamePrevious != $this->filename() ? $this->filenamePrevious : ''; + break; + case 'basename': + $value = $this->basename(); + break; + case 'ext': + case 'extension': + $value = $this->extension; + break; + case 'URL': + case 'HTTPURL': + $value = str_replace($this->pagefile->url(), $this->url(), $this->pagefile->$key); + break; + case 'pagefile': + $value = $this->pagefile; + break; + default: + $value = $this->pagefile->get($key); + } + return $value; + } + + /** + * @return string + * + */ + + public function __toString() { + return $this->basename(); + } +} \ No newline at end of file diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index 82baba46..1cf88ad6 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -40,11 +40,7 @@ * @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) + * @property-read PagefileExtra $webp Access webp version of image (since 3.0.132) * * Properties inherited from Pagefile * ================================== @@ -72,6 +68,7 @@ * @property Pagefiles $pagefiles The Pagefiles WireArray that contains this file. #pw-group-other * @property Page $page The Page object that this file is part of. #pw-group-other * @property Field $field The Field object that this file is part of. #pw-group-other + * @property PageimageDebugInfo $debugInfo * * Hookable methods * ================ @@ -122,7 +119,13 @@ class Pageimage extends Pagefile { private $imageInfo = array( 'width' => 0, 'height' => 0, - ); + ); + + /** + * @var PageimageDebugInfo|null + * + */ + private $pageimageDebugInfo = null; /** * Last size error, if one occurred. @@ -132,6 +135,14 @@ class Pageimage extends Pagefile { */ protected $error = ''; + /** + * Last Pageimage::size() $options argument + * + * @var array + * + */ + static protected $lastSizeOptions = array(); + /** * Construct a new Pageimage * @@ -161,6 +172,7 @@ class Pageimage extends Pagefile { public function __clone() { $this->imageInfo['width'] = 0; $this->imageInfo['height'] = 0; + $this->extras = array(); parent::__clone(); } @@ -180,45 +192,6 @@ 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 * @@ -435,17 +408,21 @@ class Pageimage extends Pagefile { $value = parent::get('src'); if($value === null) $value = $this->url(); break; - case 'hasWebp': - $value = $this->hasWebp(); + case 'webp': + $value = $this->webp(); + break; + case 'hasWebp': + $value = $this->webp()->exists(); break; case 'webpUrl': - case 'webpSrc': - case 'urlWebp': - case 'srcWebp': - $value = $this->webpUrl(); + $value = $this->webp()->url(); break; case 'webpFilename': - $value = $this->webpFilename(); + $value = $this->webp()->filename(); + break; + case 'debugInfo': + if(!$this->pageimageDebugInfo) $this->pageimageDebugInfo = new PageimageDebugInfo($this); + $value = $this->pageimageDebugInfo; break; default: $value = parent::get($key); @@ -596,6 +573,8 @@ class Pageimage extends Pagefile { * - `focus` (bool): Should resizes that result in crop use focus area if available? (default=true). * In order for focus to be applicable, resize must include both width and height. * - `allowOriginal` (bool): Return original if already at width/height? May not be combined with other options. (default=false) + * - `webpAdd` (bool): Also create a secondary .webp image variation? (default=false) + * - `webpQuality` (int): Quality setting for extra webp images (default=90). * * **Possible values for "cropping" option** * @@ -640,10 +619,16 @@ class Pageimage extends Pagefile { public function size($width, $height, $options = array()) { if($this->wire('hooks')->isHooked('Pageimage::size()')) { - return $this->__call('size', array($width, $height, $options)); - } else { - return $this->___size($width, $height, $options); + $result = $this->__call('size', array($width, $height, $options)); + } else { + $result = $this->___size($width, $height, $options); } + + $options['_width'] = $width; + $options['_height'] = $height; + self::$lastSizeOptions = $options; + + return $result; } /** @@ -707,10 +692,14 @@ class Pageimage extends Pagefile { ); $this->error = ''; + + /** @var WireFileTools $files */ /** @var Config $config */ + $files = $this->wire('files'); $config = $this->wire('config'); $debug = $config->debug; $configOptions = $config->imageSizerOptions; + if(!is_array($configOptions)) $configOptions = array(); $options = array_merge($defaultOptions, $configOptions, $options); if($options['cropping'] === 1) $options['cropping'] = true; @@ -789,36 +778,36 @@ class Pageimage extends Pagefile { $nameHeight = is_int($options['nameHeight']) ? $options['nameHeight'] : $height; // i.e. myfile.100x100.jpg or myfile.100x100nw-suffix1-suffix2.jpg - $basename .= '.' . $nameWidth . 'x' . $nameHeight . $crop . $suffixStr . "." . $this->ext(); - $filenameFinal = $this->pagefiles->path() . $basename; + $basenameNoExt = $basename . '.' . $nameWidth . 'x' . $nameHeight . $crop . $suffixStr; // basename without ext + $basename = $basenameNoExt . '.' . $this->ext(); // basename with ext + $filenameUnvalidated = ''; - $exists = file_exists($filenameFinal); - - $path_parts = pathinfo($filenameFinal); - $filenameFinalWebp = $this->pagefiles->path() . $path_parts['filename'] . '.webp'; + $filenameUnvalidatedWebp = ''; + + $filenameFinal = $this->pagefiles->path() . $basename; + $filenameFinalExists = file_exists($filenameFinal); + $filenameFinalWebp = $this->pagefiles->path() . $basenameNoExt . '.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; - } + 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())) { + if(!$filenameFinalExists && !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']) { - + } else if(!$filenameFinalExists || $options['forceNew']) { + // filenameUnvalidated is temporary filename used for resize $tempDir = $this->pagefiles->page->filesManager()->getTempPath(); $filenameUnvalidated = $tempDir . $basename; - $path_parts = pathinfo($filenameUnvalidated); - $filenameUnvalidatedWebp = $tempDir . $path_parts['filename'] . '.webp'; + $filenameUnvalidatedWebp = $tempDir . basename($filenameFinalWebp); - if($exists && $options['forceNew']) $this->wire('files')->unlink($filenameFinal, true); - if(file_exists($filenameFinalWebp) && $options['forceNew']) $this->wire('files')->unlink($filenameFinalWebp, true); + if($filenameFinalExists && $options['forceNew']) $files->unlink($filenameFinal, true); + if(file_exists($filenameFinalWebp) && $options['forceNew']) $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(file_exists($filenameUnvalidated)) $files->unlink($filenameUnvalidated, true); + if(file_exists($filenameUnvalidatedWebp)) $files->unlink($filenameUnvalidatedWebp, true); if(@copy($this->filename(), $filenameUnvalidated)) { try { @@ -832,7 +821,7 @@ class Pageimage extends Pagefile { $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')) { + if(!empty($options['webpAdd']) && !$engine->supported('webp')) { $options['webpAdd'] = false; $engine->setOptions($options); } @@ -851,23 +840,18 @@ 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); + if($sizer->resize($width, $height) && $files->rename($filenameUnvalidated, $filenameFinal)) { + if($options['webpAdd'] && file_exists($filenameUnvalidatedWebp)) { + $files->rename($filenameUnvalidatedWebp, $filenameFinalWebp); } } else { $this->error = "ImageSizer::resize($width, $height) failed for $filenameUnvalidated"; } - $timer = $debug ? Debug::timer($timer) : null; if($debug) $this->wire('log')->save('image-sizer', str_replace('ImageSizerEngine', '', $sizer->getEngine()) . ' ' . - ($this->error ? "FAILED Resize: " : "Resized: ") . - "$originalName => " . - basename($filenameFinal) . " " . - "({$width}x{$height}) $timer secs " . - "$originalSize => " . filesize($filenameFinal) . " bytes " . + ($this->error ? "FAILED Resize: " : "Resized: ") . "$originalName => " . basename($filenameFinal) . " " . + "({$width}x{$height}) " . Debug::timer($timer) . " secs $originalSize => " . filesize($filenameFinal) . " bytes " . "(quality=$options[quality], sharpening=$options[sharpening]) " ); @@ -885,11 +869,11 @@ class Pageimage extends Pagefile { // if desired, user can check for property of $pageimage->error to see if an error occurred. // if an error occurred, that error property will be populated with details if($this->error) { - // 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); + // error condition: unlink copied files + if($filenameFinal && is_file($filenameFinal)) $files->unlink($filenameFinal, true); + if($filenameUnvalidated && is_file($filenameUnvalidated)) $files->unlink($filenameUnvalidated); + if($filenameFinalWebp && is_file($filenameFinalWebp)) $files->unlink($filenameFinalWebp, true); + if($filenameUnvalidatedWebp && is_file($filenameUnvalidatedWebp)) $files->unlink($filenameUnvalidatedWebp); // we also tell PW about it for logging and/or admin purposes $this->error($this->error); @@ -1668,16 +1652,13 @@ class Pageimage extends Pagefile { } 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); + foreach($this->extras() as $extra) { + if($options['dryRun']) { + $deletedFiles[] = $extra->filename(); + } else if($extra->unlink()) { + $deletedFiles[] = $extra->filename(); + } } - if($success) $deletedFiles[] = $webp; } if(!$options['dryRun']) $this->variations = null; @@ -1728,7 +1709,7 @@ class Pageimage extends Pagefile { } /** - * Copy this Pageimage and any of it's variations to another path + * Copy this Pageimage and any of its variations to another path * * #pw-internal * @@ -1739,10 +1720,8 @@ class Pageimage extends Pagefile { public function copyToPath($path) { if(parent::copyToPath($path)) { foreach($this->getVariations() as $variation) { - if(is_file($variation->filename)) { - copy($variation->filename, $path . $variation->basename); - if($this->config->chmodFile) chmod($path . $variation->basename, octdec($this->config->chmodFile)); - } + if(!is_file($variation->filename)) continue; + $this->wire('files')->copy($variation->filename, $path); } return true; } @@ -1906,222 +1885,95 @@ class Pageimage extends Pagefile { } } - /** - * Get verbose DebugInfo, optionally with individual options array, @horst - * (without invoking the magic debug) + * Get WebP "extra" version of this Pageimage + * + * @return PagefileExtra + * @since 3.0.132 + * + */ + public function webp() { + $webp = $this->extras('webp'); + if(!$webp) { + $webp = new PagefileExtra($this, 'webp'); + $this->extras('webp', $webp); + $webp->addHookAfter('create', $this, 'hookWebpCreate'); + } + return $webp; + } + + /** + * Hook to PageimageExtra (.webp) create method * - * @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 + * #pw-internal + * + * @param HookEvent $event * */ - 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; + public function hookWebpCreate(HookEvent $event) { + if(!$this->original) return; + /** @var PagefileExtra $webp */ + $webp = $event->object; + $webp->unlink(); + $options = self::$lastSizeOptions; + $options['webpAdd'] = true; + $this->original->size($options['_width'], $options['_height'], $options); } /** - * 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 - * + * Get all extras, add an extra, or get an extra + * + * #pw-internal + * + * @param string $name + * @param PagefileExtra $value + * @return PagefileExtra[] + * @since 3.0.132 + * */ - 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; + public function extras($name = null, PagefileExtra $value = null) { + if($name) return parent::extras($name, $value); + $extras = parent::extras(); + $extras['webp'] = $this->webp(); + return $extras; } - - /** - * Debug info + * Basic debug info * * @return array * */ public function __debugInfo() { - static $depth = 0; - $depth++; - $info = parent::__debugInfo(); - $info['width'] = $this->width(); - $info['height'] = $this->height(); - $info['suffix'] = $this->suffixStr; - if($this->hasFocus) $info['focus'] = $this->focusStr; - if(isset($info['filedata']) && isset($info['filedata']['focus'])) unset($info['filedata']['focus']); - if(empty($info['filedata'])) unset($info['filedata']); - $original = $this->original; - if($original && $original !== $this) $info['original'] = $original->basename; - if($depth < 2) { - $info['variations'] = array(); - $variations = $this->getVariations(array('info' => true, 'verbose' => false)); - foreach($variations as $name) { - $info['variations'][] = $name; - } - if(empty($info['variations'])) unset($info['variations']); - } - $depth--; - return $info; + return $this->debugInfo->getBasicDebugInfo(); + } + + /** + * Verbose debug info (via @horst) + * + * Optionally with individual options array. + * + * @param array $options The individual options you also passes with your image variation creation + * @param string $returnType 'string'|'array'|'object', default is 'string' and returns markup or plain text + * @return array|object|string + * @since 3.0.132 + * + */ + public function getDebugInfo($options = array(), $returnType = 'string') { + return $this->debugInfo->getVerboseDebugInfo($options, $returnType); + } + + /** + * Get debug info from parent class + * + * #pw-internal + * + * @return array + * @since 3.0.132 + * + */ + public function _parentDebugInfo() { + return parent::__debugInfo(); } } diff --git a/wire/core/PageimageDebugInfo.php b/wire/core/PageimageDebugInfo.php new file mode 100644 index 00000000..9c6d5324 --- /dev/null +++ b/wire/core/PageimageDebugInfo.php @@ -0,0 +1,306 @@ +wire($this); + $this->pageimage = $pageimage; + parent::__construct(); + } + + /** + * Get property + * + * This primarily delegates to the Pageimage object so that its properties can be accessed + * directly from this class. + * + * @param string $key + * @return mixed|null + * + */ + public function get($key) { + $value = $this->pageimage->get($key); + if($value === null) $value = parent::get($key); + return $value; + } + + /** + * Get basic debug info, like that used for Pageimage::__debugInfo() + * + * @return array + * + */ + public function getBasicDebugInfo() { + static $depth = 0; + $depth++; + $info = $this->pageimage->_parentDebugInfo(); + $info['width'] = $this->pageimage->width(); + $info['height'] = $this->pageimage->height(); + $info['suffix'] = $this->pageimage->suffixStr; + if($this->pageimage->hasFocus) $info['focus'] = $this->pageimage->focusStr; + if(isset($info['filedata']) && isset($info['filedata']['focus'])) unset($info['filedata']['focus']); + if(empty($info['filedata'])) unset($info['filedata']); + $original = $this->original; + if($original && $original !== $this) $info['original'] = $original->basename; + if($depth < 2) { + $info['variations'] = array(); + $variations = $this->pageimage->getVariations(array('info' => true, 'verbose' => false)); + foreach($variations as $name) { + $info['variations'][] = $name; + } + if(empty($info['variations'])) unset($info['variations']); + } + $depth--; + return $info; + } + + /** + * 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 string $returnType 'string'|'array'|'object', default is 'string' and returns markup or plain text + * @return array|object|string + * + */ + public function getVerboseDebugInfo($options = array(), $returnType = 'string') { + static $depth = 0; + $depth++; + + // fetch imagesizer, some infos and some options + $oSizer = new ImageSizer($this->filename, $options); + $this->wire($oSizer); + $osInfo = $oSizer->getImageInfo(true); + $finalOptions = $oSizer->getOptions(); + + // build some info parts and fetch some from parent (pagefile) + $thumbStyle = "max-width:120px; max-height:120px;"; + $thumbStyle .= $this->width >= $this->height ? 'width:100px; height:auto;' : 'height:100px; width:auto;'; + $thumb = array( + 'thumb' => "" + ); + + if($this->original) { + $original = array( + 'original' => $this->original->basename, + 'basename' => $this->basename + ); + } else { + $original = array( + 'original' => '{SELF}', + 'basename' => $this->basename + ); + } + + $parent = array( + 'files' => array_merge( + $original, + $this->pageimage->_parentDebugInfo(), + array( + 'suffix' => isset($finalOptions['suffix']) ? $finalOptions['suffix'] : '', + 'extension' => $osInfo['extension'] + ) + ) + ); + + // rearange parts + unset($parent['files']['filesize']); + $parent['files']['filesize'] = filesize($this->filename); + + // VARIATIONS + $variationArray = array(); + if($depth < 2) { + $variations = $this->pageimage->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 = $this->pageimage->webp(); + $webpSize = $webp->exists() ? filesize($webp->filename()) : 0; + $webpInfo = array( + 'webp_copy' => array( + 'hasWebp' => $webpSize ? true : false, + 'webpUrl' => (!$webpSize ? NULL : $webp->url()), + 'webpQuality' => (!isset($finalOptions['webpQuality']) ? NULL : $finalOptions['webpQuality']), + 'filesize' => $webpSize, + 'savings' => (!$webpSize ? 0 : intval($info['files']['filesize'] - $webpSize)), + 'savings_percent' => (!$webpSize ? 0 : 100 - intval($webpSize / ($info['files']['filesize'] / 100))), + ) + ); + + // ENGINES + $a = array(); + $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, $webpInfo, + array( + 'engines' => $enginesArray + ), + // OPTIONS + array( + 'options_hierarchy' => array( + 'imageSizerOptions' => $this->wire('config')->imageSizerOptions, + 'individualOptions' => $options, + 'finalOptions' => $finalOptions + ) + ) + ); + unset($variationArray, $webpInfo, $enginesArray, $options, $finalOptions); + + // If not in browser environment, remove the thumb image + if($this->wire('config')->cli) 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->arrayToObject($array, $object->$group); + } + return $object; + } + + // make a beautified 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($this->wire('config')->cli) { + // output for Console + $return = $content; + } else { + // build output for HTML + $return = "
" . $this->wire('sanitizer')->entities($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 arrayToObject($array, &$object, $multidim = true) { + foreach($array as $key => $value) { + if($multidim && is_array($value)) { + $object->$key = new \stdClass(); + $this->arrayToObject($value, $object->$key, false); + } else { + $object->$key = $value; + } + } + return $object; + } + + +} \ No newline at end of file diff --git a/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module b/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module index 9cecb6a0..f42a52a8 100755 --- a/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module +++ b/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module @@ -35,6 +35,15 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { */ protected $imWebp = null; + /** + * Webp support available? + * + * @var bool|null + * + */ + static protected $webpSupport = null; + + // @todo the following need phpdoc protected $workspaceColorspace; protected $imageFormat; @@ -163,18 +172,12 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { 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); + if(self::$webpSupport === null) { + $im = new \IMagick(); + $formats = $im->queryformats('WEBP*'); + self::$webpSupport = count($formats) > 0; } - return $this->wire('config')->webpSupportIM; + return self::$webpSupport; break; case 'install': @@ -384,7 +387,7 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { $this->im->setImageDepth(($this->imageDepth > 8 ? 8 : $this->imageDepth)); // prepare to save file(s) - if($this->webpAdd) { + if($this->webpAdd && $this->supported('webp')) { $this->imWebp = clone $this->im; // make a copy before compressions take effect } @@ -416,12 +419,12 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { // 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'; + $pathinfo = pathinfo($srcFilename); + $webpFilename = $pathinfo['dirname'] . '/' . $pathinfo['filename'] . '.webp'; if(file_exists($webpFilename)) $this->wire('files')->unlink($webpFilename); // optionally create a WebP dependency file - if($this->webpAdd) { + if($this->webpAdd && $this->imWebp) { // prepare for webp output $this->imWebp->setImageFormat('webp'); $this->imWebp->setImageCompressionQuality($this->webpQuality);