praiadeseselle/wire/core/PageimageVariations.php

637 lines
22 KiB
PHP
Raw Normal View History

2022-03-08 15:55:41 +01:00
<?php namespace ProcessWire;
/**
* ProcessWire PageimageVariations
*
* Helper class for Pageimage that handles variation collection methods
*
* ProcessWire 3.x, Copyright 2019 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();
}
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
*
*/
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 instances file, or false if not.
*
* Returned array includes the following indexes:
*
* - `original` (string): Original basename
* - `url` (string): URL to image
* - `path` (string): Full path + filename to image
* - `width` (int): Specified width in filename
* - `height` (int): Specified height in filename
* - `actualWidth` (int): Actual width when checked manually
* - `actualHeight` (int): Acual height when checked manually
* - `crop` (string): Cropping info string or blank if none
* - `suffix` (array): Array of suffixes
*
* The following are only present if variation is based on another variation, and thus has a parent variation
* image between it and the original:
*
* - `suffixAll` (array): Contains all suffixes including among parent variations
* - `parent` (array): Variation info array of direct parent variation file
*
* @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()) continue;
$info["{$name}Url"] = $extra->url(false);
$info["{$name}Path"] = $extra->filename();
}
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']) {
$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()) {
$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'])) {
$this->wire('files')->unlink($info['path'], true);
if(!empty($info['webpPath']) && is_file($info['webpPath'])) {
$this->wire('files')->unlink($info['webpPath'], true);
$hadWebp = true;
}
}
/*
if(!$info['width'] && $info['actualWidth']) {
$info['width'] = $info['actualWidth'];
$options['nameWidth'] = 0;
}
if(!$info['height'] && $info['actualHeight']) {
$info['height'] = $info['actualHeight'];
$options['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'], $options);
} else if($info['crop']) {
// direct cropping info contained in filename
$options['cropping'] = $info['crop'];
$variation = $this->pageimage->size($info['width'], $info['height'], $options);
} 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'], $options);
} else {
// no crop, no focus, just resize
$variation = $this->pageimage->size($info['width'], $info['height'], $options);
}
if($variation) {
if($variation->name != $name) {
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();
}
}
}
}