From 6afc59e41255c32d9f2a31af64798dcdc032d661 Mon Sep 17 00:00:00 2001 From: Matias Griese Date: Wed, 18 May 2022 13:21:24 +0300 Subject: [PATCH] Improved retina image support --- CHANGELOG.md | 2 + .../Common/Media/Traits/ImageMediaTrait.php | 72 ++++++---- .../Grav/Common/Page/Medium/AbstractMedia.php | 46 +++---- .../Grav/Common/Page/Medium/ImageMedium.php | 111 +++++++++++---- .../Contracts/Image/ImageAdapterInterface.php | 23 ++-- .../Framework/Image/Adapter/GdAdapter.php | 56 ++++---- system/src/Grav/Framework/Image/Image.php | 126 +++++++++++++----- 7 files changed, 280 insertions(+), 156 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7063e5981..9606dcd09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php b/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php index c4172ba91..3034a2071 100644 --- a/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php +++ b/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php @@ -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]; + } } diff --git a/system/src/Grav/Common/Page/Medium/AbstractMedia.php b/system/src/Grav/Common/Page/Medium/AbstractMedia.php index bf9ddcc1e..f42d3ddd7 100644 --- a/system/src/Grav/Common/Page/Medium/AbstractMedia.php +++ b/system/src/Grav/Common/Page/Medium/AbstractMedia.php @@ -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; - } } } diff --git a/system/src/Grav/Common/Page/Medium/ImageMedium.php b/system/src/Grav/Common/Page/Medium/ImageMedium.php index c09e5a393..70abbf5b9 100644 --- a/system/src/Grav/Common/Page/Medium/ImageMedium.php +++ b/system/src/Grav/Common/Page/Medium/ImageMedium.php @@ -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]; + } } diff --git a/system/src/Grav/Framework/Contracts/Image/ImageAdapterInterface.php b/system/src/Grav/Framework/Contracts/Image/ImageAdapterInterface.php index 757f1daea..c9c282212 100644 --- a/system/src/Grav/Framework/Contracts/Image/ImageAdapterInterface.php +++ b/system/src/Grav/Framework/Contracts/Image/ImageAdapterInterface.php @@ -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 diff --git a/system/src/Grav/Framework/Image/Adapter/GdAdapter.php b/system/src/Grav/Framework/Image/Adapter/GdAdapter.php index 5c05abaac..83a07752a 100644 --- a/system/src/Grav/Framework/Image/Adapter/GdAdapter.php +++ b/system/src/Grav/Framework/Image/Adapter/GdAdapter.php @@ -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); diff --git a/system/src/Grav/Framework/Image/Image.php b/system/src/Grav/Framework/Image/Image.php index 2e3431b35..b34b6f325 100644 --- a/system/src/Grav/Framework/Image/Image.php +++ b/system/src/Grav/Framework/Image/Image.php @@ -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