Handle SVG and animated images correctly

This commit is contained in:
Giuseppe Criscione 2024-05-12 22:25:57 +02:00
parent d9a0f08203
commit efc52336e0
10 changed files with 206 additions and 20 deletions

View File

@ -202,7 +202,8 @@ final class App
->parameter('associations.image/jpeg', [ImageFactory::class, 'make'])
->parameter('associations.image/png', [ImageFactory::class, 'make'])
->parameter('associations.image/webp', [ImageFactory::class, 'make'])
->parameter('associations.image/gif', [ImageFactory::class, 'make']);
->parameter('associations.image/gif', [ImageFactory::class, 'make'])
->parameter('associations.image/svg+xml', [ImageFactory::class, 'make']);
$container->define(ImageFactory::class);

View File

@ -0,0 +1,37 @@
<?php
namespace Formwork\Images\Decoder;
use DOMDocument;
use DOMElement;
use Generator;
use InvalidArgumentException;
class SvgDecoder implements DecoderInterface
{
public function decode(string &$data): Generator
{
$domDocument = new DOMDocument();
$domDocument->loadXML($data);
$root = $domDocument->documentElement;
if (!($root instanceof DOMElement && $root->nodeName === 'svg')) {
throw new InvalidArgumentException('Invalid SVG data');
}
if (!$root->hasAttribute('width')) {
return;
}
if (!$root->hasAttribute('height')) {
return;
}
yield [
'width' => $root->getAttribute('width'),
'height' => $root->getAttribute('height'),
];
}
}

View File

@ -47,6 +47,8 @@ abstract class AbstractHandler implements HandlerInterface
*/
abstract public function getInfo(): ImageInfo;
abstract public function supportsTransforms(): bool;
abstract public static function supportsColorProfile(): bool;
/**
@ -136,6 +138,10 @@ abstract class AbstractHandler implements HandlerInterface
public function process(?TransformCollection $transformCollection = null, ?string $handler = null): AbstractHandler
{
if (!$this->supportsTransforms()) {
throw new RuntimeException(sprintf('Image handler of type %s does not support transforms for the current image', static::class));
}
$handler ??= static::class;
if (!is_subclass_of($handler, self::class)) {

View File

@ -62,6 +62,11 @@ class GifHandler extends AbstractHandler
return new ImageInfo($info);
}
public function supportsTransforms(): bool
{
return !$this->getInfo()->isAnimation();
}
public static function supportsColorProfile(): bool
{
return false;

View File

@ -28,6 +28,8 @@ interface HandlerInterface
*/
public function getInfo(): ImageInfo;
public function supportsTransforms(): bool;
public static function supportsColorProfile(): bool;
/**

View File

@ -50,6 +50,11 @@ class JpegHandler extends AbstractHandler
return new ImageInfo($info);
}
public function supportsTransforms(): bool
{
return true;
}
public static function supportsColorProfile(): bool
{
return true;

View File

@ -53,6 +53,11 @@ class PngHandler extends AbstractHandler
return new ImageInfo($info);
}
public function supportsTransforms(): bool
{
return !$this->getInfo()->isAnimation();
}
public static function supportsColorProfile(): bool
{
return true;

View File

@ -0,0 +1,102 @@
<?php
namespace Formwork\Images\Handler;
use Formwork\Images\ColorProfile\ColorProfile;
use Formwork\Images\ColorProfile\ColorSpace;
use Formwork\Images\Decoder\SvgDecoder;
use Formwork\Images\Exif\ExifData;
use Formwork\Images\Handler\Exceptions\UnsupportedFeatureException;
use Formwork\Images\ImageInfo;
use GdImage;
class SvgHandler extends AbstractHandler
{
public function getInfo(): ImageInfo
{
$info = [
'mimeType' => 'image/svg+xml',
'width' => 0,
'height' => 0,
'colorSpace' => ColorSpace::RGB,
'colorDepth' => 8,
'colorNumber' => null,
'hasAlphaChannel' => true,
'isAnimation' => false,
'animationFrames' => null,
'animationRepeatCount' => null,
];
foreach ($this->decoder->decode($this->data) as $dimensions) {
$info['width'] = (int) $dimensions['width'];
$info['height'] = (int) $dimensions['height'];
}
return new ImageInfo($info);
}
public function supportsTransforms(): bool
{
return false;
}
public static function supportsColorProfile(): bool
{
return false;
}
public function hasColorProfile(): bool
{
return false;
}
public function getColorProfile(): ?ColorProfile
{
throw new UnsupportedFeatureException('SVG does not support color profiles');
}
public function setColorProfile(ColorProfile $colorProfile): void
{
throw new UnsupportedFeatureException('SVG does not support color profiles');
}
public function removeColorProfile(): void
{
throw new UnsupportedFeatureException('SVG does not support color profiles');
}
public static function supportsExifData(): bool
{
return false;
}
public function hasExifData(): bool
{
return false;
}
public function getExifData(): ?ExifData
{
throw new UnsupportedFeatureException('SVG does not support EXIF data');
}
public function setExifData(ExifData $exifData): void
{
throw new UnsupportedFeatureException('SVG does not support EXIF data');
}
public function removeExifData(): void
{
throw new UnsupportedFeatureException('SVG does not support EXIF data');
}
protected function getDecoder(): SvgDecoder
{
return new SvgDecoder();
}
protected function setDataFromGdImage(GdImage $gdImage): void
{
throw new UnsupportedFeatureException('SVG does not support GdImage data');
}
}

View File

@ -80,6 +80,11 @@ class WebpHandler extends AbstractHandler
return new ImageInfo($info);
}
public function supportsTransforms(): bool
{
return !$this->getInfo()->isAnimation();
}
public static function supportsColorProfile(): bool
{
return true;

View File

@ -9,6 +9,7 @@ use Formwork\Images\Handler\AbstractHandler;
use Formwork\Images\Handler\GifHandler;
use Formwork\Images\Handler\JpegHandler;
use Formwork\Images\Handler\PngHandler;
use Formwork\Images\Handler\SvgHandler;
use Formwork\Images\Handler\WebpHandler;
use Formwork\Images\Transform\Blur;
use Formwork\Images\Transform\BlurMode;
@ -73,11 +74,17 @@ class Image extends File
if (!isset($this->mimeType)) {
$info = getimagesize($this->path);
if ($info === false) {
throw new RuntimeException('Failed to get image info');
if ($info !== false) {
return $this->mimeType = $info['mime'];
}
$this->mimeType = $info['mime'];
$mimeTypeFromFile = MimeType::fromFile($this->path);
if ($mimeTypeFromFile === 'image/svg+xml') {
return $this->mimeType = $mimeTypeFromFile;
}
throw new RuntimeException('Failed to get image info');
}
return $this->mimeType;
@ -288,7 +295,7 @@ class Image extends File
{
$mimeType ??= $this->mimeType();
if (!$forceCache && $mimeType === $this->mimeType() && $this->transforms->isEmpty()) {
if (!$forceCache && $mimeType === $this->mimeType() && (!$this->handler()->supportsTransforms() || $this->transforms->isEmpty())) {
return $this;
}
@ -332,13 +339,22 @@ class Image extends File
public function saveAs(string $path, ?string $mimeType = null): void
{
$handler = match ($mimeType ?? $this->mimeType()) {
'image/jpeg' => JpegHandler::class,
'image/png' => PngHandler::class,
'image/gif' => GifHandler::class,
'image/webp' => WebpHandler::class,
default => throw new RuntimeException(sprintf('Unsupported image type %s', $mimeType))
'image/jpeg' => JpegHandler::class,
'image/png' => PngHandler::class,
'image/gif' => GifHandler::class,
'image/webp' => WebpHandler::class,
'image/svg+xml' => SvgHandler::class,
default => throw new RuntimeException(sprintf('Unsupported image type %s', $mimeType))
};
if (!$this->handler()->supportsTransforms()) {
if ($mimeType === $this->mimeType()) {
$this->handler()->saveAs($path);
return;
}
throw new RuntimeException(sprintf('Unsupported image conversion from %s to %s', $this->mimeType(), $mimeType));
}
$this->handler()->process($this->transforms, $handler)->saveAs($path);
}
@ -364,11 +380,12 @@ class Image extends File
$mimeType ??= $this->mimeType();
$format = match ($mimeType) {
'image/jpeg' => $mimeType . $this->options['jpegQuality'] . $this->options['jpegProgressive'] . $this->options['preserveColorProfile'] . $this->options['preserveExifData'],
'image/png' => $mimeType . $this->options['pngCompression'] . $this->options['preserveColorProfile'] . $this->options['preserveExifData'],
'image/webp' => $mimeType . $this->options['webpQuality'] . $this->options['preserveColorProfile'] . $this->options['preserveExifData'],
'image/gif' => $mimeType . $this->options['gifColors'],
default => throw new RuntimeException(sprintf('Unsupported image type %s', $mimeType))
'image/jpeg' => $mimeType . $this->options['jpegQuality'] . $this->options['jpegProgressive'] . $this->options['preserveColorProfile'] . $this->options['preserveExifData'],
'image/png' => $mimeType . $this->options['pngCompression'] . $this->options['preserveColorProfile'] . $this->options['preserveExifData'],
'image/webp' => $mimeType . $this->options['webpQuality'] . $this->options['preserveColorProfile'] . $this->options['preserveExifData'],
'image/gif' => $mimeType . $this->options['gifColors'],
'image/svg+xml' => $mimeType,
default => throw new RuntimeException(sprintf('Unsupported image type %s', $mimeType))
};
return substr(hash('sha256', $this->path . $this->transforms->getSpecifier() . $format . FileSystem::lastModifiedTime($this->path)), 0, 32);
@ -388,11 +405,12 @@ class Image extends File
protected function getHandler(): AbstractHandler
{
return match ($this->mimeType()) {
'image/jpeg' => JpegHandler::fromPath($this->path),
'image/png' => PngHandler::fromPath($this->path),
'image/gif' => GifHandler::fromPath($this->path),
'image/webp' => WebpHandler::fromPath($this->path),
default => throw new RuntimeException('Unsupported image type'),
'image/jpeg' => JpegHandler::fromPath($this->path),
'image/png' => PngHandler::fromPath($this->path),
'image/gif' => GifHandler::fromPath($this->path),
'image/webp' => WebpHandler::fromPath($this->path),
'image/svg+xml' => SvgHandler::fromPath($this->path),
default => throw new RuntimeException('Unsupported image type'),
};
}