From 84bd29119208c9a831c355f97f8fb3f90e92ce94 Mon Sep 17 00:00:00 2001 From: Guy Sartorelli Date: Thu, 4 Apr 2024 17:19:14 +1300 Subject: [PATCH] NEW File converter API --- src/Conversion/FileConversionException.php | 13 ++ src/Conversion/FileConversionManager.php | 31 +++ src/Conversion/FileConverter.php | 24 +++ src/Conversion/InterventionImageConverter.php | 198 ++++++++++++++++++ src/ImageManipulation.php | 14 ++ 5 files changed, 280 insertions(+) create mode 100644 src/Conversion/FileConversionException.php create mode 100644 src/Conversion/FileConversionManager.php create mode 100644 src/Conversion/FileConverter.php create mode 100644 src/Conversion/InterventionImageConverter.php diff --git a/src/Conversion/FileConversionException.php b/src/Conversion/FileConversionException.php new file mode 100644 index 00000000..84350c5b --- /dev/null +++ b/src/Conversion/FileConversionException.php @@ -0,0 +1,13 @@ +supportsConversion($from, $toFormat, $options)) { + return $converter->convert($from, $toFormat, $options); + } + } + throw new FileConversionException('No file converter available for this conversion.'); + } +} diff --git a/src/Conversion/FileConverter.php b/src/Conversion/FileConverter.php new file mode 100644 index 00000000..cbbe11d5 --- /dev/null +++ b/src/Conversion/FileConverter.php @@ -0,0 +1,24 @@ +validateOptions($options); + if (!empty($unsupportedOptions)) { + return false; + } + // If backend is not InterventionBackend, return false + if ($from instanceof DBFile) { + $backend = $from->getImageBackend(); + $from = $from->getExtension() ?: $from->getMimeType(); + } else { + $backend = Injector::inst()->get(Image_Backend::class); + } + // This converter requires intervention image + if (!is_a($backend, InterventionBackend::class)) { + return false; + } + /** @var InterventionBackend $backend */ + $driver = $backend->getImageManager()->config['driver'] ?? null; + return $this->supportedByIntervention($from, $toFormat, $driver); + } + + public function convert(DBFile $original, string $toFormat, array $options = []): DBFile + { + $problems = $this->validateOptions($options); + if (!empty($problems)) { + throw new FileConversionException('Invalid options provided: ' . implode(', ', $problems)); + } + $quality = $options['quality'] ?? null; + try { + $result = $original->manipulateExtension( + $toFormat, + function (AssetStore $store, string $filename, string $hash, string $variant) use ($original, $quality) { + $backend = clone $original->getImageBackend(); + if ($quality !== null) { + $backend->setQuality($quality); + } + $config = ['conflict' => AssetStore::CONFLICT_USE_EXISTING]; + $tuple = $backend->writeToStore($store, $filename, $hash, $variant, $config); + return [$tuple, $backend]; + } + ); + } catch (ImageException $e) { + throw new FileConversionException('Failed to convert: ' . $e->getMessage(), $e->getCode(), $e); + } + if ($result === null) { + throw new FileConversionException('File conversion resulted in null. Check whether original file actually exists.'); + } + return $result; + } + + private function validateOptions(array $options): array + { + $problems = []; + foreach ($options as $key => $value) { + if ($key !== 'quality') { + $problems[] = "unexpected option '$key'"; + continue; + } + if (!is_int($value)) { + $problems[] = "quality value must be an integer"; + } + } + return $problems; + } + + private function supportedByIntervention(string $from, string $to, string $driver): bool + { + // If the driver is somehow not GD or Imagick, we have no way to know what it might support + if ($driver !== 'gd' && $driver !== 'imagick') { + return false; + } + + foreach ([$from, $to] as $format) { + // GD and Imagick support different things. + // This follows the logic in intervention's AbstractEncoder::process() method + // and the various methods in the Encoder classes for GD and Imagick + switch (strtolower($format)) { + case 'gif': + case 'image/gif': + // always supported + break; + case 'png': + case 'image/png': + case 'image/x-png': + // always supported + break; + case 'jpg': + case 'jpeg': + case 'jfif': + case 'image/jp2': + case 'image/jpg': + case 'image/jpeg': + case 'image/pjpeg': + case 'image/jfif': + // always supported + break; + case 'tif': + case 'tiff': + case 'image/tiff': + case 'image/tif': + case 'image/x-tif': + case 'image/x-tiff': + if ($driver === 'gd') { + false; + } + // always supported by imagick + break; + case 'bmp': + case 'ms-bmp': + case 'x-bitmap': + case 'x-bmp': + case 'x-ms-bmp': + case 'x-win-bitmap': + case 'x-windows-bmp': + case 'x-xbitmap': + case 'image/ms-bmp': + case 'image/x-bitmap': + case 'image/x-bmp': + case 'image/x-ms-bmp': + case 'image/x-win-bitmap': + case 'image/x-windows-bmp': + case 'image/x-xbitmap': + if ($driver === 'gd' && !function_exists('imagebmp')) { + return false; + } + // always supported by imagick + break; + case 'ico': + case 'image/x-ico': + case 'image/x-icon': + case 'image/vnd.microsoft.icon': + if ($driver === 'gd') { + return false; + } + // always supported by imagick + break; + case 'psd': + case 'image/vnd.adobe.photoshop': + if ($driver === 'gd') { + return false; + } + // always supported by imagick + break; + case 'webp': + case 'image/webp': + case 'image/x-webp': + if ($driver === 'gd' && !function_exists('imagewebp')) { + return false; + } + if ($driver === 'imagick' && !\Imagick::queryFormats('WEBP')) { + return false; + } + break; + case 'avif': + case 'image/avif': + if ($driver === 'gd' && !function_exists('imageavif')) { + return false; + } + if ($driver === 'imagick' && !\Imagick::queryFormats('AVIF')) { + return false; + } + break; + case 'heic': + case 'image/heic': + case 'image/heif': + if ($driver === 'gd') { + return false; + } + if ($driver === 'imagick' && !\Imagick::queryFormats('HEIC')) { + return false; + } + break; + default: + // Anything else is not supported + return false; + } + } + // If we get here both formats are in the explicit list above and are supported + return true; + } +} diff --git a/src/ImageManipulation.php b/src/ImageManipulation.php index b6c0cd82..942df7f9 100644 --- a/src/ImageManipulation.php +++ b/src/ImageManipulation.php @@ -4,6 +4,7 @@ use InvalidArgumentException; use LogicException; +use SilverStripe\Assets\Conversion\FileConversionManager; use SilverStripe\Assets\FilenameParsing\AbstractFileIDHelper; use SilverStripe\Assets\Storage\AssetContainer; use SilverStripe\Assets\Storage\AssetStore; @@ -709,6 +710,19 @@ public function ThumbnailURL($width, $height) return $this->getIcon(); } + public function convert(string $toFormat): AssetContainer + { + $converter = Injector::inst()->get(FileConversionManager::class); + if ($this instanceof File) { + $from = $this->File; + } elseif ($this instanceof DBFile) { + $from = $this; + } else { + $from = pathinfo($this->getFilename(), PATHINFO_EXTENSION) ?: $this->getMimeType(); + } + return $converter->convert($from, $toFormat); + } + /** * Return the relative URL of an icon for the file type, * based on the {@link appCategory()} value.