Start using the new Image classes in Media

This commit is contained in:
Matias Griese
2022-02-23 22:09:09 +02:00
parent 035e533c02
commit bac5c756fe
14 changed files with 654 additions and 643 deletions

View File

@@ -45,8 +45,6 @@
"filp/whoops": "~2.9",
"matthiasmullie/minify": "^1.3",
"monolog/monolog": "~1.25",
"getgrav/image": "^3.0",
"getgrav/cache": "^2.0",
"donatj/phpuseragentparser": "~1.1",
"pimple/pimple": "~3.5.0",
"rockettheme/toolbox": "~1.5",

147
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "1067938388862c52927c6450e8a36df0",
"content-hash": "ddf2ee1968a5086f59be53180004e557",
"packages": [
{
"name": "composer/ca-bundle",
@@ -617,117 +617,6 @@
],
"time": "2022-01-07T12:00:00+00:00"
},
{
"name": "getgrav/cache",
"version": "v2.0.0",
"target-dir": "Gregwar/Cache",
"source": {
"type": "git",
"url": "https://github.com/getgrav/Cache.git",
"reference": "56fd63f752779928fcd1074ab7d12f406dde8861"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/getgrav/Cache/zipball/56fd63f752779928fcd1074ab7d12f406dde8861",
"reference": "56fd63f752779928fcd1074ab7d12f406dde8861",
"shasum": ""
},
"require": {
"php": ">=5.3"
},
"type": "library",
"autoload": {
"psr-0": {
"Gregwar\\Cache": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gregwar",
"email": "g.passault@gmail.com"
},
{
"name": "Grav CMS",
"email": "hello@getgrav.org",
"homepage": "https://getgrav.org"
}
],
"description": "A lightweight file-system cache system",
"keywords": [
"cache",
"caching",
"file-system",
"system"
],
"support": {
"source": "https://github.com/getgrav/Cache/tree/v2.0.0"
},
"time": "2021-04-20T05:48:00+00:00"
},
{
"name": "getgrav/image",
"version": "v3.0.0",
"target-dir": "Gregwar/Image",
"source": {
"type": "git",
"url": "https://github.com/getgrav/Image.git",
"reference": "02c1bb2c179dd894c4f6610c9c49da364ee7d264"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/getgrav/Image/zipball/02c1bb2c179dd894c4f6610c9c49da364ee7d264",
"reference": "02c1bb2c179dd894c4f6610c9c49da364ee7d264",
"shasum": ""
},
"require": {
"ext-gd": "*",
"getgrav/cache": "^2.0",
"php": "^5.6 || ^7.0 || ^8.0"
},
"require-dev": {
"sllh/php-cs-fixer-styleci-bridge": "~1.0",
"symfony/phpunit-bridge": "^2.7.4 || ^3.0"
},
"suggest": {
"behat/transliterator": "Transliterator provides ability to set non-latin1 pretty names"
},
"type": "library",
"autoload": {
"psr-0": {
"Gregwar\\Image": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Grégoire Passault",
"email": "g.passault@gmail.com",
"homepage": "http://www.gregwar.com/"
},
{
"name": "Grav CMS",
"email": "hello@getgrav.org",
"homepage": "https://getgrav.org"
}
],
"description": "Image handling",
"homepage": "https://github.com/Gregwar/Image",
"keywords": [
"gd",
"image"
],
"support": {
"source": "https://github.com/getgrav/Image/tree/v3.0.0"
},
"time": "2021-04-20T05:50:18+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "1.8.3",
@@ -4126,12 +4015,12 @@
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
},
"files": [
"src/functions_include.php"
]
],
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@@ -4748,16 +4637,16 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "9.2.11",
"version": "9.2.13",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "665a1ac0a763c51afc30d6d130dac0813092b17f"
"reference": "deac8540cb7bd40b2b8cfa679b76202834fd04e8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/665a1ac0a763c51afc30d6d130dac0813092b17f",
"reference": "665a1ac0a763c51afc30d6d130dac0813092b17f",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/deac8540cb7bd40b2b8cfa679b76202834fd04e8",
"reference": "deac8540cb7bd40b2b8cfa679b76202834fd04e8",
"shasum": ""
},
"require": {
@@ -4813,7 +4702,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.11"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.13"
},
"funding": [
{
@@ -4821,7 +4710,7 @@
"type": "github"
}
],
"time": "2022-02-18T12:46:09+00:00"
"time": "2022-02-23T17:02:38+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -5066,16 +4955,16 @@
},
{
"name": "phpunit/phpunit",
"version": "9.5.14",
"version": "9.5.16",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "1883687169c017d6ae37c58883ca3994cfc34189"
"reference": "5ff8c545a50226c569310a35f4fa89d79f1ddfdc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1883687169c017d6ae37c58883ca3994cfc34189",
"reference": "1883687169c017d6ae37c58883ca3994cfc34189",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5ff8c545a50226c569310a35f4fa89d79f1ddfdc",
"reference": "5ff8c545a50226c569310a35f4fa89d79f1ddfdc",
"shasum": ""
},
"require": {
@@ -5091,7 +4980,7 @@
"phar-io/version": "^3.0.2",
"php": ">=7.3",
"phpspec/prophecy": "^1.12.1",
"phpunit/php-code-coverage": "^9.2.7",
"phpunit/php-code-coverage": "^9.2.13",
"phpunit/php-file-iterator": "^3.0.5",
"phpunit/php-invoker": "^3.1.1",
"phpunit/php-text-template": "^2.0.3",
@@ -5153,7 +5042,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.14"
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.16"
},
"funding": [
{
@@ -5165,7 +5054,7 @@
"type": "github"
}
],
"time": "2022-02-18T12:54:07+00:00"
"time": "2022-02-23T17:10:58+00:00"
},
{
"name": "psr/http-client",

View File

@@ -10,17 +10,15 @@
namespace Grav\Common\Media\Traits;
use BadFunctionCallException;
use Grav\Common\Config\Config;
use Grav\Common\Grav;
use Grav\Common\Media\Interfaces\ImageMediaInterface;
use Grav\Common\Page\Medium\ImageFile;
use Grav\Common\Page\Medium\ImageMedium;
use Grav\Common\Page\Medium\MediumFactory;
use Grav\Framework\File\Formatter\JsonFormatter;
use Grav\Framework\File\JsonFile;
use Grav\Framework\Image\Adapter\GdAdapter;
use Grav\Framework\Image\Image;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use function array_key_exists;
use function extension_loaded;
use function func_num_args;
use function function_exists;
use function in_array;
/**
@@ -29,15 +27,13 @@ use function in_array;
*/
trait ImageMediaTrait
{
/** @var ImageFile|null */
/** @var Image|null */
protected $image;
/** @var string */
protected $format = 'guess';
/** @var int */
protected $quality;
/** @var bool */
protected $debug_watermarked = false;
/** @var bool */
protected $watermark;
/** @var array */
@@ -101,9 +97,6 @@ trait ImageMediaTrait
public function setImagePrettyName($name)
{
$this->set('prettyname', $name);
if ($this->image) {
$this->image->setPrettyName($name);
}
}
/**
@@ -111,8 +104,9 @@ trait ImageMediaTrait
*/
public function getImagePrettyName()
{
if ($this->get('prettyname')) {
return $this->get('prettyname');
$prettyName = $this->get('prettyname');
if ($prettyName) {
return $prettyName;
}
$basename = $this->get('basename');
@@ -149,55 +143,46 @@ trait ImageMediaTrait
*/
public function derivatives($min_width, $max_width = 2500, $step = 200)
{
if (!empty($this->alternatives)) {
// Get the largest image to be the base.
if (empty($this->alternatives)) {
$base = $this;
} else {
$max = max(array_keys($this->alternatives));
$base = $this->alternatives[$max];
} else {
$base = $this;
}
$widths = [];
$baseWidth = $base->get('width');
$filepath = $base->get('filepath');
$widths = [];
if (func_num_args() === 1) {
foreach ((array) func_get_arg(0) as $width) {
if ($width < $base->get('width')) {
$widths[] = $width;
foreach ((array) $min_width as $width) {
if ($width < $baseWidth) {
$widths[] = (int)$width;
}
}
} else {
$max_width = min($max_width, $base->get('width'));
$max_width = min($max_width, $baseWidth);
for ($width = $min_width; $width < $max_width; $width += $step) {
$widths[] = $width;
$widths[] = (int)$width;
}
}
foreach ($widths as $width) {
// Only generate image alternatives that don't already exist
if (array_key_exists((int) $width, $this->alternatives)) {
if (isset($this->alternatives[$width])) {
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
$derivative = $this->getMedia()->createFromFile($filepath);
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;
$ratio = $baseWidth / $width;
$height = $derivative->get('height') / $ratio;
$derivative->resize($width, $height);
@@ -263,7 +248,7 @@ trait ImageMediaTrait
* Set or get sizes parameter for srcset media action
*
* @param string|null $sizes
* @return string
* @return string|$this
*/
public function sizes($sizes = null)
{
@@ -330,9 +315,9 @@ trait ImageMediaTrait
*/
public function filter($filter = 'image.filters.default')
{
$filters = (array) $this->get($filter, []);
$filters = (array)$this->get($filter, []);
foreach ($filters as $params) {
$params = (array) $params;
$params = (array)$params;
$method = array_shift($params);
$this->__call($method, $params);
}
@@ -368,15 +353,13 @@ trait ImageMediaTrait
*
* @return $this
*/
public function cropZoom()
public function cropZoom(...$args)
{
$this->__call('zoomCrop', func_get_args());
$this->__call('zoomCrop', $args);
return $this;
}
/**
* @param string|null $image
* @param string|null $position
@@ -385,6 +368,8 @@ trait ImageMediaTrait
*/
public function watermark($image = null, $position = null, $scale = null)
{
// TODO:
/*
$grav = $this->getGrav();
$locator = $grav['locator'];
@@ -440,6 +425,7 @@ trait ImageMediaTrait
}
$this->__call('merge', [$watermark,$positionX, $positionY]);
*/
return $this;
}
@@ -451,7 +437,9 @@ trait ImageMediaTrait
*/
public function addFrame(int $border = 10, string $color = '0x000000')
{
if($border > 0 && preg_match('/^0x[a-f0-9]{6}$/i', $color)) { // $border must be an integer and bigger than 0; $color must be formatted as an HEX value (0x??????).
// TODO:
/*
if( $border > 0 && preg_match('/^0x[a-f0-9]{6}$/i', $color)) { // $border must be an integer and bigger than 0; $color must be formatted as an HEX value (0x??????).
$image = ImageFile::fromData($this->readFile());
}
else {
@@ -470,6 +458,7 @@ trait ImageMediaTrait
$this->__call('merge', [$image, $border, $border]);
$this->saveImage();
*/
return $this;
}
@@ -525,30 +514,14 @@ trait ImageMediaTrait
*/
protected function image()
{
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
$filepath = $this->filepath;
$webroot = preg_quote(GRAV_WEBROOT, '`');
$root = preg_quote(GRAV_WEBROOT, '`');
$filepath = preg_replace(['`^' . $webroot . '/`u', '`^' . $root . '/`u'], ['GRAV_WEBROOT/', 'GRAV_ROOT/'], $filepath);
// 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);
$this->image = ImageFile::fromData($this->readFile());
$this->image
->setCacheDir($cacheDir)
->setActualCacheDir($cacheDir)
->setPrettyName($this->getImagePrettyName());
// Fix orientation if enabled
/** @var Config $config */
$config = Grav::instance()['config'];
if ($config->get('system.images.auto_fix_orientation', false) &&
extension_loaded('exif') && function_exists('exif_read_data')) {
$this->image->fixOrientation();
}
$this->watermark = $config->get('system.images.watermark.watermark_all', false);
// Create a new image.
$this->image = new Image($filepath, $this->getItems());
$this->image->fixOrientation();
return $this;
}
@@ -570,27 +543,102 @@ trait ImageMediaTrait
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;
}
if ($this->get('debug')) {
$ratio = min(1, (int)$this->get('ratio'));
/** @var UniformResourceLocator $locator */
$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));
// FIXME
$info = [
'modified' => 0,
'size' => 0,
'width' => 0,
'height' => 0,
];
$overlayImage = new Image($overlay, $info);
$this->image->merge($overlayImage);
}
if ($this->watermark) {
$this->watermark();
}
return $this->image->cacheFile($this->format, $this->quality, false, [$this->get('width'), $this->get('height'), $this->get('modified')]);
return $this->generateCache();
}
/**
* @return string
*/
protected function generateCache(): string
{
$quality = $this->quality;
$format = $this->format;
if ($format === 'guess') {
$extension = strtolower($this->get('extension'));
$format = $extension;
}
$image = $this->image;
$image->extra['format'] = $format;
$image->extra['quality'] = $quality;
$data = $image->jsonSerialize();
$hash = $data['hash'];
$d1 = substr($hash, 0, 2);
$d2 = substr($hash, 2, 2);
$d3 = substr($hash, 4);
$prettyName = $this->getImagePrettyName();
$imageFile = "cache://images/{$d1}/{$d2}/{$d3}/{$prettyName}.{$format}";
$cacheFile = "{$imageFile}.json";
/** @var UniformResourceLocator $locator */
$locator = $this->getGrav()['locator'];
$imageFile = '/' . $locator->getResource($imageFile, false);
$cacheFile = $locator->getResource($cacheFile, true);
$file = $this->getCacheMetaFile($cacheFile);
if (!$file->exists()) {
$file->save($data);
} else {
$file->touch();
}
return $this->generateCacheImage(GRAV_WEBROOT . '/' . $imageFile);
}
/**
* @param string $filepath
* @return string
*/
protected function generateCacheImage(string $filepath): string
{
if (file_exists($filepath)) {
return $filepath;
}
$adapter = GdAdapter::createFromString($this->readFile());
$image = $this->image;
$image->setAdapter($adapter);
$image->save($filepath, 'jpg', $this->quality);
$image->freeAdapter();
return $filepath;
}
/**
* @param string $filepath
* @return JsonFile
*/
protected function getCacheMetaFile(string $filepath): JsonFile
{
$formatter = new JsonFormatter(['encode_options' => JSON_PRETTY_PRINT]);
return new JsonFile($filepath, $formatter);
}
}

View File

@@ -13,6 +13,7 @@ use Grav\Common\Data\Data;
use Grav\Common\Media\Interfaces\MediaFileInterface;
use Grav\Common\Media\Interfaces\MediaLinkInterface;
use Grav\Common\Media\Interfaces\MediaObjectInterface;
use Grav\Common\Page\Medium\ImageMedium;
use Grav\Common\Page\Medium\ThumbnailImageMedium;
use Grav\Common\Utils;
use RuntimeException;
@@ -553,9 +554,9 @@ trait MediaObjectTrait
/**
* Get the thumbnail Medium object
*
* @return ThumbnailImageMedium
* @return ThumbnailImageMedium|ImageMedium
*/
protected function getThumbnail(): ThumbnailImageMedium
protected function getThumbnail(): ImageMedium
{
if (null === $this->_thumbnail) {
$thumbnails = (array)$this->get('thumbnails') + ['system' => 'system://images/media/thumb.png'];

View File

@@ -583,6 +583,7 @@ abstract class AbstractMedia implements ExportInterface, MediaCollectionInterfac
}
$pathInfo = Utils::pathinfo($filename);
$info['basename'] = $pathInfo['filename'];
$info['filename'] = $pathInfo['basename'];
if (!isset($info['path'])) {
$info['path'] = $pathInfo['dirname'] === '.' ? $this->getPath() : $pathInfo['dirname'];

View File

@@ -1,218 +0,0 @@
<?php
/**
* @package Grav\Common\Page
*
* @copyright Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
use Exception;
use Grav\Common\Config\Config;
use Grav\Common\Grav;
use Gregwar\Image\Exceptions\GenerationError;
use Gregwar\Image\Image;
use Gregwar\Image\Source;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RuntimeException;
use function array_key_exists;
use function count;
use function extension_loaded;
use function in_array;
/**
* Class ImageFile
* @package Grav\Common\Page\Medium
*
* @method Image applyExifOrientation($exif_orienation)
*/
class ImageFile extends Image
{
/**
* Destruct also image object.
*/
public function __destruct()
{
$adapter = $this->adapter;
if ($adapter) {
$adapter->deinit();
}
}
/**
* Clear previously applied operations
*
* @return void
*/
public function clearOperations()
{
$this->operations = [];
}
/**
* This is the same as the Gregwar Image class except this one fires a Grav Event on creation of new cached file
*
* @param string $type the image type
* @param int $quality the quality (for JPEG)
* @param bool $actual
* @param array $extras
* @return string
*/
public function cacheFile($type = 'jpg', $quality = 80, $actual = false, $extras = [])
{
if ($type === 'guess') {
$type = $this->guessType();
}
if (!$this->forceCache && !count($this->operations) && $type === $this->guessType()) {
return $this->getFilename($this->getFilePath());
}
// Computes the hash
$this->hash = $this->getHash($type, $quality, $extras);
/** @var Config $config */
$config = Grav::instance()['config'];
// Seo friendly image names
$seofriendly = $config->get('system.images.seofriendly', false);
if ($seofriendly) {
$mini_hash = substr($this->hash, 0, 4) . substr($this->hash, -4);
$cacheFile = "{$this->prettyName}-{$mini_hash}";
} else {
$cacheFile = "{$this->hash}-{$this->prettyName}";
}
$cacheFile .= '.' . $type;
// If the files does not exists, save it
$image = $this;
// Target file should be younger than all the current image
// dependencies
$conditions = array(
'younger-than' => $this->getDependencies()
);
// The generating function
$generate = function ($target) use ($image, $type, $quality) {
$result = $image->save($target, $type, $quality);
if ($result !== $target) {
throw new GenerationError($result);
}
Grav::instance()->fireEvent('onImageMediumSaved', new Event(['image' => $target]));
};
// Asking the cache for the cacheFile
try {
$perms = $config->get('system.images.cache_perms', '0755');
$perms = octdec($perms);
$file = $this->getCacheSystem()->setDirectoryMode($perms)->getOrCreateFile($cacheFile, $conditions, $generate, $actual);
} catch (GenerationError $e) {
$file = $e->getNewFile();
}
// Nulling the resource
$adapter = $this->getAdapter();
$adapter->setSource(new Source\File($file));
$adapter->deinit();
if ($actual) {
return $file;
}
return $this->getFilename($file);
}
/**
* Gets the hash.
*
* @param string $type
* @param int $quality
* @param array $extras
* @return string
*/
public function getHash($type = 'guess', $quality = 80, $extras = [])
{
if (null === $this->hash) {
$this->generateHash($type, $quality, $extras);
}
return $this->hash;
}
/**
* Generates the hash.
*
* @param string $type
* @param int $quality
* @param array $extras
*/
public function generateHash($type = 'guess', $quality = 80, $extras = [])
{
$inputInfos = $this->source->getInfos();
$data = [
$inputInfos,
$this->serializeOperations(),
$type,
$quality,
$extras
];
$this->hash = sha1(serialize($data));
}
/**
* Read exif rotation from file and apply it.
*
* @return $this
*/
public function fixOrientation()
{
$info = $this->source->getInfos();
if (!\is_string($info) || !file_exists($info)) {
return $this;
}
if (!extension_loaded('exif')) {
throw new RuntimeException('You need to EXIF PHP Extension to use this function');
}
if (!in_array(exif_imagetype($info), [IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM], true)) {
return $this;
}
// resolve any streams
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
$filepath = $this->source->getInfos();
if ($locator->isStream($filepath)) {
$filepath = $locator->findResource($info, true, true);
}
// Make sure file exists
if (!file_exists($filepath)) {
return $this;
}
try {
$exif = @exif_read_data($filepath);
} catch (Exception $e) {
Grav::instance()['log']->error($filepath . ' - ' . $e->getMessage());
return $this;
}
if ($exif === false || !array_key_exists('Orientation', $exif)) {
return $this;
}
return $this->applyExifOrientation($exif['Orientation']);
}
}

View File

@@ -32,7 +32,7 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
/** @var array */
protected $defaults = [];
/** @var array */
protected $options = [];
protected $imageSettings = [];
/** @var string|null */
private $saved_image_path;
@@ -47,6 +47,7 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
/** @var Config $config */
$config = $this->getGrav()['config'];
$this->watermark = $config->get('system.images.watermark.watermark_all', false);
$this->thumbnailTypes = ['page', 'media', 'default'];
$this->defaults = [
'quality' => (int)$config->get('system.images.default_image_quality', 85),
@@ -54,7 +55,6 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
'auto_sizes' => (bool)$config->get('system.images.cls.auto_sizes', false),
'aspect_ratio' => (bool)$config->get('system.images.cls.aspect_ratio', false),
'retina_scale' => (int)$config->get('system.images.cls.retina_scale', 1)
];
parent::__construct($items, $blueprint);
@@ -64,7 +64,7 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
$path = $this->get('filepath');
$this->set('thumbnails.media', $path);
// TODO:
// TODO: We shouldn't have this here
$exists = $path && file_exists($path) && filesize($path);
if ($exists && !($this->offsetExists('width') && $this->offsetExists('height') && $this->offsetExists('mime'))) {
$image_info = getimagesize($path);
@@ -101,9 +101,8 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
parent::reset();
$this->format = 'guess';
$this->options = $this->defaults;
$this->imageSettings = $this->defaults;
$this->quality = $this->defaults['quality'];
$this->debug_watermarked = false;
$this->resetImage();
@@ -135,13 +134,13 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
*/
public function url($reset = true)
{
$grav = $this->getGrav();
// FIXME: update this code
$saved_image_path = $this->saved_image_path = $this->saveImage();
$output = preg_replace('|^' . preg_quote(GRAV_WEBROOT, '|') . '|', '', $saved_image_path) ?: $saved_image_path;
$grav = $this->getGrav();
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
@@ -208,18 +207,18 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
$attributes['sizes'] = $this->sizes();
}
if ($this->saved_image_path && $this->options['auto_sizes']) {
if ($this->saved_image_path && $this->imageSettings['auto_sizes']) {
// FIXME: we can calculate this from the new image object..?
if (!array_key_exists('height', $this->attributes) && !array_key_exists('width', $this->attributes)) {
$info = getimagesize($this->saved_image_path);
$width = (int)$info[0];
$height = (int)$info[1];
$scaling_factor = min(1, $this->options['retina_scale']);
$scaling_factor = min(1, $this->imageSettings['retina_scale']);
$attributes['width'] = (int)($width / $scaling_factor);
$attributes['height'] = (int)($height / $scaling_factor);
if ($this->options['aspect_ratio']) {
if ($this->imageSettings['aspect_ratio']) {
$style = ($attributes['style'] ?? ' ') . "--aspect-ratio: $width/$height;";
$attributes['style'] = trim($style);
}
@@ -276,7 +275,7 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
{
$enabled = \is_bool($enabled) ? $enabled : $enabled === 'true';
$this->options['auto_sizes'] = $enabled;
$this->imageSettings['auto_sizes'] = $enabled;
return $this;
}
@@ -289,18 +288,18 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
{
$enabled = \is_bool($enabled) ? $enabled : $enabled === 'true';
$this->options['aspect_ratio'] = $enabled;
$this->imageSettings['aspect_ratio'] = $enabled;
return $this;
}
/**
* @param int $scale
* @param string|int $scale
* @return $this
*/
public function retinaScale($scale = 1)
{
$this->options['retina_scale'] = (int)$scale;
$this->imageSettings['retina_scale'] = (int)$scale;
return $this;
}

View File

@@ -182,11 +182,7 @@ class MediumFactory
*/
public static function scaledFromMedium($medium, $from, $to)
{
if (!$medium instanceof ImageMedium) {
return $medium;
}
if ($to > $from) {
if (!$medium instanceof ImageMedium || $to > $from) {
return $medium;
}
@@ -206,13 +202,15 @@ class MediumFactory
$medium->set('debug', $debug);
$medium->setImagePrettyName($prev_basename);
$size = filesize($file);
$medium = self::fromFile($file);
if ($medium) {
$size = filesize($file);
$medium->set('basename', $basename);
$medium->set('filename', $basename . '.' . $medium->extension);
$medium->set('size', $size);
} else {
$size = 0;
}
return ['file' => $medium, 'size' => $size];

View File

@@ -116,6 +116,12 @@ interface ImageOperationsInterface extends ImageResizeInterface, ImageInfoInterf
*/
public function sepia();
/**
* @param int $blurFactor
* @return $this
*/
public function gaussianBlur(int $blurFactor = 1);
/**
* Merge with another image.
*

View File

@@ -10,34 +10,34 @@ interface ImageSaveInterface
/**
* Save the image as a gif.
*
* @param string $file
* @param string|null $filepath
* @return $this
*/
public function saveGif(string $file);
public function saveGif(?string $filepath);
/**
* Save the image as a png.
*
* @param string $file
* @param string|null $filepath
* @return $this
*/
public function savePng(string $file);
public function savePng(?string $filepath);
/**
* Save the image as a Webp.
*
* @param string $file
* @param string|null $filepath
* @param int $quality
* @return $this
*/
public function saveWebp(string $file, int $quality);
public function saveWebp(?string $filepath, int $quality);
/**
* Save the image as a jpeg.
*
* @param string $file
* @param string|null $filepath
* @param int $quality
* @return $this
*/
public function saveJpeg(string $file, int $quality);
public function saveJpeg(?string $filepath, int $quality);
}

View File

@@ -15,6 +15,7 @@ use Grav\Common\Media\Factories\MediaFactory;
use Grav\Common\Media\Interfaces\MediaCollectionInterface;
use Grav\Common\Media\Interfaces\MediaUploadInterface;
use Grav\Common\Media\Traits\MediaTrait;
use Grav\Common\Page\Media;
use Grav\Common\Page\Medium\Medium;
use Grav\Common\Page\Medium\MediumFactory;
use Grav\Common\Utils;
@@ -205,10 +206,12 @@ trait FlexMediaTrait
}
$media = $this->getMedia();
/** @var Media|null $originalMedia */
$originalMedia = is_callable([$this, 'getOriginalMedia']) ? $this->getOriginalMedia() : null;
$list = [];
foreach ($value as $filename => $info) {
$filename = (string)$filename;
if (!is_array($info)) {
$list[$filename] = $info;
continue;

View File

@@ -2,10 +2,31 @@
namespace Grav\Framework\Image\Adapter;
use Grav\Common\Utils;
use Grav\Framework\Contracts\Image\ImageAdapterInterface;
use InvalidArgumentException;
use RuntimeException;
use UnexpectedValueException;
use function count;
use function define;
use function defined;
use function extension_loaded;
use function function_exists;
use function is_resource;
// Make sure DG defines have been set!
if (!defined('IMG_GIF')) {
define('IMG_GIF', 1);
}
if (!defined('IMG_JPG')) {
define('IMG_JPG', 2);
}
if (!defined('IMG_PNG')) {
define('IMG_PNG', 4);
}
if (!defined('IMG_WEBP')) {
define('IMG_WEBP', 32);
}
/**
* GD Image adapter.
@@ -42,6 +63,58 @@ class GdAdapter extends Adapter
return (bool)(imagetypes() & $test);
}
/**
* Creates a new image.
*
* @param int $width
* @param int $height
* @return static
*/
public static function create(int $width, int $height): GdAdapter
{
$resource = static::createResource($width, $height);
return new static($resource);
}
/**
* Creates image from a file.
*
* @param string $filepath
* @return static
*/
public static function createFromFile(string $filepath): GdAdapter
{
$extension = strtolower(Utils::pathinfo($filepath, PATHINFO_EXTENSION));
$resource = static::createResourceFromFile($filepath, $extension);
return new static($resource);
}
/**
* Creates image from string of image data.
*
* @param string $data
* @return static
*/
public static function createFromString(string $data): GdAdapter
{
$resource = static::createResourceFromString($data);
return new static($resource);
}
/**
* Creates an instance of image from resource.
*
* @param \GdImage|resource $resource
* @return static
*/
public static function createFromImage($resource): GdAdapter
{
return new static($resource);
}
/**
* @param \GdImage|resource $resource
*/
@@ -56,6 +129,15 @@ class GdAdapter extends Adapter
}
$this->resource = $resource;
$this->convertToTrueColor();
}
public function __destruct()
{
if ($this->resource) {
imagedestroy($this->resource);
}
}
/**
@@ -77,7 +159,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function fillBackground(?int $background = 0xffffff)
public function fillBackground(?int $background = 0xffffff): GdAdapter
{
$w = $this->width();
$h = $this->height();
@@ -96,10 +178,13 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function resize(?int $background, int $target_width, int $target_height, int $new_width, int $new_height)
public function resize(?int $background, int $target_width, int $target_height, int $new_width, int $new_height): GdAdapter
{
$width = $this->width();
$height = $this->height();
$dst_x = (int)(($target_width - $new_width) / 2);
$dst_y = (int)(($target_height - $new_height) / 2);
$n = imagecreatetruecolor($target_width, $target_height);
if (!$n) {
throw new RuntimeException('Failed to resize image: image creation failed');
@@ -115,7 +200,7 @@ class GdAdapter extends Adapter
imagesavealpha($n, true);
}
imagecopyresampled($n, $this->resource, ($target_width - $new_width) / 2, ($target_height - $new_height) / 2, 0, 0, $new_width, $new_height, $width, $height);
imagecopyresampled($n, $this->resource, $dst_x, $dst_y, 0, 0, $new_width, $new_height, $width, $height);
imagedestroy($this->resource);
$this->resource = $n;
@@ -126,7 +211,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function crop(int $x, int $y, int $width, int $height)
public function crop(int $x, int $y, int $width, int $height): GdAdapter
{
$destination = imagecreatetruecolor($width, $height);
if (!$destination) {
@@ -146,7 +231,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function negate()
public function negate(): GdAdapter
{
imagefilter($this->resource, IMG_FILTER_NEGATE);
@@ -156,7 +241,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function brightness($brightness)
public function brightness($brightness): GdAdapter
{
imagefilter($this->resource, IMG_FILTER_BRIGHTNESS, $brightness);
@@ -166,7 +251,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function contrast($contrast)
public function contrast($contrast): GdAdapter
{
imagefilter($this->resource, IMG_FILTER_CONTRAST, $contrast);
@@ -176,7 +261,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function grayscale()
public function grayscale(): GdAdapter
{
imagefilter($this->resource, IMG_FILTER_GRAYSCALE);
@@ -186,7 +271,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function emboss()
public function emboss(): GdAdapter
{
imagefilter($this->resource, IMG_FILTER_EMBOSS);
@@ -196,7 +281,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function smooth(int $p)
public function smooth(int $p): GdAdapter
{
imagefilter($this->resource, IMG_FILTER_SMOOTH, $p);
@@ -206,7 +291,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function sharp()
public function sharp(): GdAdapter
{
imagefilter($this->resource, IMG_FILTER_MEAN_REMOVAL);
@@ -216,7 +301,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function edge()
public function edge(): GdAdapter
{
imagefilter($this->resource, IMG_FILTER_EDGEDETECT);
@@ -226,7 +311,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function colorize(int $red, int $green, int $blue)
public function colorize(int $red, int $green, int $blue): GdAdapter
{
imagefilter($this->resource, IMG_FILTER_COLORIZE, $red, $green, $blue);
@@ -236,7 +321,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function sepia()
public function sepia(): GdAdapter
{
imagefilter($this->resource, IMG_FILTER_GRAYSCALE);
imagefilter($this->resource, IMG_FILTER_COLORIZE, 100, 50, 0);
@@ -247,7 +332,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function gaussianBlur(int $blurFactor = 1)
public function gaussianBlur(int $blurFactor = 1): GdAdapter
{
if ($blurFactor < 1) {
return $this;
@@ -267,8 +352,8 @@ class GdAdapter extends Adapter
// scale way down and gradually scale back up, blurring all the way
for ($i = 0; $i < $blurFactor; ++$i) {
// determine dimensions of next image
$nextWidth = (int)($smallestWidth * (2 ** $i));
$nextHeight = (int)($smallestHeight * (2 ** $i));
$nextWidth = $smallestWidth * (2 ** $i);
$nextHeight = $smallestHeight * (2 ** $i);
// resize previous image to next size
$nextImage = imagecreatetruecolor($nextWidth, $nextHeight);
@@ -288,8 +373,7 @@ class GdAdapter extends Adapter
}
// scale back to original size and blur one more time
imagecopyresized($this->resource, $nextImage,
0, 0, 0, 0, $originalWidth, $originalHeight, $nextWidth, $nextHeight);
imagecopyresized($this->resource, $nextImage, 0, 0, 0, 0, $originalWidth, $originalHeight, $nextWidth, $nextHeight);
imagefilter($this->resource, IMG_FILTER_GAUSSIAN_BLUR);
// clean up
@@ -303,7 +387,7 @@ class GdAdapter extends Adapter
*
* @param GdAdapter $other
*/
public function merge(ImageAdapterInterface $other, int $x = 0, int $y = 0, int $width = null, int $height = null)
public function merge(ImageAdapterInterface $other, int $x = 0, int $y = 0, int $width = null, int $height = null): GdAdapter
{
if (!$other instanceof self) {
throw new InvalidArgumentException('Image to be merged needs to be instance of GdAdapter');
@@ -327,7 +411,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function rotate(float $angle, ?int $background = 0xffffff)
public function rotate(float $angle, ?int $background = 0xffffff): GdAdapter
{
$resource = imagerotate($this->resource, $angle, $this->allocateColor($background));
if (!$resource) {
@@ -344,7 +428,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function fill(int $color = 0xffffff, int $x = 0, int $y = 0)
public function fill(int $color = 0xffffff, int $x = 0, int $y = 0): GdAdapter
{
imagealphablending($this->resource, false);
imagefill($this->resource, $x, $y, $this->allocateColor($color));
@@ -355,7 +439,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function write(string $font, string $text, int $x = 0, int $y = 0, float $size = 12.0, float $angle = 0.0, int $color = 0x000000, string $align = 'left')
public function write(string $font, string $text, int $x = 0, int $y = 0, float $size = 12.0, float $angle = 0.0, int $color = 0x000000, string $align = 'left'): GdAdapter
{
imagealphablending($this->resource, true);
@@ -363,7 +447,7 @@ class GdAdapter extends Adapter
$sim_size = $this->getTTFBox($font, $text, $size, $angle);
if ($align === 'center') {
$x -= $sim_size['width'] / 2;
$x -= (int)($sim_size['width'] / 2);
}
if ($align === 'right') {
@@ -379,7 +463,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function rectangle(int $x1, int $y1, int $x2, int $y2, int $color, bool $filled = false)
public function rectangle(int $x1, int $y1, int $x2, int $y2, int $color, bool $filled = false): GdAdapter
{
$c = $this->allocateColor($color);
if ($filled) {
@@ -394,7 +478,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function roundedRectangle(int $x1, int $y1, int $x2, int $y2, int $radius, int $color, bool $filled = false)
public function roundedRectangle(int $x1, int $y1, int $x2, int $y2, int $radius, int $color, bool $filled = false): GdAdapter
{
$c = $this->allocateColor($color);
@@ -425,7 +509,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function line(int $x1, int $y1, int $x2, int $y2, $color = 0x000000)
public function line(int $x1, int $y1, int $x2, int $y2, $color = 0x000000): GdAdapter
{
imageline($this->resource, $x1, $y1, $x2, $y2, $this->allocateColor($color));
@@ -435,7 +519,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function ellipse(int $cx, int $cy, int $width, int $height, $color = 0x000000, bool $filled = false)
public function ellipse(int $cx, int $cy, int $width, int $height, $color = 0x000000, bool $filled = false): GdAdapter
{
$c = $this->allocateColor($color);
if ($filled) {
@@ -450,7 +534,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function circle(int $cx, int $cy, int $r, $color = 0x000000, bool $filled = false)
public function circle(int $cx, int $cy, int $r, $color = 0x000000, bool $filled = false): GdAdapter
{
return $this->ellipse($cx, $cy, $r, $r, $this->allocateColor($color), $filled);
}
@@ -458,7 +542,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function polygon(array $points, $color, bool $filled = false)
public function polygon(array $points, $color, bool $filled = false): GdAdapter
{
$num = (int)(count($points) / 2);
$c = $this->allocateColor($color);
@@ -475,7 +559,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function flip(bool $flipVertical, bool $flipHorizontal)
public function flip(bool $flipVertical, bool $flipHorizontal): GdAdapter
{
if (!$flipVertical && !$flipHorizontal) {
return $this;
@@ -548,7 +632,7 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function saveGif(string $filepath)
public function saveGif(?string $filepath): GdAdapter
{
$transColor = imagecolorallocatealpha($this->resource, 255, 255, 255, 127);
if (!$transColor) {
@@ -556,7 +640,10 @@ class GdAdapter extends Adapter
}
imagecolortransparent($this->resource, $transColor);
imagegif($this->resource, $filepath);
$result = imagegif($this->resource, $filepath);
if (false === $result) {
throw new RuntimeException('Failed to save image as gif');
}
return $this;
}
@@ -564,9 +651,13 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function savePng(string $filepath)
public function savePng(?string $filepath): GdAdapter
{
imagepng($this->resource, $filepath);
$result = imagepng($this->resource, $filepath);
if (false === $result) {
throw new RuntimeException('Failed to save image as png');
}
return $this;
}
@@ -574,9 +665,12 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function saveWebp(string $filepath, int $quality)
public function saveWebp(?string $filepath, int $quality): GdAdapter
{
imagewebp($this->resource, $filepath, $quality);
$result = imagewebp($this->resource, $filepath, $quality);
if (false === $result) {
throw new RuntimeException('Failed to save image as webp');
}
return $this;
}
@@ -584,9 +678,12 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function saveJpeg(string $filepath, int $quality)
public function saveJpeg(?string $filepath, int $quality): GdAdapter
{
imagejpeg($this->resource, $filepath, $quality);
$result = imagejpeg($this->resource, $filepath, $quality);
if (false === $result) {
throw new RuntimeException('Failed to save image as jpeg');
}
return $this;
}
@@ -594,108 +691,27 @@ class GdAdapter extends Adapter
/**
* {@inheritdoc}
*/
public function enableProgressive()
public function enableProgressive(): GdAdapter
{
imageinterlace($this->resource, true);
return $this;
}
/**
* Create empty image.
*
* @param int $width
* @param int $height
* @return void
*/
protected function createImage(int $width, int $height): void
{
$this->resource = imagecreatetruecolor($width, $height) ?: null;
}
/**
* Create image from a string.
*
* @param string $data
* @return void
*/
protected function createImageFromString(string $data): void
{
$this->resource = @imagecreatefromstring($data) ?: null;
}
/**
* Converts the image to true color.
*
* @return void
* @return static
*/
protected function convertToTrueColor(): void
protected function convertToTrueColor(): GdAdapter
{
if (!imageistruecolor($this->resource)) {
imagepalettetotruecolor($this->resource);
}
imagesavealpha($this->resource, true);
}
/**
* Try to open the file using jpeg.
*
* @param string $filepath
* @return void
*/
protected function openJpeg(string $filepath): void
{
if (file_exists($filepath) && filesize($filepath)) {
$this->resource = @imagecreatefromjpeg($filepath) ?: null;
} else {
$this->resource = null;
}
}
/**
* Try to open the file using gif.
*
* @param string $filepath
* @return void
*/
protected function openGif(string $filepath): void
{
if (file_exists($filepath) && filesize($filepath)) {
$this->resource = @imagecreatefromgif($filepath) ?: null;
} else {
$this->resource = null;
}
}
/**
* Try to open the file using PNG.
*
* @param string $filepath
* @return void
*/
protected function openPng(string $filepath): void
{
if (file_exists($filepath) && filesize($filepath)) {
$this->resource = @imagecreatefrompng($filepath) ?: null;
} else {
$this->resource = null;
}
}
/**
* Try to open the file using WEBP.
*
* @param string $filepath
* @return void
*/
protected function openWebp(string $filepath): void
{
if (file_exists($filepath) && filesize($filepath)) {
$this->resource = @imagecreatefromwebp($filepath) ?: null;
} else {
$this->resource = null;
}
return $this;
}
/**
@@ -710,56 +726,6 @@ class GdAdapter extends Adapter
return imagecolorat($this->resource, $x, $y);
}
/**
* Load image resource.
*
* @param \GdImage|resource $resource
*/
protected function loadResource($resource): void
{
$this->resource = $resource;
imagesavealpha($this->resource, true);
}
/**
* Load file.
*
* @param string $filepath
* @param string $type
* @return void
* @throws UnexpectedValueException
*/
protected function loadFile(string $filepath, string $type): void
{
if (!static::isSupported($type)) {
throw new UnexpectedValueException('Type ' . $type . ' is not supported by GD');
}
switch ($type) {
case 'jpeg':
$this->openJpeg($filepath);
break;
case 'gif':
$this->openGif($filepath);
break;
case 'png':
$this->openPng($filepath);
break;
case 'webp':
$this->openWebp($filepath);
break;
default:
throw new UnexpectedValueException('Unable to open file (' . $filepath . ')');
}
if (null === $this->getResource()) {
throw new UnexpectedValueException('Unable to open file (' . $filepath . ')');
}
$this->convertToTrueColor();
}
/**
* Give the bounding box of a text using TrueType fonts.
*
@@ -813,4 +779,70 @@ class GdAdapter extends Adapter
return $c;
}
/**
* Load file.
*
* @param string $filepath
* @param string $type
* @return \GdImage|resource|null
* @throws UnexpectedValueException
*/
protected static function createResourceFromFile(string $filepath, string $type)
{
if (!static::isSupported($type)) {
throw new UnexpectedValueException(sprintf('Type %s is not supported by GD', $type));
}
// Check if file exists.
if (!file_exists($filepath) || !filesize($filepath)) {
return null;
}
$test = self::$types[$type] ?? 0;
$resource = null;
switch ($test) {
case \IMG_JPG:
$resource = @imagecreatefromjpeg($filepath) ?: null;
break;
case \IMG_GIF:
$resource = @imagecreatefromgif($filepath) ?: null;
break;
case \IMG_PNG:
$resource = @imagecreatefrompng($filepath) ?: null;
break;
case \IMG_WEBP:
$resource = @imagecreatefromwebp($filepath) ?: null;
break;
}
if (null === $resource) {
throw new UnexpectedValueException(sprintf('Unable to open file (%s)', $filepath));
}
return $resource;
}
/**
* Create image from a string.
*
* @param string $data
* @return \GdImage|resource|null
*/
protected static function createResourceFromString(string $data)
{
return @imagecreatefromstring($data) ?: null;
}
/**
* Create empty image.
*
* @param int $width
* @param int $height
* @return \GdImage|resource|null
*/
protected static function createResource(int $width, int $height)
{
return imagecreatetruecolor($width, $height) ?: null;
}
}

View File

@@ -2,11 +2,19 @@
namespace Grav\Framework\Image;
use Exception;
use Grav\Common\Filesystem\Folder;
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 RuntimeException;
use Symfony\Component\Filesystem\Exception\IOException;
use function array_slice;
use function dirname;
use function is_int;
/**
* Image class.
@@ -16,6 +24,23 @@ class Image implements ImageOperationsInterface, JsonSerializable
use ImageOperationsTrait;
use Serializable;
/**
* Supported types.
* @var array
*/
public static $types = [
'jpg' => 'jpeg',
'jpeg' => 'jpeg',
'webp' => 'webp',
'png' => 'png',
'gif' => 'gif',
];
/** @var array */
public $extra = [];
/** @var ImageAdapterInterface */
protected $adapter;
/** @var int */
protected $origWidth;
/** @var int */
@@ -26,8 +51,10 @@ class Image implements ImageOperationsInterface, JsonSerializable
protected $modified;
/** @var int */
protected $size;
/** @var array */
public $extra = [];
/** @var int */
protected $operationsCursor = 0;
/**
* @param string $filepath
@@ -36,10 +63,10 @@ class Image implements ImageOperationsInterface, JsonSerializable
public function __construct(string $filepath, array $info)
{
$this->filepath = $filepath;
$this->modified = $info['modified'] ?? 0;
$this->size = $info['size'] ?? 0;
$this->origWidth = $this->width = $info['width'] ?? 0;
$this->origHeight = $this->height = $info['height'] ?? 0;
$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->orientation = isset($info['exif']['Orientation']) ? (int)$info['exif']['Orientation'] : null;
}
@@ -105,4 +132,218 @@ class Image implements ImageOperationsInterface, JsonSerializable
{
return sha1(serialize($this));
}
/**
* Get image adapter.
*
* @return ImageAdapterInterface|null
*/
public function getAdapter(): ?ImageAdapterInterface
{
return $this->adapter;
}
/**
* Set image adapter.
*
* Note: You should always call $this->freeAdapter() as soon as you have generated the image!
*
* @param ImageAdapterInterface|string $adapter
* @return $this
* @throws RuntimeException
*/
public function setAdapter(ImageAdapterInterface $adapter): Image
{
$this->adapter = $adapter;
return $this;
}
/**
* Free image adapter to free some memory.
*
* @return void
*/
public function freeAdapter(): void
{
$this->adapter = null;
}
/**
* @param string $type
* @param int $quality
* @param bool $actual
* @return string
* @throws IOException|InvalidArgumentException|RuntimeException
*/
public function cacheFile(string $type = 'jpg', int $quality = 80, bool $actual = false): string
{
$filepath = $actual ? null : $this->filepath;
if (file_exists($filepath)) {
return $filepath;
}
return $this->save($filepath, $type, $quality);
}
/**
* @param int $quality
* @return string
* @throws IOException|InvalidArgumentException|RuntimeException
*/
public function jpeg(int $quality = 80): string
{
return $this->cacheFile('jpg', $quality);
}
/**
* @return string
* @throws IOException|InvalidArgumentException|RuntimeException
*/
public function gif(): string
{
return $this->cacheFile('gif');
}
/**
* @return string
* @throws IOException|InvalidArgumentException|RuntimeException
*/
public function png(): string
{
return $this->cacheFile('png');
}
/**
* @param int $quality
* @return string
* @throws IOException|InvalidArgumentException|RuntimeException
*/
public function webp(int $quality = 80): string
{
return $this->cacheFile('webp', $quality);
}
/**
* @param int $quality
* @return string
* @throws IOException|InvalidArgumentException|RuntimeException
*/
public function guess(int $quality = 80): string
{
return $this->cacheFile('guess', $quality);
}
/**
* @return string
*/
public function guessType(): string
{
return pathinfo($this->filepath, PATHINFO_EXTENSION);
}
/**
* Save the file to a given output.
*
* Note: to use this method, you need to call `setAdapter()` first.
*
* @param string|null $filepath
* @param string|int $type
* @param int $quality
* @return string
* @throws IOException|InvalidArgumentException|RuntimeException
*/
public function save(?string $filepath, $type = 'guess', int $quality = 80): string
{
if (is_int($type)) {
$quality = $type;
$type = 'jpeg';
}
if ($type === 'guess') {
$type = $this->guessType();
}
$type = self::$types[$type] ?? '';
if ('' === $type) {
throw new InvalidArgumentException(sprintf("Given image type '%s' is not valid", $type));
}
$adapter = $this->getAdapter();
if (null === $adapter) {
throw new RuntimeException('You need to set image adapter first!');
}
if ($filepath) {
$this->mkdir(dirname($filepath));
}
try {
$this->applyOperations();
if (null === $filepath) {
ob_start();
}
switch ($type) {
case 'jpeg';
$adapter->saveJpeg($filepath, $quality);
break;
case 'gif';
$adapter->saveGif($filepath);
break;
case 'png':
$adapter->savePng($filepath);
break;
case 'webp':
$adapter->saveWebP($filepath, $quality);
break;
}
return $filepath ?? ob_get_clean();
} catch (Exception $e) {
throw new RuntimeException('', $e->getCode(), $e);
}
}
/**
* Apply image operations.
*
* Note: to use this method, you need to call `setAdapter()` first.
*
* @return $this
*/
public function applyOperations(): Image
{
$adapter = $this->adapter;
if (!$adapter) {
throw new RuntimeException('You need to set image adapter first!');
}
// Only get the remaining operations.
$operations = $this->operations;
$cursor = $this->operationsCursor;
if ($cursor) {
$operations = array_slice($operations, $cursor, null, true);
}
foreach ($operations as $cursor => $operation) {
[$method, $params] = $operation;
$adapter->{$method}(...$params);
}
$this->operationsCursor = $cursor;
return $this;
}
/**
* @param string $directory
* @return void
*/
private function mkdir(string $directory): void
{
Folder::mkdir($directory);
}
}

View File

@@ -147,7 +147,7 @@ trait ImageOperationsTrait
$height = $new_height;
}
if ($width === $new_width && $height === $new_height) {
if ($width === $new_width && $height === $new_height && $width === $this->width() && $height === $this->height()) {
// Nothing to resize.
return $this;
}
@@ -465,6 +465,17 @@ trait ImageOperationsTrait
return $this;
}
/**
* @param int $blurFactor
* @return $this
*/
public function gaussianBlur(int $blurFactor = 1)
{
$this->operations[] = ['gaussianBlur', [$blurFactor]];
return $this;
}
/**
* Merge with another image.
*
@@ -478,8 +489,10 @@ trait ImageOperationsTrait
public function merge(Image $other, int $x = 0, int $y = 0, int $width = 0, int $height = 0)
{
$serialized = $other->jsonSerialize();
$deps = $other->dependencies;
$deps[] = $serialized;
$dependencies[] = $serialized;
$this->dependencies = array_merge($this->dependencies, $deps);
$this->operations[] = ['merge', [$serialized, $x, $y, $width, $height]];
return $this;