Rev 1 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download
<?php namespace ProcessWire;/*** ProcessWire PageimageVariations** Helper class for Pageimage that handles variation collection methods** ProcessWire 3.x, Copyright 2022 by Ryan Cramer* https://processwire.com** @since 3.0.137**/class PageimageVariations extends Wire implements \IteratorAggregate, \Countable {/*** @var Pageimage**/protected $pageimage;/*** @var Pagefiles|Pageimages**/protected $pagefiles;/*** @var Pageimages|null**/protected $variations = null;/*** Construct** @param Pageimage $pageimage**/public function __construct(Pageimage $pageimage) {$this->pageimage = $pageimage;$this->pagefiles = $pageimage->pagefiles;$pageimage->wire($this);parent::__construct();}#[\ReturnTypeWillChange]public function getIterator() {return $this->find();}/*** Return a total or filtered count of variations** This method is also here to implement the Countable interface.** @param array $options See options for find() method* @return int**/#[\ReturnTypeWillChange]public function count($options = array()) {if($this->variations) {$count = $this->variations->count();} else {$options['count'] = true;$count = $this->find($options);}return $count;}/*** 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** @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 getInfo($basename, $options = array()) {$defaults = array('allowSelf' => false,'verbose' => true,);if(!is_array($options)) $options = array('allowSelf' => (bool) $options);$options = array_merge($defaults, $options);static $level = 0;$variationName = basename($basename);$originalName = $this->pageimage->basename;// that that everything from the beginning up to the first period is exactly the same// otherwise, they are different source files$test1 = substr($variationName, 0, strpos($variationName, '.'));$test2 = substr($originalName, 0, strpos($originalName, '.'));if($test1 !== $test2) return false;// remove extension from originalName$originalName = basename($originalName, "." . $this->pageimage->ext());// if originalName is already a variation filename, remove the variation info from it.// reduce to original name, i.e. all info after (and including) a periodif(strpos($originalName, '.') && preg_match('/^([^.]+)\.(?:\d+x\d+|-[_a-z0-9]+)/', $originalName, $matches)) {$originalName = $matches[1];}// if file is the same as the original, then it's not a variationif(!$options['allowSelf'] && $variationName == $this->pageimage->basename) return false;// if file doesn't start with the original name then it's not a variationif(strpos($variationName, $originalName) !== 0) return false;// get down to the meat and the base// meat is the part of the filename containing variation info like dimensions, crop, suffix, etc.// base is the part before that, which may include parent meat$pos = strrpos($variationName, '.'); // get extension$ext = substr($variationName, $pos);$base = substr($variationName, 0, $pos); // get without extension$rpos = strrpos($base, '.'); // get last data chunk after dotif($rpos !== false) {$meat = substr($base, $rpos+1) . $ext; // the part of the filename we're interested in$base = substr($base, 0, $rpos); // the rest of the filename$parent = "$base." . $this->pageimage->ext();} else {$meat = $variationName;$parent = null;}// identify parent and any parent suffixes$suffixAll = array();if($options['verbose']) {while(($pos = strrpos($base, '.')) !== false) {$part = substr($base, $pos + 1);$base = substr($base, 0, $pos);while(($rpos = strrpos($part, '-')) !== false) {$suffixAll[] = substr($part, $rpos + 1);$part = substr($part, 0, $rpos);}}}// variation name with size dimensions and optionally suffix$re1 = '/^' .'(\d+)x(\d+)' . // 50x50'([pd]\d+x\d+|[a-z]{1,2})?' . // nw or p30x40 or d30x40'(?:-([-_a-z0-9]+))?' . // -suffix1 or -suffix1-suffix2, etc.'\.' . $this->pageimage->ext() .'$/';// variation name with suffix only$re2 = '/^' .'-([-_a-z0-9]+)' . // suffix1 or suffix1-suffix2, etc.'(?:\.' . // optional extras for dimensions/crop, starts with period'(\d+)x(\d+)' . // optional 50x50'([pd]\d+x\d+|[a-z]{1,2})?' . // nw or p30x40 or d30x40')?' .'\.' . $this->pageimage->ext() .'$/';// if regex does not match, return falseif(preg_match($re1, $meat, $matches)) {// this is a variation with dimensions, return array of info$width = (int) $matches[1];$height = (int) $matches[2];$crop = isset($matches[3]) ? $matches[3] : '';$suffix = isset($matches[4]) ? explode('-', $matches[4]) : array();} else if(preg_match($re2, $meat, $matches)) {// this is a variation only with suffix$width = isset($matches[2]) ? (int) $matches[2] : 0;$height = isset($matches[3]) ? (int) $matches[3] : 0;$crop = isset($matches[4]) ? $matches[4] : '';$suffix = explode('-', $matches[1]);} else {return false;}// if not in verbose mode, just return variation basenameif(!$options['verbose']) return $variationName;$path = $this->pagefiles->path . $basename;$actualInfo = $this->pageimage->getImageInfo($path);$info = array('name' => $basename,'url' => $this->pagefiles->url . $basename,'path' => $path,'original' => $originalName . '.' . $this->pageimage->ext(),'width' => $width,'height' => $height,'crop' => $crop,'suffix' => $suffix,'suffixAll' => array(), // present only when image has a parent variation'actualWidth' => $actualInfo['width'],'actualHeight' => $actualInfo['height'],'hidpiWidth' => $this->pageimage->hidpiWidth(0, $actualInfo['width']),'hidpiHeight' => $this->pageimage->hidpiWidth(0, $actualInfo['height']),'parentName' => '', // present only when image has a parent variation'parent' => null, // present only when image has a parent variation'webpUrl' => '','webpPath' => '',);foreach($this->pageimage->extras() as $name => $extra) {if($extra->exists()) {$info["{$name}Url"] = $extra->url(false);$info["{$name}Path"] = $extra->filename();continue;}$f = "$basename.$extra->ext"; // useSrcExt, i.e. file.png.webpif(is_readable($this->pagefiles->path . $f)) {$info["{$name}Url"] = $this->pagefiles->url . $f;$info["{$name}Path"] = $this->pagefiles->path . $f;continue;}$f = basename($basename, '.' . $this->pageimage->ext()) . ".$extra->ext";if(is_readable($this->pagefiles->path . $f)) {$info["{$name}Url"] = $this->pagefiles->url . $f;$info["{$name}Path"] = $this->pagefiles->path . $f;continue;}}if(empty($info['crop'])) {// attempt to extract crop info from suffixforeach($info['suffix'] as $key => $suffix) {if(strpos($suffix, 'cropx') === 0) {$info['crop'] = ltrim($suffix, 'crop'); // i.e. x123y456}}}if($parent) {// suffixAll includes all parent suffix in addition to current suffixif(!$level) $info['suffixAll'] = array_unique(array_merge($info['suffix'], $suffixAll));// parent property is set with more variation info, when available$level++;$info['parentName'] = $parent;$info['parent'] = $this->getInfo($parent);$level--;} else {unset($info['parent'], $info['parentName'], $info['suffixAll']);}if(!$this->pageimage->__isset('original') && $info['original']) {$original = $this->pagefiles->get($info['original']);if($original) $this->pageimage->setOriginal($original);}return $info;}/*** 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.** @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).* - `count` (bool): when true, only a count of variations is returned (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|int Returns Pageimages array of Pageimage instances.* Only returns regular array if provided `$options['info']` is true.* Returns integer if count option is specified.**/public function find(array $options = array()) {if(!is_null($this->variations) && empty($options)) return $this->variations;$defaults = array('info' => false,'verbose' => true,'count' => false,);$options = array_merge($defaults, $options);if($options['count']) {$options['verbose'] = false;$options['info'] = false;} else if(!$options['verbose'] && !$options['info']) {$options['verbose'] = true; // non-verbose only allowed if info==true}$variations = null;$dir = new \DirectoryIterator($this->pagefiles->path);$infos = array();$count = 0;if(!$options['info'] && !$options['count']) {$variations = $this->wire(new Pageimages($this->pagefiles->page));}// if suffix or noSuffix option contains space, convert it to suffixes or noSuffixes array optionforeach(array('suffix', 'noSuffix') as $key) {if(!isset($options[$key])) continue;if(strpos(trim($options[$key]), ' ') === false) continue;$keyPlural = $key . 'es';$value = isset($options[$keyPlural]) ? $options[$keyPlural] : array();$options[$keyPlural] = array_merge($value, explode(' ', trim($options[$key])));unset($options[$key]);}foreach($dir as $file) {if($file->isDir() || $file->isDot()) continue;$info = $this->getInfo($file->getFilename(), array('verbose' => $options['verbose']));if(!$info) continue;if($options['info'] && !$options['verbose']) {$infos[] = $info;continue;}$allow = true;foreach($options as $option => $value) {switch($option) {case 'width': $allow = $info['width'] == $value; break;case 'width>=': $allow = $info['width'] >= $value; break;case 'width<=': $allow = $info['width'] <= $value; break;case 'height': $allow = $info['height'] == $value; break;case 'height>=': $allow = $info['height'] >= $value; break;case 'height<=': $allow = $info['height'] <= $value; break;case 'name': $allow = stripos($file->getBasename(), $value) !== false; break;case 'noName': $allow = stripos($file->getBasename(), $value) === false; break;case 'regexName': $allow = preg_match($value, $file->getBasename()); break;case 'suffix': $allow = in_array($value, $info['suffix']); break;case 'noSuffix': $allow = !in_array($value, $info['suffix']); break;case 'suffixes':// any one of given suffixes will allow the variation$allow = false;foreach($value as $suffix) {$allow = in_array($suffix, $info['suffix']);if($allow) break;}break;case 'noSuffixes':// any one of the given suffixes will disallow the variation$allow = true;foreach($value as $noSuffix) {if(!in_array($noSuffix, $info['suffix'])) continue;$allow = false;break;}break;}if(!$allow) break;}if(!$allow) continue;$basename = $file->getBasename();if($options['count']) {$count++;continue;}if(empty($options['info']) || $options['verbose'] === 1) {$pageimage = clone $this->pageimage;$pathname = $file->getPathname();if(DIRECTORY_SEPARATOR != '/') $pathname = str_replace(DIRECTORY_SEPARATOR, '/', $pathname);$pageimage->setFilename($pathname);$pageimage->setOriginal($this->pageimage);if($options['verbose'] === 1) {$info['pageimage'] = $pageimage;} else if($variations) {$variations->add($pageimage);}}if(!empty($options['info'])) {$infos[$basename] = $info;}}if($options['count']) return $count;if(!empty($options['info'])) return $infos;if(empty($options)) $this->variations = $variations;return $variations;}/*** 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.** @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 rebuild($mode = 0, array $suffix = array(), array $options = array()) {$files = $this->wire()->files;$skipped = array();$rebuilt = array();$errors = array();$reasons = array();$options['forceNew'] = true;foreach($this->find(array('info' => true)) as $info) {$o = $options;unset($o['cropping']);$skip = false;$name = $info['name'];$hadWebp = false;if($info['crop'] && !$mode) {// skip crops when mode is 0$reasons[$name] = "$name: Crop is $info[crop] and mode is 0";$skip = true;} else if(count($info['suffix'])) {// check suffixesforeach($info['suffix'] as $k => $s) {if($s === 'hidpi') {// allow hidpi to passthru$o['hidpi'] = true;} else if($s == 'is') {// this is a known core suffix that we allow} else if(strpos($s, 'cropx') === 0) {// skip cropx suffix (already known from $info[crop])unset($info['suffix'][$k]);continue;} else if(strpos($s, 'pid') === 0 && preg_match('/^pid\d+$/', $s)) {// allow pid123 to pass through} else if(in_array($s, $suffix)) {// suffix is one provided in $suffix argumentif($mode == 2) {// mode 2 where $suffix is an exclusion list$skip = true;$reasons[$name] = "$name: Suffix '$s' is one provided in exclusion list (mode==true)";} else {// allowed suffix}} else {// image has suffix not specified in $suffix argumentif($mode == 0 || $mode == 1 || $mode == 3) {$skip = true;$reasons[$name] = "$name: Image has suffix '$s' not provided in allowed list: " . implode(', ', $suffix);}}}}if($mode == 4 && ($info['width'] == 0 || $info['height'] == 0)) {// skip images that don't specify both width and height$skip = true;}if($skip) {$skipped[] = $name;continue;}// rebuild the variation$o['forceNew'] = true;$o['suffix'] = $info['suffix'];if(is_file($info['path'])) {$files->unlink($info['path'], true);if(!empty($info['webpPath']) && $files->exists($info['webpPath'])) {$files->unlink($info['webpPath'], true);$hadWebp = true;}}/*if(!$info['width'] && $info['actualWidth']) {$info['width'] = $info['actualWidth'];$o['nameWidth'] = 0;}if(!$info['height'] && $info['actualHeight']) {$info['height'] = $info['actualHeight'];$o['nameHeight'] = 0;}*/if($info['crop'] && preg_match('/^x(\d+)y(\d+)$/', $info['crop'], $matches)) {// dimensional cropping info contained in filename$cropX = (int) $matches[1];$cropY = (int) $matches[2];$variation = $this->pageimage->crop($cropX, $cropY, $info['width'], $info['height'], $o);} else if($info['crop']) {// direct cropping info contained in filename$options['cropping'] = $info['crop'];$variation = $this->pageimage->size($info['width'], $info['height'], $o);} else if($this->pageimage->hasFocus) {// crop to focus area, which the size() method will determine on its own$variation = $this->pageimage->size($info['width'], $info['height'], $o);} else {// no crop, no focus, just resize$variation = $this->pageimage->size($info['width'], $info['height'], $o);}if($variation) {if($variation->name != $name) {$files->rename($variation->filename(), $info['path']);$variation->data('basename', $name);}$rebuilt[] = $name;if($hadWebp) {// forces create of webp version$webpName = basename($variation->webp()->url());if($webpName) $rebuilt[] = $webpName;}} else {$errors[] = $name;}}return array('rebuilt' => $rebuilt,'skipped' => $skipped,'reasons' => $reasons,'errors' => $errors);}/*** Delete all the alternate sizes associated with this Pageimage** @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 $this|array Returns $this by default, or array of deleted filenames if the `getFiles` option is specified**/public function remove(array $options = array()) {$defaults = array('dryRun' => false,'getFiles' => false);$variations = $this->find($options);if(!empty($options['dryrun'])) $defaults['dryRun'] = $options['dryrun']; // case insurance$options = array_merge($defaults, $options); // placement after getVariations() intended/** @var WireFileTools $files */$files = $this->wire('files');$deletedFiles = array();$this->removeExtras($this->pageimage, $deletedFiles, $options);foreach($variations as $variation) {/** @var Pageimage $variation */$filename = $variation->filename;if(!is_file($filename)) continue;if($options['dryRun']) {$success = true;} else {$success = $files->unlink($filename, true);}if($success) $deletedFiles[] = $filename;$this->removeExtras($variation, $deletedFiles, $options);}if(!$options['dryRun']) $this->variations = null;return ($options['dryRun'] || $options['getFiles'] ? $deletedFiles : $this);}/*** Remove extras** @param Pageimage $pageimage* @param array $deletedFiles* @param array $options See options for remove() method**/protected function removeExtras(Pageimage $pageimage, array &$deletedFiles, array $options) {foreach($pageimage->extras() as $extra) {if(!$extra->exists()) {// nothing to do} else if(!empty($options['dryRun'])) {$deletedFiles[] = $extra->filename();} else if($extra->unlink()) {$deletedFiles[] = $extra->filename();}}}}