421 lines
12 KiB
PHP
421 lines
12 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @package Grav\Common\Media
|
|
*
|
|
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
|
* @license MIT License; see LICENSE file for details.
|
|
*/
|
|
|
|
namespace Grav\Common\Media\Traits;
|
|
|
|
use Grav\Common\Grav;
|
|
use Grav\Common\Media\Interfaces\ImageMediaInterface;
|
|
use Grav\Common\Media\Interfaces\MediaCollectionInterface;
|
|
use Grav\Common\Page\Medium\ImageFile;
|
|
use Grav\Common\Page\Medium\ImageMedium;
|
|
use Grav\Common\Page\Medium\MediumFactory;
|
|
use function array_key_exists;
|
|
use function extension_loaded;
|
|
use function func_num_args;
|
|
use function function_exists;
|
|
|
|
/**
|
|
* Trait ImageMediaTrait
|
|
* @package Grav\Common\Media\Traits
|
|
*/
|
|
trait ImageMediaTrait
|
|
{
|
|
/** @var ImageFile|null */
|
|
protected $image;
|
|
|
|
/** @var string */
|
|
protected $format = 'guess';
|
|
|
|
/** @var int */
|
|
protected $quality;
|
|
|
|
/** @var int */
|
|
protected $default_quality;
|
|
|
|
/** @var bool */
|
|
protected $debug_watermarked = false;
|
|
|
|
/** @var bool */
|
|
protected $auto_sizes;
|
|
|
|
/** @var bool */
|
|
protected $aspect_ratio;
|
|
|
|
/** @var integer */
|
|
protected $retina_scale;
|
|
|
|
|
|
/** @var array */
|
|
public static $magic_actions = [
|
|
'resize', 'forceResize', 'cropResize', 'crop', 'zoomCrop',
|
|
'negate', 'brightness', 'contrast', 'grayscale', 'emboss',
|
|
'smooth', 'sharp', 'edge', 'colorize', 'sepia', 'enableProgressive',
|
|
'rotate', 'flip', 'fixOrientation', 'gaussianBlur', 'format', 'create', 'fill', 'merge'
|
|
];
|
|
|
|
/** @var array */
|
|
public static $magic_resize_actions = [
|
|
'resize' => [0, 1],
|
|
'forceResize' => [0, 1],
|
|
'cropResize' => [0, 1],
|
|
'crop' => [0, 1, 2, 3],
|
|
'zoomCrop' => [0, 1]
|
|
];
|
|
|
|
/** @var string */
|
|
protected $sizes = '100vw';
|
|
|
|
|
|
/**
|
|
* Allows the ability to override the image's pretty name stored in cache
|
|
*
|
|
* @param string $name
|
|
*/
|
|
public function setImagePrettyName($name)
|
|
{
|
|
$this->set('prettyname', $name);
|
|
if ($this->image) {
|
|
$this->image->setPrettyName($name);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getImagePrettyName()
|
|
{
|
|
if ($this->get('prettyname')) {
|
|
return $this->get('prettyname');
|
|
}
|
|
|
|
$basename = $this->get('basename');
|
|
if (preg_match('/[a-z0-9]{40}-(.*)/', $basename, $matches)) {
|
|
$basename = $matches[1];
|
|
}
|
|
return $basename;
|
|
}
|
|
|
|
/**
|
|
* Simply processes with no extra methods. Useful for triggering events.
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function cache()
|
|
{
|
|
if (!$this->image) {
|
|
$this->image();
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Generate alternative image widths, using either an array of integers, or
|
|
* a min width, a max width, and a step parameter to fill out the necessary
|
|
* widths. Existing image alternatives won't be overwritten.
|
|
*
|
|
* @param int|int[] $min_width
|
|
* @param int $max_width
|
|
* @param int $step
|
|
* @return $this
|
|
*/
|
|
public function derivatives($min_width, $max_width = 2500, $step = 200)
|
|
{
|
|
if (!empty($this->alternatives)) {
|
|
$max = max(array_keys($this->alternatives));
|
|
$base = $this->alternatives[$max];
|
|
} else {
|
|
$base = $this;
|
|
}
|
|
|
|
$widths = [];
|
|
|
|
if (func_num_args() === 1) {
|
|
foreach ((array) func_get_arg(0) as $width) {
|
|
if ($width < $base->get('width')) {
|
|
$widths[] = $width;
|
|
}
|
|
}
|
|
} else {
|
|
$max_width = min($max_width, $base->get('width'));
|
|
|
|
for ($width = $min_width; $width < $max_width; $width += $step) {
|
|
$widths[] = $width;
|
|
}
|
|
}
|
|
|
|
foreach ($widths as $width) {
|
|
// Only generate image alternatives that don't already exist
|
|
if (array_key_exists((int) $width, $this->alternatives)) {
|
|
continue;
|
|
}
|
|
|
|
$derivative = MediumFactory::fromFile($base->get('filepath'));
|
|
|
|
// It's possible that MediumFactory::fromFile returns null if the
|
|
// original image file no longer exists and this class instance was
|
|
// retrieved from the page cache
|
|
if (null !== $derivative) {
|
|
$index = 2;
|
|
$alt_widths = array_keys($this->alternatives);
|
|
sort($alt_widths);
|
|
|
|
foreach ($alt_widths as $i => $key) {
|
|
if ($width > $key) {
|
|
$index += max($i, 1);
|
|
}
|
|
}
|
|
|
|
$basename = preg_replace('/(@\d+x)?$/', "@{$width}w", $base->get('basename'), 1);
|
|
$derivative->setImagePrettyName($basename);
|
|
|
|
$ratio = $base->get('width') / $width;
|
|
$height = $derivative->get('height') / $ratio;
|
|
|
|
$derivative->resize($width, $height);
|
|
$derivative->set('width', $width);
|
|
$derivative->set('height', $height);
|
|
|
|
$this->addAlternative($ratio, $derivative);
|
|
}
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Clear out the alternatives.
|
|
*/
|
|
public function clearAlternatives()
|
|
{
|
|
$this->alternatives = [];
|
|
}
|
|
|
|
/**
|
|
* Sets or gets the quality of the image
|
|
*
|
|
* @param int|null $quality 0-100 quality
|
|
* @return int|$this
|
|
*/
|
|
public function quality($quality = null)
|
|
{
|
|
if ($quality) {
|
|
if (!$this->image) {
|
|
$this->image();
|
|
}
|
|
|
|
$this->quality = $quality;
|
|
|
|
return $this;
|
|
}
|
|
|
|
return $this->quality;
|
|
}
|
|
|
|
/**
|
|
* Sets image output format.
|
|
*
|
|
* @param string $format
|
|
* @return $this
|
|
*/
|
|
public function format($format)
|
|
{
|
|
if (!$this->image) {
|
|
$this->image();
|
|
}
|
|
|
|
$this->format = $format;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set or get sizes parameter for srcset media action
|
|
*
|
|
* @param string|null $sizes
|
|
* @return string
|
|
*/
|
|
public function sizes($sizes = null)
|
|
{
|
|
if ($sizes) {
|
|
$this->sizes = $sizes;
|
|
|
|
return $this;
|
|
}
|
|
|
|
return empty($this->sizes) ? '100vw' : $this->sizes;
|
|
}
|
|
|
|
/**
|
|
* Allows to set the width attribute from Markdown or Twig
|
|
* Examples: 
|
|
* 
|
|
* 
|
|
* 
|
|
* {{ page.media['myimg.png'].width().height().html }}
|
|
* {{ page.media['myimg.png'].resize(100,200).width(100).height(200).html }}
|
|
*
|
|
* @param string|int $value A value or 'auto' or empty to use the width of the image
|
|
* @return $this
|
|
*/
|
|
public function width($value = 'auto')
|
|
{
|
|
if (!$value || $value === 'auto') {
|
|
$this->attributes['width'] = $this->get('width');
|
|
} else {
|
|
$this->attributes['width'] = $value;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Allows to set the height attribute from Markdown or Twig
|
|
* Examples: 
|
|
* 
|
|
* 
|
|
* 
|
|
* {{ page.media['myimg.png'].width().height().html }}
|
|
* {{ page.media['myimg.png'].resize(100,200).width(100).height(200).html }}
|
|
*
|
|
* @param string|int $value A value or 'auto' or empty to use the height of the image
|
|
* @return $this
|
|
*/
|
|
public function height($value = 'auto')
|
|
{
|
|
if (!$value || $value === 'auto') {
|
|
$this->attributes['height'] = $this->get('height');
|
|
} else {
|
|
$this->attributes['height'] = $value;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Filter image by using user defined filter parameters.
|
|
*
|
|
* @param string $filter Filter to be used.
|
|
* @return $this
|
|
*/
|
|
public function filter($filter = 'image.filters.default')
|
|
{
|
|
$filters = (array) $this->get($filter, []);
|
|
foreach ($filters as $params) {
|
|
$params = (array) $params;
|
|
$method = array_shift($params);
|
|
$this->__call($method, $params);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Return the image higher quality version
|
|
*
|
|
* @return ImageMediaInterface|$this the alternative version with higher quality
|
|
*/
|
|
public function higherQualityAlternative()
|
|
{
|
|
if ($this->alternatives) {
|
|
/** @var ImageMedium $max */
|
|
$max = reset($this->alternatives);
|
|
/** @var ImageMedium $alternative */
|
|
foreach ($this->alternatives as $alternative) {
|
|
if ($alternative->quality() > $max->quality()) {
|
|
$max = $alternative;
|
|
}
|
|
}
|
|
|
|
return $max;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Gets medium image, resets image manipulation operations.
|
|
*
|
|
* @return $this
|
|
*/
|
|
protected function image()
|
|
{
|
|
$locator = Grav::instance()['locator'];
|
|
|
|
// Use existing cache folder or if it doesn't exist, create it.
|
|
$cacheDir = $locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true);
|
|
|
|
// Make sure we free previous image.
|
|
unset($this->image);
|
|
|
|
/** @var MediaCollectionInterface $media */
|
|
$media = $this->get('media');
|
|
if ($media && method_exists($media, 'getImageFileObject')) {
|
|
$this->image = $media->getImageFileObject($this);
|
|
} else {
|
|
$this->image = ImageFile::open($this->get('filepath'));
|
|
}
|
|
|
|
$this->image
|
|
->setCacheDir($cacheDir)
|
|
->setActualCacheDir($cacheDir)
|
|
->setPrettyName($this->getImagePrettyName());
|
|
|
|
// Fix orientation if enabled
|
|
$config = Grav::instance()['config'];
|
|
if ($config->get('system.images.auto_fix_orientation', false) &&
|
|
extension_loaded('exif') && function_exists('exif_read_data')) {
|
|
$this->image->fixOrientation();
|
|
}
|
|
|
|
// Set CLS configuration
|
|
$this->auto_sizes = $config->get('system.images.cls.auto_sizes', false);
|
|
$this->aspect_ratio = $config->get('system.images.cls.aspect_ratio', false);
|
|
$this->retina_scale = $config->get('system.images.cls.retina_scale', 1);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Save the image with cache.
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function saveImage()
|
|
{
|
|
if (!$this->image) {
|
|
return parent::path(false);
|
|
}
|
|
|
|
$this->filter();
|
|
|
|
if (isset($this->result)) {
|
|
return $this->result;
|
|
}
|
|
|
|
if ($this->format === 'guess') {
|
|
$extension = strtolower($this->get('extension'));
|
|
$this->format($extension);
|
|
}
|
|
|
|
if (!$this->debug_watermarked && $this->get('debug')) {
|
|
$ratio = $this->get('ratio');
|
|
if (!$ratio) {
|
|
$ratio = 1;
|
|
}
|
|
|
|
$locator = Grav::instance()['locator'];
|
|
$overlay = $locator->findResource("system://assets/responsive-overlays/{$ratio}x.png") ?: $locator->findResource('system://assets/responsive-overlays/unknown.png');
|
|
$this->image->merge(ImageFile::open($overlay));
|
|
}
|
|
|
|
return $this->image->cacheFile($this->format, $this->quality, false, [$this->get('width'), $this->get('height'), $this->get('modified')]);
|
|
}
|
|
}
|