From f4727eb2881e22802b0af26fb537892c6c3d37d9 Mon Sep 17 00:00:00 2001 From: Benni Mack <benni@typo3.org> Date: Wed, 20 Sep 2023 16:02:53 +0200 Subject: [PATCH] [TASK] Centralize ImageMagick logic for FAL in GraphicalFunctions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When FAL was introduced, some code from old GifBuilder generation was copied to the Helper classes. These now reside in GraphicalFunctions for further refactorings and better API support. Resolves: #101983 Releases: main Change-Id: I5818e8b2e8c0ca51fee25547d2952f55c13eee37 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/81107 Reviewed-by: Stefan Bürk <stefan@buerk.tech> Tested-by: Andreas Nedbal <andy@pixelde.su> Tested-by: core-ci <typo3@b13.com> Reviewed-by: Andreas Nedbal <andy@pixelde.su> Tested-by: Stefan Bürk <stefan@buerk.tech> --- .../Classes/Imaging/GraphicalFunctions.php | 48 ++++++++ .../Processing/LocalCropScaleMaskHelper.php | 110 +++++------------- .../Processing/LocalPreviewHelper.php | 19 +-- 3 files changed, 81 insertions(+), 96 deletions(-) diff --git a/typo3/sysext/core/Classes/Imaging/GraphicalFunctions.php b/typo3/sysext/core/Classes/Imaging/GraphicalFunctions.php index a2eed617adaf..1f32964d5a94 100644 --- a/typo3/sysext/core/Classes/Imaging/GraphicalFunctions.php +++ b/typo3/sysext/core/Classes/Imaging/GraphicalFunctions.php @@ -406,6 +406,54 @@ class GraphicalFunctions return null; } + /** + * @internal until API is finalized + */ + public function crop(string $imageFile, string $targetFileExtension, string $cropInformation): ?array + { + // check if it is a json object + $cropData = json_decode($cropInformation); + if ($cropData) { + $offsetLeft = (int)($cropData->x ?? 0); + $offsetTop = (int)($cropData->y ?? 0); + $newWidth = (int)($cropData->width ?? 0); + $newHeight = (int)($cropData->height ?? 0); + } else { + [$offsetLeft, $offsetTop, $newWidth, $newHeight] = explode(',', $cropInformation, 4); + } + + return $this->imageMagickConvert( + $imageFile, + $targetFileExtension, + '', + '', + sprintf('-crop %dx%d+%d+%d +repage -quality %d', $newWidth, $newHeight, $offsetLeft, $offsetTop, $this->jpegQuality), + '', + ['noScale' => true], + true + ); + } + + /** + * This applies an image onto the $inputFile with an additional backgroundImage for the mask + * @internal until API is finalized + */ + public function mask(string $inputFile, string $outputFile, string $maskImage, string $maskBackgroundImage, string $params) + { + $tmpStr = $this->randomName(); + // m_mask + $intermediateMaskFile = $tmpStr . '_mask.png'; + $this->imageMagickExec($maskImage, $intermediateMaskFile, $params); + // m_bgImg + $intermediateMaskBackgroundFile = $tmpStr . '_bgImg.miff'; + $this->imageMagickExec($maskBackgroundImage, $intermediateMaskBackgroundFile, $params); + // The image onto the background + $this->combineExec($intermediateMaskBackgroundFile, $inputFile, $intermediateMaskFile, $outputFile); + // Unlink the temp-images... + @unlink($intermediateMaskFile); + @unlink($intermediateMaskBackgroundFile); + } + /** * Gets the input image dimensions. * diff --git a/typo3/sysext/core/Classes/Resource/Processing/LocalCropScaleMaskHelper.php b/typo3/sysext/core/Classes/Resource/Processing/LocalCropScaleMaskHelper.php index 43c5dda78d95..2d2c4aeb825b 100644 --- a/typo3/sysext/core/Classes/Resource/Processing/LocalCropScaleMaskHelper.php +++ b/typo3/sysext/core/Classes/Resource/Processing/LocalCropScaleMaskHelper.php @@ -20,7 +20,6 @@ use TYPO3\CMS\Core\Imaging\GraphicalFunctions; use TYPO3\CMS\Core\Resource\FileInterface; use TYPO3\CMS\Core\Resource\ProcessedFile; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Core\Utility\MathUtility; /** * Helper class to locally perform a crop/scale/mask task with the TYPO3 image processing classes. @@ -69,39 +68,12 @@ class LocalCropScaleMaskHelper $configuration['fileExtension'] = $task->getTargetFileExtension(); } - $options = $this->getConfigurationForImageCropScaleMask($targetFile, $imageOperations); + $options = $this->getConfigurationForImageCropScaleMask($targetFile); $croppedImage = null; if (!empty($configuration['crop'])) { - // check if it is a json object - $cropData = json_decode($configuration['crop']); - if ($cropData) { - $offsetLeft = (int)($cropData->x ?? 0); - $offsetTop = (int)($cropData->y ?? 0); - $newWidth = (int)($cropData->width ?? 0); - $newHeight = (int)($cropData->height ?? 0); - } else { - [$offsetLeft, $offsetTop, $newWidth, $newHeight] = explode(',', $configuration['crop'], 4); - } - - $backupPrefix = $imageOperations->filenamePrefix; - $imageOperations->filenamePrefix = 'crop_'; - - $jpegQuality = MathUtility::forceIntegerInRange($GLOBALS['TYPO3_CONF_VARS']['GFX']['jpg_quality'], 10, 100, 85); - // the result info is an array with 0=width,1=height,2=extension,3=filename - $result = $imageOperations->imageMagickConvert( - $originalFileName, - $configuration['fileExtension'], - '', - '', - sprintf('-crop %dx%d+%d+%d +repage -quality %d', $newWidth, $newHeight, $offsetLeft, $offsetTop, $jpegQuality), - '', - ['noScale' => true], - true - ); - $imageOperations->filenamePrefix = $backupPrefix; - + $result = $imageOperations->crop($originalFileName, $configuration['fileExtension'], $configuration['crop']); if ($result !== null) { $originalFileName = $croppedImage = $result[3]; } @@ -118,8 +90,8 @@ class LocalCropScaleMaskHelper $configuration['additionalParameters'], $configuration['frame'] ?? '', $options, - // in case file is in `/typo3temp/`, it must create a result - $this->isTemporaryFile($originalFileName) + // in case file is in `/typo3temp/` from the crop operation above, it must create a result + $result !== null ); } else { $targetFileName = $this->getFilenameForImageCropScaleMask($task); @@ -127,10 +99,10 @@ class LocalCropScaleMaskHelper $maskImage = $configuration['maskImages']['maskImage'] ?? null; $maskBackgroundImage = $configuration['maskImages']['backgroundImage']; if ($maskImage instanceof FileInterface && $maskBackgroundImage instanceof FileInterface) { - $temporaryExtension = 'png'; + // This converts the original image to a temporary PNG file during all steps of the masking process $tempFileInfo = $imageOperations->imageMagickConvert( $originalFileName, - $temporaryExtension, + 'png', $configuration['width'] ?? '', $configuration['height'] ?? '', $configuration['additionalParameters'], @@ -138,43 +110,30 @@ class LocalCropScaleMaskHelper $options ); if (is_array($tempFileInfo)) { - $maskBottomImage = $configuration['maskImages']['maskBottomImage']; - if ($maskBottomImage instanceof FileInterface) { - $maskBottomImageMask = $configuration['maskImages']['maskBottomImageMask']; - } else { - $maskBottomImageMask = null; - } - - // Scaling: **** - $tempScale = []; + // Scaling $command = '-geometry ' . $tempFileInfo[0] . 'x' . $tempFileInfo[1] . '!'; $command = $this->modifyImageMagickStripProfileParameters($command, $configuration); - $tmpStr = $imageOperations->randomName(); - // m_mask - $tempScale['m_mask'] = $tmpStr . '_mask.' . $temporaryExtension; - $imageOperations->imageMagickExec($maskImage->getForLocalProcessing(true), $tempScale['m_mask'], $command); - // m_bgImg - $tempScale['m_bgImg'] = $tmpStr . '_bgImg.miff'; - $imageOperations->imageMagickExec($maskBackgroundImage->getForLocalProcessing(), $tempScale['m_bgImg'], $command); - // m_bottomImg / m_bottomImg_mask + + $imageOperations->mask( + $tempFileInfo[3], + $temporaryFileName, + $maskImage->getForLocalProcessing(), + $maskBackgroundImage->getForLocalProcessing(), + $command + ); + $maskBottomImage = $configuration['maskImages']['maskBottomImage'] ?? null; + $maskBottomImageMask = $configuration['maskImages']['maskBottomImageMask'] ?? null; if ($maskBottomImage instanceof FileInterface && $maskBottomImageMask instanceof FileInterface) { - $tempScale['m_bottomImg'] = $tmpStr . '_bottomImg.' . $temporaryExtension; - $imageOperations->imageMagickExec($maskBottomImage->getForLocalProcessing(), $tempScale['m_bottomImg'], $command); - $tempScale['m_bottomImg_mask'] = ($tmpStr . '_bottomImg_mask.') . $temporaryExtension; - $imageOperations->imageMagickExec($maskBottomImageMask->getForLocalProcessing(), $tempScale['m_bottomImg_mask'], $command); - // BEGIN combining: - // The image onto the background - $imageOperations->combineExec($tempScale['m_bgImg'], $tempScale['m_bottomImg'], $tempScale['m_bottomImg_mask'], $tempScale['m_bgImg']); + // Uses the temporary PNG file from the previous step and applies another mask + $imageOperations->mask( + $temporaryFileName, + $temporaryFileName, + $maskBottomImage->getForLocalProcessing(), + $maskBottomImageMask->getForLocalProcessing(), + $command + ); } - // The image onto the background - $imageOperations->combineExec($tempScale['m_bgImg'], $tempFileInfo[3], $tempScale['m_mask'], $temporaryFileName); $tempFileInfo[3] = $temporaryFileName; - // Unlink the temp-images... - foreach ($tempScale as $tempFile) { - if (@is_file($tempFile)) { - unlink($tempFile); - } - } } $result = $tempFileInfo; } @@ -202,10 +161,7 @@ class LocalCropScaleMaskHelper return $result; } - /** - * @return array - */ - protected function getConfigurationForImageCropScaleMask(ProcessedFile $processedFile, GraphicalFunctions $imageOperations) + protected function getConfigurationForImageCropScaleMask(ProcessedFile $processedFile): array { $configuration = $processedFile->getProcessingConfiguration(); @@ -239,14 +195,7 @@ class LocalCropScaleMaskHelper protected function getFilenameForImageCropScaleMask(TaskInterface $task) { $configuration = $task->getTargetFile()->getProcessingConfiguration(); - $targetFileExtension = $task->getSourceFile()->getExtension(); - $processedFileExtension = $GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib_png'] ? 'png' : 'gif'; - if (is_array($configuration['maskImages']) && $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_enabled'] && $task->getSourceFile()->getExtension() != $processedFileExtension) { - $targetFileExtension = 'jpg'; - } elseif ($configuration['fileExtension']) { - $targetFileExtension = $configuration['fileExtension']; - } - + $targetFileExtension = $configuration['fileExtension'] ?? $task->getSourceFile()->getExtension(); return $task->getTargetFile()->generateProcessedFileNameWithoutExtension() . '.' . ltrim(trim($targetFileExtension), '.'); } @@ -272,9 +221,4 @@ class LocalCropScaleMaskHelper } return $parameters; } - - protected function isTemporaryFile(string $filePath): bool - { - return str_starts_with($filePath, Environment::getPublicPath() . '/typo3temp/'); - } } diff --git a/typo3/sysext/core/Classes/Resource/Processing/LocalPreviewHelper.php b/typo3/sysext/core/Classes/Resource/Processing/LocalPreviewHelper.php index 8b12df30c94f..fdb8c9a48a40 100644 --- a/typo3/sysext/core/Classes/Resource/Processing/LocalPreviewHelper.php +++ b/typo3/sysext/core/Classes/Resource/Processing/LocalPreviewHelper.php @@ -15,9 +15,8 @@ namespace TYPO3\CMS\Core\Resource\Processing; -use TYPO3\CMS\Core\Imaging\ImageMagickFile; +use TYPO3\CMS\Core\Imaging\GraphicalFunctions; use TYPO3\CMS\Core\Resource\File; -use TYPO3\CMS\Core\Utility\CommandUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\MathUtility; use TYPO3\CMS\Frontend\Imaging\GifBuilder; @@ -148,17 +147,11 @@ class LocalPreviewHelper { // Create the temporary file if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_enabled']) { - $arguments = CommandUtility::escapeShellArguments([ - 'width' => $configuration['width'], - 'height' => $configuration['height'], - ]); - $parameters = '-sample ' . $arguments['width'] . 'x' . $arguments['height'] - . ' ' . ImageMagickFile::fromFilePath($originalFileName, 0) - . ' ' . CommandUtility::escapeShellArgument($targetFilePath); - - $cmd = CommandUtility::imageMagickCommand('convert', $parameters) . ' 2>&1'; - CommandUtility::exec($cmd); - + $imageService = GeneralUtility::makeInstance(GraphicalFunctions::class); + $result = $imageService->imageMagickConvert($originalFileName, 'WEB', $configuration['width'], $configuration['height'], '', '0', ['sample' => true]); + if ($result) { + $targetFilePath = $result[3]; + } if (!file_exists($targetFilePath)) { // Create an error gif $graphicalFunctions = GeneralUtility::makeInstance(GifBuilder::class); -- GitLab