diff --git a/modules/backend/traits/SessionMaker.php b/modules/backend/traits/SessionMaker.php
index d327cd0c72..2ca8340e66 100644
--- a/modules/backend/traits/SessionMaker.php
+++ b/modules/backend/traits/SessionMaker.php
@@ -17,7 +17,7 @@ trait SessionMaker
/**
* Saves a widget related key/value pair in to session data.
* @param string $key Unique key for the data store.
- * @param string $value The value to store.
+ * @param mixed $value The value to store.
* @return void
*/
protected function putSession($key, $value)
diff --git a/modules/backend/widgets/MediaManager.php b/modules/backend/widgets/MediaManager.php
index 6e49b8b779..ea29929c09 100644
--- a/modules/backend/widgets/MediaManager.php
+++ b/modules/backend/widgets/MediaManager.php
@@ -7,12 +7,14 @@
use Input;
use Config;
use Backend;
+use Storage;
use Request;
use Response;
use Exception;
use SystemException;
use ApplicationException;
use Backend\Classes\WidgetBase;
+use System\Classes\ImageResizer;
use System\Classes\MediaLibrary;
use System\Classes\MediaLibraryItem;
use Winter\Storm\Database\Attach\Resizer;
@@ -128,8 +130,8 @@ public function onSearch()
$this->prepareVars();
return [
- '#'.$this->getId('item-list') => $this->makePartial('item-list'),
- '#'.$this->getId('folder-path') => $this->makePartial('folder-path')
+ '#' . $this->getId('item-list') => $this->makePartial('item-list'),
+ '#' . $this->getId('folder-path') => $this->makePartial('folder-path')
];
}
@@ -153,8 +155,8 @@ public function onGoToFolder()
$this->prepareVars();
return [
- '#'.$this->getId('item-list') => $this->makePartial('item-list'),
- '#'.$this->getId('folder-path') => $this->makePartial('folder-path')
+ '#' . $this->getId('item-list') => $this->makePartial('item-list'),
+ '#' . $this->getId('folder-path') => $this->makePartial('folder-path')
];
}
@@ -175,7 +177,7 @@ public function onGenerateThumbnails()
}
return [
- 'generatedThumbnails'=>$result
+ 'generatedThumbnails' => $result
];
}
@@ -185,33 +187,17 @@ public function onGenerateThumbnails()
*/
public function onGetSidebarThumbnail()
{
- $path = Input::get('path');
+ $path = MediaLibrary::validatePath(Input::get('path'));
$lastModified = Input::get('lastModified');
- $thumbnailParams = $this->getThumbnailParams();
- $thumbnailParams['width'] = 300;
- $thumbnailParams['height'] = 255;
- $thumbnailParams['mode'] = 'auto';
-
- $path = MediaLibrary::validatePath($path);
-
if (!is_numeric($lastModified)) {
throw new ApplicationException('Invalid input data');
}
- /*
- * If the thumbnail file exists, just return the thumbnail markup,
- * otherwise generate a new thumbnail.
- */
- $thumbnailPath = $this->thumbnailExists($thumbnailParams, $path, $lastModified);
- if ($thumbnailPath) {
- return [
- 'markup' => $this->makePartial('thumbnail-image', [
- 'isError' => $this->thumbnailIsError($thumbnailPath),
- 'imageUrl' => $this->getThumbnailImageUrl($thumbnailPath)
- ])
- ];
- }
+ $thumbnailParams = $this->getThumbnailParams();
+ $thumbnailParams['width'] = 300;
+ $thumbnailParams['height'] = 255;
+ $thumbnailParams['mode'] = 'auto';
$thumbnailInfo = $thumbnailParams;
$thumbnailInfo['path'] = $path;
@@ -236,9 +222,9 @@ public function onChangeView()
$this->prepareVars();
return [
- '#'.$this->getId('item-list') => $this->makePartial('item-list'),
- '#'.$this->getId('folder-path') => $this->makePartial('folder-path'),
- '#'.$this->getId('view-mode-buttons') => $this->makePartial('view-mode-buttons')
+ '#' . $this->getId('item-list') => $this->makePartial('item-list'),
+ '#' . $this->getId('folder-path') => $this->makePartial('folder-path'),
+ '#' . $this->getId('view-mode-buttons') => $this->makePartial('view-mode-buttons')
];
}
@@ -257,9 +243,9 @@ public function onSetFilter()
$this->prepareVars();
return [
- '#'.$this->getId('item-list') => $this->makePartial('item-list'),
- '#'.$this->getId('folder-path') => $this->makePartial('folder-path'),
- '#'.$this->getId('filters') => $this->makePartial('filters')
+ '#' . $this->getId('item-list') => $this->makePartial('item-list'),
+ '#' . $this->getId('folder-path') => $this->makePartial('folder-path'),
+ '#' . $this->getId('filters') => $this->makePartial('filters')
];
}
@@ -280,8 +266,8 @@ public function onSetSorting()
$this->prepareVars();
return [
- '#'.$this->getId('item-list') => $this->makePartial('item-list'),
- '#'.$this->getId('folder-path') => $this->makePartial('folder-path')
+ '#' . $this->getId('item-list') => $this->makePartial('item-list'),
+ '#' . $this->getId('folder-path') => $this->makePartial('folder-path')
];
}
@@ -315,8 +301,7 @@ public function onDeleteItem()
* Add to bulk collection
*/
$filesToDelete[] = $path;
- }
- elseif ($type === MediaLibraryItem::TYPE_FOLDER) {
+ } elseif ($type === MediaLibraryItem::TYPE_FOLDER) {
/*
* Delete single folder
*/
@@ -378,7 +363,7 @@ public function onDeleteItem()
$this->prepareVars();
return [
- '#'.$this->getId('item-list') => $this->makePartial('item-list')
+ '#' . $this->getId('item-list') => $this->makePartial('item-list')
];
}
@@ -420,7 +405,7 @@ public function onApplyName()
$originalPath = Input::get('originalPath');
$originalPath = MediaLibrary::validatePath($originalPath);
- $newPath = dirname($originalPath).'/'.$newName;
+ $newPath = dirname($originalPath) . '/' . $newName;
$type = Input::get('type');
if ($type == MediaLibraryItem::TYPE_FILE) {
@@ -454,8 +439,7 @@ public function onApplyName()
*
*/
$this->fireSystemEvent('media.file.rename', [$originalPath, $newPath]);
- }
- else {
+ } else {
/*
* Move single folder
*/
@@ -504,7 +488,7 @@ public function onCreateFolder()
$path = Input::get('path');
$path = MediaLibrary::validatePath($path);
- $newFolderPath = $path.'/'.$name;
+ $newFolderPath = $path . '/' . $name;
$library = MediaLibrary::instance();
@@ -543,7 +527,7 @@ public function onCreateFolder()
$this->prepareVars();
return [
- '#'.$this->getId('item-list') => $this->makePartial('item-list')
+ '#' . $this->getId('item-list') => $this->makePartial('item-list')
];
}
@@ -568,10 +552,9 @@ public function onLoadMovePopup()
if ($folder == '/') {
$name = Lang::get('backend::lang.media.library');
- }
- else {
+ } else {
$segments = explode('/', $folder);
- $name = str_repeat(' ', (count($segments)-1)*4).basename($folder);
+ $name = str_repeat(' ', (count($segments) - 1) * 4) . basename($folder);
}
$folderList[$path] = $name;
@@ -617,7 +600,7 @@ public function onMoveItems()
/*
* Move a single file
*/
- $library->moveFile($path, $dest.'/'.basename($path));
+ $library->moveFile($path, $dest . '/' . basename($path));
/**
* @event media.file.move
@@ -643,7 +626,7 @@ public function onMoveItems()
/*
* Move a single folder
*/
- $library->moveFolder($path, $dest.'/'.basename($path));
+ $library->moveFolder($path, $dest . '/' . basename($path));
/**
* @event media.folder.move
@@ -670,7 +653,7 @@ public function onMoveItems()
$this->prepareVars();
return [
- '#'.$this->getId('item-list') => $this->makePartial('item-list')
+ '#' . $this->getId('item-list') => $this->makePartial('item-list')
];
}
@@ -700,25 +683,23 @@ public function onLoadPopup()
/**
* Load image for cropping AJAX handler
- * @return array
+ * @return string
*/
- public function onLoadImageCropPopup()
+ public function onLoadImageCropPopup(): string
{
$this->abortIfReadOnly();
$path = Input::get('path');
$path = MediaLibrary::validatePath($path);
- $cropSessionKey = md5(FormHelper::getSessionKey());
$selectionParams = $this->getSelectionParams();
- $urlAndSize = $this->getCropEditImageUrlAndSize($path, $cropSessionKey);
+ $urlAndSize = $this->getCropEditImageUrlAndSize($path);
$width = $urlAndSize['dimensions'][0];
$height = $urlAndSize['dimensions'][1] ?: 1;
$this->vars['currentSelectionMode'] = $selectionParams['mode'];
$this->vars['currentSelectionWidth'] = $selectionParams['width'];
$this->vars['currentSelectionHeight'] = $selectionParams['height'];
- $this->vars['cropSessionKey'] = $cropSessionKey;
$this->vars['imageUrl'] = $urlAndSize['url'];
$this->vars['dimensions'] = $urlAndSize['dimensions'];
$this->vars['originalRatio'] = round($width / $height, 5);
@@ -734,13 +715,6 @@ public function onLoadImageCropPopup()
public function onEndCroppingSession()
{
$this->abortIfReadOnly();
-
- $cropSessionKey = Input::get('cropSessionKey');
- if (!preg_match('/^[0-9a-z]+$/', $cropSessionKey)) {
- throw new ApplicationException('Invalid input data');
- }
-
- $this->removeCropEditDir($cropSessionKey);
}
/**
@@ -751,25 +725,52 @@ public function onCropImage()
{
$this->abortIfReadOnly();
- $imageSrcPath = trim(Input::get('img'));
$selectionData = Input::get('selection');
- $cropSessionKey = Input::get('cropSessionKey');
- $path = Input::get('path');
- $path = MediaLibrary::validatePath($path);
+ $sourceImageUrl = Input::get('img');
+ $mediaItemPath = Input::get('path');
- if (!strlen($imageSrcPath)) {
+ if (!is_array($selectionData)) {
throw new ApplicationException('Invalid input data');
}
- if (!preg_match('/^[0-9a-z]+$/', $cropSessionKey)) {
- throw new ApplicationException('Invalid input data');
+ foreach (['x', 'y', 'w', 'h'] as $key) {
+ if (!isset($selectionData[$key]) || !is_numeric($selectionData[$key])) {
+ throw new SystemException('Invalid selection data.');
+ }
+
+ $selectionData[$key] = (int) $selectionData[$key];
}
- if (!is_array($selectionData)) {
- throw new ApplicationException('Invalid input data');
+ if ($selectionData['h'] === 0 || $selectionData['w'] === 0) {
+ throw new ApplicationException('You must define a crop size before inserting');
}
- $result = $this->cropImage($imageSrcPath, $selectionData, $cropSessionKey, $path);
+ $croppedPath = $this->cropImage($sourceImageUrl, [
+ 'height' => $selectionData['h'],
+ 'width' => $selectionData['w'],
+ 'offset' => [
+ $selectionData['x'],
+ $selectionData['y'],
+ ],
+ ]);
+
+ // Generate the target path for the cropped image
+ $targetPath = $this->deduplicatePath($mediaItemPath, '_cropped');
+
+ // Move the cropped image to the target path
+ MediaLibrary::instance()->put(
+ $targetPath,
+ ImageResizer::getDefaultDisk()->get($croppedPath)
+ );
+
+ $result = [
+ 'publicUrl' => MediaLibrary::url($targetPath),
+ 'documentType' => MediaLibraryItem::FILE_TYPE_IMAGE,
+ 'itemType' => MediaLibraryItem::TYPE_FILE,
+ 'path' => $targetPath,
+ 'title' => basename($targetPath),
+ 'folder' => dirname($targetPath),
+ ];
$selectionMode = Input::get('selectionMode');
$selectionWidth = Input::get('selectionWidth');
@@ -788,11 +789,6 @@ public function onResizeImage()
{
$this->abortIfReadOnly();
- $cropSessionKey = Input::get('cropSessionKey');
- if (!preg_match('/^[0-9a-z]+$/', $cropSessionKey)) {
- throw new ApplicationException('Invalid input data');
- }
-
$width = trim(Input::get('width'));
if (!strlen($width) || !ctype_digit($width)) {
throw new ApplicationException('Invalid input data');
@@ -806,12 +802,16 @@ public function onResizeImage()
$path = Input::get('path');
$path = MediaLibrary::validatePath($path);
- $params = [
+ $croppedPath = $this->resizeImage(MediaLibrary::url($path), [
+ 'mode' => 'exact',
'width' => $width,
'height' => $height
- ];
+ ]);
- return $this->getCropEditImageUrlAndSize($path, $cropSessionKey, $params);
+ return [
+ 'url' => $this->getThumbnailImageUrl($croppedPath),
+ 'dimensions' => [$width, $height]
+ ];
}
//
@@ -1163,27 +1163,28 @@ protected function getViewMode()
/**
* Returns thumbnail parameters
- * @param string $viewMode
- * @return array
*/
- protected function getThumbnailParams($viewMode = null)
+ protected function getThumbnailParams(string $viewMode = null): array
{
$result = [
'mode' => 'crop'
];
- if ($viewMode) {
- if ($viewMode == self::VIEW_MODE_LIST) {
- $result['width'] = 75;
- $result['height'] = 75;
- }
- else {
- $result['width'] = 165;
- $result['height'] = 165;
- }
+ if (!$viewMode) {
+ return $result;
}
- return $result;
+ if ($viewMode === self::VIEW_MODE_LIST) {
+ return array_merge($result, [
+ 'width' => 75,
+ 'height' => 75
+ ]);
+ }
+
+ return array_merge($result, [
+ 'width' => 165,
+ 'height' => 165
+ ]);
}
/**
@@ -1227,44 +1228,15 @@ protected function getThumbnailImageExtension($itemPath)
/**
* Returns the URL to a thumbnail
- * @param string $imagePath
- * @return string
*/
- protected function getThumbnailImageUrl($imagePath)
+ protected function getThumbnailImageUrl(string $imagePath): string
{
- return Url::to('/storage/temp'.$imagePath);
- }
-
- /**
- * Check if a thumbnail exists
- * @param array|null $thumbnailParams
- * @param string $itemPath
- * @param int $lastModified
- * @return bool
- */
- protected function thumbnailExists($thumbnailParams, $itemPath, $lastModified)
- {
- $thumbnailPath = $this->getThumbnailImagePath($thumbnailParams, $itemPath, $lastModified);
-
- $fullPath = temp_path(ltrim($thumbnailPath, '/'));
-
- if (File::exists($fullPath)) {
- return $thumbnailPath;
- }
-
- return false;
- }
-
- /**
- * Check if a thumbnail has caused an error
- * @param string $thumbnailPath
- * @return bool
- */
- protected function thumbnailIsError($thumbnailPath)
- {
- $fullPath = temp_path(ltrim($thumbnailPath, '/'));
-
- return hash_file('crc32', $fullPath) == $this->getBrokenImageHash();
+ return Url::to(
+ rtrim(
+ Config::get('cms.storage.resized.path', ''),
+ Config::get('cms.storage.resized.folder', '')
+ ) . $imagePath
+ );
}
/**
@@ -1316,80 +1288,55 @@ protected function getPlaceholderId($item)
* @param array|null $thumbnailParams
* @return array
*/
- protected function generateThumbnail($thumbnailInfo, $thumbnailParams = null)
+ protected function generateThumbnail($thumbnailInfo, $thumbnailParams = null): array
{
- $tempFilePath = null;
- $fullThumbnailPath = null;
- $thumbnailPath = null;
$markup = null;
- try {
- $path = $thumbnailInfo['path'];
+ $path = $thumbnailInfo['path'];
- if ($this->isVector($path)) {
- $markup = $this->makePartial('thumbnail-image', [
+ if ($this->isVector($path) && ($id = $thumbnailInfo['id'])) {
+ return [
+ 'id' => $id,
+ 'markup' => $this->makePartial('thumbnail-image', [
'isError' => false,
'imageUrl' => Url::to(config('cms.storage.media.path') . $thumbnailInfo['path'])
- ]);
- } else {
- /*
- * Get and validate input data
- */
- $width = $thumbnailInfo['width'];
- $height = $thumbnailInfo['height'];
- $lastModified = $thumbnailInfo['lastModified'];
-
- if (!is_numeric($width) || !is_numeric($height) || !is_numeric($lastModified)) {
- throw new ApplicationException('Invalid input data');
- }
-
- if (!$thumbnailParams) {
- $thumbnailParams = $this->getThumbnailParams();
- $thumbnailParams['width'] = $width;
- $thumbnailParams['height'] = $height;
- }
-
- $thumbnailPath = $this->getThumbnailImagePath($thumbnailParams, $path, $lastModified);
- $fullThumbnailPath = temp_path(ltrim($thumbnailPath, '/'));
-
- /*
- * Save the file locally
- */
- $library = MediaLibrary::instance();
- $tempFilePath = $this->getLocalTempFilePath($path);
-
- if (!@File::put($tempFilePath, $library->get($path))) {
- throw new SystemException('Error saving remote file to a temporary location');
- }
+ ])
+ ];
+ }
- /*
- * Resize the thumbnail and save to the thumbnails directory
- */
- $this->resizeImage($fullThumbnailPath, $thumbnailParams, $tempFilePath);
+ try {
+ /*
+ * Get and validate input data
+ */
+ $width = $thumbnailInfo['width'];
+ $height = $thumbnailInfo['height'];
+ $lastModified = $thumbnailInfo['lastModified'];
- /*
- * Delete the temporary file
- */
- File::delete($tempFilePath);
- $markup = $this->makePartial('thumbnail-image', [
- 'isError' => false,
- 'imageUrl' => $this->getThumbnailImageUrl($thumbnailPath)
- ]);
- }
- } catch (Exception $ex) {
- if ($tempFilePath) {
- File::delete($tempFilePath);
+ if (!is_numeric($width) || !is_numeric($height) || !is_numeric($lastModified)) {
+ throw new ApplicationException('Invalid input data');
}
- if ($fullThumbnailPath) {
- $this->copyBrokenImage($fullThumbnailPath);
+ if (!$thumbnailParams) {
+ $thumbnailParams = $this->getThumbnailParams();
+ $thumbnailParams['width'] = $width;
+ $thumbnailParams['height'] = $height;
}
- $markup = $this->makePartial('thumbnail-image', ['isError' => true]);
+ /*
+ * Resize the thumbnail and save to the thumbnails directory
+ */
+ $fullThumbnailPath = $this->resizeImage(MediaLibrary::url($path), $thumbnailParams);
/*
- * @todo We need to log all types of exceptions here
+ * Delete the temporary file
*/
+ $markup = $this->makePartial('thumbnail-image', [
+ 'isError' => false,
+ 'imageUrl' => $this->getThumbnailImageUrl($fullThumbnailPath)
+ ]);
+ } catch (\Throwable $ex) {
+ $markup = $this->makePartial('thumbnail-image', ['isError' => true]);
+
traceLog($ex->getMessage());
}
@@ -1399,39 +1346,42 @@ protected function generateThumbnail($thumbnailInfo, $thumbnailParams = null)
'markup' => $markup
];
}
+
+ return [];
}
/**
* Resize an image
- * @param string $fullThumbnailPath
- * @param array $thumbnailParams
- * @param string $tempFilePath
- * @return void
*/
- protected function resizeImage($fullThumbnailPath, $thumbnailParams, $tempFilePath)
+ protected function resizeImage(string $image, array $params): string
{
- $thumbnailDir = dirname($fullThumbnailPath);
- if (
- !File::isDirectory($thumbnailDir)
- && File::makeDirectory($thumbnailDir, 0777, true) === false
- ) {
- throw new SystemException('Error creating thumbnail directory');
- }
-
- $targetDimensions = $this->getTargetDimensions($thumbnailParams['width'], $thumbnailParams['height'], $tempFilePath);
-
- $targetWidth = $targetDimensions[0];
- $targetHeight = $targetDimensions[1];
-
- Resizer::open($tempFilePath)
- ->resize($targetWidth, $targetHeight, [
- 'mode' => $thumbnailParams['mode'],
- 'offset' => [0, 0]
- ])
- ->save($fullThumbnailPath)
- ;
+ return ImageResizer::processImage(
+ $image,
+ $params['width'],
+ $params['height'],
+ array_merge(
+ ['mode' => 'exact'],
+ $params
+ ),
+ ImageResizer::METHOD_RESIZE
+ );
+ }
- File::chmod($fullThumbnailPath);
+ /**
+ * Crop an image
+ */
+ protected function cropImage(string $image, array $params): string
+ {
+ return ImageResizer::processImage(
+ $image,
+ $params['width'],
+ $params['height'],
+ array_merge(
+ ['mode' => 'exact'],
+ $params
+ ),
+ ImageResizer::METHOD_CROP
+ );
}
/**
@@ -1440,7 +1390,7 @@ protected function resizeImage($fullThumbnailPath, $thumbnailParams, $tempFilePa
*/
protected function getBrokenImagePath()
{
- return __DIR__.'/mediamanager/assets/images/broken-thumbnail.gif';
+ return __DIR__ . '/mediamanager/assets/images/broken-thumbnail.gif';
}
/**
@@ -1458,278 +1408,87 @@ protected function getBrokenImageHash()
return $this->brokenImageHash = hash_file('crc32', $fullPath);
}
- /**
- * Copy broken image to destination
- * @param string $path
- * @return void
- */
- protected function copyBrokenImage($path)
- {
- try {
- $thumbnailDir = dirname($path);
- if (!File::isDirectory($thumbnailDir) && File::makeDirectory($thumbnailDir, 0777, true) === false) {
- return;
- }
-
- File::copy($this->getBrokenImagePath(), $path);
- }
- catch (Exception $ex) {
- traceLog($ex->getMessage());
- }
- }
-
- /**
- * Get target dimensions
- * @param int $width
- * @param int $height
- * @param string $originalImagePath
- * @return void
- */
- protected function getTargetDimensions($width, $height, $originalImagePath)
- {
- $originalDimensions = [$width, $height];
-
- try {
- $dimensions = getimagesize($originalImagePath);
- if (!$dimensions) {
- return $originalDimensions;
- }
-
- if ($dimensions[0] > $width || $dimensions[1] > $height) {
- return $originalDimensions;
- }
-
- return $dimensions;
- }
- catch (Exception $ex) {
- return $originalDimensions;
- }
- }
-
//
// Cropping
//
- /**
- * Returns the crop session working directory path
- * @param string $cropSessionKey
- * @return string
- */
- protected function getCropSessionDirPath($cropSessionKey)
- {
- return $this->getThumbnailDirectory().'edit-crop-'.$cropSessionKey;
- }
-
/**
* Prepares an image for cropping and returns payload containing a URL
* @param string $path
- * @param string $cropSessionKey
* @param array $params
* @return array
*/
- protected function getCropEditImageUrlAndSize($path, $cropSessionKey, $params = null)
+ protected function getCropEditImageUrlAndSize($path, $params = null)
{
- $sessionDirectoryPath = $this->getCropSessionDirPath($cropSessionKey);
- $fullSessionDirectoryPath = temp_path($sessionDirectoryPath);
- $sessionDirectoryCreated = false;
-
- if (!File::isDirectory($fullSessionDirectoryPath)) {
- File::makeDirectory($fullSessionDirectoryPath, 0777, true, true);
- $sessionDirectoryCreated = true;
- }
-
- $tempFilePath = null;
-
- try {
- $extension = pathinfo($path, PATHINFO_EXTENSION);
- $library = MediaLibrary::instance();
- $originalThumbFileName = 'original.'.$extension;
-
- /*
- * If the target dimensions are not provided, save the original image to the
- * crop session directory and return its URL.
- */
- if (!$params) {
- $tempFilePath = $fullSessionDirectoryPath.'/'.$originalThumbFileName;
-
- if (!@File::put($tempFilePath, $library->get($path))) {
- throw new SystemException('Error saving remote file to a temporary location.');
- }
-
- $url = $this->getThumbnailImageUrl($sessionDirectoryPath.'/'.$originalThumbFileName);
- $dimensions = getimagesize($tempFilePath);
-
- return [
- 'url' => $url,
- 'dimensions' => $dimensions
- ];
- }
- /*
- * If the target dimensions are provided, resize the original image and
- * return its URL and dimensions.
- */
+ $url = MediaLibrary::url($path);
- $originalFilePath = $fullSessionDirectoryPath.'/'.$originalThumbFileName;
- if (!File::isFile($originalFilePath)) {
- throw new SystemException('The original image is not found in the cropping session directory.');
- }
-
- $resizedThumbFileName = 'resized-'.$params['width'].'-'.$params['height'].'.'.$extension;
- $tempFilePath = $fullSessionDirectoryPath.'/'.$resizedThumbFileName;
-
- Resizer::open($originalFilePath)
- ->resize($params['width'], $params['height'], [
- 'mode' => 'exact'
- ])
- ->save($tempFilePath)
- ;
-
- $url = $this->getThumbnailImageUrl($sessionDirectoryPath.'/'.$resizedThumbFileName);
- $dimensions = getimagesize($tempFilePath);
-
- return [
- 'url' => $url,
- 'dimensions' => $dimensions
- ];
+ if ($params) {
+ $url = $this->resizeImage($url, [
+ 'mode' => 'exact',
+ 'width' => $params['width'],
+ 'height' => $params['height'],
+ ]);
}
- catch (Exception $ex) {
- if ($sessionDirectoryCreated) {
- @File::deleteDirectory($fullSessionDirectoryPath);
- }
-
- if ($tempFilePath) {
- File::delete($tempFilePath);
- }
- throw $ex;
- }
- }
-
- /**
- * Cleans up the directory used for cropping based on the session key
- * @param string $cropSessionKey
- * @return void
- */
- protected function removeCropEditDir($cropSessionKey)
- {
- $sessionDirectoryPath = $this->getCropSessionDirPath($cropSessionKey);
- $fullSessionDirectoryPath = temp_path($sessionDirectoryPath);
-
- if (File::isDirectory($fullSessionDirectoryPath)) {
- @File::deleteDirectory($fullSessionDirectoryPath);
- }
+ return [
+ 'url' => $url,
+ 'dimensions' => Str::startsWith($url, '/')
+ ? getimagesize(base_path($url))
+ : getimagesize($url)
+ ];
}
/**
- * Business logic to crop a media library image
- * @param string $imageSrcPath
- * @param string $selectionData
- * @param string $cropSessionKey
- * @param string $path
- * @return array
+ * Process the provided path and add a suffix of _$int to prevent conflicts
+ * with existing paths
+ * @TODO: Consider moving this into the File helper and accepting a $disk instance
*/
- protected function cropImage($imageSrcPath, $selectionData, $cropSessionKey, $path)
+ protected function deduplicatePath(string $path, string $suffix = null): string
{
- $originalFileName = basename($path);
-
- $path = rtrim(dirname($path), '/').'/';
- $fileName = basename($imageSrcPath);
-
- if (
- strpos($fileName, '..') !== false ||
- strpos($fileName, '/') !== false ||
- strpos($fileName, '\\') !== false
- ) {
- throw new SystemException('Invalid image file name.');
- }
-
- $selectionParams = ['x', 'y', 'w', 'h'];
-
- foreach ($selectionParams as $paramName) {
- if (!array_key_exists($paramName, $selectionData)) {
- throw new SystemException('Invalid selection data.');
- }
-
- if (!is_numeric($selectionData[$paramName])) {
- throw new SystemException('Invalid selection data.');
- }
+ $parts = pathinfo($path);
+ $i = 1;
- $selectionData[$paramName] = (int) $selectionData[$paramName];
- }
+ // Path generation adds a DIRECTORY_SEPARATOR between the dirname and
+ // the filename so ensure that the dirname doesn't already end with one
+ $parts['dirname'] = rtrim($parts['dirname'], DIRECTORY_SEPARATOR);
- $sessionDirectoryPath = $this->getCropSessionDirPath($cropSessionKey);
- $fullSessionDirectoryPath = temp_path($sessionDirectoryPath);
+ // Apply the requested suffix to the path
+ if (!empty($suffix)) {
+ $parts['filename'] = preg_replace(
+ // Remove the suffix if it's already there before re-adding it
+ '/' . preg_quote($suffix, '/') . '(_\d)?/',
+ '',
+ $parts['filename']
+ ) . $suffix;
- if (!File::isDirectory($fullSessionDirectoryPath)) {
- throw new SystemException('The image editing session is not found.');
+ // Regenerate the path so that it can be checked for existance
+ $path = sprintf(
+ '%s%s%s.%s',
+ $parts['dirname'],
+ DIRECTORY_SEPARATOR,
+ $parts['filename'],
+ $parts['extension']
+ );
}
- /*
- * Find the image on the disk and resize it
- */
- $imagePath = $fullSessionDirectoryPath.'/'.$fileName;
- if (!File::isFile($imagePath)) {
- throw new SystemException('The image is not found on the disk.');
+ while (MediaLibrary::instance()->exists($path)) {
+ $path = sprintf(
+ '%s%s%s_%d.%s',
+ $parts['dirname'],
+ DIRECTORY_SEPARATOR,
+ $parts['filename'],
+ $i++,
+ $parts['extension']
+ );
}
- $extension = pathinfo($originalFileName, PATHINFO_EXTENSION);
-
- $targetImageName = basename($originalFileName, '.'.$extension).'-'
- .$selectionData['x'].'-'
- .$selectionData['y'].'-'
- .$selectionData['w'].'-'
- .$selectionData['h'].'-';
-
- $targetImageName .= time();
- $targetImageName .= '.'.$extension;
-
- $targetTmpPath = $fullSessionDirectoryPath.'/'.$targetImageName;
-
- /*
- * Crop the image, otherwise copy original to target destination.
- */
- if ($selectionData['w'] == 0 || $selectionData['h'] == 0) {
- File::copy($imagePath, $targetTmpPath);
- }
- else {
- Resizer::open($imagePath)
- ->crop(
- $selectionData['x'],
- $selectionData['y'],
- $selectionData['w'],
- $selectionData['h'],
- $selectionData['w'],
- $selectionData['h']
- )
- ->save($targetTmpPath)
- ;
- }
-
- /*
- * Upload the cropped file to the Library
- */
- $targetFolder = $path.'cropped-images';
- $targetPath = $targetFolder.'/'.$targetImageName;
-
- $library = MediaLibrary::instance();
- $library->put($targetPath, file_get_contents($targetTmpPath));
-
- return [
- 'publicUrl' => $library->getPathUrl($targetPath),
- 'documentType' => MediaLibraryItem::FILE_TYPE_IMAGE,
- 'itemType' => MediaLibraryItem::TYPE_FILE,
- 'path' => $targetPath,
- 'title' => $targetImageName,
- 'folder' => $targetFolder
- ];
+ return $path;
}
/**
* Detect if image is vector graphic (SVG)
- * @param string $path
- * @return boolean
*/
- protected function isVector($path)
+ protected function isVector(string $path): bool
{
return (pathinfo($path, PATHINFO_EXTENSION) == 'svg');
}
diff --git a/modules/backend/widgets/mediamanager/partials/_image-crop-popup-body.php b/modules/backend/widgets/mediamanager/partials/_image-crop-popup-body.php
index 9771480bdf..95f4ee4d4a 100644
--- a/modules/backend/widgets/mediamanager/partials/_image-crop-popup-body.php
+++ b/modules/backend/widgets/mediamanager/partials/_image-crop-popup-body.php
@@ -27,7 +27,6 @@ class="btn btn-default no-margin-right">
-
@@ -40,4 +39,4 @@ class="btn btn-default no-margin-right">
= $this->makePartial('resize-image-form') ?>
-= Form::close() ?>
\ No newline at end of file
+= Form::close() ?>
diff --git a/modules/system/classes/ImageResizer.php b/modules/system/classes/ImageResizer.php
index 71738f00f2..4112fe1bc4 100644
--- a/modules/system/classes/ImageResizer.php
+++ b/modules/system/classes/ImageResizer.php
@@ -44,10 +44,16 @@
class ImageResizer
{
/**
- * @var string The cache key prefix for resizer configs
+ * The cache key prefix for resizer configs
*/
public const CACHE_PREFIX = 'system.resizer.';
+ /**
+ * Available methods to use when processing images
+ */
+ public const METHOD_RESIZE = 'resize';
+ public const METHOD_CROP = 'crop';
+
/**
* @var array Available sources to get images from
*/
@@ -103,11 +109,30 @@ public function __construct($image, $width = 0, $height = 0, $options = [])
}
/**
- * Get the default options for the resizer
+ * A simple static method for resizing an image and receiving the output path
*
- * @return array
+ * @throws ApplicationException If an invalid resize mode is passed to the the method.
+ */
+ public static function processImage(
+ mixed $image,
+ int|float $width = 0,
+ int|float $height = 0,
+ array $options = [],
+ string $method = self::METHOD_RESIZE
+ ): string {
+ if (!in_array($method, [static::METHOD_RESIZE, static::METHOD_CROP])) {
+ throw new \ApplicationException('Invalid method passed to processImage');
+ }
+
+ $resizer = new static($image, $width, $height, $options);
+ $resizer->{$method}();
+ return $resizer->getPathToResizedImage();
+ }
+
+ /**
+ * Get the default options for the resizer
*/
- public function getDefaultOptions()
+ public function getDefaultOptions(): array
{
// Default options for the built in resizing processor
$defaultOptions = [
@@ -137,10 +162,8 @@ public function getDefaultOptions()
/**
* Get the available sources for processing image resize requests from
- *
- * @return array
*/
- public static function getAvailableSources()
+ public static function getAvailableSources(): array
{
if (!empty(static::$availableSources)) {
return static::$availableSources;
@@ -201,10 +224,8 @@ public static function getAvailableSources()
/**
* Flushes the local sources cache.
- *
- * @return void
*/
- public static function flushAvailableSources()
+ public static function flushAvailableSources(): void
{
if (empty(static::$availableSources)) {
return;
@@ -215,10 +236,8 @@ public static function flushAvailableSources()
/**
* Get the current config
- *
- * @return array
*/
- public function getConfig()
+ public function getConfig(): array
{
$disk = $this->image['disk'];
@@ -231,7 +250,7 @@ public function getConfig()
$disk->setPathPrefix($realPath);
}
}
-
+
// Include last modified time to tie generated images to the source image
$mtime = $disk->lastModified($this->image['path']);
@@ -268,7 +287,7 @@ public function getConfig()
/**
* Process the resize request
*/
- public function resize()
+ public function resize(): void
{
if ($this->isResized()) {
return;
@@ -347,9 +366,95 @@ public function resize()
}
/**
- * Define the internal working path, override this method to define.
+ * Process the crop request
*/
- public function getTempPath()
+ public function crop(): void
+ {
+ 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.processCrop
+ * Halting event that enables replacement of the cropping 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.processCrop', function ((\System\Classes\ImageResizer) $resizer, (string) $localTempPath) {
+ * // Get the resizing configuration
+ * $config = $resizer->getConfig();
+ *
+ * // Resize the image
+ * $resizedImageContents = My\Custom\Resizer::crop($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.processCrop', [$this, $tempPath], true);
+ if (!$processed) {
+ // Process the crop with the default image resizer
+ DefaultResizer::open($tempPath)
+ ->crop(
+ $this->options['offset'][0],
+ $this->options['offset'][1],
+ $this->width,
+ $this->height
+ )
+ ->save($tempPath);
+ }
+
+ /**
+ * @event system.resizer.afterCrop
+ * Enables post processing of cropped images after they've been cropped before the
+ * cropping process is finalized (ex. adding watermarks, further optimizing, etc)
+ *
+ * Example usage:
+ *
+ * Event::listen('system.resizer.afterCrop', function ((\System\Classes\ImageResizer) $resizer, (string) $localTempPath) {
+ * // Get the resized image data
+ * $croppedImageContents = file_get_contents($localTempPath);
+ *
+ * // Post process the image
+ * $processedContents = TinyPNG::optimize($croppedImageContents);
+ *
+ * // Place the processed image in the correct location for the resizer to finish processing it
+ * file_put_contents($localTempPath, $processedContents);
+ * });
+ *
+ */
+ Event::fire('system.resizer.afterCrop', [$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;
+ }
+ }
+
+ /**
+ * Get the internal temporary drirectory and ensure it exists
+ */
+ public function getTempPath(): string
{
$path = temp_path() . '/resizer';
@@ -364,9 +469,8 @@ public function getTempPath()
* 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)
+ protected function getLocalTempPath($path = null): string
{
if (!is_null($path) && is_string($path)) {
$tempPath = $this->getTempPath() . '/' . $path;
@@ -384,15 +488,13 @@ protected function getLocalTempPath($path = null)
/**
* Returns the file extension.
*/
- public function getExtension()
+ public function getExtension(): string
{
return FileHelper::extension($this->image['path']);
}
/**
* Get the contents of the image file to be resized
- *
- * @return string
*/
public function getSourceFileContents()
{
@@ -401,10 +503,8 @@ public function getSourceFileContents()
/**
* Gets the current fileModel associated with the source image if one exists
- *
- * @return FileModel|null
*/
- public function getFileModel()
+ public function getFileModel(): ?FileModel
{
if ($this->fileModel) {
return $this->fileModel;
@@ -421,31 +521,48 @@ public function getFileModel()
return $this->fileModel;
}
+ /**
+ * Get the default disk used to store processed images
+ */
+ public static function getDefaultDisk(): \Illuminate\Contracts\Filesystem\Filesystem
+ {
+ return Storage::disk(Config::get('cms.storage.resized.disk', 'local'));
+ }
+
+ /**
+ * Get the disk instance for image that is currently being processed
+ */
+ public function getDisk(): \Illuminate\Contracts\Filesystem\Filesystem
+ {
+ return ($this->image['source'] === 'filemodel' && $fileModel = $this->getFileModel())
+ ? $fileModel->getDisk()
+ : static::getDefaultDisk();
+ }
+
/**
* Get the details for the target image
*
* @return array [FilesystemAdapter $disk, (string) $path]
*/
- protected function getTargetDetails()
+ protected function getTargetDetails(): array
{
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.storage.resized.disk', 'local'));
- $path = $this->getPathToResizedImage();
+ return [
+ $this->getDisk(),
+ $fileModel->getDiskPath($fileModel->getThumbFilename($this->width, $this->height, $this->options)),
+ ];
}
- return [$disk, $path];
+ return [
+ $this->getDisk(),
+ $this->getPathToResizedImage(),
+ ];
}
/**
* 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()
+ public function isResized(): bool
{
// Get the details for the target image
list($disk, $path) = $this->getTargetDetails();
@@ -457,7 +574,7 @@ public function isResized()
/**
* Get the path of the resized image
*/
- public function getPathToResizedImage()
+ public function getPathToResizedImage(): string
{
// Generate the unique file identifier for the resized image
$fileIdentifier = hash_hmac('sha1', serialize($this->getConfig()), Crypt::getKey());
@@ -475,10 +592,8 @@ public function getPathToResizedImage()
/**
* Gets the current useful URL to the resized image
* (resizer if not resized, resized image directly if resized)
- *
- * @return string
*/
- public function getUrl()
+ public function getUrl(): string
{
if ($this->isResized()) {
return $this->getResizedUrl();
@@ -489,10 +604,8 @@ public function getUrl()
/**
* Get the URL to the system resizer route for this instance's configuration
- *
- * @return string $url
*/
- public function getResizerUrl()
+ public function getResizerUrl(): string
{
// Slashes in URL params have to be double encoded to survive Laravel's router
// @see https://github.com/octobercms/october/issues/3592#issuecomment-671017380
@@ -515,10 +628,8 @@ public function getResizerUrl()
/**
* Get the URL to the resized image
- *
- * @return string
*/
- public function getResizedUrl()
+ public function getResizedUrl(): string
{
$url = '';
@@ -554,7 +665,7 @@ public function getResizedUrl()
* @return array Array containing the disk, path, source, and fileModel if applicable
* ['disk' => FilesystemAdapter, 'path' => string, 'source' => string, 'fileModel' => FileModel|void]
*/
- public static function normalizeImage($image)
+ public static function normalizeImage($image): array
{
$disk = null;
$path = null;
@@ -690,11 +801,8 @@ public static function normalizeImage($image)
*
* NOTE: Can't use Winter\Storm\FileSystem\PathResolver because it prepends
* the current working directory to relative paths
- *
- * @param string $path
- * @return string
*/
- protected static function normalizePath($path)
+ protected static function normalizePath(string $path): string
{
return str_replace('\\', '/', $path);
}
@@ -705,7 +813,7 @@ protected static function normalizePath($path)
* @param string $id
* @return bool
*/
- public static function isValidIdentifier($id)
+ public static function isValidIdentifier($id): bool
{
return is_string($id) && ctype_alnum($id) && strlen($id) === 40;
}
@@ -715,7 +823,7 @@ public static function isValidIdentifier($id)
*
* @return string 40 character string used as a unique reference to the provided configuration
*/
- public function getIdentifier()
+ public function getIdentifier(): string
{
if ($this->identifier) {
return $this->identifier;
@@ -728,7 +836,7 @@ public function getIdentifier()
/**
* Stores the resizer configuration if the resizing hasn't been completed yet
*/
- public function storeConfig()
+ public function storeConfig(): void
{
// If the image hasn't been resized yet, then store the config data for the resizer to use
if (!$this->isResized()) {
@@ -741,9 +849,8 @@ public function storeConfig()
*
* @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)
+ public static function fromIdentifier(string $identifier): self
{
$cacheKey = static::CACHE_PREFIX . $identifier;
@@ -769,11 +876,9 @@ public static function fromIdentifier(string $identifier)
/**
* 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)
+ public static function getValidResizedUrl(string $identifier, string $encodedUrl): ?string
{
// Slashes in URL params have to be double encoded to survive Laravel's router
// @see https://github.com/octobercms/october/issues/3592#issuecomment-671017380
@@ -799,9 +904,8 @@ public static function getValidResizedUrl($identifier, $encodedUrl)
* @param integer|string|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 = [])
+ public static function filterGetUrl($image, $width = null, $height = null, $options = []): string
{
// Attempt to process the provided image
try {
@@ -829,9 +933,8 @@ public static function filterGetUrl($image, $width = null, $height = null, $opti
* instance of Winter\Storm\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)
+ public static function filterGetDimensions($image): array
{
$resizer = new static($image);
diff --git a/modules/system/classes/MediaLibrary.php b/modules/system/classes/MediaLibrary.php
index f1488a2e0d..9bf4eaadb5 100644
--- a/modules/system/classes/MediaLibrary.php
+++ b/modules/system/classes/MediaLibrary.php
@@ -8,6 +8,7 @@
use Request;
use Url;
use Winter\Storm\Filesystem\Definitions as FileDefinitions;
+use Illuminate\Filesystem\FilesystemAdapter;
use ApplicationException;
use SystemException;
@@ -774,7 +775,7 @@ protected function filterItemList(&$itemList, $filter)
* communicating with the remote storage.
* @return mixed Returns the storage disk object.
*/
- public function getStorageDisk()
+ public function getStorageDisk(): FilesystemAdapter
{
if ($this->storageDisk) {
return $this->storageDisk;