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 145e9a8
Showing
14 changed files
with
707 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\FileConverterManager: | ||
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,31 @@ | ||
<?php | ||
|
||
namespace SilverStripe\Assets\Conversion; | ||
|
||
use SilverStripe\Assets\Storage\DBFile; | ||
|
||
/** | ||
* Interface providing the public API for file converters, so that FileConverterManager | ||
* can find and use suitable converters. | ||
*/ | ||
interface FileConverter | ||
{ | ||
/** | ||
* Checks whether this converter supports a conversion from one file type to another. | ||
* | ||
* @param string $fromExtension The file extension you want to convert from - e.g. "jpg". | ||
* @param string $toExtension 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 $fromExtension, string $toExtension, array $options = []): bool; | ||
|
||
/** | ||
* Converts the given DBFile instance to another file type. | ||
* | ||
* @param string $toExtension 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 FileConverterException if invalid options are passed, or the conversion is not supported or fails. | ||
*/ | ||
public function convert(DBFile $from, string $toExtension, 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,12 @@ | ||
<?php | ||
|
||
namespace SilverStripe\Assets\Conversion; | ||
|
||
use LogicException; | ||
|
||
/** | ||
* An exception that represents a failure to convert a file in a FileConverter class. | ||
*/ | ||
class FileConverterException 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 FileConverterManager | ||
{ | ||
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 $toExtension 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 FileConverterException if the conversion failed or there were no converters available. | ||
*/ | ||
public function convert(DBFile $from, string $toExtension, array $options = []): DBFile | ||
{ | ||
$fromExtension = $from->getExtension(); | ||
foreach (static::config()->get('converters') as $converterClass) { | ||
/** @var FileConverter $converter */ | ||
$converter = Injector::inst()->get($converterClass); | ||
if ($converter->supportsConversion($fromExtension, $toExtension, $options)) { | ||
return $converter->convert($from, $toExtension, $options); | ||
} | ||
} | ||
throw new FileConverterException("No file converter available to convert '$fromExtension' to '$toExtension'."); | ||
} | ||
} |
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,182 @@ | ||
<?php | ||
|
||
namespace SilverStripe\Assets\Conversion; | ||
|
||
use Imagick; | ||
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 $fromExtension, string $toExtension, 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($fromExtension, $backend) && $this->supportedByIntervention($toExtension, $backend); | ||
} | ||
|
||
public function convert(DBFile $from, string $toExtension, 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 FileConverterException('Invalid options provided: ' . implode(', ', $problems)); | ||
} | ||
$originalBackend = $from->getImageBackend(); | ||
if (!is_a($originalBackend, InterventionBackend::class)) { | ||
$actualClass = $originalBackend ? get_class($originalBackend) : 'null'; | ||
throw new FileConverterException("ImageBackend must be an instance of InterventionBackend. Got $actualClass"); | ||
} | ||
if (!$this->supportedByIntervention($toExtension, $originalBackend)) { | ||
throw new FileConverterException("Convertion to format '$toExtension' 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( | ||
$toExtension, | ||
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 FileConverterException('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 FileConverterException('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.