forked from silverstripe/silverstripe-assets
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0cee400
commit cb08b83
Showing
10 changed files
with
523 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
Name: assetsconversion | ||
--- | ||
SilverStripe\Assets\Conversion\FileConversionManager: | ||
converters: | ||
- 'SilverStripe\Assets\Conversion\InterventionImageFileConverter' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<?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 | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
<?php | ||
|
||
namespace SilverStripe\Assets\Conversion; | ||
|
||
use SilverStripe\Assets\Storage\DBFile; | ||
use SilverStripe\Core\Config\Configurable; | ||
use SilverStripe\Core\Injector\Injector; | ||
|
||
/** | ||
* This class holds a list of available file converters which it uses to convert files from one format to another. | ||
*/ | ||
class FileConversionManager | ||
{ | ||
use Configurable; | ||
|
||
/** | ||
* An array of classes or injector service names for | ||
* classes that implement the FileConverter interface. | ||
*/ | ||
private static array $converters = []; | ||
|
||
/** | ||
* Convert the file to the given format using the first available converter that can perform the conversion. | ||
* | ||
* @param string $toFormat The file extension you want to convert to - e.g. "webp". | ||
* @param array $options Any options defined for this converter which should apply to the conversion. | ||
* Note that if a converter supports the conversion generally but doesn't support these options, that converter will not be used. | ||
* @throws FileConversionException if the conversion failed or there were no converters available. | ||
*/ | ||
public function convert(DBFile $from, string $toFormat, array $options = []): DBFile | ||
{ | ||
$fromFormat = $from->getExtension(); | ||
foreach (static::config()->get('converters') as $converterClass) { | ||
/** @var FileConverter $converter */ | ||
$converter = Injector::inst()->get($converterClass); | ||
if ($converter->supportsConversion($fromFormat, $toFormat, $options)) { | ||
return $converter->convert($from, $toFormat, $options); | ||
} | ||
} | ||
throw new FileConversionException('No file converter available for this conversion.'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<?php | ||
|
||
namespace SilverStripe\Assets\Conversion; | ||
|
||
use SilverStripe\Assets\Storage\DBFile; | ||
|
||
/** | ||
* Interface providing the public API for file converters, so that FileConversionManager | ||
* can find and use suitable converters. | ||
*/ | ||
interface FileConverter | ||
{ | ||
/** | ||
* Checks whether this converter supports a conversion from one file type to another. | ||
* | ||
* @param string $fromFormat The file extension you want to convert from - e.g. "jpg". | ||
* @param string $toFormat The file extension you want to convert to - e.g. "webp". | ||
* @param array $options Any options defined for this converter which should apply to the conversion. | ||
* Note that if the converter supports this conversion generally but doesn't support these options, this method will return `false`. | ||
*/ | ||
public function supportsConversion(string $fromFormat, string $toFormat, array $options = []): bool; | ||
|
||
/** | ||
* Converts the given DBFile instance to another file type. | ||
* | ||
* @param string $toFormat The file extension you want to convert to - e.g. "webp". | ||
* @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 $from, string $toFormat, array $options = []): DBFile; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
<?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; | ||
|
||
/** | ||
* File converter powered by the Intervention Image library. | ||
* Supports any file conversions that Intervention Image can perform. | ||
*/ | ||
class InterventionImageFileConverter implements FileConverter | ||
{ | ||
public function supportsConversion(string $fromFormat, string $toFormat, array $options = []): bool | ||
{ | ||
$unsupportedOptions = $this->validateOptions($options); | ||
if (!empty($unsupportedOptions)) { | ||
return false; | ||
} | ||
// This converter requires intervention image as the image backend | ||
$backend = Injector::inst()->get(Image_Backend::class); | ||
if (!is_a($backend, InterventionBackend::class)) { | ||
return false; | ||
} | ||
return $this->supportedByIntervention($fromFormat, $backend) && $this->supportedByIntervention($toFormat, $backend); | ||
} | ||
|
||
public function convert(DBFile $from, string $toFormat, array $options = []): DBFile | ||
{ | ||
// Do some basic validation up front for things we know aren't supported | ||
$problems = $this->validateOptions($options); | ||
if (!empty($problems)) { | ||
throw new FileConversionException('Invalid options provided: ' . implode(', ', $problems)); | ||
} | ||
$originalBackend = $from->getImageBackend(); | ||
if (!is_a($originalBackend, InterventionBackend::class)) { | ||
$actualClass = $originalBackend ? get_class($originalBackend) : 'null'; | ||
throw new FileConversionException("ImageBackend must be an instance of InterventionBackend. Got $actualClass"); | ||
} | ||
if (!$this->supportedByIntervention($toFormat, $originalBackend)) { | ||
throw new FileConversionException("Convertion to format '$toFormat' is not suported."); | ||
} | ||
|
||
$quality = $options['quality'] ?? null; | ||
// Clone the backend if we're changing quality to avoid affecting other manipulations to that original image | ||
$backend = $quality === null ? $originalBackend : clone $originalBackend; | ||
// Pass through to invervention image to do the conversion for us. | ||
try { | ||
$result = $from->manipulateExtension( | ||
$toFormat, | ||
function (AssetStore $store, string $filename, string $hash, string $variant) use ($backend, $quality) { | ||
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); | ||
} | ||
// This is very unlikely but the API for `manipulateExtension()` allows for it | ||
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 $format, InterventionBackend $backend): bool | ||
{ | ||
$driver = $backend->getImageManager()->config['driver'] ?? null; | ||
// 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; | ||
} | ||
|
||
// Return early for empty values - we obviously can't support that | ||
if ($format === '') { | ||
return false; | ||
} | ||
|
||
// 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, | ||
// excluding checking for strings that were obviously mimetypes | ||
switch (strtolower($format)) { | ||
case 'gif': | ||
// always supported | ||
return true; | ||
case 'png': | ||
// always supported | ||
return true; | ||
case 'jpg': | ||
case 'jpeg': | ||
case 'jfif': | ||
// always supported | ||
return true; | ||
case 'tif': | ||
case 'tiff': | ||
if ($driver === 'gd') { | ||
false; | ||
} | ||
// always supported by imagick | ||
return true; | ||
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': | ||
if ($driver === 'gd' && !function_exists('imagebmp')) { | ||
return false; | ||
} | ||
// always supported by imagick | ||
return true; | ||
case 'ico': | ||
if ($driver === 'gd') { | ||
return false; | ||
} | ||
// always supported by imagick | ||
return true; | ||
case 'psd': | ||
if ($driver === 'gd') { | ||
return false; | ||
} | ||
// always supported by imagick | ||
return true; | ||
case 'webp': | ||
if ($driver === 'gd' && !function_exists('imagewebp')) { | ||
return false; | ||
} | ||
if ($driver === 'imagick' && !\Imagick::queryFormats('WEBP')) { | ||
return false; | ||
} | ||
return true; | ||
case 'avif': | ||
if ($driver === 'gd' && !function_exists('imageavif')) { | ||
return false; | ||
} | ||
if ($driver === 'imagick' && !\Imagick::queryFormats('AVIF')) { | ||
return false; | ||
} | ||
return true; | ||
case 'heic': | ||
if ($driver === 'gd') { | ||
return false; | ||
} | ||
if ($driver === 'imagick' && !\Imagick::queryFormats('HEIC')) { | ||
return false; | ||
} | ||
return true; | ||
default: | ||
// Anything else is not supported | ||
return false; | ||
} | ||
// This should never be reached, but return false if it is | ||
return false; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.