ORIENT/.github/DESIGN_DOCS/resizer.htm

375 lines
14 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

==
<?php
/**
* $width = numeric, 'auto' | false | null
* $height = numeric, 'auto' | false | null
* $options = null | array [
* 'mode' => [
* 'auto', // automatically choose between portrait and landscape based on the image's orientation
* 'exact', // resize to the exact dimensions given, without preserving aspect ratio
* 'portrait', // resize to the given height and adapt the width to preserve aspect ratio
* 'landscape', // resize to the given width and adapt the height to preserve aspect ratio
* 'crop', // crop to the given dimensions after fitting as much of the image as possible inside those
* 'fit', // fit the image inside the given maximal dimensions, keeping the aspect ratio
* ],
* 'quality' => numeric, 1 - 100
* 'interlace' => boolean (default false),
* 'extension' => ['auto', 'png', 'gif', 'jpg', 'jpeg', 'webp', 'bmp', 'ico'],
* 'offset' => [x, y] Offset to crop the image from
* 'sharpen' => numeric, 1 - 100
*
* // Options that could be processed by an addon
*
* 'blur' => numeric, 1 - 100
* 'brightness'=> numeric, -100 - 100
* 'contrast' => numeric, -100 - 100
* 'pixelate' => numeric, 1 - 5000
* 'greyscale' => boolean
* 'invert' => boolean
* 'opacity' => numeric, 0 - 100
* 'rotate' => numeric, 1 - 360
* 'flip' => [h, v]
* 'background' | 'fill' => string, hex value
* 'colourize' => string, RGB value
* ]
*
* Event::fire('system.resizer.afterResize')
* Event::fire('system.resizer.beforeResize')
* Event::fire('system.resizer.processResize')
* Event::fire('system.resizer.getAvailableSources', [&$sourcesArray])
*
*
*
use App;
use Cache;
use Event;
use Storage;
use October\Rain\Database\Attach\File as FileModel;
/**
* DRAFT RESIZER DESIGN
* This is a rough draft, very WIP, of what the Image resizer UX / API will look like in October.
*
* Notes:
* - Clearing the application cache should not invalidate any existing resized images
* - Invalid images should not result in a valid "image not found" image existing, it should result in a 404 or more specific error
* - Provide a new backend list column type "thumb" that will pass it through the resizer
*
* Configurations to support
*
* - Developer can provide a image (in a wide range of various formats so long as the application actually has access to the provided image
* and can understand how to access it) to the `| resize(width, height, options)` Twig filter. That filter will output either a link to the
* final generated image as requested or a link to the resizer route that will actually handle resizing the image.
* - User should be able to extend the image resizing to provide pre or post processing of the images before / after being resized
* also to include the ability to swap out the image resizer itself. The core workflow logic should remain the same though.
* Examples:
* - Post processing of resized images with TinyPNG to optimize filesize further
* - Replacement processing of resizing with Intervention Image (using GD or ImageMagick)
*/
class Resizer {
/**
* @var string The cache key prefix for resizer configs
*/
public const CACHE_PREFIX = 'system.resizer.';
/**
* @var array Image source data ['disk' => string, 'path' => string, 'source' => string]
*/
protected $image = [];
/**
* @var integer Desired width
*/
protected $width = null;
/**
* @var integer Desired height
*/
protected $height = null;
/**
* @var array Image resizing configuration data
*/
protected $options = [];
/**
* Prepare the resizer instance
*
* @param mixed $image
* @param integer|bool|null $width
* @param integer|bool|null $height
* @param array $options
*/
public function __construct($image, $width = null, $height = null, $options = [])
{
$this->image = static::normalizeImage($image);
$this->width = is_numeric($width) ? (int) $width : null;
$this->height = is_numeric($height) ? (int) $height : null;
$this->options = array_merge($this->getDefaultOptions(), $options);
}
/**
* Get the available sources for processing image resize requests from
*
* @return array
*/
public static function getAvailableSources()
{
$sources = [
'themes' => [
'disk' => 'system',
'folder' => config('cms.themesPathLocal', base_path('themes')),
'path' => config('cms.themesPath', '/themes'),
],
'plugins' => [
'disk' => 'system',
'folder' => config('cms.pluginsPathLocal', base_path('plugins')),
'path' => config('cms.pluginsPath', '/plugins'),
],
'media' => [
'disk' => config('cms.storage.media.disk', 'local'),
'folder' => config('cms.storage.media.folder', 'media'),
'path' => config('cms.storage.media.path', '/storage/app/media'),
],
'modules' => [
'disk' => 'system',
'folder' => base_path('modules'),
'path' => '/modules',
],
'uploads' => [
'disk' => config('cms.storage.uploads.disk', 'local'),
'folder' => config('cms.storage.uploads.folder', 'uploads'),
'path' => config('cms.storage.uploads.path', '/storage/app/uploads'),
],
];
/**
* @event system.resizer.getAvailableSources
* Provides an opportunity to modify the sources available for processing resize requests from
*
* Example usage:
*
* Event::listen('system.resizer.getAvailableSources', function ((array) &$sources)) {
* $sources['custom'] = [
* 'disk' => 'custom',
* 'folder' => 'relative/path/on/disk',
* 'path' => 'publicly/accessible/path',
* ];
* });
*
*/
Event::fire('system.resizer.getAvailableSources', [&$sources]);
return $sources;
}
/**
* Gets the identifier for provided resizing configuration
* This method validates, authorizes, and prepares the resizing request for execution by the resizer
* Invalid images (inaccessible, private, incompatible formats, etc) should be denied here, and only
* after successfull validation should the requested configuration be stored along with a signed hash
* of the the options
*
* @param mixed $image
* @param integer|bool|null $width
* @param integer|bool|null $height
* @param array $options
* @return string 40 character string used as a unique reference to the provided configuration
*/
public function getIdentifier($image, $width = null, $height = null, array $options = [])
{
$image = static::normalizeImage($image);
$config = [
'image' => $image,
'width' => $width,
'height' => $height,
'options' => $options,
];
$identifier = hash_hmac('sha1', json_encode($config), App::make('encrypter')->getKey());
// If the image hasn't been resized yet, then store the config data for the resizer to use
if (!static::resized($identifier)) {
Cache::put(static::CACHE_PREFIX . $identifier, $config);
}
return $identifier;
}
/**
* Normalize the provided input into information that the resizer can work with
*
* @param mixed $image Supported values below:
* ['disk' => string, 'path' => string],
* instance of October\Rain\Database\Attach\File,
* string containing URL or path accessible to the application's filesystem manager
* @throws SystemException If the image was unable to be identified
* @return array Array containing the disk, path, and selected source name ['disk' => string, 'path' => string, 'source' => string]
*/
public static function normalizeImage($image)
{
$disk = null;
$path = null;
$selectedSource = null;
// Process an array
if (is_array($image) && !empty($image['disk']) && !empty($image['path'])) {
$disk = $image['disk'];
$path = $image['path'];
// Process a FileModel
} elseif ($image instanceof FileModel) {
$disk = $image->getDisk();
$path = $image->getDiskPath();
// Process a string
} elseif (is_string($image)) {
// Parse the provided image path into a filesystem ready relative path
$relativePath = urldecode(parse_url($image, PHP_URL_PATH));
// Loop through the sources available to the application to pull from
// to identify the source most likely to be holding the image
$resizeSources = static::getAvailableSources();
foreach ($resizeSources as $source => $details) {
// Normalize the source path
$sourcePath = urldecode(parse_url($details['path'], PHP_URL_PATH));
// Identify if the current source is a match
if (starts_with($relativePath, $sourcePath)) {
// Generate a path relative to the selected disk
$path = $details['folder'] . '/' . str_after($relativePath, $sourcePath . '/');
// Handle disks of type "system" (the local file system the application is running on)
if ($details['disk'] === 'system') {
Config::set('filesystems.disks.system', [
'driver' => 'local',
'root' => base_path(),
]);
// Regenerate the path relative to the newly defined "system" disk
$path = str_after($path, base_path() . '/');
}
$disk = Storage::disk($details['disk']);
// Verify that the file exists before exiting the identification process
if ($disk->exists($path)) {
$selectedSource = $source;
break;
} else {
$disk = null;
$path = null;
continue;
}
}
}
}
if (!$disk || !$path || !$selectedSource) {
throw new SystemException("Unable to process the provided image: " . e(var_export($image)));
}
return [
'disk' => $disk,
'path' => $path,
'source' => $selectedSource,
];
}
/**
* Get the reference to the resized image if the requested resize exists
*
* @param string $identifier The Resizer Identifier that references the source image and desired resizing configuration
* @return bool|string
*/
public function resized($image, $width = null, $height = null, $options = array)
{
$targetPath = implode('/', array_slice(str_split($identifier, 10), 0, 4)) . '/' . pathinfo($image['path'], PATHINFO_FILENAME) . "_resized_{$data['width']}_{$data['height']}.{$data['options']['extension']}";
$options = static::getOptions($identifier);
$targetDisk = $options['resized_disk'];
$targetFile = $options['resized_path'];
$resized = $targetDisk->get($targetFile);
if ($resized) {
return $resized;
} else {
return false;
}
}
public function resize($image, $width = null, $height = null, $options = [])
{
$identifier = static::getIdentifier($image, $width, $height, $options);
}
public static function getResizerUrl($image, $width = null, $height = null, $options = [])
{
$image = static::normalizeImage($image);
$identifier = static::getIdentifier($image, $width, $height, $options);
$data = static::normalizeConfig($image, $width, $height, $options);
$name = pathinfo($image['path'], PATHINFO_FILENAME) . "_resized_{$data['width']}_{$data['height']}.{$data['options']['extension']}";
return Url::to("/resizer/$identifier/{$image['source']}/$name");
}
}
// Twig filter implementation
function filterResize(mixed $image, int $width, int $height, array $options) {
// Attempt to process the provided image
try {
$imageData = Helper::normalizeImage($image);
} catch (SystemException $ex) {
// Ignore processing this URL if the resizer is unable to identify it
if (is_string($image)) {
return $image;
} elseif ($image instanceof FileModel) {
return $image->getPath();
} else {
throw new SystemException("Unable to process the provided image: " . e(var_export($image)));
}
}
$resizedUrl = Helper::resized($imageData, $width, $height, $options);
if ($resizedUrl) {
return $resizedUrl;
} else {
return Helper::getResizerUrl($imageData, $width, $height, $options);
}
}
// Route handling for resizing route route
Route::get('/resize/{identifier}/{source}/{name}', function ($identifier, $source, $name) {
// Generate the URL to the final result
$resizedUrl = Helper::getResizedUrl($identifer, $source, $name);
// Attempt to retrieve the resizer configuration and remove the data from the cache after retrieval
$config = Cache::pull(Helper::CACHE_PREFIX . $identifier, null);
// If the configuration wasn't found the image has already been processed or
// is currently being processed by a different request. Either way, return a
// redirect to the final result.
if (empty($config) || Helper::resized($config['image'], $config['width'], $config['height'], $config['options'])) {
return redirect()->to($resizedUrl);
}
// Process the image resize
Helper::resize($config['image'], $config['width'], $config['height'], $config['options']);
// Return a redirect to the generated image
return redirect()->to($resizedUrl);
});
==
{##}
{{ 'assets/images/logo.png' | theme | resize(false, false, {quality: 90}) }}
{{ record.mediafinder_field | media | resize(200, false) }}
{{ record.filemodel_property | resize(false, 200, {mode: 'contain'}) }}
{{ record.filemodel_property.getPath() | resize(600, 202) }}