diff --git a/composer.json b/composer.json index 57b6256..5bd1344 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "php": ">=8.2", "ext-fileinfo": "*", "cakephp/cakephp": "^5.1", - "intervention/image": "^2.7.2", + "intervention/image": "^2 || ^3", "josegonzalez/cakephp-upload": "^8", "league/csv": "^9.8", "nette/utils": "^3.2 || ^4.0.0" diff --git a/src/Error/ModificationFailedException.php b/src/Error/ModificationFailedException.php new file mode 100644 index 0000000..a3ddc08 --- /dev/null +++ b/src/Error/ModificationFailedException.php @@ -0,0 +1,11 @@ + $driver])); + } + + private static function createV3ManagerFacade(): ImageManagerInterface + { + $driver = Configure::read('AssetsPlugin.ImageAsset.driver', 'gd'); + $legacyModifiersMap = Configure::read('AssetsPlugin.ImageAsset.legacyModifiersMap'); + + return new InterventionImageManagerFacade( + match ($driver) { + 'imagick', + ImagickDriver::class => ImageManager::imagick( + ...Configure::read('AssetsPlugin.ImageAsset.imagickOptions', []), + ), + 'gd', + GdDriver::class => ImageManager::gd( + ...Configure::read('AssetsPlugin.ImageAsset.gdOptions', []), + ), + default => throw new LogicException('no driver configured'), + }, + legacyModifiersMap: $legacyModifiersMap !== null + ? $legacyModifiersMap + : LegacySupport::V2_TO_V3_MODIFIERS_MAP, + ); + } +} diff --git a/src/ImageCreation/Intervention/InterventionImageFacade.php b/src/ImageCreation/Intervention/InterventionImageFacade.php new file mode 100644 index 0000000..5151fc5 --- /dev/null +++ b/src/ImageCreation/Intervention/InterventionImageFacade.php @@ -0,0 +1,116 @@ + $legacyMdifiersMap + */ + public function __construct( + private Intervention\ImageInterface $interventionImage, + private array $legacyMdifiersMap, + ) { + } + + public function width(): int + { + return $this->interventionImage->width(); + } + + public function height(): int + { + return $this->interventionImage->height(); + } + + /** + * @see MediaType + */ + public function mime(): string + { + return $this->interventionImage->origin()->mediaType(); + } + + public function save(string $absolutePath, ?int $quality, ?string $format): void + { + if ($format !== null && !str_ends_with($absolutePath, '.' . $format)) { + $absolutePath .= '.' . $format; + } + + $dir = dirname($absolutePath); + + if (!is_dir($dir)) { + FileSystem::createDir($dir); + } + + $this->interventionImage->save($absolutePath, quality: $quality); + } + + public function modify(string $modifier, array $params): ImageInterface + { + if ($params === []) { + throw new ModificationFailedException('Empty params given'); + } + + $modify = function (string $modifier, array $params, ?string $legacyModifier = null) { + try { + $this->interventionImage->{$modifier}(...$params); + } catch (\Throwable $throwable) { + + if ($legacyModifier === null) { + $callback = $this->legacyMdifiersMap[$modifier] ?? null; + if ($callback !== null && is_callable($callback)) { + $callback($this->interventionImage, ...$params); + return $this->interventionImage; + } + } + + throw new ModificationFailedException( + sprintf( + 'Modification `%s` failed with params: %s.%s', + $modifier, + var_export($params, true), + $legacyModifier !== null ? ' Legacy: ' . $legacyModifier : '', + ), + previous: $throwable, + ); + } + }; + + if (method_exists($this->interventionImage, $modifier)) { + $modify($modifier, $params); + return $this; + } + + $mappedFromLegacy = $this->legacyMdifiersMap[$modifier] ?? null; + + if ( + $mappedFromLegacy !== null + && method_exists($this->interventionImage, $mappedFromLegacy) + ) { + $modify($mappedFromLegacy, $params, $modifier); + return $this; + } + + throw new ModificationFailedException(sprintf('Modifier `%s` does not exist', $modifier)); + } + + public function getInterventionImage(): Intervention\ImageInterface + { + return $this->requireInterventionImage(); + } + + public function requireInterventionImage(): Intervention\ImageInterface + { + return $this->interventionImage; + } +} diff --git a/src/ImageCreation/Intervention/InterventionImageManagerFacade.php b/src/ImageCreation/Intervention/InterventionImageManagerFacade.php new file mode 100644 index 0000000..9085e80 --- /dev/null +++ b/src/ImageCreation/Intervention/InterventionImageManagerFacade.php @@ -0,0 +1,42 @@ + $legacyModifiersMap + */ + public function __construct( + private Intervention\ImageManagerInterface $interventionImageManager, + private array $legacyModifiersMap, + ) { + } + + public function read(string $absolutePath): ImageInterface + { + return new InterventionImageFacade( + $this->interventionImageManager->read($absolutePath), + $this->legacyModifiersMap, + ); + } + + public function create(int $width, int $height): ImageInterface + { + return new InterventionImageFacade( + $this->interventionImageManager->create($width, $height), + $this->legacyModifiersMap, + ); + } + + public function canvas(int $width, int $height): ImageInterface + { + return $this->create($width, $height); + } +} diff --git a/src/ImageCreation/Intervention/LegacySupport.php b/src/ImageCreation/Intervention/LegacySupport.php new file mode 100644 index 0000000..6691dae --- /dev/null +++ b/src/ImageCreation/Intervention/LegacySupport.php @@ -0,0 +1,103 @@ + 'create', + 'circle' => 'drawCircle', + 'ellipse' => 'drawEllipse', + 'line' => 'drawLine', + 'pixel' => 'drawPixel', + 'filter' => 'modify', + 'insert' => 'place', + 'make' => 'read', + 'mime' => 'encodedImage', + 'polygon' => 'drawPolygon', + 'rectangle' => 'drawRectangle', + 'limitColors' => 'reduceColors', + 'getCore' => 'core', + 'orientate' => 'orient', + 'widen' => 'scale', + 'heighten' => 'scale', + 'fit' => 'cover', + // Signature changed + 'crop' => [self::class, 'legacyCrop'], + 'encode' => [self::class, 'legacyEncode'], + 'exif' => [self::class, 'legacyExif'], + 'fill' => [self::class, 'legacyFill'], + 'flip' => [self::class, 'legacyFlip'], + 'text' => [self::class, 'legacyText'], + 'resizeCanvas' => [self::class, 'legacyResizeCanvas'], + 'trim' => [self::class, 'legacyTrim'], + 'resize' => [self::class, 'legacyResize'], + ]; + + public static function legacyCrop(Image $image, mixed ...$params): Image + { + throw new ModificationFailedException('legacy fallback for crop not implemented'); + } + + public static function legacyEncode(Image $image, mixed ...$params): EncodedImageInterface + { + $format = $params[0] ?? null; + + if (!is_string($format)) { + throw new ModificationFailedException('no format given'); + } + + $encoder = new FileExtensionEncoder(FileExtension::from($format)); + + return $image->encode($encoder); + } + + public static function legacyExif(Image $image, mixed ...$params): Image + { + throw new ModificationFailedException('legacy fallback for exif not implemented'); + } + + public static function legacyFill(Image $image, mixed ...$params): Image + { + throw new ModificationFailedException('legacy fallback for fill not implemented'); + } + + public static function legacyFlip(Image $image, mixed ...$params): Image + { + throw new ModificationFailedException('legacy fallback for flip not implemented'); + } + + public static function legacyText(Image $image, mixed ...$params): Image + { + throw new ModificationFailedException('legacy fallback for text not implemented'); + } + + public static function legacyResizeCanvas(Image $image, mixed ...$params): Image + { + throw new ModificationFailedException('legacy fallback for resizeCanvas not implemented'); + } + + public static function legacyTrim(Image $image, mixed ...$params): Image + { + throw new ModificationFailedException('legacy fallback for trim not implemented'); + } + + public static function legacyResize(Image $image, mixed ...$params): Image + { + throw new ModificationFailedException('legacy fallback for resize not implemented'); + } +} diff --git a/src/ImageCreation/LegacyV2Intervention/InterventionImageFacade.php b/src/ImageCreation/LegacyV2Intervention/InterventionImageFacade.php new file mode 100644 index 0000000..f988cd8 --- /dev/null +++ b/src/ImageCreation/LegacyV2Intervention/InterventionImageFacade.php @@ -0,0 +1,86 @@ +interventionImage->width(); + } + + public function height(): int + { + return $this->interventionImage->height(); + } + + /** + * @see MediaType + */ + public function mime(): string + { + return $this->interventionImage->origin()->mediaType(); + } + + public function save(string $absolutePath, ?int $quality, ?string $format): void + { + if ($format !== null && !str_ends_with($absolutePath, '.' . $format)) { + $absolutePath .= '.' . $format; + } + + $dir = dirname($absolutePath); + + if (!is_dir($dir)) { + FileSystem::createDir($dir); + } + + $this->interventionImage->save($absolutePath, quality: $quality); + } + + public function modify(string $modifier, array $params): ImageInterface + { + if ($params === []) { + throw new ModificationFailedException('Empty params given'); + } + + try { + if (method_exists($this->interventionImage, $modifier)) { + $this->interventionImage->{$modifier}(...$params); + } + } catch (\Throwable $throwable) { + throw new ModificationFailedException( + sprintf( + 'Modification `%s` failed with params: %s.', + $modifier, + var_export($params, true), + ), + previous: $throwable, + ); + } + + throw new ModificationFailedException(sprintf('Modifier `%s` does not exist', $modifier)); + } + + public function getInterventionImage(): Image + { + return $this->interventionImage; + } + + public function requireInterventionImage(): Image + { + return $this->interventionImage; + } +} diff --git a/src/ImageCreation/LegacyV2Intervention/InterventionImageManagerFacade.php b/src/ImageCreation/LegacyV2Intervention/InterventionImageManagerFacade.php new file mode 100644 index 0000000..4e1a84a --- /dev/null +++ b/src/ImageCreation/LegacyV2Intervention/InterventionImageManagerFacade.php @@ -0,0 +1,36 @@ +interventionImageManager->make($absolutePath), + ); + } + + public function create(int $width, int $height): ImageInterface + { + return new InterventionImageFacade( + $this->interventionImageManager->canvas($width, $height), + ); + } + + public function canvas(int $width, int $height): ImageInterface + { + return $this->create($width, $height); + } +} diff --git a/src/ImageCreation/LegacyV2Intervention/LegacySupport.php b/src/ImageCreation/LegacyV2Intervention/LegacySupport.php new file mode 100644 index 0000000..29201ec --- /dev/null +++ b/src/ImageCreation/LegacyV2Intervention/LegacySupport.php @@ -0,0 +1,103 @@ + 'create', + 'circle' => 'drawCircle', + 'ellipse' => 'drawEllipse', + 'line' => 'drawLine', + 'pixel' => 'drawPixel', + 'filter' => 'modify', + 'insert' => 'place', + 'make' => 'read', + 'mime' => 'encodedImage', + 'polygon' => 'drawPolygon', + 'rectangle' => 'drawRectangle', + 'limitColors' => 'reduceColors', + 'getCore' => 'core', + 'orientate' => 'orient', + 'widen' => 'scale', + 'heighten' => 'scale', + 'fit' => 'cover', + // Signature changed + 'crop' => [self::class, 'legacyCrop'], + 'encode' => [self::class, 'legacyEncode'], + 'exif' => [self::class, 'legacyExif'], + 'fill' => [self::class, 'legacyFill'], + 'flip' => [self::class, 'legacyFlip'], + 'text' => [self::class, 'legacyText'], + 'resizeCanvas' => [self::class, 'legacyResizeCanvas'], + 'trim' => [self::class, 'legacyTrim'], + 'resize' => [self::class, 'legacyResize'], + ]; + + public static function legacyCrop(Image $image, mixed ...$params): Image + { + throw new ModificationFailedException('legacy fallback for crop not implemented'); + } + + public static function legacyEncode(Image $image, mixed ...$params): EncodedImageInterface + { + $format = $params[0] ?? null; + + if (!is_string($format)) { + throw new ModificationFailedException('no format given'); + } + + $encoder = new FileExtensionEncoder(FileExtension::from($format)); + + return $image->encode($encoder); + } + + public static function legacyExif(Image $image, mixed ...$params): Image + { + throw new ModificationFailedException('legacy fallback for exif not implemented'); + } + + public static function legacyFill(Image $image, mixed ...$params): Image + { + throw new ModificationFailedException('legacy fallback for fill not implemented'); + } + + public static function legacyFlip(Image $image, mixed ...$params): Image + { + throw new ModificationFailedException('legacy fallback for flip not implemented'); + } + + public static function legacyText(Image $image, mixed ...$params): Image + { + throw new ModificationFailedException('legacy fallback for text not implemented'); + } + + public static function legacyResizeCanvas(Image $image, mixed ...$params): Image + { + throw new ModificationFailedException('legacy fallback for resizeCanvas not implemented'); + } + + public static function legacyTrim(Image $image, mixed ...$params): Image + { + throw new ModificationFailedException('legacy fallback for trim not implemented'); + } + + public static function legacyResize(Image $image, mixed ...$params): Image + { + throw new ModificationFailedException('legacy fallback for resize not implemented'); + } +} diff --git a/src/Utilities/ImageAsset.php b/src/Utilities/ImageAsset.php index 3ca6780..5d2dfb1 100644 --- a/src/Utilities/ImageAsset.php +++ b/src/Utilities/ImageAsset.php @@ -6,14 +6,16 @@ use Assets\Error\FileNotFoundException; use Assets\Error\FilterNotFoundException; use Assets\Error\UnkownErrorException; +use Assets\ImageCreation\FilterInterface; +use Assets\ImageCreation\ImageInterface; +use Assets\ImageCreation\ImageManagerInterface; +use Assets\ImageCreation\ImageManagerLocator; use Assets\Model\Entity\Asset; use Cake\Core\Configure; use Cake\I18n\DateTime; use Cake\View\Helper\HtmlHelper; use Cake\View\View; use Exception; -use Intervention\Image\Image; -use Intervention\Image\ImageManager; use InvalidArgumentException; use Nette\Utils\FileSystem; use Nette\Utils\Json; @@ -29,7 +31,7 @@ * * To just get a public path from /webroot, call getPath() */ -class ImageAsset +final class ImageAsset { private Asset $asset; @@ -41,11 +43,11 @@ class ImageAsset private array $modifications; - private ?Image $image; + private ?ImageInterface $image = null; private ?string $format; - private ?string $filename; + private ?string $filename = null; private string $css; @@ -62,7 +64,6 @@ public function __construct(Asset $asset, int $quality = 90) $this->asset = $asset; $this->quality = $quality; $this->modifications = []; - $this->image = null; $this->format = $this->asset->filetype; $this->filename = null; $this->lazyLoading = true; @@ -82,8 +83,8 @@ public function __construct(Asset $asset, int $quality = 90) * @param array $options - optional: * - title (string): for alt-parameter in html-output * - quality (int): for jpg compression - * @throws \InvalidArgumentException when no file was found * @return self + * @throws InvalidArgumentException when no file was found */ public static function createFromPath(string $path, array $options = []): self { @@ -117,7 +118,7 @@ public static function createFromPath(string $path, array $options = []): self $quality = $options['quality'] ?? 90; - return new ImageAsset($asset, (int)$quality); + return new self($asset, (int)$quality); } /** @@ -127,7 +128,7 @@ public static function createFromPath(string $path, array $options = []): self * @param int $width Width in px * @return $this */ - public function scaleWidth(int $width) + public function scaleWidth(int $width): self { $this->trackModification('widen', [$width]); @@ -137,7 +138,7 @@ public function scaleWidth(int $width) /** * @return $this */ - public function toWebp() + public function toWebp(): self { $this->trackModification('encode', ['webp']); $this->format = 'webp'; @@ -148,7 +149,7 @@ public function toWebp() /** * @return $this */ - public function toJpg() + public function toJpg(): self { $this->trackModification('encode', ['jpg']); $this->format = 'jpg'; @@ -162,7 +163,7 @@ public function toJpg() * @param string $css HTML class which will be added on render * @return $this */ - public function setCSS(string $css) + public function setCSS(string $css): self { $this->css = $css; @@ -173,7 +174,7 @@ public function setCSS(string $css) * @param bool $lazyLoading Control if the image shall be loaded lazily when rendered as Html * @return $this */ - public function setLazyLoading(bool $lazyLoading = true) + public function setLazyLoading(bool $lazyLoading = true): self { $this->lazyLoading = $lazyLoading; @@ -189,7 +190,7 @@ public function setLazyLoading(bool $lazyLoading = true) * @param string|null $filename the custom filename * @return $this */ - public function setFilename(?string $filename) + public function setFilename(?string $filename): self { $this->filename = $filename; @@ -197,16 +198,23 @@ public function setFilename(?string $filename) } /** - * e.g. - * ImageAsset::applyFilter(EpaperFilter::class, ['kombi']) - * !! Don't pass an ImageManager instance, only string or int properties. - * * @param string $filter ClassName of the Filter - * @param array $properties Will be passed after the ImageManager instance when calling the Filter's constructor. + * @param array $properties Will be passed to the Filter's constructor * @return $this */ - public function applyFilter(string $filter, array $properties = []) + public function applyFilter(string $filter, array $properties = []): self { + if (!is_a($filter, FilterInterface::class, allow_string: true)) { + throw new InvalidArgumentException( + sprintf( + 'Argument $filter (`%s`) of type `%s` does not implement `%s`.', + \Cake\Core\h($filter), + get_debug_type($filter), + FilterInterface::class, + ), + ); + } + $this->trackModification('filter_' . $filter, $properties); return $this; @@ -221,7 +229,7 @@ public function applyFilter(string $filter, array $properties = []) * @return $this * @link https://image.intervention.io/v2 */ - public function modify(string $method, mixed ...$params) + public function modify(string $method, mixed ...$params): self { $this->trackModification($method, $params); @@ -262,7 +270,7 @@ public function getHTML(array $params = []): string if (!$this->image) { $manager = $this->getImageManager(); - $this->image = $manager->make($this->getAbsolutePath()); + $this->image = $manager->read($this->getAbsolutePath()); } $default_params = [ @@ -407,7 +415,7 @@ private function render() $manager = $this->getImageManager(); try { - $image = $manager->make($this->asset->absolute_path); + $image = $manager->read($this->asset->absolute_path); } catch (Exception $e) { throw new UnkownErrorException("Could not call ImageManager::make on Asset #{$this->asset->id}. Error: {$e->getMessage()}."); } @@ -423,12 +431,9 @@ private function render() } /** - * @param \Intervention\Image\Image $image The image instance - * @param \Intervention\Image\ImageManager $manager The manager instance - * @return \Intervention\Image\Image * @throws \Assets\Error\FilterNotFoundException */ - private function applyModifications(Image $image, ImageManager $manager): Image + private function applyModifications(ImageInterface $image, ImageManagerInterface $manager): ImageInterface { $modifications = $this->modifications; unset($modifications['noApi']); @@ -437,33 +442,36 @@ private function applyModifications(Image $image, ImageManager $manager): Image if (str_contains($method, 'filter_')) { $filterClassName = Strings::after($method, 'filter_') ?? ''; if (!class_exists($filterClassName)) { - throw new FilterNotFoundException("Filter {$filterClassName} does not exist. "); + throw new FilterNotFoundException( + sprintf( + 'Filter `%s` does not exist.', + $filterClassName, + ), + ); + } + if (!is_a($filterClassName, FilterInterface::class, allow_string: true)) { + throw new FilterNotFoundException( + sprintf( + 'Filter `%s` does not implement `%s`.', + $filterClassName, + FilterInterface::class, + ), + ); } - /** - * @var \Intervention\Image\Filters\FilterInterface $filter - */ - $filter = new $filterClassName($manager, ...$params); - $image = $filter->applyFilter($image); + $image = $filterClassName::create($manager, ...$params)->applyFilter($image); continue; } $params = is_array($params) ? $params : [$params]; - $image->{$method}(...$params); + $image->modify($method, $params); } return $image; } - /** - * @return \Intervention\Image\ImageManager - */ - private function getImageManager(): ImageManager + private function getImageManager(): ImageManagerInterface { - $driver = Configure::read('AssetsPlugin.ImageAsset.driver', 'gd'); - - return new ImageManager([ - 'driver' => $driver, - ]); + return ImageManagerLocator::getImageManager(); } }