diff --git a/composer.json b/composer.json index 436f97291..9ad0b9c9c 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 965a63c1f..19d5a4c92 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php b/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php index 2d3a4ba24..d44f243d4 100644 --- a/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php +++ b/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php @@ -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); } } diff --git a/system/src/Grav/Common/Media/Traits/MediaObjectTrait.php b/system/src/Grav/Common/Media/Traits/MediaObjectTrait.php index e4578f8c3..ca12609dc 100644 --- a/system/src/Grav/Common/Media/Traits/MediaObjectTrait.php +++ b/system/src/Grav/Common/Media/Traits/MediaObjectTrait.php @@ -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']; diff --git a/system/src/Grav/Common/Page/Medium/AbstractMedia.php b/system/src/Grav/Common/Page/Medium/AbstractMedia.php index dbb439234..9dd389275 100644 --- a/system/src/Grav/Common/Page/Medium/AbstractMedia.php +++ b/system/src/Grav/Common/Page/Medium/AbstractMedia.php @@ -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']; diff --git a/system/src/Grav/Common/Page/Medium/ImageFile.php b/system/src/Grav/Common/Page/Medium/ImageFile.php deleted file mode 100644 index d6da3bca0..000000000 --- a/system/src/Grav/Common/Page/Medium/ImageFile.php +++ /dev/null @@ -1,218 +0,0 @@ -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']); - } -} diff --git a/system/src/Grav/Common/Page/Medium/ImageMedium.php b/system/src/Grav/Common/Page/Medium/ImageMedium.php index 8d1455202..e4beb4d5a 100644 --- a/system/src/Grav/Common/Page/Medium/ImageMedium.php +++ b/system/src/Grav/Common/Page/Medium/ImageMedium.php @@ -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; } diff --git a/system/src/Grav/Common/Page/Medium/MediumFactory.php b/system/src/Grav/Common/Page/Medium/MediumFactory.php index 913f198f1..095c86bd4 100644 --- a/system/src/Grav/Common/Page/Medium/MediumFactory.php +++ b/system/src/Grav/Common/Page/Medium/MediumFactory.php @@ -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]; diff --git a/system/src/Grav/Framework/Contracts/Image/ImageOperationsInterface.php b/system/src/Grav/Framework/Contracts/Image/ImageOperationsInterface.php index cff961441..fff49dea4 100644 --- a/system/src/Grav/Framework/Contracts/Image/ImageOperationsInterface.php +++ b/system/src/Grav/Framework/Contracts/Image/ImageOperationsInterface.php @@ -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. * diff --git a/system/src/Grav/Framework/Contracts/Image/ImageSaveInterface.php b/system/src/Grav/Framework/Contracts/Image/ImageSaveInterface.php index 2499a0110..9db926899 100644 --- a/system/src/Grav/Framework/Contracts/Image/ImageSaveInterface.php +++ b/system/src/Grav/Framework/Contracts/Image/ImageSaveInterface.php @@ -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); } diff --git a/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php b/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php index ea24fb86e..78ba1fc85 100644 --- a/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php +++ b/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php @@ -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; diff --git a/system/src/Grav/Framework/Image/Adapter/GdAdapter.php b/system/src/Grav/Framework/Image/Adapter/GdAdapter.php index 6714eaef5..ea685ce2c 100644 --- a/system/src/Grav/Framework/Image/Adapter/GdAdapter.php +++ b/system/src/Grav/Framework/Image/Adapter/GdAdapter.php @@ -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; + } } diff --git a/system/src/Grav/Framework/Image/Image.php b/system/src/Grav/Framework/Image/Image.php index 41f637d25..be2be336b 100644 --- a/system/src/Grav/Framework/Image/Image.php +++ b/system/src/Grav/Framework/Image/Image.php @@ -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); + } } diff --git a/system/src/Grav/Framework/Image/Traits/ImageOperationsTrait.php b/system/src/Grav/Framework/Image/Traits/ImageOperationsTrait.php index 42260e68e..6d307377a 100644 --- a/system/src/Grav/Framework/Image/Traits/ImageOperationsTrait.php +++ b/system/src/Grav/Framework/Image/Traits/ImageOperationsTrait.php @@ -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;