images as $image) { * // $image and $thumb are both Pageimage objects * $thumb = $image->size(200, 200); * echo ""; * echo "$image->description"; * echo ""; * } * ~~~~~ * #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 file’s description field (string), if enabled. Note you can also set this property directly. * @property string $tags Value of the file’s tags field (string), if enabled. #pw-group-tags * @property string $ext File’s 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 someone’s 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 PHP’s 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 ""; * * // 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 ""; * ~~~~~ * * #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 instance’s 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("{alt}"); * * // custom output with options * echo $image->render("{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' => "{alt}", * '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 = "{alt}"; } else { $markup = "{alt}"; } } 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, '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(); } }