praiadeseselle/wire/core/Pageimage.php

1882 lines
68 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php namespace ProcessWire;
/**
* ProcessWire Pageimage
*
* #pw-summary Represents an image item attached to a page, typically via an Image Fieldtype.
* #pw-summary-variations A variation refers to an image that is based upon another (like a resized or cropped version for example).
* #pw-order-groups common,resize-and-crop,variations,other
* #pw-use-constructor
* #pw-body =
* Pageimage objects are usually contained by a `Pageimages` object, which is a type of `Pagefiles` and `WireArray`.
* In addition to the methods and properties below, you'll also want to look at `Pagefile` which this class inherits
* several important methods and properties from.
*
* ~~~~~
* // Example of outputting a thumbnail gallery of Pageimage objects
* foreach($page->images as $image) {
* // $image and $thumb are both Pageimage objects
* $thumb = $image->size(200, 200);
* echo "<a href='$image->url'>";
* echo "<img src='$thumb->url' alt='$image->description' />";
* echo "</a>";
* }
* ~~~~~
* #pw-body
*
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer
* https://processwire.com
*
* @property-read int $width Width of image, in pixels.
* @property-read int $height Height of image, in pixels.
* @property-read int $hidpiWidth HiDPI width of image, in pixels. #pw-internal
* @property-read int $hidpiHeight HiDPI heigh of image, in pixels. #pw-internal
* @property-read string $error Last image resizing error message, when applicable. #pw-group-resize-and-crop
* @property-read Pageimage $original Reference to original $image, if this is a resized version. #pw-group-variations
* @property-read array $focus Focus array contains 'top' (float), 'left' (float), 'zoom' (int), and 'default' (bool) properties.
* @property-read string $focusStr Readable string containing focus information.
* @property-read bool $hasFocus Does this image have custom focus settings? (i.e. $focus['default'] == true)
* @property-read array $suffix Array containing file suffix(es).
* @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)
* @property-read float $ratio Image ratio where 1.0 is square, >1 is wider than tall, >2 is twice as wide as well, <1 is taller than wide, etc. (since 3.0.154+)
*
* Properties inherited from Pagefile
* ==================================
* @property-read string $url URL to the file on the server.
* @property-read string $httpUrl URL to the file on the server including scheme and hostname.
* @property-read string $URL Same as $url property but with browser cache busting query string appended. #pw-group-other
* @property-read string $HTTPURL Same as the cache-busting uppercase “URL” property, but includes scheme and hostname. #pw-group-other
* @property-read string $filename Full disk path to the file on the server.
* @property-read string $name Returns the filename without the path, same as the "basename" property.
* @property-read string $hash Get a unique hash (for the page) representing this Pagefile.
* @property-read array $tagsArray Get file tags as an array. #pw-group-tags @since 3.0.17
* @property string $basename Returns the filename without the path.
* @property string $description Value of the files description field (string), if enabled. Note you can also set this property directly.
* @property string $tags Value of the files tags field (string), if enabled. #pw-group-tags
* @property string $ext Files extension (i.e. last 3 or so characters)
* @property-read int $filesize File size (number of bytes).
* @property int $modified Unix timestamp of when Pagefile (file, description or tags) was last modified. #pw-group-date-time
* @property-read string $modifiedStr Readable date/time string of when Pagefile was last modified. #pw-group-date-time
* @property-read int $mtime Unix timestamp of when file (only) was last modified. #pw-group-date-time
* @property-read string $mtimeStr Readable date/time string when file (only) was last modified. #pw-group-date-time
* @property int $created Unix timestamp of when file was created. #pw-group-date-time
* @property-read string $createdStr Readable date/time string of when Pagefile was created #pw-group-date-time
* @property string $filesizeStr File size as a formatted string, i.e. “123 Kb”.
* @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
* ================
* @method bool|array isVariation($basename, $options = array())
* @method Pageimage crop($x, $y, $width, $height, $options = array())
* @method array rebuildVariations($mode = 0, array $suffix = array(), array $options = array())
* @method install($filename)
* @method render($markup = '', $options = array())
* @method void createdVariation(Pageimage $image, array $data) Called after new image variation created (3.0.180+)
*
*/
class Pageimage extends Pagefile {
/**
* Reference to the collection of Pageimages that this Pageimage belongs to
*
* @var Pageimages
*
*/
protected $pageimages;
/**
* Reference to the original image this variation was created from
*
* Applicable only if this image is a variation (resized version). It will be null in all other instances.
*
* @var Pageimage
*
*/
protected $original = null;
/**
* Cached result of the variations() method
*
* Don't reference this directly, because it won't be loaded unless requested, instead use the variations() method
*
* @var PageimageVariations
*
*/
private $variations = null;
/**
* Cached result of the getImageInfo() method
*
* Don't reference this directly, because it won't be loaded unless requested, instead use the getImageInfo() method
*
* @var array
*
*/
private $imageInfo = array(
'width' => 0,
'height' => 0,
);
/**
* @var PageimageDebugInfo|null
*
*/
private $pageimageDebugInfo = null;
/**
* Last size error, if one occurred.
*
* @var string
*
*/
protected $error = '';
/**
* Last used Pageimage::size() $options argument
*
* @var array
*
*/
protected $sizeOptions = array();
/**
* Construct a new Pageimage
*
* ~~~~~
* // Construct a new Pageimage, assumes that $page->images is a FieldtypeImage Field
* $pageimage = new Pageimage($page->images, '/path/to/file.png');
* ~~~~~
*
* @param Pageimages|Pagefiles $pagefiles
* @param string $filename Full path and filename to this pagefile
* @throws WireException
*
*/
public function __construct(Pagefiles $pagefiles, $filename) {
if(!$pagefiles instanceof Pageimages) throw new WireException("Pageimage::__construct requires instance of Pageimages");
$pagefiles->wire($this);
$this->pageimages = $pagefiles;
parent::__construct($pagefiles, $filename);
}
/**
* When a Pageimage is cloned, we reset it's width and height to force them to reload in the clone
*
* #pw-internal
*
*/
public function __clone() {
$this->imageInfo['width'] = 0;
$this->imageInfo['height'] = 0;
$this->pageimageDebugInfo = null;
$this->variations = null;
parent::__clone();
}
/**
* Return the web accessible URL to this image file
*
* #pw-hooks
*
* @return string
*
*/
public function url() {
$hooks = $this->wire()->hooks;
if($hooks->isHooked('Pagefile::url()') || $hooks->isHooked('Pageimage::url()')) {
return $this->__call('url', array());
} else {
return $this->___url();
}
}
/**
* Returns the full disk path to the image file
*
* #pw-hooks
*
* @return string
*
*/
public function filename() {
$hooks = $this->wire()->hooks;
if($hooks->isHooked('Pagefile::filename()') || $hooks->isHooked('Pageimage::filename()')) {
return $this->__call('filename', array());
} else {
return $this->___filename();
}
}
/**
* Returns array of suffixes for this file, or true/false if this file has the given suffix.
*
* When providing a suffix, this method can be thought of: hasSuffix(suffix)
*
* #pw-group-other
*
* @param string $s Optionally provide suffix to return true/false if file has the suffix
* @return array|bool Returns array of suffixes, or true|false if given a suffix in the arguments.
*
*/
public function suffix($s = '') {
$info = $this->isVariation(parent::get('basename'));
if(strlen($s)) {
return $info ? in_array($s, $info['suffix']) : false;
} else {
return $info ? $info['suffix'] : array();
}
}
/**
* Get or set focus area for crops to use
*
* These settings are used by $this->size() calls that specify BOTH width AND height. Focus helps to
* ensure that the important subject of the photo is not cropped out when the requested size proportion
* differs from the original image proportion. For example, not chopping off someones head in a photo.
*
* Default behavior is to return an array containing "top" and "left" indexes, representing percentages
* from top and left. When arguments are specified, you are either setting the top/left percentages, or
* unsetting focus, or getting focus in different ways, described in arguments below.
*
* A zoom argument/property is also present here for future use, but not currently supported.
*
* #pw-group-other
*
* @param null|float|int|array|false $top Omit to get focus array, or specify one of the following:
* - GET: Omit all arguments to get focus array (default behavior).
* - GET: Specify boolean TRUE to return TRUE if focus data is present or FALSE if not.
* - GET: Specify integer 1 to make this method return pixel dimensions rather than percentages.
* - SET: Specify both $top and $left arguments to set (values assumed to be percentages).
* - SET: Specify array containing "top" and "left" indexes to set (percentages).
* - SET: Specify array where index 0 is top and index 1 is left (percentages).
* - SET: Specify string in the format "top left", i.e. "25 70" or "top left zoom", i.e. "25 70 30" (percentages).
* - SET: Specify CSV key=value string in the format "top=25%, left=70%, zoom=30%" in any order
* - UNSET: Specify boolean false to remove any focus values.
* @param null|float|int $left Set left value (when $top value is float|int)
* - This argument is only used when setting focus and should be omitted otherwise.
* @param null|int $zoom Zoom percent (not currently supported)
* @return array|bool|Pageimage Returns one of the following:
* - When getting returns array containing top, left and default properties.
* - When TRUE was specified for the $top argument, it returns either TRUE (has focus) or FALSE (does not have).
* - When setting or unsetting returns $this.
*
*/
public function focus($top = null, $left = null, $zoom = null) {
if(is_string($top) && $left === null) {
if(strpos($top, '=')) {
// SET string "top=25%, left=70%, zoom=0%"
$a = array('top' => 50, 'left' => 50, 'zoom' => 0);
$parts = explode(',', str_replace(array(' ', '%'), '', $top));
foreach($parts as $part) {
if(!strpos($part, '=')) continue;
list($name, $pct) = explode('=', $part);
$a[$name] = strpos($pct, '.') !== false ? (float) $pct : (int) $pct;
}
$top = $a; // for later setting by array
unset($a);
} else if(strpos($top, ' ')) {
// SET string like "25 70 0" (representing "top left zoom")
if(strpos($top, ' ') != strrpos($top, ' ')) {
// with zoom
list($top, $left, $zoom) = explode(' ', $top, 3);
} else {
// without zoom
list($top, $left) = explode(' ', $top, 2);
$zoom = 0;
}
}
}
if($top === null || $top === true || ($top === 1 && $left === null)) {
// GET
$focus = $this->filedata('focus');
if(!is_array($focus) || empty($focus)) {
// use default
if($top === true) return false;
$focus = array(
'top' => 50,
'left' => 50,
'zoom' => 0,
'default' => true,
'str' => '50 50 0',
);
} else {
// use custom
if($top === true) return true;
if(!isset($focus['zoom'])) $focus['zoom'] = 0;
$focus['default'] = false;
$focus['str'] = "$focus[top] $focus[left] $focus[zoom]";
}
if($top === 1) {
// return pixel dimensions rather than percentages
$centerX = ($focus['left'] / 100) * $this->width(); // i.e. (50 / 100) * 500 = 250;
$centerY = ($focus['top'] / 100) * $this->height();
$focus['left'] = $centerX;
$focus['top'] = $centerY;
}
return $focus;
} else if($top === false) {
// UNSET
$this->filedata(false, 'focus');
} else if($left !== null) {
// SET
if(is_array($top)) {
if(isset($top['left'])) {
$left = $top['left'];
$top = $top['top'];
$zoom = isset($top['zoom']) ? $top['zoom'] : 0;
} else {
$top = $top[0];
$left = $top[1];
$zoom = isset($top[2]) ? $top[2] : 0;
}
}
$top = (float) $top;
$left = (float) $left;
$zoom = (int) $zoom;
if(((int) $top) == 50 && ((int) $left) == 50 && ($zoom < 2)) {
// if matches defaults, then no reason to store in filedata
$this->filedata(false, 'focus');
} else {
$this->filedata('focus', array(
'top' => round($top, 1),
'left' => round($left, 1),
'zoom' => $zoom
));
}
}
return $this;
}
/**
* Set property
*
* @param string $key
* @param mixed $value
* @return Pageimage|WireData
*
*/
public function set($key, $value) {
if($key === 'sizeOptions' && is_array($value)) {
$this->sizeOptions = $value;
return $this;
} else {
return parent::set($key, $value);
}
}
/**
* Get a property from this Pageimage
*
* #pw-internal
*
* @param string $key
* @return mixed
*
*/
public function get($key) {
switch($key) {
case 'width':
case 'height':
$value = $this->$key();
break;
case 'hidpiWidth':
case 'retinaWidth':
$value = $this->hidpiWidth();
break;
case 'hidpiHeight':
case 'retinaHeight':
$value = $this->hidpiHeight();
break;
case 'original':
$value = $this->getOriginal();
break;
case 'error':
$value = $this->error;
break;
case 'focus':
$value = $this->focus();
break;
case 'focusStr':
$focus = $this->focus();
$value = "top=$focus[top]%,left=$focus[left]%,zoom=$focus[zoom]%" . ($focus['default'] ? " (default)" : "");
break;
case 'hasFocus':
$value = $this->focus(true);
break;
case 'suffix':
$value = $this->suffix();
break;
case 'suffixStr':
$value = implode(',', $this->suffix());
break;
case 'alt':
$value = parent::get('alt');
if($value === null) $value = $this->description();
break;
case 'src':
$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;
case 'sizeOptions':
$value = $this->sizeOptions;
break;
case 'ratio':
$value = $this->ratio();
break;
default:
$value = parent::get($key);
}
return $value;
}
/**
* Set image info (internal use)
*
* #pw-internal
*
* @param array $info
*
*/
public function setImageInfo(array $info) {
// width and height less than 0 indicate percentage rather than pixels
if(isset($info['width']) && $info['width'] < 0) $info['width'] = abs($info['width']) . '%';
if(isset($info['height']) && $info['height'] < 0) $info['height'] = abs($info['height']) . '%';
$this->imageInfo = array_merge($this->imageInfo, $info);
}
/**
* Gets the image information with PHPs getimagesize function and caches the result
*
* #pw-internal
*
* @param bool|string $reset Specify true to retrieve info fresh, or filename to check and return info for.
* When specifying a filename, the info is only returned (not populated with this object).
* @return array
*
*/
public function getImageInfo($reset = false) {
$imageInfo = $this->imageInfo;
$filename = is_string($reset) && file_exists($reset) ? $reset : '';
$ext = $this->ext;
if(!$reset && $imageInfo['width'] && !$filename) {
return $imageInfo;
}
if($ext == 'svg') {
$imageInfo = array_merge($imageInfo, $this->getImageInfoSVG($filename));
} else {
if($filename) {
$info = @getimagesize($filename);
} else {
$info = @getimagesize($this->filename);
}
if((!$info || empty($info[0])) && !empty($this->sizeOptions['_width'])) {
// on fail, fallback to size options that were requested for the image (if available)
$imageInfo['width'] = $this->sizeOptions['_width'];
$imageInfo['height'] = $this->sizeOptions['_height'];
} else if($info) {
$imageInfo['width'] = $info[0];
$imageInfo['height'] = $info[1];
if(function_exists('exif_read_data') && ($ext === 'jpg' || $ext === 'jpeg')) {
$exif = $filename ? @exif_read_data($filename) : @exif_read_data($this->filename);
if(!empty($exif['Orientation']) && (int) $exif['Orientation'] > 4) {
// Image has portrait orientation so reverse width and height info
$imageInfo['width'] = $info[1];
$imageInfo['height'] = $info[0];
}
}
}
}
if(!$filename) $this->imageInfo = $imageInfo;
return $imageInfo;
}
/**
* Gets the image info/size of an SVG
*
* Returned width and height values may be integers OR percentage strings.
*
* #pw-internal
*
* @param string $filename Optional filename to check
* @return array of width and height
*
*/
protected function getImageInfoSVG($filename = '') {
$width = 0;
$height = 0;
if(!$filename) $filename = $this->filename;
$xml = @file_get_contents($filename);
if($xml) {
$a = @simplexml_load_string($xml)->attributes();
if((int) $a->width > 0) $width = (int) $a->width;
if((int) $a->height > 0) $height = (int) $a->height;
if((!$width || !$height) && $a->viewBox) {
$values = explode(' ', $a->viewBox);
if(count($values) === 4) {
$width = (int) round($values[2]);
$height = (int) round($values[3]);
}
}
}
if((!$width || !$height) && (extension_loaded('imagick') || class_exists('\IMagick'))) {
try {
$imagick = new \Imagick();
$imagick->readImage($filename);
$width = $imagick->getImageWidth();
$height = $imagick->getImageHeight();
} catch(\Exception $e) {
// fallback to 100%
}
}
if($width < 1) $width = '100%';
if($height < 1) $height = '100%';
return array(
'width' => $width,
'height' => $height
);
}
/**
* Return an image (Pageimage) sized/cropped to the specified dimensions.
*
* `$thumb = $image->size($width, $height, $options);`
*
* The default behavior of this method is to simply create and return a new resized version of the image,
* leaving the original in tact. Width and height of the target size are the the first two arguments.
* The third argument called `$options` enables you to modify the default behavior of the size() method
* in various ways. This method only creates the newly sized image once, and then it caches it. Future
* calls simply refer back to the previously resized image.
*
* ~~~~~
* // Get an image to resize
* $image = $page->images->first();
*
* // Create 400x300 thumbnail cropped to center
* $thumb = $image->size(400, 300);
*
* // Create thumbnail with cropping to top
* $thumb = $image->size(400, 300, 'north');
*
* // Create thumbnail while specifying $options
* $thumb = $image->size(400, 300, [
* 'cropping' => 'north',
* 'quality' => 60,
* 'upscaling' => false,
* 'sharpening' => 'medium'
* ]);
*
* // Output thumbnail
* echo "<img src='$thumb->url' />";
*
* // Create image of size predefined in $config->imageSizes (3.0.151+)
* $photo = $image->size('landscape');
* ~~~~~
*
* **About the $options argument**
*
* - If given a *string*, it is assumed to be the value for the "cropping" option.
* - If given an *integer*, it is assumed you are specifying a "quality" value (1-100).
* - If given a *boolean*, it is assumed you are specifying whether or not you want to allow "upscaling".
* - If given an *array*, you may specify any of the options below together:
*
* **All available $options**
*
* - `quality` (int): Quality setting 1-100 (default=90, or as specified in /site/config.php).
* - `upscaling` (bool): Allow image to be upscaled? (default=true).
* - `cropping` (string|bool|array): Cropping mode, see possible values in "cropping" section below (default=true).
* - `suffix` (string|array): Suffix word to identify the new image, or use array of words for multiple (default=none).
* - `forceNew` (bool): Force re-creation of the image even if it already exists? (default=false).
* - `sharpening` (string): Sharpening mode: "none", "soft", "medium", or "strong" (default=soft).
* - `autoRotation` (bool): Automatically correct rotation of images that provide this info? (default=true)
* - `rotate` (int): Rotate the image this many degrees, specify one of: 0, -270, -180, -90, 90, 180, or 270 (default=0).
* - `flip` (string): To flip, specify either "vertical" or "horizontal" (default=none).
* - `hidpi` (bool): Use HiDPI/retina pixel doubling? (default=false).
* - `hidpiQuality` (bool): Quality setting for HiDPI (default=40, typically lower than regular quality setting).
* - `cleanFilename` (bool): Clean filename of historical resize information for shorter filenames? (default=false).
* - `nameWidth` (int): Width to use for filename (default is to use specified $width argument).
* - `nameHeight` (int): Height to use for filename (default is to use specified $height argument).
* - `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**
*
* - `true` (bool): Auto detect and allow use of focus (default).
* - `false` (bool): Disallow cropping.
* - `center` (string): to crop to center of image.
* - `x111y222` (string): to crop by pixels, 111px from left and 222px from top (replacing 111 and 222 with your values).
* - `north` (string): Crop North (top), may also be just "n".
* - `northwest` (string): Crop from Northwest (top left), may also be just "nw".
* - `northeast` (string): Crop from Northeast (top right), may also be just "ne".
* - `south` (string): Crop South (bottom), may also be just "s".
* - `southwest` (string): Crop Southwest (bottom left), may also be just "sw".
* - `southeast` (string): Crop Southeast (bottom right), may also be just "se".
* - `west` (string): Crop West (left), may also be just "w".
* - `east` (string): Crop East (right), may alos be just "e".
* - `blank` (string): Specify a blank string to disallow cropping during resize.
* - `array(111,222)` (array): Array of integers index 0 is left pixels and index 1 is top pixels.
* - `array('11%','22%')` (array): Array of '%' appended strings where index 0 is left percent and index 1 is top percent.
*
* **Note about "quality" and "upscaling" options**
*
* ProcessWire doesn't keep separate copies of images with different "quality" or "upscaling" values.
* If you change these and a variation image at the existing dimensions already exists, then you'll still get the old version.
* To clear out an old version of an image, use the `Pageimage::removeVariations()` method in this class before calling
* size() with new quality or upscaling settings.
*
* #pw-group-resize-and-crop
* #pw-group-common
* #pw-hooks
*
* @param int|string $width Target width of new image or (3.0.151+) specify prefined image size name
* @param int|array $height Target height of new image or (3.0.151+) options array if no height argument needed
* @param array|string|int $options Array of options to override default behavior:
* - Specify `array` of options as indicated in the section above.
* - Or you may specify type `string` containing "cropping" value.
* - 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 will be returned.
*
*/
public function size($width, $height = 0, $options = array()) {
if(is_array($height)) {
$options = $height;
$height = 0;
}
if(!is_array($options)) {
$options = $this->sizeOptionsToArray($options);
}
if(is_string($width) && $width && !ctype_digit($width)) {
// named image size
return $this->sizeName($width, $options);
}
if($this->wire()->hooks->isHooked('Pageimage::size()')) {
$result = $this->__call('size', array($width, $height, $options));
} else {
$result = $this->___size($width, $height, $options);
}
if($result && $result !== $this) {
$options['_width'] = $width;
$options['_height'] = $height;
$result->set('sizeOptions', $options);
}
return $result;
}
/**
* Hookable version of size() with implementation
*
* See comments for size() method above.
*
* #pw-internal
*
* @param int $width
* @param int $height
* @param array|string|int $options
* @return Pageimage
*
*/
protected function ___size($width, $height, $options) {
$this->error = '';
if($this->ext === 'svg') return $this;
if(!is_array($options)) $options = $this->sizeOptionsToArray($options);
// originally requested options
$requestOptions = $options;
// default options
$defaultOptions = array(
'upscaling' => true,
'cropping' => true,
'interlace' => false,
'sharpening' => 'soft',
'quality' => 90,
'hidpiQuality' => 40,
'webpQuality' => 90,
'webpAdd' => false,
'webpName' => '', // use this for the webp file basename rather than mirroring from the jpg/png
'webpOnly' => false, // only keep the webp version (requires webpAdd option)
'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,
'cleanFilename' => false, // clean filename of historial resize information
'rotate' => 0,
'flip' => '',
'nameWidth' => null, // override width to use for filename, int when populated
'nameHeight' => null, // override height to use for filename, int when populated
'focus' => true, // allow single dimension resizes to use focus area?
'zoom' => null, // zoom override, used only if focus is applicable, int when populated
'allowOriginal' => false, // Return original image if already at requested dimensions? (must be only specified option)
);
$files = $this->wire()->files;
$config = $this->wire()->config;
$debug = $config->debug;
$configOptions = $config->imageSizerOptions;
$webpOptions = $config->webpOptions;
$createdVariationHookData = null; // populated as array only when new variation created (for createdVariation hook)
if(!empty($webpOptions['quality'])) $defaultOptions['webpQuality'] = $webpOptions['quality'];
if(!is_array($configOptions)) $configOptions = array();
$options = array_merge($defaultOptions, $configOptions, $options);
if($options['cropping'] === 1) $options['cropping'] = true;
$width = (int) $width;
$height = (int) $height;
if($options['allowOriginal'] && count($requestOptions) === 1) {
if((!$width || $this->width() == $width) && (!$height || $this->height() == $height)) {
// return original image if already at requested width/height
return $this;
}
}
if($options['cropping'] === true
&& empty($options['cropExtra'])
&& $options['focus'] && $this->hasFocus
&& $width && $height) {
// crop to focus area
$focus = $this->focus();
if(is_int($options['zoom'])) $focus['zoom'] = $options['zoom']; // override
$options['cropping'] = array("$focus[left]%", "$focus[top]%", "$focus[zoom]");
$crop = ''; // do not add suffix
} else if(is_string($options['cropping'])
&& strpos($options['cropping'], 'x') === 0
&& preg_match('/^x(\d+)[yx](\d+)/', $options['cropping'], $matches)) {
$options['cropping'] = true;
$options['cropExtra'] = array((int) $matches[1], (int) $matches[2], $width, $height);
$crop = '';
} else {
$crop = ImageSizer::croppingValueStr($options['cropping']);
}
if(!is_array($options['suffix'])) {
// convert to array
$options['suffix'] = empty($options['suffix']) ? array() : explode(' ', $options['suffix']);
}
if($options['rotate'] && !in_array(abs((int) $options['rotate']), array(90, 180, 270))) {
$options['rotate'] = 0;
}
if($options['rotate']) {
$options['suffix'][] = ($options['rotate'] > 0 ? "rot" : "tor") . abs($options['rotate']);
}
if($options['flip']) {
$options['suffix'][] = strtolower(substr($options['flip'], 0, 1)) == 'v' ? 'flipv' : 'fliph';
}
$suffixStr = '';
if(!empty($options['suffix'])) {
$suffix = $options['suffix'];
sort($suffix);
foreach($suffix as $key => $s) {
$s = strtolower($this->wire()->sanitizer->fieldName($s));
if(empty($s)) {
unset($suffix[$key]);
} else {
$suffix[$key] = $s;
}
}
if(count($suffix)) $suffixStr = '-' . implode('-', $suffix);
}
if($options['hidpi']) {
$suffixStr .= '-hidpi';
if($options['hidpiQuality']) $options['quality'] = $options['hidpiQuality'];
}
$originalName = $this->basename();
// determine basename without extension, i.e. myfile
$basename = basename($originalName, "." . $this->ext());
$originalSize = $debug ? @filesize($this->filename) : 0;
if($options['cleanFilename'] && strpos($basename, '.') !== false) {
$basename = substr($basename, 0, strpos($basename, '.'));
}
// filename uses requested width/height unless another specified via nameWidth or nameHeight options
$nameWidth = is_int($options['nameWidth']) ? $options['nameWidth'] : $width;
$nameHeight = is_int($options['nameHeight']) ? $options['nameHeight'] : $height;
// i.e. myfile.100x100.jpg or myfile.100x100nw-suffix1-suffix2.jpg
$basenameNoExt = $basename . '.' . $nameWidth . 'x' . $nameHeight . $crop . $suffixStr; // basename without ext
$basename = $basenameNoExt . '.' . $this->ext(); // basename with ext
$filenameUnvalidated = '';
$filenameUnvalidatedWebp = '';
$filenameFinal = $this->pagefiles->path() . $basename;
$filenameFinalExists = file_exists($filenameFinal);
if(!empty($options['webpName'])) {
$filenameFinalWebp = $this->pagefiles->path() . basename($options['webpName'], '.webp') . '.webp';
} else if(!empty($webpOptions['useSrcExt'])) {
$filenameFinalWebp = $this->pagefiles->path() . $basename . '.webp'; // file.jpg.webp
} else {
$filenameFinalWebp = $this->pagefiles->path() . $basenameNoExt . '.webp'; // file.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(!$filenameFinalExists && !file_exists($this->filename())) {
// no original file exists to create variation from
$this->error = "Original image does not exist to create size variation: " . $this->url();
} else if(!$filenameFinalExists || $options['forceNew']) {
// filenameUnvalidated is temporary filename used for resize
$tempDir = $this->pagefiles->page->filesManager()->getTempPath();
$filenameUnvalidated = $tempDir . $basename;
$filenameUnvalidatedWebp = $tempDir . $basenameNoExt . '.webp';
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($files->copy($this->filename(), $filenameUnvalidated)) {
try {
$timer = $debug ? Debug::timer() : null;
/** @var ImageSizer $sizer */
$sizer = $this->wire(new ImageSizer($filenameUnvalidated, $options));
/** @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')) {
// no engines support webp
$options['webpAdd'] = false;
$options['webpOnly'] = 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();
if(!empty($engineConfigData)) {
if(!empty($engineConfigData['quality']) && empty($options['hidpi']) && empty($requestOptions['quality'])) {
$engine->setQuality($engineConfigData['quality']);
$options['quality'] = $engineConfigData['quality'];
}
if(!empty($engineConfigData['sharpening']) && empty($requestOptions['sharpening'])) {
$engine->setSharpening($engineConfigData['sharpening']);
$options['sharpening'] = $engineConfigData['sharpening'];
}
}
if($sizer->resize($width, $height)) {
if($options['webpAdd'] && $options['webpOnly']) {
if(is_file($filenameUnvalidated)) $files->unlink($filenameUnvalidated);
} else {
clearstatcache();
if(!$files->rename($filenameUnvalidated, $filenameFinal)) {
if($files->exists($filenameFinal)) {
// potential race condition: another request won
} else {
$this->error = "Rename failed: $filenameUnvalidated => $filenameFinal";
}
}
}
if($options['webpAdd'] && file_exists($filenameUnvalidatedWebp)) {
$files->rename($filenameUnvalidatedWebp, $filenameFinalWebp);
}
} else {
$this->error = "ImageSizer::resize($width, $height) failed for $filenameUnvalidated";
}
if($debug && empty($options['webpOnly'])) $this->wire()->log->save('image-sizer',
str_replace('ImageSizerEngine', '', $sizer->getEngine()) . ' ' .
($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]) "
);
if(!$this->error) {
$createdVariationHookData = array(
'width' => $width,
'height' => $height,
'options' => $options,
'filenameUnvalidated' => $filenameUnvalidated,
'filenameFinal' => $filenameFinal,
'filenameUnvalidatedWebp' => $filenameUnvalidatedWebp,
'filenameFinalWebp' => $filenameFinalWebp,
);
}
} catch(\Exception $e) {
$this->trackException($e, false);
$this->error = $e->getMessage();
}
} else {
$this->error = "Unable to copy $this->filename => $filenameUnvalidated";
}
}
$pageimage = clone $this;
// 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 files
if($filenameFinal && $files->exists($filenameFinal)) $files->unlink($filenameFinal, true);
if($filenameUnvalidated && $files->exists($filenameUnvalidated)) $files->unlink($filenameUnvalidated);
if($filenameFinalWebp && $files->exists($filenameFinalWebp)) $files->unlink($filenameFinalWebp, true);
if($filenameUnvalidatedWebp && $files->exists($filenameUnvalidatedWebp)) $files->unlink($filenameUnvalidatedWebp);
// we also tell PW about it for logging and/or admin purposes
$this->error($this->error);
$logError = str_replace($config->paths('root'), $config->urls('root'), $filenameFinal) . " - $this->error";
$this->wire()->log->save('image-sizer', $logError);
}
$pageimage->setFilename($filenameFinal);
$pageimage->setOriginal($this);
if($createdVariationHookData) $this->createdVariation($pageimage, $createdVariationHookData);
return $pageimage;
}
protected function sizeOptionsToArray($options) {
if(is_array($options)) return $options;
if(is_string($options)) {
// optionally allow a string to be specified with crop direction, for shorter syntax
if(strpos($options, ',') !== false) $options = explode(',', $options); // 30,40
$options = array('cropping' => $options);
} else if(is_int($options)) {
// optionally allow an integer to be specified with quality, for shorter syntax
$options = array('quality' => $options);
} else if(is_bool($options)) {
// optionally allow a boolean to be specified with upscaling toggle on/off
$options = array('upscaling' => $options);
} else {
// unknown options type
$options = array();
}
return $options;
}
/**
* Same as size() but with width/height assumed to be hidpi width/height
*
* #pw-internal
*
* @param int|string $width
* @param int|array $height
* @param array $options See options in size() method.
* @return Pageimage
*
*/
public function hidpiSize($width, $height = 0, $options = array()) {
if(is_array($height)) {
$height['hidpi'] = true;
} else {
$options['hidpi'] = true;
}
return $this->size($width, $height, $options);
}
/**
* Return image of size indicated by predefined setting
*
* Settings for predefined sizes can be specified in `$config->imageSizes` array.
* Each named item in this array must contain at least 'width' and 'height, but can also
* contain any other option from the `Pageimage::size()` argument `$options`.
*
* @param string $name Image size name
* @param array $options Optionally add or override options defined for size.
* @return Pageimage
* @since 3.0.151
* @throws WireException If given a $name that is not present in $config->imageSizes
*
*/
public function sizeName($name, array $options = array()) {
$sizes = $this->wire()->config->imageSizes;
if(!isset($sizes[$name])) throw new WireException("Unknown image size '$name' (not in \$config->imageSizes)");
$size = $sizes[$name];
$options = array_merge($size, $options);
unset($options['width'], $options['height']);
if(!isset($size['width'])) $size['width'] = 0;
if(!isset($size['height'])) $size['height'] = 0;
return $this->size((int) $size['width'], (int) $size['height'], $options);
}
/**
* Create a crop and return it as a new Pageimage.
*
* ~~~~~
* // Create a crop starting 100 pixels from left, 200 pixels from top
* // at 150 pixels wide and 100 pixels tall
* $image = $page->images->first();
* $crop = $image->crop(100, 200, 150, 100);
*
* // Output the crop
* echo "<img src='$crop->url' />";
* ~~~~~
*
* #pw-group-resize-and-crop
*
* @param int $x Starting X position (left) in pixels
* @param int $y Starting Y position (top) in pixels
* @param int $width Width of crop in pixels
* @param int $height Height of crop in pixels
* @param array $options See options array for `Pageimage::size()` method.
* Avoid setting crop properties in $options since we are overriding them.
* @return Pageimage
*
*/
public function ___crop($x, $y, $width, $height, $options = array()) {
$x = (int) $x;
$y = (int) $y;
$width = (int) $width;
$height = (int) $height;
if(empty($options['suffix'])) {
$options['suffix'] = array();
} else if(!is_array($options['suffix'])) {
$options['suffix'] = array($options['suffix']);
}
$options['suffix'][] = "cropx{$x}y{$y}";
$options['cropExtra'] = array($x, $y, $width, $height);
$options['cleanFilename'] = true;
return $this->size($width, $height, $options);
}
/**
* Return the width of this image OR return an image sized with a given width (and proportional height).
*
* - If given a width, it'll return a new Pageimage object sized to that width.
* - If not given a width, it'll return the current width of this Pageimage.
*
* #pw-group-resize-and-crop
* #pw-group-common
*
* ~~~~~
* // Get width of image
* $px = $image->width();
*
* // Create a new variation at 200px width
* $thumb = $image->width(200);
* ~~~~~
*
* @param int $n Optional width if you are creating a new size.
* @param array|string|int|bool $options See `Pageimage::size()` $options argument for details.
* @return int|Pageimage Returns width (in px) when given no arguments, or Pageimage when given a width argument.
*
*/
public function width($n = 0, $options = array()) {
if($n) return $this->size($n, 0, $options);
$info = $this->getImageInfo();
return $info['width'];
}
/**
* Return the height of this image OR return an image sized with a given height (and proportional width).
*
* - If given a height, it'll return a new Pageimage object sized to that height.
* - If not given a height, it'll return the height of this Pageimage.
*
* #pw-group-resize-and-crop
* #pw-group-common
*
* ~~~~~
* // Get height of image
* $px = $image->height();
*
* // Create a new variation at 200px height
* $thumb = $image->height(200);
* ~~~~~
*
* @param int $n Optional height if you are creating a new size.
* @param array|string|int|bool $options See `Pageimage::size()` $options argument for details.
* @return int|Pageimage Returns height (in px) when given no arguments, or Pageimage when given a height argument.
*
*/
public function height($n = 0, $options = array()) {
if($n) return $this->size(0, $n, $options);
$info = $this->getImageInfo();
return $info['height'];
}
/**
* Return width for hidpi/retina use, or resize an image for desired hidpi width
*
* If the $width argument is omitted or provided as a float, hidpi width (int) is returned (default scale=0.5)
* If $width is provided (int) then a new Pageimage is returned at that width x 2 (for hidpi use).
*
* #pw-internal
*
* @param int|float $width Specify int to return resized image for hidpi, or float (or omit) to return current width at hidpi.
* @param array $options Optional options for use when resizing, see size() method for details.
* Or you may specify an int as if you want to return a hidpi width and want to calculate with that width rather than current image width.
* @return int|Pageimage|string
*
*/
public function hidpiWidth($width = 0, $options = array()) {
if(is_string($width)) {
if(ctype_digit("$width")) {
$width = (int) $width;
} else if($width === "100%") {
return $this;
} else if(ctype_digit(str_replace(".", "", $width))) {
$width = (float) $width;
}
}
if(is_float($width) || $width < 1) {
// return hidpi width intended: scale omitted or provided in $width argument
$scale = $width;
if(!$scale || $scale < 0 || $scale > 1) $scale = 0.5;
if($options === "100%") return $options;
$width = is_array($options) ? 0 : (int) $options;
if($width < 1) $width = $this->width();
if($width === "100%") return $width;
return ceil($width * $scale);
} else if($width && is_int($width)) {
// resize intended
if(!is_array($options)) $options = array();
return $this->hidpiSize((int) $width, 0, $options);
}
return 0; // not possible to reach, but pleases the inspection
}
/**
* Return height for hidpi/retina use, or resize an image for desired hidpi height
*
* If the $height argument is omitted or provided as a float, hidpi height (int) is returned (default scale=0.5)
* If $height is provided (int) then a new Pageimage is returned at that height x 2 (for hidpi use).
*
* #pw-internal
*
* @param int|float $height Specify int to return resized image for hidpi, or float (or omit) to return current width at hidpi.
* @param array|int $options Optional options for use when resizing, see size() method for details.
* Or you may specify an int as if you want to return a hidpi height and want to calculate with that height rather than current image height.
* @return int|Pageimage
*
*/
public function hidpiHeight($height = 0, $options = array()) {
if(is_float($height) || $height < 1) {
// return hidpi height intended: scale omitted or provided in $height argument
$scale = $height;
if(!$scale || $scale < 0 || $scale > 1) $scale = 0.5;
$height = is_array($options) ? 0 : (int) $options;
if($height < 1) $height = $this->height();
return ceil($height * $scale);
} else if($height) {
// resize intended
if(!is_array($options)) $options = array();
return $this->hidpiSize(0, (int) $height, $options);
}
return 0; // not possible to reach but pleases the inspection
}
/**
* Return an image no larger than the given width.
*
* If source image is equal to or smaller than the requested dimension,
* then it will remain that way and the source image is returned (not a copy).
*
* If the source image is larger than the requested dimension, then a new copy
* will be returned at the requested dimension.
*
* #pw-group-resize-and-crop
*
* @param int $n Maximum width
* @param array $options See `Pageimage::size()` method for options
* @return Pageimage
*
*/
public function maxWidth($n, array $options = array()) {
$options['upscaling'] = false;
if($this->width() > $n) return $this->width($n, $options);
return $this;
}
/**
* Return an image no larger than the given height.
*
* If source image is equal to or smaller than the requested dimension,
* then it will remain that way and the source image is returned (not a copy).
*
* If the source image is larger than the requested dimension, then a new copy
* will be returned at the requested dimension.
*
* #pw-group-resize-and-crop
*
* @param int $n Maximum height
* @param array $options See `Pageimage::size()` method for options
* @return Pageimage
*
*/
public function maxHeight($n, array $options = array()) {
$options['upscaling'] = false;
if($this->height() > $n) return $this->height($n, $options);
return $this;
}
/**
* Return an image no larger than the given width and height
*
* #pw-group-resize-and-crop
*
* @param int $width Max allowed width
* @param int $height Max allowed height
* @param array $options See `Pageimage::size()` method for options, or these additional options:
* - `allowOriginal` (bool): Allow original image to be returned if already within max requested dimensions? (default=false)
* @return Pageimage
*
*/
public function maxSize($width, $height, $options = array()) {
$defaults = array(
'allowOriginal' => false,
'upscaling' => false,
'cropping' => false
);
$options = array_merge($defaults, $options);
$adjustedWidth = $width < 1 || $this->width() <= $width ? 0 : $width;
$adjustedHeight = $height < 1 || $this->height() <= $height ? 0 : $height;
// if already within maxSize dimensions then do nothing
if(!$adjustedWidth && !$adjustedHeight) {
if($options['allowOriginal']) return $this; // image already within target
$adjustedWidth = $width;
$options['nameHeight'] = $height;
} else if(!$adjustedWidth) {
$options['nameWidth'] = $width;
} else if(!$adjustedHeight) {
$options['nameHeight'] = $height;
}
if($this->wire()->config->installed > 1513336849) {
// New installations from 2017-12-15 forward use an "ms" suffix for images from maxSize() method
$suffix = isset($options['suffix']) ? $options['suffix'] : array();
if(!is_array($suffix)) $suffix = array();
$suffix[] = 'ms';
$options['suffix'] = $suffix;
}
return $this->size($adjustedWidth, $adjustedHeight, $options);
}
/**
* Get ratio of width divided by height
*
* @param int $precision Optionally specify a value >2 for custom precision (default=2) 3.0.211+
* @return float
* @since 3.0.154
*
*/
public function ratio($precision = 2) {
$width = $this->width();
$height = $this->height();
if($width === $height) return 1.0;
$ratio = $width / $height;
$ratio = round($ratio, max(2, (int) $precision));
if($ratio > 99.99) $ratio = 99.99; // max allowed width>height ratio
if($ratio < 0.01) $ratio = 0.01; // min allowed height>width ratio
return $ratio;
}
/**
* Get the PageimageVariations helper instancd
*
* #pw-internal
*
* @return PageimageVariations
*
*/
public function variations() {
if($this->variations === null) $this->variations = new PageimageVariations($this);
return $this->variations;
}
/**
* Get all size variations of this image
*
* This is useful after a delete of an image (for example). This method can be used to track down all the
* child files that also need to be deleted.
*
* #pw-group-variations
*
* @param array $options Optional, one or more options in an associative array of the following:
* - `info` (bool): when true, method returns variation info arrays rather than Pageimage objects (default=false).
* - `verbose` (bool|int): Return verbose array of info. If false, returns only filenames (default=true).
* This option does nothing unless the `info` option is true. Also note that if verbose is false, then all options
* following this one no longer apply (since it is no longer returning width/height info).
* When integer 1, returned info array also includes Pageimage variation options in 'pageimage' index of
* returned arrays (since 3.0.137).
* - `width` (int): only variations with given width will be returned
* - `height` (int): only variations with given height will be returned
* - `width>=` (int): only variations with width greater than or equal to given will be returned
* - `height>=` (int): only variations with height greater than or equal to given will be returned
* - `width<=` (int): only variations with width less than or equal to given will be returned
* - `height<=` (int): only variations with height less than or equal to given will be returned
* - `suffix` (string): only variations having the given suffix will be returned
* - `suffixes` (array): only variations having one of the given suffixes will be returned
* - `noSuffix` (string): exclude variations having this suffix
* - `noSuffixes` (array): exclude variations having any of these suffixes
* - `name` (string): only variations containing this text in filename will be returned (case insensitive)
* - `noName` (string): only variations NOT containing this text in filename will be returned (case insensitive)
* - `regexName` (string): only variations that match this PCRE regex will be returned
* @return Pageimages|array Returns Pageimages array of Pageimage instances.
* Only returns regular array if provided `$options['info']` is true.
*
*/
public function getVariations(array $options = array()) {
return $this->variations()->find($options);
}
/**
* Rebuilds variations of this image
*
* By default, this excludes crops and images with suffixes, but can be overridden with the `$mode` and `$suffix` arguments.
*
* **Options for $mode argument**
*
* - `0` (int): Rebuild only non-suffix, non-crop variations, and those w/suffix specified in $suffix argument. ($suffix is INCLUSION list)
* - `1` (int): Rebuild all non-suffix variations, and those w/suffix specifed in $suffix argument. ($suffix is INCLUSION list)
* - `2` (int): Rebuild all variations, except those with suffix specified in $suffix argument. ($suffix is EXCLUSION list)
* - `3` (int): Rebuild only variations specified in the $suffix argument. ($suffix is ONLY-INCLUSION list)
* - `4` (int): Rebuild only non-proportional, non-crop variations (variations that specify both width and height)
*
* Mode 0 is the only truly safe mode, as in any other mode there are possibilities that the resulting
* rebuild of the variation may not be exactly what was intended. The issues with other modes primarily
* arise when the suffix means something about the technical details of the produced image, or when
* rebuilding variations that include crops from an original image that has since changed dimensions or crops.
*
* #pw-group-variations
*
* @param int $mode See the options for $mode argument above (default=0).
* @param array $suffix Optional argument to specify suffixes to include or exclude (according to $mode).
* @param array $options See $options for `Pageimage::size()` for details.
* @return array Returns an associative array with with the following indexes:
* - `rebuilt` (array): Names of files that were rebuilt.
* - `skipped` (array): Names of files that were skipped.
* - `errors` (array): Names of files that had errors.
* - `reasons` (array): Reasons why files were skipped or had errors, associative array indexed by file name.
*
*/
public function ___rebuildVariations($mode = 0, array $suffix = array(), array $options = array()) {
return $this->variations()->rebuild($mode, $suffix, $options);
}
/**
* Given a file name (basename), return array of info if this is a variation for this instances file, or false if not.
*
* Returned array includes the following indexes:
*
* - `original` (string): Original basename
* - `url` (string): URL to image
* - `path` (string): Full path + filename to image
* - `width` (int): Specified width in filename
* - `height` (int): Specified height in filename
* - `actualWidth` (int): Actual width when checked manually
* - `actualHeight` (int): Acual height when checked manually
* - `crop` (string): Cropping info string or blank if none
* - `suffix` (array): Array of suffixes
*
* The following are only present if variation is based on another variation, and thus has a parent variation
* image between it and the original:
*
* - `suffixAll` (array): Contains all suffixes including among parent variations
* - `parent` (array): Variation info array of direct parent variation file
*
* #pw-group-variations
*
* @param string $basename Filename to check (basename, which excludes path)
* @param array|bool $options Array of options to modify behavior, or boolean to only specify `allowSelf` option.
* - `allowSelf` (bool): When true, it will return variation info even if same as current Pageimage. (default=false)
* - `verbose` (bool): Return verbose array of info? If false, just returns basename (string) or false. (default=true)
* @return bool|string|array Returns false if not a variation, or array (verbose) or string (non-verbose) of info if it is.
*
*/
public function ___isVariation($basename, $options = array()) {
return $this->variations()->getInfo($basename, $options);
}
/**
* Delete all the alternate sizes associated with this Pageimage
*
* #pw-group-variations
*
* @param array $options See options for getVariations() method to limit what variations are removed, plus these:
* - `dryRun` (bool): Do not remove now and instead only return the filenames of variations that would be deleted (default=false).
* - `getFiles` (bool): Return deleted filenames? Also assumed if the test option is used (default=false).
* @return PageImageVariations|array Returns $this by default, or array of deleted filenames if the `getFiles` option is specified
*
*/
public function removeVariations(array $options = array()) {
return $this->variations()->remove($options);
}
/**
* Hook called after successful creation of image variation
*
* @param Pageimage $image The variation image that was created
* @param array $data Verbose associative array of data used to create the variation
* @since 3.0.180
*
*/
protected function ___createdVariation(Pageimage $image, array $data) { }
/**
* Identify this Pageimage as a variation, by setting the Pageimage it was resized from.
*
* #pw-group-variations
*
* @param Pageimage $image
* @return $this
*
*/
public function setOriginal(Pageimage $image) {
$this->original = $image;
return $this;
}
/**
* If this image is a variation, return the original, otherwise return null.
*
* #pw-group-variations
*
* @return Pageimage|null
*
*/
public function getOriginal() {
if($this->original) return $this->original;
$info = $this->isVariation($this->basename(), true);
if($info === false) return null;
$this->original = $this->pagefiles->get($info['original']);
return $this->original;
}
/**
* Delete the physical file(s) associated with this Pagefile
*
* #pw-internal Public API should use delete method from Pageimages
*
*/
public function unlink() {
parent::unlink();
$this->removeVariations();
return $this;
}
/**
* Copy this Pageimage and any of its variations to another path
*
* #pw-internal
*
* @param string $path
* @return bool True if successful
*
*/
public function copyToPath($path) {
$files = $this->wire()->files;
if(parent::copyToPath($path)) {
foreach($this->getVariations() as $variation) {
/** @var Pageimage $variation */
if(!is_file($variation->filename)) continue;
$files->copy($variation->filename, $path);
}
return true;
}
return false;
}
/**
* Render markup for this image (optionally using a provided markup template string and/or image size options)
*
* Given template string can contain any of the placeholders, which will be replaced:
* - `{url}` or `{src}` Image URL (typically used for src attribute)
* - `{httpUrl}` File URL with scheme and hostname (alternate for src attribute)
* - `{URL}` Same as url but with cache busting query string
* - `{HTTPURL}` Same as httpUrl but with cache busting query string
* - `{description}` or `{alt}` Image description (typically used in alt attribute)
* - `{tags}` File tags (might be useful in class attribute)
* - `{width}` Width of image
* - `{height}` Height of image
* - `{hidpiWidth}` HiDPI width of image
* - `{hidpiHeight}` HiDPI height of image
* - `{ext}` File extension
* - `{original.name}` Replace “name” with any of the properties above to refer to original image.
* If there is no original image then these just refer back to the current image.
*
* ~~~~~
* $image = $page->images->first();
* if($image) {
* // default output
* echo $image->render();
*
* // custom output
* echo $image->render("<img class='pw-image' src='{url}' alt='{alt}'>");
*
* // custom output with options
* echo $image->render("<img src='{url}' alt='{alt}'>", [ 'width' => 300 ]);
*
* // options can go in first argument if you prefer
* echo $image->render([ 'width' => 300, 'height' => 200 ]);
*
* // if only width/height are needed, they can also be specified as a string (1st or 2nd arg)
* echo $image->render('300x200');
*
* // custom output with link to original/full-size and square crop of 300x300 for thumbnail
* echo $image->render([
* 'markup' => "<a href='{original.url}'><img src='{url}' alt='{alt}'></a>",
* 'width' => 300,
* 'height' => 300
* ]);
* }
* ~~~~~
*
* @param string|array $markup Markup template string or optional $options array if you do not want the template string here.
* @param array|string $options Optionally resize image with these options sent to size() method:
* - `width` (int): Target width or 0 for current image size (or proportional if height specified).
* - `height` (int): Target height or 0 for current image size (or proportional if width specified).
* - `markup` (string): Markup template string (same as $markup argument), or omit for default (same as $markup argument).
* - `link` (bool): Link image to original size? Though you may prefer to do this with your own $markup (see examples). (default=false)
* - `alt` (string): Text to use for “alt” attribute (default=text from image description).
* - `class` (string): Text to use for “class” attribute, if `{class}` present in markup (default='').
* - Plus any option available to the $options argument on the `Pageimage::size()` method.
* - If you only need width and/or height, you can specify a width x height string, i.e. 123x456 (use 0 for proportional).
* @return string
* @see Pageimages::render()
* @since 3.0.126
*
*/
public function ___render($markup = '', $options = array()) {
if(is_array($markup) || ($markup && strpos($markup, '}') === false)) {
$options = $markup;
$markup = isset($options['markup']) ? $options['markup'] : '';
}
if(empty($markup)) {
// provide default markup
if(empty($options['class'])) {
$markup = "<img src='{url}' alt='{alt}' />";
} else {
$markup = "<img src='{url}' alt='{alt}' class='{class}' />";
}
}
if(is_string($options)) {
if(ctype_digit(str_ireplace('x', '', $options))) {
if(stripos($options, 'x') === false) $options .= 'x0';
list($w, $h) = explode('x', strtolower($options));
$options = array('width' => (int) $w, 'height' => (int) $h);
} else {
$options = array();
}
}
$sanitizer = $this->wire()->sanitizer;
$image = $this;
$original = null;
$replacements = array();
$properties = array(
'url', 'src', 'httpUrl', 'URL', 'HTTPURL',
'description', 'alt', 'tags', 'ext', 'class',
'width', 'height', 'hidpiWidth', 'hidpiHeight',
);
if(!empty($options['width']) || !empty($options['height'])) {
$w = isset($options['width']) ? (int) $options['width'] : 0;
$h = isset($options['height']) ? (int) $options['height'] : 0;
$original = $this;
$image = $this->size($w, $h, $options);
}
if(!empty($options['link']) && strpos($markup, '<a ') === false) {
$markup = "<a href='{original.url}'>$markup</a>";
}
foreach($properties as $property) {
$tag = '{' . $property . '}';
if(strpos($markup, $tag) === false) continue;
if(($property === 'alt' || $property === 'class') && isset($options[$property])) {
$value = $sanitizer->entities($options[$property]);
} else {
$value = $sanitizer->entities1($image->get($property));
}
$replacements[$tag] = $value;
}
if(strpos($markup, '{class}')) {
$class = isset($options['class']) ? $sanitizer->entities($options['class']) : 'pw-pageimage';
$replacements["{class}"] = $class;
}
if(strpos($markup, '{original.') !== false) {
if(!$original) $original = $image->getOriginal();
if(!$original) $original = $image;
foreach($properties as $property) {
$tag = '{original.' . $property . '}';
if(strpos($markup, $tag) === false) continue;
$value = $sanitizer->entities1($original->get($property));
$replacements[$tag] = $value;
}
}
return str_replace(array_keys($replacements), array_values($replacements), $markup);
}
/**
* Install this Pagefile
*
* Implies copying the file to the correct location (if not already there), and populating it's name
*
* #pw-internal
*
* @param string $filename Full path and filename of file to install
* @throws WireException
*
*/
protected function ___install($filename) {
parent::___install($filename);
if(!$this->width()) {
parent::unlink();
throw new WireException($this->_('Unable to install invalid image'));
}
}
/**
* 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');
$webp->setArray($this->wire()->config->webpOptions);
$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) {
$original = $this->original;
/** @var PagefileExtra $webp */
$webp = $event->object;
$webp->unlink();
if($original && isset($this->sizeOptions['_width'])) {
// we are in an image resized from an original
$options = $this->sizeOptions;
$width = $options['_width'];
$height = $options['_height'];
} else {
// we are the original
// create a file with same name as original but with .webp extension
$original = $this;
$options = array(
'allowOriginal' => false,
'webpName' => $webp->useSrcExt ? $this->basename() : basename($this->basename(), ".$this->ext"),
'webpOnly' => true
);
$width = $this->width;
$height = 0;
}
$options['webpAdd'] = true;
try {
$original->size($width, $height, $options);
} catch(\Exception $e) {
$this->error = ($this->error ? "$this->error - " : "") . $e->getMessage();
}
$error = $this->error;
$event->return = empty($error);
}
/**
* 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;
}
/**
* Rename this file
*
* Remember to follow this up with a `$page->save()` for the page that the file lives on.
*
* #pw-group-manipulation
*
* @param string $basename New name to use. Must be just the file basename (no path).
* @return string|bool Returns new name (basename) on success, or boolean false if rename failed.
*
*/
public function rename($basename) {
$variations = $this->getVariations();
$oldBasename = $this->basename;
$newBasename = parent::rename($basename);
if($newBasename === false) return false;
$ext = '.' . $this->ext();
$oldName = basename($oldBasename, $ext);
$newName = basename($newBasename, $ext);
foreach($variations as $pageimage) {
/** @var Pageimage $pageimage */
if(strpos($pageimage->basename, $oldName) !== 0) continue;
$newVariationName = $newName . substr($pageimage->basename, strlen($oldName));
$pageimage->rename($newVariationName);
}
return $newBasename;
}
/**
* Replace file with another
*
* Should be followed up with a save() to ensure related properties are also committed to DB.
*
* #pw-internal
*
* @param string $filename File to replace current one with
* @param bool $move Move given $filename rather than copy? (default=true)
* @return bool
* @throws WireException
* @since 3.0.154
*
*/
public function replaceFile($filename, $move = true) {
if(!parent::replaceFile($filename, $move)) return false;
$this->getImageInfo(true);
return true;
}
public function __isset($key) {
if($key === 'original') return $this->original !== null;
return parent::__isset($key);
}
/**
* Basic debug info
*
* @return array
*
*/
public function __debugInfo() {
return $this->debugInfo->getBasicDebugInfo();
}
/**
* Verbose debug info (via @horst)
*
* Optionally with individual options array.
*
* @param array $options The individual options you also passes with your image variation creation
* @param string $returnType 'string'|'array'|'object', default is 'string' and returns markup or plain text
* @return array|object|string
* @since 3.0.132
*
*/
public function getDebugInfo($options = array(), $returnType = 'string') {
return $this->debugInfo->getVerboseDebugInfo($options, $returnType);
}
/**
* Get debug info from parent class
*
* #pw-internal
*
* @return array
* @since 3.0.132
*
*/
public function _parentDebugInfo() {
return parent::__debugInfo();
}
}