mirror of
https://github.com/getgrav/grav.git
synced 2026-06-25 11:31:39 +02:00
Improved retina image support
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user