Improved retina image support

This commit is contained in:
Matias Griese
2022-05-18 13:21:24 +03:00
parent cb69b8a275
commit 6afc59e412
7 changed files with 280 additions and 156 deletions

View File

@@ -12,6 +12,8 @@
2. [](#improved)
* **BC BREAK** `Medium` no longer extends `Data`
* By default, add media to only pages which have been initialized in pages loop
* Removed `system.images.seofriendly` option as it's not needed anymore
* Improved retina image support
3. [](#bugfix)
* Fixed locking and reading files using `Framework\File` classes
* Fixed `Utils::resolveTokenPath()` with `@variable@` not working properly

View File

@@ -53,6 +53,7 @@ trait ImageMediaTrait
protected ?Image $image = null;
protected string $format = 'guess';
protected int $quality = 85;
protected int $scale = 1;
protected bool $watermark = false;
protected string $sizes = '100vw';
@@ -66,6 +67,7 @@ trait ImageMediaTrait
'image' => $this->image,
'format' => $this->format,
'quality' => $this->quality,
'scale' => $this->scale,
'watermark' => $this->watermark,
'sizes' => $this->sizes,
];
@@ -81,6 +83,7 @@ trait ImageMediaTrait
$this->image = $data['image'];
$this->format = $data['format'];
$this->quality = $data['quality'];
$this->scale = $data['scale'];
$this->watermark = $data['watermark'];
$this->sizes = $data['sizes'];
}
@@ -90,35 +93,30 @@ trait ImageMediaTrait
*
* NOTE: If timestamp is 0, file wasn't created.
*
* @param string $path
* @param string $filepath
* @return array
* @phpstan-impure
*/
protected static function createImageFromCache(string $path): array
protected static function createImageFromCache(string $filepath): array
{
[$path, $basename, $ext, $scale] = static::parseFilepath($filepath);
// Find out if browser wants a larger image.
if (!preg_match('/(.*)(?:@(\d+)x)?\.(.*)$/Uu', $path, $matches)) {
if (null === $basename) {
return [$path, '', 0, 0, 1, null];
}
[,$basepath,$scale,$ext] = $matches;
// Prevent bad retina scales.
$scale = $scale !== '' ? (int)$scale : 1;
$scale = $scale ?? 1;
if ($scale < 1 || $scale > 3) {
return [$path, '', 0, 0, $scale, $ext];
}
$filepath = GRAV_WEBROOT . $path;
$filepath = GRAV_WEBROOT . "{$path}{$basename}.{$ext}";
$cachepath = "{$filepath}.json";
$file = static::getCacheMetaFile($cachepath);
if (!$file->exists()) {
$filepath = GRAV_WEBROOT . $basepath . '.' . $ext;
$cachepath = "{$filepath}.json";
$file = static::getCacheMetaFile($cachepath);
if (!$file->exists()) {
return [$path, '', 0, 0, $scale, $ext];
}
return [$path, '', 0, 0, $scale, $ext];
}
$data = $file->load();
@@ -134,12 +132,11 @@ trait ImageMediaTrait
$fileData = $mediaFactory->readFile($mediaUri);
$adapter = GdAdapter::createFromString($fileData);
if ($scale > 1) {
$filepath = GRAV_WEBROOT . "{$basepath}@{$scale}x.{$ext}";
$adapter->setRetinaScale($scale);
$filepath = GRAV_WEBROOT . "{$path}{$basename}@{$scale}x.{$ext}";
}
$image = Image::createFromArray($data);
$image->setRetinaScale($scale);
// If debugging is turned on, show overlay for retina scaling.
if ($debug) {
@@ -155,7 +152,7 @@ trait ImageMediaTrait
'width' => $image_info[0],
'height' => $image_info[0],
'mime' => $image_info['mime'],
'retina' => $scale
'scale' => $scale
];
$overlayImage = new Image($overlay, $info);
@@ -583,7 +580,7 @@ trait ImageMediaTrait
'mime' => $image_info['mime']
];
$watermark = new Image($watermark, $info);
$watermarkImage = new Image($watermark, $info);
if (null === $this->image) {
$this->image();
@@ -592,25 +589,25 @@ trait ImageMediaTrait
$width = $this->image->width();
$height = $this->image->width();
// Scaling operations
$wwidth = $width * $scale;
$wheight = $height * $scale;
// Position operations
$positionParts = strpos($position, '-') ? explode('-', $position, 2) : [];
$positionY = $positionParts[0] ?? $config->get('system.images.watermark.position_y', 'center');
$positionX = $positionParts[1] ?? $config->get('system.images.watermark.position_x', 'center');
// Scaling operations
$scaledWidth = (int)($width * $scale);
$scaledHeight = (int)($height * $scale);
switch ($positionY) {
case 'top':
$positionY = 0;
break;
case 'bottom':
$positionY = $height - $wheight;
$positionY = $height - $scaledHeight;
break;
case 'center':
default:
$positionY = (int)($height / 2 - $wheight / 2);
$positionY = (int)(($height - $scaledHeight) / 2);
break;
}
@@ -619,15 +616,15 @@ trait ImageMediaTrait
$positionX = 0;
break;
case 'right':
$positionX = $width - $wwidth;
$positionX = $width - $scaledWidth;
break;
case 'center':
default:
$positionX = (int)($width / 2 - $wwidth / 2);
$positionX = (int)(($width - $scaledWidth) / 2);
break;
}
$this->image->merge($watermark, $positionX, $positionY, $wwidth, $wheight);
$this->image->merge($watermarkImage, $positionX, $positionY, $scaledWidth, $scaledHeight);
// Do not apply watermark more than once.
$this->watermark = false;
@@ -804,14 +801,15 @@ trait ImageMediaTrait
$d2 = substr($hash, 2, 2);
$d3 = substr($hash, 4);
$prettyName = $this->getImagePrettyName();
$basename = preg_replace('/(@\d+x)?$/', '', $prettyName, 1);
$imageFile = "cache://images/{$d1}/{$d2}/{$d3}/{$prettyName}.{$format}";
$cacheFile = "{$imageFile}.json";
$cacheFile = "cache://images/{$d1}/{$d2}/{$d3}/{$basename}.{$format}.json";
/** @var UniformResourceLocator $locator */
$locator = $this->getGrav()['locator'];
$imageFile = '/' . $locator->getResource($imageFile, false);
$cacheFile = $locator->getResource($cacheFile, true);
$cacheFile = $locator->getResource($cacheFile);
$file = static::getCacheMetaFile($cacheFile);
if (!$file->exists()) {
@@ -834,4 +832,20 @@ trait ImageMediaTrait
return new JsonFile($filepath, $formatter);
}
/**
* @param string $path
* @return array|null
* @phpstan-pure
*/
protected static function parseFilepath(string $path): ?array
{
if (!preg_match('{^(.*)?([^/]+)(?:@(\d+)x)?\.([^.]+)$}Uu', $path, $matches)) {
return null;
}
$scale = '' !== $matches[3] ? (int)$matches[3] : null;
return [$matches[1], $matches[2], $matches[4], $scale];
}
}

View File

@@ -31,6 +31,7 @@ use RuntimeException;
use function count;
use function in_array;
use function is_array;
use function strlen;
/**
* Class AbstractMedia
@@ -1040,35 +1041,26 @@ abstract class AbstractMedia implements ExportInterface, MediaCollectionInterfac
*/
protected function getFileParts(string $filename): array
{
if (preg_match('/(.*)@(\d+)x\.(.*)$/', $filename, $matches)) {
$name = $matches[1];
$extension = $matches[3];
$extra = (int) $matches[2];
$type = 'alternative';
$type = null;
$extra = null;
if (str_ends_with($filename, $ext = '.meta.yaml')) {
$type = 'meta';
$filename = substr($filename, 0, -strlen($ext));
} elseif (preg_match('/^(.*)\.thumb\.(.*)$/Uu', $filename, $matches)) {
$type = 'thumb';
[, $filename, $extra] = $matches;
}
if ($extra === 1) {
$parts = explode('.', $filename);
$extension = count($parts) > 1 ? array_pop($parts) : null;
$name = implode('.', $parts);
if (!$type && $extension) {
if (preg_match('/^(.*)@(\d+)x$/Uu', $name, $matches)) {
$type = 'alternative';
[, $name, $extra] = $matches;
} else {
$type = 'base';
$extra = null;
}
} else {
$fileParts = explode('.', $filename);
$name = array_shift($fileParts);
$extension = null;
$extra = null;
$type = 'base';
while (($part = array_shift($fileParts)) !== null) {
if ($part !== 'meta' && $part !== 'thumb') {
if (null !== $extension) {
$name .= '.' . $extension;
}
$extension = $part;
} else {
$type = $part;
$extra = '.' . $part . '.' . implode('.', $fileParts);
break;
}
}
}

View File

@@ -10,7 +10,6 @@
namespace Grav\Common\Page\Medium;
use Grav\Common\Config\Config;
use Grav\Common\Data\Blueprint;
use Grav\Common\Media\Interfaces\ImageManipulateInterface;
use Grav\Common\Media\Interfaces\ImageMediaInterface;
use Grav\Common\Media\Interfaces\MediaLinkInterface;
@@ -18,7 +17,6 @@ use Grav\Common\Media\Traits\ImageLoadingTrait;
use Grav\Common\Media\Traits\ImageMediaTrait;
use Grav\Common\Utils;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use function array_key_exists;
use function is_bool;
/**
@@ -34,7 +32,10 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
use ImageMediaTrait;
use ImageLoadingTrait;
protected array $imageSettings = [];
protected int $retina = 1;
protected ?bool $autoSizes = null;
protected ?bool $aspectRatio = null;
private ?string $saved_image_path = null;
/**
@@ -49,23 +50,26 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
/** @var Config $config */
$config = $this->getGrav()['config'];
$filepath = $this->get('filepath');
[, , , $scale] = static::parseFilepath($filepath);
if ($scale && $scale > 1) {
$this->scale = $this->retina = $scale;
}
$this->quality = (int)$config->get('system.images.default_image_quality', 85);
$this->watermark = $config->get('system.images.watermark.watermark_all', false);
$this->thumbnailTypes = ['page', 'media', 'default'];
$this->imageSettings = [];
$this->quality = (int)$config->get('system.images.default_image_quality', 85);
$this->def('debug', $config->get('system.images.debug'));
$path = $this->get('filepath');
$this->set('thumbnails.media', $path);
$this->set('thumbnails.media', $filepath);
if (!($this->offsetExists('width') && $this->offsetExists('height') && $this->offsetExists('mime'))) {
user_error(__METHOD__ . '() Creating image without width, height and mime type is deprecated since Grav 1.8', E_USER_DEPRECATED);
$exists = $path && file_exists($path) && filesize($path);
$exists = $filepath && file_exists($filepath) && filesize($filepath);
if ($exists) {
$image_info = getimagesize($path);
$image_info = getimagesize($filepath);
if ($image_info) {
$this->set('width', $image_info[0]);
$this->set('height', $image_info[1]);
@@ -87,7 +91,9 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
return parent::__serialize() +
$this->serializeImageMediaTrait() +
[
'imageSettings' => $this->imageSettings,
'retina' => $this->retina,
'auto_sizes' => $this->autoSizes,
'aspect_ratio' => $this->aspectRatio,
'saved_image_path' => $this->saved_image_path
];
}
@@ -101,10 +107,43 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
parent::__unserialize($data);
$this->unserializeImageMediaTrait($data);
$this->imageSettings = $data['imageSettings'];
$this->retina = $data['retina'];
$this->autoSizes = $data['auto_sizes'];
$this->aspectRatio = $data['aspect_ratio'];
$this->saved_image_path = $data['saved_image_path'];
}
/**
* @param string $name
* @param mixed $default
* @param string|null $separator
* @return mixed
*/
public function get($name, $default = null, $separator = null)
{
// Special logic to get resized image data.
switch ($name) {
case 'format':
case 'quality':
case 'scale':
case 'watermark':
case 'sizes':
case 'retina':
return $this->{$name};
case 'width':
case 'height':
$value = parent::get($name, $default, $separator);
if (!$value) {
return $value;
}
return null !== $this->image ? $this->image->{$name}() * $this->retina : (int)($value * $this->retina / $this->scale);
}
return parent::get($name, $default, $separator);
}
/**
* @param string|int $timestamp
* @return $this|ImageMedium
@@ -129,6 +168,7 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
return [
'width' => $this->width,
'height' => $this->height,
'scale' => $this->scale,
'orientation' => $this->orientation,
] + parent::getInfo();
}
@@ -142,6 +182,7 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
return [
'width' => $this->width,
'height' => $this->height,
'scale' => $this->scale,
'orientation' => $this->orientation,
] + parent::getMeta();
}
@@ -160,7 +201,8 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
$config = $this->getGrav()['config'];
$this->format = 'guess';
$this->imageSettings = [];
$this->autoSizes = null;
$this->aspectRatio = null;
$this->quality = (int)$config->get('system.images.default_image_quality', 85);
$this->resetImage();
@@ -213,7 +255,7 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
$output = (string)($locator->findResource($output, false) ?: $locator->findResource($output, false, true));
}
$image_path = (string)($locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true));
$image_path = (string)($locator->findResource('cache://images') ?: $locator->findResource('cache://images', true, true));
if (Utils::startsWith($output, $image_path)) {
$image_dir = $locator->findResource('cache://images', false);
$output = '/' . $image_dir . preg_replace('|^' . preg_quote($image_path, '|') . '|', '', $output);
@@ -244,10 +286,13 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
}
$srcset = [];
foreach ($this->alternatives as $medium) {
$srcset[] = $medium->url($reset) . ' ' . $medium->get('width') . 'w';
foreach ([$this] + $this->alternatives as $medium) {
$url = str_replace(' ', '%20', $medium->url($reset));
$width = (int)$medium->get('width');
$srcset[$width] = $url . ' ' . $width . 'w';
}
$srcset[] = str_replace(' ', '%20', $this->url($reset)) . ' ' . $this->get('width') . 'w';
ksort($srcset, SORT_NUMERIC);
return implode(', ', $srcset);
}
@@ -273,7 +318,7 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
$attributes['sizes'] = $this->sizes();
}
if ($this->imageSettings['auto_sizes'] ?? $this->getConfig('system.images.cls.auto_sizes', false)) {
if ($this->autoSizes ?? $this->getConfig('system.images.cls.auto_sizes', false)) {
if ($this->image) {
$width = $this->image->width();
$height = $this->image->height();
@@ -282,7 +327,7 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
$height = $this->height;
}
$scaling_factor = max(1, $this->imageSettings['retina_scale'] ?? (int)$this->getConfig('system.images.cls.retina_scale', 1));
$scaling_factor = max(1, $this->scale ?? (int)$this->getConfig('system.images.cls.retina_scale', 1));
if (!isset($attributes['width'])) {
$attributes['width'] = (int)($width / $scaling_factor);
@@ -291,7 +336,7 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
$attributes['height'] = (int)($height / $scaling_factor);
}
if ($this->imageSettings['aspect_ratio'] ?? $this->getConfig('system.images.cls.aspect_ratio', false)) {
if ($this->aspectRatio ?? $this->getConfig('system.images.cls.aspect_ratio', false)) {
$style = ($attributes['style'] ?? ' ') . "--aspect-ratio: {$width}/{$height};";
$attributes['style'] = trim($style);
}
@@ -348,9 +393,7 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
*/
public function autoSizes($enabled = true)
{
$enabled = is_bool($enabled) ? $enabled : $enabled === 'true';
$this->imageSettings['auto_sizes'] = $enabled;
$this->autoSizes = is_bool($enabled) ? $enabled : $enabled === 'true';
return $this;
}
@@ -362,9 +405,7 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
*/
public function aspectRatio($enabled = true)
{
$enabled = is_bool($enabled) ? $enabled : $enabled === 'true';
$this->imageSettings['aspect_ratio'] = $enabled;
$this->aspectRatio = is_bool($enabled) ? $enabled : $enabled === 'true';
return $this;
}
@@ -376,8 +417,22 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
*/
public function retinaScale(int $scale = 1)
{
$this->imageSettings['retina_scale'] = $scale;
if ($scale === $this->retina) {
return $this;
}
if (!$this->image) {
$this->image();
}
$this->retina = $scale;
$this->image->setRetinaScale($scale);
return $this;
}
protected function getItems(): array
{
return parent::getItems() + ['scale' => $this->scale];
}
}

View File

@@ -50,6 +50,7 @@ interface ImageAdapterInterface extends ImageInfoInterface, ImageSaveInterface
*
* NOTE: Set this before any image operations.
*
* @param positive-int $scale
* @return $this
*/
public function setRetinaScale(int $scale);
@@ -58,10 +59,10 @@ interface ImageAdapterInterface extends ImageInfoInterface, ImageSaveInterface
* Resizes the image.
*
* @param int|null $background
* @param int $target_width
* @param int $target_height
* @param int $new_width
* @param int $new_height
* @param positive-int $target_width
* @param positive-int $target_height
* @param positive-int $new_width
* @param positive-int $new_height
* @return $this
*/
public function resize(?int $background, int $target_width, int $target_height, int $new_width, int $new_height);
@@ -71,8 +72,8 @@ interface ImageAdapterInterface extends ImageInfoInterface, ImageSaveInterface
*
* @param int $x The top-left x position of the crop box
* @param int $y The top-left y position of the crop box
* @param int $width The width of the crop box
* @param int $height The height of the crop box
* @param positive-int $width The width of the crop box
* @param positive-int $height The height of the crop box
* @return $this
*/
public function crop(int $x, int $y, int $width, int $height);
@@ -190,8 +191,8 @@ interface ImageAdapterInterface extends ImageInfoInterface, ImageSaveInterface
* @param ImageAdapterInterface $other
* @param int $x
* @param int $y
* @param int $width
* @param int $height
* @param positive-int $width
* @param positive-int $height
* @return $this
*/
public function merge(ImageAdapterInterface $other, int $x = 0, int $y = 0, int $width = 0, int $height = 0);
@@ -274,8 +275,8 @@ interface ImageAdapterInterface extends ImageInfoInterface, ImageSaveInterface
*
* @param int $cx
* @param int $cy
* @param int $width
* @param int $height
* @param positive-int $width
* @param positive-int $height
* @param int $color
* @param bool $filled
* @return $this
@@ -287,7 +288,7 @@ interface ImageAdapterInterface extends ImageInfoInterface, ImageSaveInterface
*
* @param int $cx
* @param int $cy
* @param int $r
* @param positive-int $r
* @param int $color
* @param bool $filled
* @return $this

View File

@@ -63,66 +63,67 @@ class GdAdapter extends Adapter
return (bool)(imagetypes() & $test);
}
/**
/**
* Creates a new image.
*
* @param int $width
* @param int $height
* @param int $scale
* @param positive-int $width
* @param positive-int $height
* @param positive-int $scale
* @return static
*/
public static function create(int $width, int $height, int $scale = 1): GdAdapter
{
$resource = static::createResource($width, $height);
$image = new static($resource);
$image->scale = $scale;
return $image;
return new static($resource, $scale);
}
/**
* Creates image from a file.
*
* @param string $filepath
* @param positive-int $scale
* @return static
*/
public static function createFromFile(string $filepath): GdAdapter
public static function createFromFile(string $filepath, int $scale = 1): GdAdapter
{
$extension = strtolower(Utils::pathinfo($filepath, PATHINFO_EXTENSION));
$resource = static::createResourceFromFile($filepath, $extension);
return new static($resource);
return new static($resource, $scale);
}
/**
* Creates image from string of image data.
*
* @param string $data
* @param positive-int $scale
* @return static
*/
public static function createFromString(string $data): GdAdapter
public static function createFromString(string $data, int $scale = 1): GdAdapter
{
$resource = static::createResourceFromString($data);
return new static($resource);
return new static($resource, $scale);
}
/**
* Creates an instance of image from resource.
*
* @param \GdImage|resource $resource
* @param positive-int $scale
* @return static
*/
public static function createFromImage($resource): GdAdapter
public static function createFromImage($resource, int $scale = 1): GdAdapter
{
return new static($resource);
return new static($resource, $scale);
}
/**
* @param \GdImage|resource $resource
* @param positive-int $scale
*/
public function __construct($resource)
public function __construct($resource, int $scale = 1)
{
if (PHP_VERSION_ID > 80000) {
if (!$resource instanceof \GdImage) {
@@ -133,6 +134,7 @@ class GdAdapter extends Adapter
}
$this->resource = $resource;
$this->scale = $scale;
$this->convertToTrueColor();
}
@@ -400,10 +402,8 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*
* @param GdAdapter $other
*/
public function merge(ImageAdapterInterface $other, int $x = 0, int $y = 0, int $width = null, int $height = null): GdAdapter
public function merge(ImageAdapterInterface $other, int $x = 0, int $y = 0, int $width = 0, int $height = 0): GdAdapter
{
if (!$other instanceof self) {
throw new InvalidArgumentException('Image to be merged needs to be instance of GdAdapter');
@@ -415,20 +415,18 @@ class GdAdapter extends Adapter
$x *= $scale;
$y *= $scale;
if (null !== $width) {
$otherWidth = $width * $otherScale;
$width *= $scale;
} else {
$otherWidth = $other->width();
$width = (int)($otherWidth * $scale / $otherScale);
}
$otherWidth = $other->width();
$otherHeight = $other->height();
if (null !== $height) {
$otherHeight = $height * $otherScale;
$height *= $scale;
if (!$width) {
$width = (int)($otherWidth * $scale / $otherScale);
} else {
$otherHeight = $other->height();
$width *= $scale;
}
if (!$height) {
$height = (int)($otherHeight * $scale / $otherScale);
} else {
$height *= $scale;
}
imagealphablending($this->resource, true);

View File

@@ -4,12 +4,14 @@ namespace Grav\Framework\Image;
use Exception;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Framework\Compat\Serializable;
use Grav\Framework\Contracts\Image\ImageAdapterInterface;
use Grav\Framework\Contracts\Image\ImageOperationsInterface;
use Grav\Framework\Image\Traits\ImageOperationsTrait;
use InvalidArgumentException;
use JsonSerializable;
use Psr\Log\LoggerInterface;
use RuntimeException;
use function array_slice;
use function dirname;
@@ -38,13 +40,13 @@ class Image implements ImageOperationsInterface, JsonSerializable
public array $extra = [];
protected ImageAdapterInterface $adapter;
protected int $origWidth;
protected int $origHeight;
protected int $imageWidth;
protected int $imageHeight;
protected int $scale;
protected string $filepath;
protected int $modified;
protected int $size;
protected int $operationsCursor = 0;
protected int $operationsCursor;
protected int $retina;
/**
@@ -68,10 +70,13 @@ class Image implements ImageOperationsInterface, JsonSerializable
$this->filepath = $filepath;
$this->modified = (int)($info['modified'] ?? 0);
$this->size = (int)($info['size'] ?? 0);
$this->origWidth = $this->width = (int)($info['width'] ?? 0);
$this->origHeight = $this->height = (int)($info['height'] ?? 0);
$this->imageWidth = (int)($info['width'] ?? 0);
$this->imageHeight = (int)($info['height'] ?? 0);
$this->orientation = isset($info['exif']['Orientation']) ? (int)$info['exif']['Orientation'] : null;
$this->retina = (int)($info['retina'] ?? 1);
$this->scale = (int)($info['scale'] ?? 1);
$this->width = (int)($this->imageWidth / $this->scale);
$this->height = (int)($this->imageHeight / $this->scale);
$this->retina = $this->scale;
}
public function getFilepath(): string
@@ -89,12 +94,12 @@ class Image implements ImageOperationsInterface, JsonSerializable
'filepath' => $this->filepath,
'modified' => $this->modified,
'size' => $this->size,
'image_width' => $this->imageWidth,
'image_height' => $this->imageHeight,
'orientation' => $this->orientation,
'orig_width' => $this->origWidth,
'orig_height' => $this->origHeight,
'width' => $this->width,
'height' => $this->height,
'retina' => $this->retina,
'scale' => $this->scale,
'dependencies' => $this->dependencies,
'operations' => $this->operations,
'extra' => $this->extra
@@ -115,15 +120,16 @@ class Image implements ImageOperationsInterface, JsonSerializable
$this->filepath = $data['filepath'];
$this->modified = $data['modified'];
$this->size = $data['size'];
$this->origWidth = $data['orig_width'];
$this->origHeight = $data['orig_height'];
$this->imageWidth = $data['image_width'];
$this->imageHeight = $data['image_height'];
$this->orientation = $data['orientation'];
$this->width = $data['width'];
$this->height = $data['height'];
$this->retina = $data['retina'] ?? 1;
$this->scale = $data['scale'];
$this->dependencies = $data['dependencies'];
$this->operations = $data['operations'];
$this->extra = $data['extra'];
$this->retina = $this->scale;
}
/**
@@ -144,6 +150,29 @@ class Image implements ImageOperationsInterface, JsonSerializable
return sha1(serialize($this));
}
/**
* @return int
*/
public function getRetinaScale(): int
{
return $this->retina;
}
/**
* @param int $scale
* @return $this
*/
public function setRetinaScale(int $scale)
{
if (isset($this->operationsCursor)) {
throw new RuntimeException('You can set retina scale only before applying operations!');
}
$this->retina = $scale;
return $this;
}
/**
* Get image adapter.
*
@@ -326,39 +355,55 @@ class Image implements ImageOperationsInterface, JsonSerializable
*/
public function applyOperations(): Image
{
$adapter = $this->adapter;
$operations = $this->operations;
if (!$operations) {
return $this;
// On first run we need to initialize the state of the image.
if (!isset($this->operationsCursor)) {
$this->operationsCursor = 0;
// Apply retina scale and resize the image to have the correct size.
if ($this->retina !== 1) {
$width = (int)($this->imageWidth * $this->retina / $this->scale);
$height = (int)($this->imageHeight * $this->retina / $this->scale);
} else {
$width = $this->width;
$height = $this->height;
}
if ($width !== $this->imageWidth || $height !== $this->imageHeight) {
$adapter->resize(null, $width, $height, $width, $height);
}
// Set retina scaling for the rest of the operations.
$adapter->setRetinaScale($this->retina);
}
$adapter = $this->adapter;
// Only get the remaining operations.
// Only get the remaining operations (this method can be called more than once).
$cursor = $this->operationsCursor;
if ($cursor) {
$operations = array_slice($operations, $cursor, null, true);
}
// Apply all the remaining operations.
foreach ($operations as $operation) {
[$method, $params] = $operation;
if ($method === 'merge') {
$image = static::createFromArray($params[0]);
try {
[$method, $params] = $operation;
// FIXME: Right now this only works for the local files.
try {
$imgAdapter = $adapter::createFromFile($image->filepath);
$imgAdapter->setRetinaScale($image->retina);
$image->setAdapter($imgAdapter);
} catch (\InvalidArgumentException $e) {
// TODO: log errors on missing files?
continue;
if ($method === 'merge') {
$params[0] = $this->getImageParameter($params[0]);
}
// Apply all operations to the image that is being merged and get the adapter.
$params[0] = $image->applyOperations()->getAdapter();
$adapter->{$method}(...$params);
} catch (\InvalidArgumentException $e) {
/** @var LoggerInterface $log */
$log = Grav::instance()['log'];
$log->warning(sprintf('Image operation %s failed: %s', $method, $e->getMessage()));
continue;
}
$adapter->{$method}(...$params);
$cursor++;
}
@@ -367,6 +412,23 @@ class Image implements ImageOperationsInterface, JsonSerializable
return $this;
}
/**
* @param array $param
* @return ImageAdapterInterface|null
*/
protected function getImageParameter(array $param): ?ImageAdapterInterface
{
$adapter = $this->adapter;
$image = static::createFromArray($param);
// TODO: Right now this only works for the local files.
$imgAdapter = $adapter::createFromFile($image->filepath, $image->scale);
$image->setAdapter($imgAdapter);
// Apply all operations to the image that is being merged and get the adapter.
return $image->applyOperations()->getAdapter();
}
/**
* @param string $directory
* @return void