1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-10 16:54:44 +02:00

Merge branch 'horst-n-webP-support' into dev

This commit is contained in:
Ryan Cramer
2019-05-23 15:50:21 -04:00
8 changed files with 1040 additions and 110 deletions

View File

@@ -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
);
/**

View File

@@ -3,7 +3,7 @@
/**
* 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
@@ -18,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 {
@@ -60,6 +63,30 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*/
protected $quality = 90;
/**
* WebP Image quality setting, 1..100
*
* @var int
*
*/
protected $webpQuality = 90;
/**
* Also create a WebP Image with this variation?
*
* @var bool
*
*/
protected $webpAdd = false;
/**
* webp result (null=not known or not applicable)
*
* @var bool|null
*
*/
protected $webpResult = null;
/**
* Image interlace setting, false or true
*
@@ -219,6 +246,8 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
'cropping',
'interlace',
'quality',
'webpQuality',
'webpAdd',
'sharpening',
'defaultGamma',
'scale',
@@ -819,7 +848,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
* disable cropping, specify boolean false. To enable cropping with default (center), you may also specify
* boolean true.
*
* @return $this
* @return self
*
*/
public function setCropping($cropping = true) {
@@ -834,7 +863,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*
* @param array $value containing 4 params (x y w h) indexed or associative
*
* @return $this
* @return self
* @throws WireException when given invalid value
*
*/
@@ -878,14 +907,37 @@ 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;
$this->quality = $this->getIntegerValue($n, 1, 100);
return $this;
}
/**
* Set the image quality 1-100 for WebP output, where 100 is highest quality
*
* @param int $n
*
* @return self
*
*/
public function setWebpQuality($n) {
$this->webpQuality = $this->getIntegerValue($n, 1, 100);
return $this;
}
/**
* Set flag to also create a webp file or not
*
* @param bool $value
*
* @return self
*
*/
public function setWebpAdd($value) {
$this->webpAdd = (bool) $value;
return $this;
}
@@ -928,7 +980,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*
* @param mixed $value
*
* @return $this
* @return self
* @throws WireException
*
*/
@@ -957,7 +1009,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*
* @param bool $value Whether to auto-rotate or not (default = true)
*
* @return $this
* @return self
*
*/
public function setAutoRotation($value = true) {
@@ -970,7 +1022,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*
* @param bool $value Whether to upscale or not (default = true)
*
* @return $this
* @return self
*
*/
public function setUpscaling($value = true) {
@@ -983,7 +1035,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*
* @param bool $value Whether to upscale or not (default = true)
*
* @return $this
* @return self
*
*/
public function setInterlace($value = true) {
@@ -996,7 +1048,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*
* @param float|int $value 0.5 to 4.0 or -1 to disable
*
* @return $this
* @return self
* @throws WireException when given invalid value
*
*/
@@ -1016,7 +1068,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*
* @param int $value 10 to 60 recommended, default is 30
*
* @return $this
* @return self
*
*/
public function setTimeLimit($value = 30) {
@@ -1042,7 +1094,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*
* @param float $scale
*
* @return $this
* @return self
*
*/
public function setScale($scale) {
@@ -1057,7 +1109,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*
* @param bool $hidpi True or false (default=true)
*
* @return $this
* @return self
*
*/
public function setHidpi($hidpi = true) {
@@ -1071,7 +1123,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*
* @param $degrees
*
* @return $this
* @return self
*
*/
public function setRotate($degrees) {
@@ -1089,7 +1141,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*
* @param $flip
*
* @return $this
* @return self
*
*/
public function setFlip($flip) {
@@ -1103,7 +1155,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*
* @param bool $value Whether to USM is used or not (default = true)
*
* @return $this
* @return self
*
*/
public function setUseUSM($value = true) {
@@ -1116,6 +1168,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*
* @param array $options May contain the following (show with default values):
* 'quality' => 90,
* 'webpQuality' => 90,
* 'cropping' => true,
* 'upscaling' => true,
* 'autoRotation' => true,
@@ -1125,7 +1178,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
* 'rotate' => 0 (90, 180, 270 or negative versions of those)
* 'flip' => '', (vertical|horizontal)
*
* @return $this
* @return self
*
*/
public function setOptions(array $options) {
@@ -1148,6 +1201,12 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
case 'quality':
$this->setQuality($value);
break;
case 'webpQuality':
$this->setWebpQuality($value);
break;
case 'webpAdd':
$this->setWebpAdd($value);
break;
case 'cropping':
$this->setCropping($value);
break;
@@ -1197,6 +1256,25 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
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
*
@@ -1207,6 +1285,8 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
$options = array(
'quality' => $this->quality,
'webpQuality' => $this->webpQuality,
'webpAdd' => $this->webpAdd,
'cropping' => $this->cropping,
'upscaling' => $this->upscaling,
'interlace' => $this->interlace,
@@ -1247,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];
@@ -1387,7 +1468,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*
* @param bool $modified
*
* @return $this
* @return self
*
*/
public function setModified($modified) {

View File

@@ -8,11 +8,13 @@
*
* 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
*
@@ -57,6 +75,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);
@@ -70,6 +89,15 @@ class ImageSizerEngineGD extends ImageSizerEngine {
}
break;
case 'webp':
if(self::$webpSupport === null) {
// only call it once
$gd = gd_info();
self::$webpSupport = isset($gd['WebP Support']) ? $gd['WebP Support'] : false;
}
return self::$webpSupport;
break;
case 'install':
/*
$gd = gd_info();
@@ -77,6 +105,7 @@ class ImageSizerEngineGD extends ImageSizerEngine {
$png = isset($gd['PNG Support']) ? $gd['PNG Support'] : false;
$gif = isset($gd['GIF Read Support']) && isset($gd['GIF Create Support']) ? $gd['GIF Create Support'] : false;
$freetype = isset($gd['FreeType Support']) ? $gd['FreeType Support'] : false;
$webp = isset($gd['WebP Support']) ? $gd['WebP Support'] : false;
$this->config->gdReady = true;
*/
return true;
@@ -322,41 +351,91 @@ 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) {
// write to file(s)
if(file_exists($dstFilename)) $this->wire('files')->unlink($dstFilename);
$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);
// 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);
// 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($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)) {
// log that setting the interlace bit has failed ?
// ...
}
}
// write to file
$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);
break;
case \IMAGETYPE_PNG:
if(!$this->hasAlphaChannel()) $this->gammaCorrection($thumb, false);
// always use highest compression level for PNG (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);
// 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 $filename
* @param int $quality
*
* @return boolean true | false
*
*/
protected function imSaveWebP($im, $filename, $quality = 90) {
if(!function_exists('imagewebp')) return false;
$path_parts = pathinfo($filename);
$webpFilename = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.webp';
if(file_exists($webpFilename)) $this->wire('files')->unlink($webpFilename);
return imagewebp($im, $webpFilename, $quality);
}
/**
* Rotate image (@horst)
*
@@ -509,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
*
*/
@@ -744,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) {

View File

@@ -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() {
/** @var WireFileTools $files */
if(!strlen($this->basename) || !is_file($this->filename)) return true;
return $this->wire('files')->unlink($this->filename, 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
*

228
wire/core/PagefileExtra.php Normal file
View File

@@ -0,0 +1,228 @@
<?php namespace ProcessWire;
/**
* Extra extension for Pagefile or Pageimage objects
*
* @property string $url Local URL/path to file
* @property string $httpUrl Full HTTP URL with scheme and host
* @property string $URL No-cache version of url
* @property string $HTTPURL No-cache version of httpUrl
* @property string $filename Full disk path/file
* @property string $pathname Alias of filename
* @property string $basename Just the basename without path
* @property string $extension File extension
* @property string $ext Alias of extension
* @property bool $exists Does the file exist?
* @property int $filesize Size of file in bytes
* @property Pageimage $pagefile Source Pageimage objerct
*
* @method create()
*
*/
class PagefileExtra extends WireData {
/**
* @var Pagefile|Pageimage
*
*/
protected $pagefile;
/**
* @var string
*
*/
protected $extension = '';
/**
* Previous filename, if it changed
*
* @var string
*
*/
protected $filenamePrevious = '';
/**
* Construct
*
* @param Pagefile|Pageimage $pagefile
* @param $extension
*
*/
public function __construct(Pagefile $pagefile, $extension) {
$pagefile->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();
}
}

View File

@@ -40,6 +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 PagefileExtra $webp Access webp version of image (since 3.0.132)
*
* Properties inherited from Pagefile
* ==================================
@@ -67,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
* ================
@@ -119,6 +121,12 @@ class Pageimage extends Pagefile {
'height' => 0,
);
/**
* @var PageimageDebugInfo|null
*
*/
private $pageimageDebugInfo = null;
/**
* Last size error, if one occurred.
*
@@ -127,6 +135,14 @@ class Pageimage extends Pagefile {
*/
protected $error = '';
/**
* Last Pageimage::size() $options argument
*
* @var array
*
*/
static protected $lastSizeOptions = array();
/**
* Construct a new Pageimage
*
@@ -156,6 +172,7 @@ class Pageimage extends Pagefile {
public function __clone() {
$this->imageInfo['width'] = 0;
$this->imageInfo['height'] = 0;
$this->extras = array();
parent::__clone();
}
@@ -391,6 +408,22 @@ class Pageimage extends Pagefile {
$value = parent::get('src');
if($value === null) $value = $this->url();
break;
case 'webp':
$value = $this->webp();
break;
case 'hasWebp':
$value = $this->webp()->exists();
break;
case 'webpUrl':
$value = $this->webp()->url();
break;
case '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);
}
@@ -540,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**
*
@@ -578,16 +613,22 @@ 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()) {
if($this->wire('hooks')->isHooked('Pageimage::size()')) {
return $this->__call('size', array($width, $height, $options));
$result = $this->__call('size', array($width, $height, $options));
} else {
return $this->___size($width, $height, $options);
$result = $this->___size($width, $height, $options);
}
$options['_width'] = $width;
$options['_height'] = $height;
self::$lastSizeOptions = $options;
return $result;
}
/**
@@ -635,6 +676,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,
@@ -649,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;
@@ -731,21 +778,37 @@ 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);
$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;
// 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
$filenameUnvalidated = $this->pagefiles->page->filesManager()->getTempPath() . $basename;
if($exists && $options['forceNew']) $this->wire('files')->unlink($filenameFinal, true);
if(file_exists($filenameUnvalidated)) $this->wire('files')->unlink($filenameUnvalidated, true);
$tempDir = $this->pagefiles->page->filesManager()->getTempPath();
$filenameUnvalidated = $tempDir . $basename;
$filenameUnvalidatedWebp = $tempDir . basename($filenameFinalWebp);
if($filenameFinalExists && $options['forceNew']) $files->unlink($filenameFinal, true);
if(file_exists($filenameFinalWebp) && $options['forceNew']) $files->unlink($filenameFinalWebp, true);
if(file_exists($filenameUnvalidated)) $files->unlink($filenameUnvalidated, true);
if(file_exists($filenameUnvalidatedWebp)) $files->unlink($filenameUnvalidatedWebp, true);
if(@copy($this->filename(), $filenameUnvalidated)) {
try {
@@ -757,6 +820,12 @@ class Pageimage extends Pagefile {
/** @var ImageSizerEngine $engine */
$engine = $sizer->getEngine();
/* if the current engine installation does not support webp, modify the options param */
if(!empty($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();
@@ -771,20 +840,18 @@ class Pageimage extends Pagefile {
}
}
if($sizer->resize($width, $height) && @rename($filenameUnvalidated, $filenameFinal)) {
$this->wire('files')->chmod($filenameFinal);
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]) "
);
@@ -802,9 +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);
// 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);
@@ -1582,6 +1651,14 @@ class Pageimage extends Pagefile {
$success = $files->unlink($filename, true);
}
if($success) $deletedFiles[] = $filename;
foreach($this->extras() as $extra) {
if($options['dryRun']) {
$deletedFiles[] = $extra->filename();
} else if($extra->unlink()) {
$deletedFiles[] = $extra->filename();
}
}
}
if(!$options['dryRun']) $this->variations = null;
@@ -1632,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
*
@@ -1643,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;
}
@@ -1811,33 +1886,94 @@ class Pageimage extends Pagefile {
}
/**
* Debug info
* 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
*
* #pw-internal
*
* @param HookEvent $event
*
*/
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);
}
/**
* Get all extras, add an extra, or get an extra
*
* #pw-internal
*
* @param string $name
* @param PagefileExtra $value
* @return PagefileExtra[]
* @since 3.0.132
*
*/
public function extras($name = null, PagefileExtra $value = null) {
if($name) return parent::extras($name, $value);
$extras = parent::extras();
$extras['webp'] = $this->webp();
return $extras;
}
/**
* 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;
return $this->debugInfo->getBasicDebugInfo();
}
if(empty($info['variations'])) unset($info['variations']);
/**
* 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);
}
$depth--;
return $info;
/**
* Get debug info from parent class
*
* #pw-internal
*
* @return array
* @since 3.0.132
*
*/
public function _parentDebugInfo() {
return parent::__debugInfo();
}
}

View File

@@ -0,0 +1,306 @@
<?php namespace ProcessWire;
/**
* Debug info for Pageimage
*
* By Horst Nogajski for ProcessWire
*
* @property string $url
* @property string $filename
* @property string $basename
* @property Pageimage $original
* @property int $width
* @property int $height
* @property bool $hasFocus
* @property string $focusStr
* @property string $suffixStr
*
*/
class PageimageDebugInfo extends WireData {
/**
* @var Pageimage
*
*/
protected $pageimage;
/**
* Construct
*
* @param Pageimage $pageimage
*
*/
public function __construct(Pageimage $pageimage) {
$pageimage->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' => "<img src='$this->url' style='$thumbStyle' alt='' />"
);
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('<pre>', '</pre>'), '', $content);
if($this->wire('config')->cli) {
// output for Console
$return = $content;
} else {
// build output for HTML
$return = "<pre>" . $this->wire('sanitizer')->entities($content) . "</pre>";
}
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;
}
}

View File

@@ -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,11 +20,30 @@ 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;
/**
* Webp support available?
*
* @var bool|null
*
*/
static protected $webpSupport = null;
// @todo the following need phpdoc
protected $workspaceColorspace;
protected $imageFormat;
@@ -80,10 +99,15 @@ class ImageSizerEngineIMagick extends ImageSizerEngine {
*
*/
protected function release() {
if(!is_object($this->im)) return;
if(is_object($this->im)) {
$this->im->clear();
$this->im->destroy();
}
if(is_object($this->imWebp)) {
$this->imWebp->clear();
$this->imWebp->destroy();
}
}
/**
* Get valid image source formats
@@ -147,6 +171,15 @@ class ImageSizerEngineIMagick extends ImageSizerEngine {
}
break;
case 'webp':
if(self::$webpSupport === null) {
$im = new \IMagick();
$formats = $im->queryformats('WEBP*');
self::$webpSupport = count($formats) > 0;
}
return self::$webpSupport;
break;
case 'install':
return true;
@@ -353,7 +386,11 @@ 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->supported('webp')) {
$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'))) {
@@ -367,8 +404,8 @@ class ImageSizerEngineIMagick extends ImageSizerEngine {
$this->im->setImageCompressionQuality($this->quality);
}
// save to file
$this->wire('files')->unlink($dstFilename);
// write to file
if(file_exists($dstFilename)) $this->wire('files')->unlink($dstFilename);
@clearstatcache(dirname($dstFilename));
##if(!$this->im->writeImage($this->destFilename)) {
// We use this approach for saving so that it behaves the same like core ImageSizer with images that
@@ -379,10 +416,29 @@ class ImageSizerEngineIMagick extends ImageSizerEngine {
return false;
}
// set modified flag and delete optional webp dependency file
$this->modified = true;
$return = true;
$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 && $this->imWebp) {
// prepare for webp output
$this->imWebp->setImageFormat('webp');
$this->imWebp->setImageCompressionQuality($this->webpQuality);
$this->imWebp->setOption('webp:method', '6');
//$this->imWebp->setOption('webp:lossless', 'true'); // is this useful?
//$this->imWebp->setImageAlphaChannel(imagick::ALPHACHANNEL_ACTIVATE); // is this useful?
//$this->imWebp->setBackgroundColor(new ImagickPixel('transparent')); // is this useful?
// save to file
$return = $this->imWebp->writeImage($webpFilename);
}
// release and return to event-object
$this->release();
$this->modified = true;
return true;
return $return;
}
/**