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 period if(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 variation if(!$options['allowSelf'] && $variationName == $this->pageimage->basename) return false; // if file doesn't start with the original name then it's not a variation if(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 dot if($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 false if(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 basename if(!$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.webp if(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 suffix foreach($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 suffix if(!$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']) { /** @var Pageimages $variations */ $variations = $this->wire(new Pageimages($this->pagefiles->page)); } // if suffix or noSuffix option contains space, convert it to suffixes or noSuffixes array option foreach(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 suffixes foreach($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 argument if($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 argument if($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()) { $files = $this->wire()->files; $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 $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(); } } } }