Merge pull request #5231 from octobercms/wip/image-resizing
Implement core support for `| resize(width, height, options)` filter
This commit is contained in:
commit
c1c728e413
|
|
@ -286,6 +286,7 @@ return [
|
|||
|
|
||||
| media - generated by the media manager.
|
||||
| uploads - generated by attachment model relationships.
|
||||
| resized - generated by System\Classes\ImageResizer or the resize() Twig filter
|
||||
|
|
||||
| For each resource you can specify:
|
||||
|
|
||||
|
|
@ -325,6 +326,12 @@ return [
|
|||
'path' => '/storage/app/media',
|
||||
],
|
||||
|
||||
'resized' => [
|
||||
'disk' => 'local',
|
||||
'folder' => 'resized',
|
||||
'path' => '/storage/app/resized',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ use October\Rain\Html\Helper as HtmlHelper;
|
|||
use October\Rain\Router\Helper as RouterHelper;
|
||||
use System\Helpers\DateTime as DateTimeHelper;
|
||||
use System\Classes\PluginManager;
|
||||
use System\Classes\MediaLibrary;
|
||||
use System\Classes\ImageResizer;
|
||||
use Backend\Classes\ListColumn;
|
||||
use Backend\Classes\WidgetBase;
|
||||
use October\Rain\Database\Model;
|
||||
|
|
@ -1187,6 +1189,42 @@ class Lists extends WidgetBase
|
|||
return htmlentities($value, ENT_QUOTES, 'UTF-8', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an image value
|
||||
* @return string
|
||||
*/
|
||||
protected function evalImageTypeValue($record, $column, $value)
|
||||
{
|
||||
$config = $column->config;
|
||||
|
||||
// Get config options with defaults
|
||||
$width = isset($config['width']) ? $config['width'] : 50;
|
||||
$height = isset($config['height']) ? $config['height'] : 50;
|
||||
$options = isset($config['options']) ? $config['options'] : [];
|
||||
|
||||
// Handle attachMany relationships
|
||||
if (isset($record->attachMany[$column->columnName])) {
|
||||
$image = $value->first();
|
||||
|
||||
// Handle attachOne relationships
|
||||
} elseif (isset($record->attachOne[$column->columnName])) {
|
||||
$image = $value;
|
||||
|
||||
// Handle absolute URLs
|
||||
} elseif (str_contains($value, '://')) {
|
||||
$value = $value;
|
||||
|
||||
// Assume all other values to be from the media library
|
||||
} else {
|
||||
$value = MediaLibrary::url($value);
|
||||
}
|
||||
|
||||
if (!is_null($value)) {
|
||||
$imageUrl = ImageResizer::filterGetUrl($image, $width, $height, $options);
|
||||
return "<img src='$imageUrl' width='$width' height='$height' />";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process as number, proxy to text
|
||||
* @return string
|
||||
|
|
|
|||
|
|
@ -0,0 +1,794 @@
|
|||
<?php namespace System\Classes;
|
||||
|
||||
use Url;
|
||||
use Crypt;
|
||||
use Cache;
|
||||
use Event;
|
||||
use Config;
|
||||
use Storage;
|
||||
use Exception;
|
||||
use SystemException;
|
||||
use File as FileHelper;
|
||||
use System\Models\File as SystemFileModel;
|
||||
use October\Rain\Database\Attach\File as FileModel;
|
||||
use October\Rain\Database\Attach\Resizer as DefaultResizer;
|
||||
|
||||
/**
|
||||
* Image Resizing class used for resizing any image resources accessible
|
||||
* to the application.
|
||||
*
|
||||
* This works by accepting a variety of image sources and normalizing the
|
||||
* pipeline for storing the desired resizing configuration and then
|
||||
* deferring the actual resizing of the images until requested by the browser.
|
||||
*
|
||||
* When the resizer route is hit, the configuration is retrieved from the cache
|
||||
* and used to generate the desired image and then redirect to the generated images
|
||||
* static path to minimize the load on the server. Future loads of the image are
|
||||
* automatically pointed to the static URL of the resized image without even hitting
|
||||
* the resizer route.
|
||||
*
|
||||
* The functionality of this class is controlled by these config items:
|
||||
*
|
||||
* - cms.resized.disk - The disk to store resized images on
|
||||
* - cms.resized.folder - The folder on the disk to store resized images in
|
||||
* - cms.resized.path - The public path to the resized images as returned
|
||||
* by the storage disk's URL method, used to identify
|
||||
* already resized images
|
||||
*
|
||||
* @see System\Classes\SystemController System controller
|
||||
* @see System\Twig\Extension Twig filters for this class defined
|
||||
* @package october\system
|
||||
* @author Luke Towers
|
||||
*/
|
||||
class ImageResizer
|
||||
{
|
||||
/**
|
||||
* @var string The cache key prefix for resizer configs
|
||||
*/
|
||||
public const CACHE_PREFIX = 'system.resizer.';
|
||||
|
||||
/**
|
||||
* @var array Available sources to get images from
|
||||
*/
|
||||
protected static $availableSources = [];
|
||||
|
||||
/**
|
||||
* @var string Unique identifier for the current configuration
|
||||
*/
|
||||
protected $identifier = null;
|
||||
|
||||
/**
|
||||
* @var array Image source data ['disk' => string, 'path' => string, 'source' => string]
|
||||
*/
|
||||
protected $image = [];
|
||||
|
||||
/**
|
||||
* @var FileModel The instance of the FileModel for the source image
|
||||
*/
|
||||
protected $fileModel = null;
|
||||
|
||||
/**
|
||||
* @var integer Desired width
|
||||
*/
|
||||
protected $width = 0;
|
||||
|
||||
/**
|
||||
* @var integer Desired height
|
||||
*/
|
||||
protected $height = 0;
|
||||
|
||||
/**
|
||||
* @var array Image resizing configuration data
|
||||
*/
|
||||
protected $options = [];
|
||||
|
||||
/**
|
||||
* Prepare the resizer instance
|
||||
*
|
||||
* @param mixed $image Supported values below:
|
||||
* ['disk' => Illuminate\Filesystem\FilesystemAdapter, 'path' => string, 'source' => string, 'fileModel' => FileModel|void],
|
||||
* instance of October\Rain\Database\Attach\File,
|
||||
* string containing URL or path accessible to the application's filesystem manager
|
||||
* @param integer|string|bool|null $width Desired width of the resized image
|
||||
* @param integer|string|bool|null $height Desired height of the resized image
|
||||
* @param array|null $options Array of options to pass to the resizer
|
||||
*/
|
||||
public function __construct($image, $width = 0, $height = 0, $options = [])
|
||||
{
|
||||
$this->image = static::normalizeImage($image);
|
||||
$this->width = (int) (($width === 'auto') ? 0 : $width);
|
||||
$this->height = (int) (($height === 'auto') ? 0 : $height);
|
||||
$this->options = array_merge($this->getDefaultOptions(), $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default options for the resizer
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getDefaultOptions()
|
||||
{
|
||||
// Default options for the built in resizing processor
|
||||
$defaultOptions = [
|
||||
'mode' => 'auto',
|
||||
'offset' => [0, 0],
|
||||
'sharpen' => 0,
|
||||
'interlace' => false,
|
||||
'quality' => 90,
|
||||
'extension' => $this->getExtension(),
|
||||
];
|
||||
|
||||
/**
|
||||
* @event system.resizer.getDefaultOptions
|
||||
* Provides an opportunity to modify the default options used when generating image resize requests
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* Event::listen('system.resizer.getDefaultOptions', function ((array) &$defaultOptions)) {
|
||||
* $defaultOptions['background'] = '#f2f2f2';
|
||||
* });
|
||||
*
|
||||
*/
|
||||
Event::fire('system.resizer.getDefaultOptions', [&$defaultOptions]);
|
||||
|
||||
return $defaultOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available sources for processing image resize requests from
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getAvailableSources()
|
||||
{
|
||||
if (!empty(static::$availableSources)) {
|
||||
return static::$availableSources;
|
||||
}
|
||||
|
||||
$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'),
|
||||
],
|
||||
'resized' => [
|
||||
'disk' => config('cms.storage.resized.disk', 'local'),
|
||||
'folder' => config('cms.storage.resized.folder', 'resized'),
|
||||
'path' => config('cms.storage.resized.path', '/storage/app/resized'),
|
||||
],
|
||||
'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',
|
||||
],
|
||||
'filemodel' => [
|
||||
'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 static::$availableSources = $sources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes the local sources cache.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function flushAvailableSources()
|
||||
{
|
||||
if (empty(static::$availableSources)) {
|
||||
return;
|
||||
}
|
||||
|
||||
static::$availableSources = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current config
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getConfig()
|
||||
{
|
||||
$config = [
|
||||
'image' => [
|
||||
'disk' => $this->image['disk'],
|
||||
'path' => $this->image['path'],
|
||||
'source' => $this->image['source'],
|
||||
],
|
||||
'width' => $this->width,
|
||||
'height' => $this->height,
|
||||
'options' => $this->options,
|
||||
];
|
||||
|
||||
if ($fileModel = $this->getFileModel()) {
|
||||
$config['image']['fileModel'] = [
|
||||
'class' => get_class($fileModel),
|
||||
'key' => $fileModel->getKey(),
|
||||
];
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the resize request
|
||||
*/
|
||||
public function resize()
|
||||
{
|
||||
if ($this->isResized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the details for the target image
|
||||
list($disk, $path) = $this->getTargetDetails();
|
||||
|
||||
// Copy the image to be resized to the temp directory
|
||||
$tempPath = $this->getLocalTempPath();
|
||||
|
||||
try {
|
||||
/**
|
||||
* @event system.resizer.processResize
|
||||
* Halting event that enables replacement of the resizing process. There should only ever be
|
||||
* one listener handling this event per project at most, as other listeners would be ignored.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* Event::listen('system.resizer.processResize', function ((\System\Classes\ImageResizer) $resizer, (string) $localTempPath)) {
|
||||
* // Get the resizing configuration
|
||||
* $config = $resizer->getConfig();
|
||||
*
|
||||
* // Resize the image
|
||||
* $resizedImageContents = My\Custom\Resizer::resize($localTempPath, $config['width], $config['height'], $config['options']);
|
||||
*
|
||||
* // Place the resized image in the correct location for the resizer to finish processing it
|
||||
* file_put_contents($localTempPath, $resizedImageContents);
|
||||
*
|
||||
* // Prevent any other resizing replacer logic from running
|
||||
* return true;
|
||||
* });
|
||||
*
|
||||
*/
|
||||
$processed = Event::fire('system.resizer.processResize', [$this, $tempPath], true);
|
||||
if (!$processed) {
|
||||
// Process the resize with the default image resizer
|
||||
DefaultResizer::open($tempPath)
|
||||
->resize($this->width, $this->height, $this->options)
|
||||
->save($tempPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @event system.resizer.afterResize
|
||||
* Enables post processing of resized images after they've been resized before the
|
||||
* resizing process is finalized (ex. adding watermarks, further optimizing, etc)
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* Event::listen('system.resizer.afterResize', function ((\System\Classes\ImageResizer) $resizer, (string) $localTempPath)) { *
|
||||
* // Get the resized image data
|
||||
* $resizedImageContents = file_get_contents($localTempPath);
|
||||
*
|
||||
* // Post process the image
|
||||
* $processedContents = TinyPNG::optimize($resizedImageContents);
|
||||
*
|
||||
* // Place the processed image in the correct location for the resizer to finish processing it
|
||||
* file_put_contents($localTempPath, $processedContents);
|
||||
* });
|
||||
*
|
||||
*/
|
||||
Event::fire('system.resizer.afterResize', [$this, $tempPath]);
|
||||
|
||||
// Store the resized image
|
||||
$disk->put($path, file_get_contents($tempPath));
|
||||
|
||||
// Clean up
|
||||
unlink($tempPath);
|
||||
} catch (Exception $ex) {
|
||||
// Clean up in case of any issues
|
||||
unlink($tempPath);
|
||||
|
||||
// Pass the exception up
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the internal working path, override this method to define.
|
||||
*/
|
||||
public function getTempPath()
|
||||
{
|
||||
$path = temp_path() . '/resizer';
|
||||
|
||||
if (!FileHelper::isDirectory($path)) {
|
||||
FileHelper::makeDirectory($path, 0777, true, true);
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the current source image in the temp directory and returns the path to it
|
||||
*
|
||||
* @param string $path The path to suffix the temp directory path with, defaults to $identifier.$ext
|
||||
* @return string $tempPath
|
||||
*/
|
||||
protected function getLocalTempPath($path = null)
|
||||
{
|
||||
if (!is_null($path) && is_string($path)) {
|
||||
$tempPath = $this->getTempPath() . '/' . $path;
|
||||
} else {
|
||||
$tempPath = $this->getTempPath() . '/' . $this->getIdentifier() . '.' . $this->getExtension();
|
||||
}
|
||||
|
||||
if (!file_exists($tempPath)) {
|
||||
FileHelper::put($tempPath, $this->getSourceFileContents());
|
||||
}
|
||||
|
||||
return $tempPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file extension.
|
||||
*/
|
||||
public function getExtension()
|
||||
{
|
||||
return FileHelper::extension($this->image['path']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of the image file to be resized
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSourceFileContents()
|
||||
{
|
||||
return $this->image['disk']->get($this->image['path']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current fileModel associated with the source image if one exists
|
||||
*
|
||||
* @return FileModel|null
|
||||
*/
|
||||
public function getFileModel()
|
||||
{
|
||||
if ($this->fileModel) {
|
||||
return $this->fileModel;
|
||||
}
|
||||
|
||||
if ($this->image['source'] === 'filemodel') {
|
||||
if ($this->image['fileModel'] instanceof FileModel) {
|
||||
$this->fileModel = $this->image['fileModel'];
|
||||
} else {
|
||||
$this->fileModel = $this->image['fileModel']['class']::findOrFail($this->image['fileModel']['key']);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->fileModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the details for the target image
|
||||
*
|
||||
* @return array [Illuminate\Filesystem\FilesystemAdapter $disk, (string) $path]
|
||||
*/
|
||||
protected function getTargetDetails()
|
||||
{
|
||||
if ($this->image['source'] === 'filemodel' && $fileModel = $this->getFileModel()) {
|
||||
$disk = $fileModel->getDisk();
|
||||
$path = $fileModel->getDiskPath($fileModel->getThumbFilename($this->width, $this->height, $this->options));
|
||||
} else {
|
||||
$disk = Storage::disk(Config::get('cms.resized.disk', 'local'));
|
||||
$path = $this->getPathToResizedImage();
|
||||
}
|
||||
|
||||
return [$disk, $path];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 isResized()
|
||||
{
|
||||
// Get the details for the target image
|
||||
list($disk, $path) = $this->getTargetDetails();
|
||||
|
||||
// Return true if the path is a file and it exists on the target disk
|
||||
return !empty(FileHelper::extension($path)) && $disk->exists($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path of the resized image
|
||||
*/
|
||||
public function getPathToResizedImage()
|
||||
{
|
||||
// Generate the unique file identifier for the resized image
|
||||
$fileIdentifier = hash_hmac('sha1', serialize($this->getConfig()), Crypt::getKey());
|
||||
|
||||
// Generate the filename for the resized image
|
||||
$name = pathinfo($this->image['path'], PATHINFO_FILENAME) . "_resized_$fileIdentifier.{$this->options['extension']}";
|
||||
|
||||
// Generate the path to the containing folder for the resized image
|
||||
$folder = implode('/', array_slice(str_split(str_limit($fileIdentifier, 9), 3), 0, 3));
|
||||
|
||||
// Generate and return the full path
|
||||
return Config::get('cms.resized.folder', 'resized') . '/' . $folder . '/' . $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current useful URL to the resized image
|
||||
* (resizer if not resized, resized image directly if resized)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl()
|
||||
{
|
||||
if ($this->isResized()) {
|
||||
return $this->getResizedUrl();
|
||||
} else {
|
||||
return $this->getResizerUrl();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL to the system resizer route for this instance's configuration
|
||||
*
|
||||
* @return string $url
|
||||
*/
|
||||
public function getResizerUrl()
|
||||
{
|
||||
// Slashes in URL params have to be double encoded to survive Laravel's router
|
||||
// @see https://github.com/octobercms/october/issues/3592#issuecomment-671017380
|
||||
$resizedUrl = urlencode(urlencode($this->getResizedUrl()));
|
||||
|
||||
// Get the current configuration's identifier
|
||||
$identifier = $this->getIdentifier();
|
||||
|
||||
// Store the current configuration
|
||||
$this->storeConfig();
|
||||
|
||||
return Url::to("/resizer/$identifier/$resizedUrl");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL to the resized image
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getResizedUrl()
|
||||
{
|
||||
$url = '';
|
||||
|
||||
if ($this->image['source'] === 'filemodel') {
|
||||
$model = $this->getFileModel();
|
||||
$thumbFile = $model->getThumbFilename($this->width, $this->height, $this->options);
|
||||
$url = $model->getPath($thumbFile);
|
||||
} else {
|
||||
$resizedDisk = Storage::disk(Config::get('cms.resized.disk', 'local'));
|
||||
$url = $resizedDisk->url($this->getPathToResizedImage());
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the provided input into information that the resizer can work with
|
||||
*
|
||||
* @param mixed $image Supported values below:
|
||||
* ['disk' => Illuminate\Filesystem\FilesystemAdapter, 'path' => string, 'source' => string, 'fileModel' => FileModel|void],
|
||||
* 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, source, and fileModel if applicable
|
||||
* ['disk' => Illuminate\Filesystem\FilesystemAdapter, 'path' => string, 'source' => string, 'fileModel' => FileModel|void]
|
||||
*/
|
||||
public static function normalizeImage($image)
|
||||
{
|
||||
$disk = null;
|
||||
$path = null;
|
||||
$selectedSource = null;
|
||||
$fileModel = null;
|
||||
|
||||
// Process an array
|
||||
if (is_array($image) && !empty($image['disk']) && !empty($image['path']) && !empty($image['source'])) {
|
||||
$disk = $image['disk'];
|
||||
$path = $image['path'];
|
||||
$selectedSource = $image['source'];
|
||||
|
||||
// Verify that the source file exists
|
||||
if (empty(FileHelper::extension($path)) || !$disk->exists($path)) {
|
||||
$disk = null;
|
||||
$path = null;
|
||||
$selectedSource = null;
|
||||
}
|
||||
|
||||
if (!empty($image['fileModel'])) {
|
||||
$fileModel = $image['fileModel'];
|
||||
}
|
||||
|
||||
// Process a FileModel
|
||||
} elseif ($image instanceof FileModel) {
|
||||
$disk = $image->getDisk();
|
||||
$path = $image->getDiskPath();
|
||||
$selectedSource = 'filemodel';
|
||||
$fileModel = $image;
|
||||
|
||||
// Verify that the source file exists
|
||||
if (empty(FileHelper::extension($path)) || !$disk->exists($path)) {
|
||||
$disk = null;
|
||||
$path = null;
|
||||
$selectedSource = null;
|
||||
$fileModel = null;
|
||||
}
|
||||
|
||||
// Process a string
|
||||
} elseif (is_string($image)) {
|
||||
// Parse the provided image path into a filesystem ready relative path
|
||||
$relativePath = static::normalizePath(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 = static::normalizePath(urldecode(parse_url($details['path'], PHP_URL_PATH)));
|
||||
|
||||
// Identify if the current source is a match
|
||||
if (starts_with($relativePath, $sourcePath)) {
|
||||
// Attempt to handle FileModel URLs passed as strings
|
||||
if ($source === 'filemodel') {
|
||||
$diskName = pathinfo($relativePath, PATHINFO_BASENAME);
|
||||
$model = SystemFileModel::where('disk_name', $diskName)->first();
|
||||
if ($model && $image = static::normalizeImage($model)) {
|
||||
$disk = $image['disk'];
|
||||
$path = $image['path'];
|
||||
$selectedSource = $image['source'];
|
||||
$fileModel = $image['fileModel'];
|
||||
}
|
||||
// Stop any further path processing from happening on filemodel sources
|
||||
break;
|
||||
}
|
||||
|
||||
// Generate a path relative to the selected disk
|
||||
$path = static::normalizePath($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, static::normalizePath(base_path()) . '/');
|
||||
}
|
||||
|
||||
$disk = Storage::disk($details['disk']);
|
||||
|
||||
// Verify that the file exists before exiting the identification process
|
||||
if (!empty(FileHelper::extension($path)) && $disk->exists($path)) {
|
||||
$selectedSource = $source;
|
||||
break;
|
||||
} else {
|
||||
$disk = null;
|
||||
$path = null;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$disk || !$path || !$selectedSource) {
|
||||
if (is_object($image)) {
|
||||
$image = get_class($image);
|
||||
}
|
||||
throw new SystemException("Unable to process the provided image: " . e(var_export($image, true)));
|
||||
}
|
||||
|
||||
$data = [
|
||||
'disk' => $disk,
|
||||
'path' => $path,
|
||||
'source' => $selectedSource,
|
||||
];
|
||||
|
||||
if ($fileModel) {
|
||||
$data['fileModel'] = $fileModel;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the provided path to Unix style directory seperators to ensure
|
||||
* that path manipulation operations succeed regardless of environment
|
||||
*
|
||||
* NOTE: Can't use October\Rain\FileSystem\PathResolver because it prepends
|
||||
* the current working directory to relative paths
|
||||
*
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
protected static function normalizePath($path)
|
||||
{
|
||||
return str_replace('\\', '/', $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the provided identifier looks like a valid identifier
|
||||
*
|
||||
* @param string $id
|
||||
* @return bool
|
||||
*/
|
||||
public static function isValidIdentifier($id)
|
||||
{
|
||||
return is_string($id) && ctype_alnum($id) && strlen($id) === 40;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the identifier for provided resizing configuration
|
||||
*
|
||||
* @return string 40 character string used as a unique reference to the provided configuration
|
||||
*/
|
||||
public function getIdentifier()
|
||||
{
|
||||
if ($this->identifier) {
|
||||
return $this->identifier;
|
||||
}
|
||||
|
||||
// Generate & return the identifier
|
||||
return $this->identifier = hash_hmac('sha1', $this->getResizedUrl(), Crypt::getKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the resizer configuration if the resizing hasn't been completed yet
|
||||
*/
|
||||
public function storeConfig()
|
||||
{
|
||||
// If the image hasn't been resized yet, then store the config data for the resizer to use
|
||||
if (!$this->isResized()) {
|
||||
Cache::put(static::CACHE_PREFIX . $this->getIdentifier(), $this->getConfig());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a resizer instance from the provided identifier
|
||||
*
|
||||
* @param string $identifier The 40 character cache identifier for the desired resizer configuration
|
||||
* @throws SystemException If the identifier is unable to be loaded
|
||||
* @return static
|
||||
*/
|
||||
public static function fromIdentifier(string $identifier)
|
||||
{
|
||||
// Attempt to retrieve the resizer configuration and remove the data from the cache after retrieval
|
||||
$config = Cache::pull(static::CACHE_PREFIX . $identifier, null);
|
||||
|
||||
// Validate that the desired config was able to be loaded
|
||||
if (empty($config)) {
|
||||
throw new SystemException("Unable to retrieve the configuration for " . e($identifier));
|
||||
}
|
||||
|
||||
return new static($config['image'], $config['width'], $config['height'], $config['options']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the provided encoded URL to verify its signature and return the decoded URL
|
||||
*
|
||||
* @param string $identifier
|
||||
* @param string $encodedUrl
|
||||
* @return string|null Returns null if the provided value was invalid
|
||||
*/
|
||||
public static function getValidResizedUrl($identifier, $encodedUrl)
|
||||
{
|
||||
// Slashes in URL params have to be double encoded to survive Laravel's router
|
||||
// @see https://github.com/octobercms/october/issues/3592#issuecomment-671017380
|
||||
$decodedUrl = urldecode(urldecode($encodedUrl));
|
||||
$url = null;
|
||||
|
||||
// The identifier should be the signed version of the decoded URL
|
||||
if (static::isValidIdentifier($identifier) && $identifier === hash_hmac('sha1', $decodedUrl, Crypt::getKey())) {
|
||||
$url = $decodedUrl;
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts supplied input into a URL that will return the desired resized image
|
||||
*
|
||||
* @param mixed $image Supported values below:
|
||||
* ['disk' => Illuminate\Filesystem\FilesystemAdapter, 'path' => string, 'source' => string, 'fileModel' => FileModel|void],
|
||||
* instance of October\Rain\Database\Attach\File,
|
||||
* string containing URL or path accessible to the application's filesystem manager
|
||||
* @param integer|bool|null $width Desired width of the resized image
|
||||
* @param integer|bool|null $height Desired height of the resized image
|
||||
* @param array|null $options Array of options to pass to the resizer
|
||||
* @throws Exception If the provided image was unable to be processed
|
||||
* @return string
|
||||
*/
|
||||
public static function filterGetUrl($image, $width = null, $height = null, $options = [])
|
||||
{
|
||||
// Attempt to process the provided image
|
||||
try {
|
||||
$resizer = new static($image, $width, $height, $options);
|
||||
} 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 $ex;
|
||||
}
|
||||
}
|
||||
|
||||
return $resizer->getUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the dimensions of the provided image file
|
||||
* NOTE: Doesn't currently support being passed a FileModel image that has already been resized
|
||||
*
|
||||
* @param mixed $image Supported values below:
|
||||
* ['disk' => Illuminate\Filesystem\FilesystemAdapter, 'path' => string, 'source' => string, 'fileModel' => FileModel|void],
|
||||
* instance of October\Rain\Database\Attach\File,
|
||||
* string containing URL or path accessible to the application's filesystem manager
|
||||
* @throws SystemException If the provided input was unable to be processed
|
||||
* @return array ['width' => int, 'height' => int]
|
||||
*/
|
||||
public static function filterGetDimensions($image)
|
||||
{
|
||||
$resizer = new static($image);
|
||||
|
||||
return Cache::rememberForever(static::CACHE_PREFIX . 'dimensions.' . $resizer->getIdentifier(), function () use ($resizer) {
|
||||
// Prepare the local file for assessment
|
||||
$tempPath = $resizer->getLocalTempPath();
|
||||
$dimensions = [];
|
||||
|
||||
// Attempt to get the image size
|
||||
try {
|
||||
$size = getimagesize($tempPath);
|
||||
$dimensions['width'] = $size[0];
|
||||
$dimensions['height'] = $size[1];
|
||||
} catch (\Exception $ex) {
|
||||
@unlink($tempPath);
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
// Cleanup afterwards
|
||||
@unlink($tempPath);
|
||||
|
||||
return $dimensions;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
<?php namespace System\Classes;
|
||||
|
||||
use Lang;
|
||||
use Response;
|
||||
use Exception;
|
||||
use SystemException;
|
||||
use ApplicationException;
|
||||
use Illuminate\Routing\Controller as ControllerBase;
|
||||
use Exception;
|
||||
use Response;
|
||||
|
||||
/**
|
||||
* The is the master controller for system related routing.
|
||||
|
|
@ -12,7 +13,7 @@ use Response;
|
|||
*
|
||||
* @see System\Classes\CombineAssets Asset combiner class
|
||||
* @package october\system
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
* @author Alexey Bobkov, Samuel Georges, Luke Towers
|
||||
*/
|
||||
class SystemController extends ControllerBase
|
||||
{
|
||||
|
|
@ -35,9 +36,44 @@ class SystemController extends ControllerBase
|
|||
$combiner = CombineAssets::instance();
|
||||
|
||||
return $combiner->getContents($cacheId);
|
||||
}
|
||||
catch (Exception $ex) {
|
||||
} catch (Exception $ex) {
|
||||
return Response::make('/* '.e($ex->getMessage()).' */', 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes an image using the provided configuration
|
||||
* and returns a redirect to the resized image
|
||||
*
|
||||
* @param string $identifier The identifier used to retrieve the image configuration
|
||||
* @param string $encodedUrl The double-encoded URL of the resized image, see https://github.com/octobercms/october/issues/3592#issuecomment-671017380
|
||||
* @return RedirectResponse
|
||||
*/
|
||||
public function resizer(string $identifier, string $encodedUrl)
|
||||
{
|
||||
$resizedUrl = ImageResizer::getValidResizedUrl($identifier, $encodedUrl);
|
||||
if (empty($resizedUrl)) {
|
||||
return response('Invalid identifier or redirect URL', 400);
|
||||
}
|
||||
|
||||
// Attempt to process the resize
|
||||
try {
|
||||
$resizer = ImageResizer::fromIdentifier($identifier);
|
||||
$resizer->resize();
|
||||
} catch (SystemException $ex) {
|
||||
// If the resizing failed with a SystemException, it was most
|
||||
// likely because it is in progress or has already finished
|
||||
} catch (Exception $ex) {
|
||||
// If it failed for any other reason, restore the config so that
|
||||
// the resizer route will continue to work until it succeeds
|
||||
if ($resizer) {
|
||||
$resizer->storeConfig();
|
||||
}
|
||||
|
||||
// Rethrow the exception
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
return redirect()->to($resizedUrl);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class OctoberMirror extends Command
|
|||
protected $directories = [
|
||||
'storage/app/uploads/public',
|
||||
'storage/app/media',
|
||||
'storage/app/resized',
|
||||
'storage/temp/public',
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -94,15 +94,6 @@ class File extends FileBase
|
|||
return $uploadsFolder . '/protected/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if storage.uploads.disk in config/cms.php is "local".
|
||||
* @return bool
|
||||
*/
|
||||
protected function isLocalStorage()
|
||||
{
|
||||
return Config::get('cms.storage.uploads.disk') == 'local';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage disk the file is stored on
|
||||
* @return FilesystemAdapter
|
||||
|
|
|
|||
|
|
@ -8,4 +8,9 @@ App::before(function ($request) {
|
|||
* Combine JavaScript and StyleSheet assets
|
||||
*/
|
||||
Route::any('combine/{file}', 'System\Classes\SystemController@combine');
|
||||
|
||||
/*
|
||||
* Resize image assets
|
||||
*/
|
||||
Route::get('resizer/{identifier}/{encodedUrl}', 'System\Classes\SystemController@resizer');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
<?php namespace System\Twig;
|
||||
|
||||
use Url;
|
||||
use Twig\Extension\AbstractExtension as TwigExtension;
|
||||
use Twig\TwigFilter as TwigSimpleFilter;
|
||||
use System\Classes\ImageResizer;
|
||||
use System\Classes\MediaLibrary;
|
||||
use System\Classes\MarkupManager;
|
||||
use Twig\TwigFilter as TwigSimpleFilter;
|
||||
use Twig\Extension\AbstractExtension as TwigExtension;
|
||||
|
||||
/**
|
||||
* The System Twig extension class implements common Twig functions and filters.
|
||||
|
|
@ -54,6 +55,9 @@ class Extension extends TwigExtension
|
|||
$filters = [
|
||||
new TwigSimpleFilter('app', [$this, 'appFilter'], ['is_safe' => ['html']]),
|
||||
new TwigSimpleFilter('media', [$this, 'mediaFilter'], ['is_safe' => ['html']]),
|
||||
new TwigSimpleFilter('resize', [$this, 'resizeFilter'], ['is_safe' => ['html']]),
|
||||
new TwigSimpleFilter('imageWidth', [$this, 'imageWidthFilter'], ['is_safe' => ['html']]),
|
||||
new TwigSimpleFilter('imageHeight', [$this, 'imageHeightFilter'], ['is_safe' => ['html']]),
|
||||
];
|
||||
|
||||
/*
|
||||
|
|
@ -100,4 +104,50 @@ class Extension extends TwigExtension
|
|||
{
|
||||
return MediaLibrary::url($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts supplied input into a URL that will return the desired resized image
|
||||
*
|
||||
* @param mixed $image Supported values below:
|
||||
* ['disk' => Illuminate\Filesystem\FilesystemAdapter, 'path' => string, 'source' => string, 'fileModel' => FileModel|void],
|
||||
* instance of October\Rain\Database\Attach\File,
|
||||
* string containing URL or path accessible to the application's filesystem manager
|
||||
* @param integer|bool|null $width Desired width of the resized image
|
||||
* @param integer|bool|null $height Desired height of the resized image
|
||||
* @param array|null $options Array of options to pass to the resizer
|
||||
* @throws Exception If the provided image was unable to be processed
|
||||
* @return string
|
||||
*/
|
||||
public function resizeFilter($image, $width = null, $height = null, $options = [])
|
||||
{
|
||||
return ImageResizer::filterGetUrl($image, $width, $height, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the width in pixels of the provided image source
|
||||
*
|
||||
* @param mixed $image Supported values below:
|
||||
* ['disk' => Illuminate\Filesystem\FilesystemAdapter, 'path' => string, 'source' => string, 'fileModel' => FileModel|void],
|
||||
* instance of October\Rain\Database\Attach\File,
|
||||
* string containing URL or path accessible to the application's filesystem manager
|
||||
* @return int
|
||||
*/
|
||||
public function imageWidthFilter($image)
|
||||
{
|
||||
return @ImageResizer::filterGetDimensions($image)['width'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the height in pixels of the provided image source
|
||||
*
|
||||
* @param mixed $image Supported values below:
|
||||
* ['disk' => Illuminate\Filesystem\FilesystemAdapter, 'path' => string, 'source' => string, 'fileModel' => FileModel|void],
|
||||
* instance of October\Rain\Database\Attach\File,
|
||||
* string containing URL or path accessible to the application's filesystem manager
|
||||
* @return int
|
||||
*/
|
||||
public function imageHeightFilter($image)
|
||||
{
|
||||
return @ImageResizer::filterGetDimensions($image)['height'];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
|
|
@ -0,0 +1,366 @@
|
|||
<?php
|
||||
|
||||
use Cms\Classes\Theme;
|
||||
use System\Classes\ImageResizer;
|
||||
use System\Classes\MediaLibrary;
|
||||
use System\Models\File as FileModel;
|
||||
use Cms\Classes\Controller as CmsController;
|
||||
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
|
||||
use October\Rain\Exception\SystemException;
|
||||
|
||||
class ImageResizerTest extends PluginTestCase
|
||||
{
|
||||
use ArraySubsetAsserts;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Config::set('cms.activeTheme', 'test');
|
||||
Event::flush('cms.theme.getActiveTheme');
|
||||
Theme::resetCache();
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
$this->removeMedia();
|
||||
ImageResizer::flushAvailableSources();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests configuration through the constructor as well as events.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testConfiguration()
|
||||
{
|
||||
// Resize with default options
|
||||
$imageResizer = new ImageResizer(
|
||||
(new CmsController())->themeUrl('assets/images/october.png'),
|
||||
100,
|
||||
100
|
||||
);
|
||||
self::assertArraySubset([
|
||||
'width' => 100,
|
||||
'height' => 100,
|
||||
'options' => [
|
||||
'mode' => 'auto',
|
||||
'offset' => [0, 0],
|
||||
'sharpen' => 0,
|
||||
'interlace' => false,
|
||||
'quality' => 90,
|
||||
'extension' => 'png',
|
||||
],
|
||||
], $imageResizer->getConfig());
|
||||
|
||||
// Resize with customised options
|
||||
$imageResizer = new ImageResizer(
|
||||
(new CmsController())->themeUrl('assets/images/october.png'),
|
||||
150,
|
||||
120,
|
||||
[
|
||||
'mode' => 'fit',
|
||||
'offset' => [2, 2],
|
||||
'sharpen' => 23,
|
||||
'interlace' => true,
|
||||
'quality' => 73,
|
||||
'extension' => 'jpg'
|
||||
]
|
||||
);
|
||||
self::assertArraySubset([
|
||||
'width' => 150,
|
||||
'height' => 120,
|
||||
'options' => [
|
||||
'mode' => 'fit',
|
||||
'offset' => [2, 2],
|
||||
'sharpen' => 23,
|
||||
'interlace' => true,
|
||||
'quality' => 73,
|
||||
'extension' => 'jpg'
|
||||
],
|
||||
], $imageResizer->getConfig());
|
||||
|
||||
// Resize with an customised defaults
|
||||
Event::listen('system.resizer.getDefaultOptions', function (&$options) {
|
||||
$options = array_merge($options, [
|
||||
'mode' => 'fit',
|
||||
'offset' => [2, 2],
|
||||
'sharpen' => 23,
|
||||
'interlace' => true,
|
||||
'quality' => 73,
|
||||
]);
|
||||
});
|
||||
|
||||
$imageResizer = new ImageResizer(
|
||||
(new CmsController())->themeUrl('assets/images/october.png'),
|
||||
100,
|
||||
100,
|
||||
[]
|
||||
);
|
||||
self::assertArraySubset([
|
||||
'width' => 100,
|
||||
'height' => 100,
|
||||
'options' => [
|
||||
'mode' => 'fit',
|
||||
'offset' => [2, 2],
|
||||
'sharpen' => 23,
|
||||
'interlace' => true,
|
||||
'quality' => 73,
|
||||
'extension' => 'png',
|
||||
],
|
||||
], $imageResizer->getConfig());
|
||||
|
||||
Event::forget('system.resizer.getDefaultOptions');
|
||||
|
||||
// Resize with a falsey height specified
|
||||
$imageResizer = new ImageResizer(
|
||||
(new CmsController())->themeUrl('assets/images/october.png'),
|
||||
100,
|
||||
false
|
||||
);
|
||||
self::assertArraySubset([
|
||||
'width' => 100,
|
||||
'height' => 0,
|
||||
], $imageResizer->getConfig());
|
||||
|
||||
$imageResizer = new ImageResizer(
|
||||
(new CmsController())->themeUrl('assets/images/october.png'),
|
||||
100,
|
||||
null
|
||||
);
|
||||
self::assertArraySubset([
|
||||
'width' => 100,
|
||||
'height' => 0,
|
||||
], $imageResizer->getConfig());
|
||||
|
||||
// Resize with a falsey width specified
|
||||
$imageResizer = new ImageResizer(
|
||||
(new CmsController())->themeUrl('assets/images/october.png'),
|
||||
'',
|
||||
100
|
||||
);
|
||||
self::assertArraySubset([
|
||||
'width' => 0,
|
||||
'height' => 100,
|
||||
], $imageResizer->getConfig());
|
||||
|
||||
$imageResizer = new ImageResizer(
|
||||
(new CmsController())->themeUrl('assets/images/october.png'),
|
||||
"0",
|
||||
100
|
||||
);
|
||||
self::assertArraySubset([
|
||||
'width' => 0,
|
||||
'height' => 100,
|
||||
], $imageResizer->getConfig());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests URLs for sources that can be accessed via URL.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testURLSources()
|
||||
{
|
||||
// Theme URL (absolute URL)
|
||||
$this->setUpStorage();
|
||||
$this->copyMedia();
|
||||
|
||||
$imageResizer = new ImageResizer(
|
||||
(new CmsController())->themeUrl('assets/images/october.png'),
|
||||
100,
|
||||
100
|
||||
);
|
||||
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
|
||||
|
||||
// Theme URL (relative URL)
|
||||
$this->setUpStorage();
|
||||
$this->copyMedia();
|
||||
|
||||
$imageResizer = new ImageResizer(
|
||||
'/themes/test/assets/images/october.png',
|
||||
100,
|
||||
100
|
||||
);
|
||||
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
|
||||
|
||||
// Media URL (absolute URL)
|
||||
$this->setUpStorage();
|
||||
$this->copyMedia();
|
||||
|
||||
$imageResizer = new ImageResizer(
|
||||
URL::to(MediaLibrary::url('october.png')),
|
||||
100,
|
||||
100
|
||||
);
|
||||
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
|
||||
|
||||
// Media URL (relative URL)
|
||||
$this->setUpStorage();
|
||||
$this->copyMedia();
|
||||
|
||||
$imageResizer = new ImageResizer(
|
||||
MediaLibrary::url('october.png'),
|
||||
100,
|
||||
100
|
||||
);
|
||||
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
|
||||
|
||||
// Media URL (absolute URL)
|
||||
$this->setUpStorage();
|
||||
$this->copyMedia();
|
||||
|
||||
$imageResizer = new ImageResizer(
|
||||
URL::to(MediaLibrary::url('october.png')),
|
||||
100,
|
||||
100
|
||||
);
|
||||
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
|
||||
|
||||
// Plugin URL (relative URL)
|
||||
$imageResizer = new ImageResizer(
|
||||
'/plugins/database/tester/assets/images/avatar.png',
|
||||
100,
|
||||
100
|
||||
);
|
||||
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
|
||||
|
||||
// Plugin URL (absolute URL)
|
||||
$imageResizer = new ImageResizer(
|
||||
URL::to('plugins/database/tester/assets/images/avatar.png'),
|
||||
100,
|
||||
100
|
||||
);
|
||||
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
|
||||
|
||||
// Module URL (relative URL)
|
||||
$imageResizer = new ImageResizer(
|
||||
'/modules/backend/assets/images/favicon.png',
|
||||
100,
|
||||
100
|
||||
);
|
||||
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
|
||||
|
||||
// Module URL (absolute URL)
|
||||
$imageResizer = new ImageResizer(
|
||||
Backend::skinAsset('assets/images/favicon.png'),
|
||||
100,
|
||||
100
|
||||
);
|
||||
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
|
||||
|
||||
// URL for a FileModel instance (absolute URL)
|
||||
$fileModel = new FileModel();
|
||||
$fileModel->fromFile(base_path('tests/fixtures/plugins/database/tester/assets/images/avatar.png'));
|
||||
$fileModel->save();
|
||||
|
||||
$imageResizer = new ImageResizer(
|
||||
FileModel::first()->getPath(),
|
||||
100,
|
||||
100
|
||||
);
|
||||
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
|
||||
|
||||
// Remove FileModel instance
|
||||
$fileModel->delete();
|
||||
|
||||
// URL of a FileModel instance (relative URL)
|
||||
$fileModel = new FileModel();
|
||||
$fileModel->fromFile(base_path('tests/fixtures/plugins/database/tester/assets/images/avatar.png'));
|
||||
$fileModel->save();
|
||||
|
||||
$imageResizer = new ImageResizer(
|
||||
str_replace(url('') . '/', '/', FileModel::first()->getPath()),
|
||||
100,
|
||||
100
|
||||
);
|
||||
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
|
||||
}
|
||||
|
||||
public function testDirectSources()
|
||||
{
|
||||
// FileModel instance itself
|
||||
$fileModel = new FileModel();
|
||||
$fileModel->fromFile(base_path('tests/fixtures/plugins/database/tester/assets/images/avatar.png'));
|
||||
$fileModel->save();
|
||||
|
||||
$imageResizer = new ImageResizer(
|
||||
$fileModel,
|
||||
100,
|
||||
100
|
||||
);
|
||||
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
|
||||
|
||||
// Remove FileModel instance
|
||||
$fileModel->delete();
|
||||
}
|
||||
|
||||
public function testInvalidInputPath()
|
||||
{
|
||||
$this->expectException(SystemException::class);
|
||||
$this->expectExceptionMessageMatches('/^Unable to process the provided image/');
|
||||
|
||||
$imageResizer = new ImageResizer(
|
||||
'/plugins/database/tester/assets/images/MISSING.png',
|
||||
100,
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
public function testInvalidInputFileModel()
|
||||
{
|
||||
$this->expectException(SystemException::class);
|
||||
$this->expectExceptionMessageMatches('/^Unable to process the provided image/');
|
||||
|
||||
$imageResizer = new ImageResizer(
|
||||
FileModel::first(),
|
||||
100,
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
protected function setUpStorage()
|
||||
{
|
||||
$this->app->useStoragePath(base_path('storage/temp'));
|
||||
|
||||
Config::set('filesystems.disks.test_local', [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app'),
|
||||
]);
|
||||
|
||||
Config::set('cms.storage.media', [
|
||||
'disk' => 'test_local',
|
||||
'folder' => 'media',
|
||||
'path' => '/storage/temp/app/media',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function copyMedia()
|
||||
{
|
||||
$mediaPath = storage_path('app/media');
|
||||
|
||||
if (!is_dir($mediaPath)) {
|
||||
mkdir($mediaPath, 0777, true);
|
||||
}
|
||||
|
||||
foreach (glob(base_path('tests/fixtures/media/*')) as $file) {
|
||||
$path = pathinfo($file);
|
||||
copy($file, $mediaPath . DIRECTORY_SEPARATOR . $path['basename']);
|
||||
}
|
||||
}
|
||||
|
||||
protected function removeMedia()
|
||||
{
|
||||
if ($this->app->storagePath() !== base_path('storage/temp')) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (glob(storage_path('app/media/*')) as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
|
||||
rmdir(storage_path('app/media'));
|
||||
rmdir(storage_path('app'));
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ use System\Classes\MediaLibrary;
|
|||
|
||||
class MediaLibraryTest extends TestCase // @codingStandardsIgnoreLine
|
||||
{
|
||||
protected function tearDown() : void
|
||||
public function tearDown(): void
|
||||
{
|
||||
$this->removeMedia();
|
||||
parent::tearDown();
|
||||
|
|
@ -76,12 +76,17 @@ class MediaLibraryTest extends TestCase // @codingStandardsIgnoreLine
|
|||
|
||||
$contents = MediaLibrary::instance()->listFolderContents();
|
||||
$this->assertNotEmpty($contents, 'Media library item is not discovered');
|
||||
$this->assertCount(2, $contents);
|
||||
|
||||
$item = reset($contents);
|
||||
$this->assertEquals('file', $item->type, 'Media library item does not have the right type');
|
||||
$this->assertEquals('/text.txt', $item->path, 'Media library item does not have the right path');
|
||||
$this->assertNotEmpty($item->lastModified, 'Media library item last modified is empty');
|
||||
$this->assertNotEmpty($item->size, 'Media library item size is empty');
|
||||
$this->assertEquals('file', $contents[0]->type, 'Media library item does not have the right type');
|
||||
$this->assertEquals('/october.png', $contents[0]->path, 'Media library item does not have the right path');
|
||||
$this->assertNotEmpty($contents[0]->lastModified, 'Media library item last modified is empty');
|
||||
$this->assertNotEmpty($contents[0]->size, 'Media library item size is empty');
|
||||
|
||||
$this->assertEquals('file', $contents[1]->type, 'Media library item does not have the right type');
|
||||
$this->assertEquals('/text.txt', $contents[1]->path, 'Media library item does not have the right path');
|
||||
$this->assertNotEmpty($contents[1]->lastModified, 'Media library item last modified is empty');
|
||||
$this->assertNotEmpty($contents[1]->size, 'Media library item size is empty');
|
||||
}
|
||||
|
||||
protected function setUpStorage()
|
||||
|
|
|
|||
Loading…
Reference in New Issue