Skip to content

Commit

Permalink
NEW File converter API
Browse files Browse the repository at this point in the history
  • Loading branch information
GuySartorelli committed Apr 5, 2024
1 parent 0cee400 commit 2c8704f
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 0 deletions.
13 changes: 13 additions & 0 deletions src/Conversion/FileConversionException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace SilverStripe\Assets\Conversion;

use LogicException;

/**
* An exception that represents a failure to convert a file in a FileConverter class.
*/
class FileConversionException extends LogicException
{

}
31 changes: 31 additions & 0 deletions src/Conversion/FileConversionManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace SilverStripe\Assets\Conversion;

use SilverStripe\Assets\Storage\DBFile;
use SilverStripe\Core\Config\Configurable;

class FileConversionManager
{
use Configurable;

private static $converters = [];

/**
* Undocumented function
*
* @throws FileConversionException if the conversion failed or there were no converters available
*/
public function convert(DBFile|string $from, string $toFormat, array $options = []): DBFile
{
// TODO uhhhh figure out a nicer way to organise the converters....
// we ideally don't want to even ask an image converter when the formats are doc and pdf, for example...
foreach ($converters as $converter) {
/** @var FileConverter $converter */
if ($converter->supportsConversion($from, $toFormat, $options)) {
return $converter->convert($from, $toFormat, $options);
}
}
throw new FileConversionException('No file converter available for this conversion.');
}
}
24 changes: 24 additions & 0 deletions src/Conversion/FileConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace SilverStripe\Assets\Conversion;

use SilverStripe\Assets\Storage\DBFile;

interface FileConverter
{
/**
* Checks whether this converter supports a conversion from one file type to another.
*
* @param DBFile|string $from A DBFile instance or specific file extension
* @param array $options Any options defined for this converter which should apply to the conversion
*/
public function supportsConversion(DBFile|string $from, string $toFormat, array $options = []): bool;

/**
* Converts the given DBFile instance to another file type.
*
* @param array $options Any options defined for this converter which should apply to the conversion
* @throws FileConversionException if invalid options are passed, or the conversion is not supported or fails
*/
public function convert(DBFile $original, string $toFormat, array $options = []): DBFile;
}
198 changes: 198 additions & 0 deletions src/Conversion/InterventionImageConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<?php

namespace SilverStripe\Assets\Conversion;

use Intervention\Image\Exception\ImageException;
use SilverStripe\Assets\Image_Backend;
use SilverStripe\Assets\InterventionBackend;
use SilverStripe\Assets\Storage\AssetStore;
use SilverStripe\Assets\Storage\DBFile;
use SilverStripe\Core\Injector\Injector;

/**
* Image converter powered by the Intervention Image library, assuming you haven't swapped out the ImageBackend implementation.
*/
class InterventionImageConverter implements FileConverter
{
public function supportsConversion(DBFile|string $from, string $toFormat, array $options = []): bool
{
$unsupportedOptions = $this->validateOptions($options);
if (!empty($unsupportedOptions)) {
return false;
}
// If backend is not InterventionBackend, return false
// GD and Imagick support different things so be dynamic between those
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) {
// 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;
}
}
14 changes: 14 additions & 0 deletions src/ImageManipulation.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 2c8704f

Please sign in to comment.