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