diff --git a/typo3/sysext/core/Classes/Imaging/GraphicalFunctions.php b/typo3/sysext/core/Classes/Imaging/GraphicalFunctions.php index f9ea1256cfc2c59cee98fd25a93f41f1a4b48481..7c49ec2f381830e8639b5013c4a4e5def70977d0 100644 --- a/typo3/sysext/core/Classes/Imaging/GraphicalFunctions.php +++ b/typo3/sysext/core/Classes/Imaging/GraphicalFunctions.php @@ -339,13 +339,15 @@ class GraphicalFunctions $frame = $this->addFrameSelection && isset($options['frame']) ? (int)$options['frame'] : 0; $processingInstructions = ImageProcessingInstructions::fromCropScaleValues($info->getWidth(), $info->getHeight(), $width, $height, $options); - $w = $processingInstructions->originalWidth; - $h = $processingInstructions->originalHeight; + + $originalWidth = $info->getWidth() ?: $width; + $originalHeight = $info->getHeight() ?: $height; + // Check if conversion should be performed ($noScale - no processing needed). // $noScale flag is TRUE if the width / height does NOT dictate the image to be scaled. That is if no // width / height is given or if the destination w/h matches the original image dimensions, or if // the option to not scale the image is set. - $noScale = !$processingInstructions->originalWidth && !$processingInstructions->originalHeight || $processingInstructions->width === $info->getWidth() && $processingInstructions->height === $info->getHeight() || !empty($options['noScale']); + $noScale = !$originalWidth && !$originalHeight || $processingInstructions->width === $info->getWidth() && $processingInstructions->height === $info->getHeight() || !empty($options['noScale']); if ($noScale && !$processingInstructions->cropArea && !$additionalParameters && !$frame && $targetFileExtension === $info->getExtension() && !$forceCreation) { // Set the new width and height before returning, // if the noScale option is set, otherwise the incoming @@ -360,12 +362,18 @@ class GraphicalFunctions return $info; } + $command = ''; + if ($processingInstructions->cropArea) { + $cropArea = $processingInstructions->cropArea; + $command .= ' -crop ' . $cropArea->getWidth() . 'x' . $cropArea->getHeight() . '+' . $cropArea->getOffsetLeft() . '+' . $cropArea->getOffsetTop() . '! +repage '; + } + // Start with the default scale command // check if we should use -sample or -geometry if ($options['sample'] ?? false) { - $command = '-auto-orient -sample'; + $command .= '-auto-orient -sample'; } else { - $command = $this->scalecmd; + $command .= $this->scalecmd; } // from the IM docs -- https://imagemagick.org/script/command-line-processing.php // "We see that ImageMagick is very good about preserving aspect ratios of images, to prevent distortion @@ -374,10 +382,6 @@ class GraphicalFunctions // operator to the geometry. This will force the image size to exactly what you specify. // So, for example, if you specify 100x200! the dimensions will become exactly 100x200" $command .= ' ' . $processingInstructions->width . 'x' . $processingInstructions->height . '!'; - if ($processingInstructions->cropArea) { - $cropArea = $processingInstructions->cropArea; - $command .= ' -crop ' . $cropArea->getWidth() . 'x' . $cropArea->getHeight() . '+' . $cropArea->getOffsetLeft() . '+' . $cropArea->getOffsetTop() . '! +repage'; - } // Add params $additionalParameters = $this->modifyImageMagickStripProfileParameters($additionalParameters, $options); $command .= ($additionalParameters ? ' ' . $additionalParameters : $this->cmds[$targetFileExtension] ?? ''); @@ -451,36 +455,6 @@ class GraphicalFunctions return $result?->toLegacyArray(); } - /** - * This only crops the image, but does not take other "options" such as maxWidth etc. not into account. Do not use - * standalone if you don't know what you are doing. - * - * @internal until API is finalized - */ - public function crop(string $imageFile, string $targetFileExtension, string $cropInformation, array $options): ?ImageProcessingResult - { - // 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->resize( - $imageFile, - $targetFileExtension, - '', - '', - sprintf('-crop %dx%d+%d+%d! +repage', $newWidth, $newHeight, $offsetLeft, $offsetTop), - isset($options['skipProfile']) ? ['skipProfile' => $options['skipProfile']] : [], - true - ); - } - /** * This applies an image onto the $inputFile with an additional backgroundImage for the mask * @internal until API is finalized diff --git a/typo3/sysext/core/Classes/Imaging/ImageDimension.php b/typo3/sysext/core/Classes/Imaging/ImageDimension.php index 676ff087b4dfcacbc96bdec08e07852bfa319823..ce17cfe0a2bd6eb565a20407163e8cfabd1787fc 100644 --- a/typo3/sysext/core/Classes/Imaging/ImageDimension.php +++ b/typo3/sysext/core/Classes/Imaging/ImageDimension.php @@ -18,8 +18,6 @@ declare(strict_types=1); namespace TYPO3\CMS\Core\Imaging; use TYPO3\CMS\Core\Imaging\Exception\ZeroImageDimensionException; -use TYPO3\CMS\Core\Imaging\ImageManipulation\Area; -use TYPO3\CMS\Core\Resource\ProcessedFile; use TYPO3\CMS\Core\Resource\Processing\TaskInterface; /** @@ -53,117 +51,12 @@ class ImageDimension return $this->height; } - public static function fromProcessingTask(TaskInterface $task): self - { - $config = self::getConfigurationForImageCropScaleMask($task); - $processedFile = $task->getTargetFile(); - $isCropped = false; - if (($config['crop'] ?? null) instanceof Area) { - $isCropped = true; - $imageWidth = (int)round($config['crop']->getWidth()); - $imageHeight = (int)round($config['crop']->getHeight()); - } else { - $imageWidth = (int)$processedFile->getOriginalFile()->getProperty('width'); - $imageHeight = (int)$processedFile->getOriginalFile()->getProperty('height'); - } - if ($imageWidth <= 0 || $imageHeight <= 0) { - throw new ZeroImageDimensionException('Width and height of the image must be greater than zero.', 1597310560); - } - $result = ImageProcessingInstructions::fromCropScaleValues( - $imageWidth, - $imageHeight, - $config['width'] ?? '', - $config['height'] ?? '', - $config - ); - $imageWidth = $geometryWidth = $result->width; - $imageHeight = $geometryHeight = $result->height; - - if ($result->useCropScaling) { - $cropWidth = $result->originalWidth; - $cropHeight = $result->originalHeight; - // If the image is crop-scaled, use the dimension of the crop - // unless crop area exceeds the dimension of the scaled image - if ($cropWidth <= $geometryWidth && $cropHeight <= $geometryHeight) { - $imageWidth = $cropWidth; - $imageHeight = $cropHeight; - } - if (!$isCropped && $task->getTargetFileExtension() === 'svg') { - // Keep aspect ratio of SVG files, when crop-scaling is requested - // but no crop is applied - if ($geometryWidth > $geometryHeight) { - $imageHeight = (int)round($imageWidth * $geometryHeight / $geometryWidth); - } else { - $imageWidth = (int)round($imageHeight * $geometryWidth / $geometryHeight); - } - } - } - - return new self($imageWidth, $imageHeight); - } - /** - * @return array{ - * width?: int<0, max>|string, - * height?: int<0, max>|string, - * maxWidth?: int<0, max>, - * maxHeight?: int<0, max>, - * maxW?: int<0, max>, - * maxH?: int<0, max>, - * minW?: int<0, max>, - * minH?: int<0, max>, - * crop?: Area, - * noScale?: bool - * } + * @throws ZeroImageDimensionException */ - private static function getConfigurationForImageCropScaleMask(TaskInterface $task): array + public static function fromProcessingTask(TaskInterface $task): self { - $configuration = $task->getConfiguration(); - - if ($task->getTargetFile()->getTaskIdentifier() === ProcessedFile::CONTEXT_IMAGEPREVIEW) { - $task->sanitizeConfiguration(); - // @todo: this transformation needs to happen in the PreviewTask, but if we do this, - // all preview images would be re-created, so we should be careful when to do this. - $configuration = $task->getConfiguration(); - $configuration['maxWidth'] = $configuration['width']; - unset($configuration['width']); - $configuration['maxHeight'] = $configuration['height']; - unset($configuration['height']); - } - - $options = $configuration; - if ($configuration['maxWidth'] ?? null) { - $options['maxW'] = $configuration['maxWidth']; - } - if ($configuration['maxHeight'] ?? null) { - $options['maxH'] = $configuration['maxHeight']; - } - if ($configuration['minWidth'] ?? null) { - $options['minW'] = $configuration['minWidth']; - } - if ($configuration['minHeight'] ?? null) { - $options['minH'] = $configuration['minHeight']; - } - if ($configuration['crop'] ?? null) { - $options['crop'] = $configuration['crop']; - if (is_string($configuration['crop'])) { - // check if it is a json object - $cropData = json_decode($configuration['crop']); - if ($cropData) { - $options['crop'] = new Area((float)$cropData->x, (float)$cropData->y, (float)$cropData->width, (float)$cropData->height); - } else { - [$offsetLeft, $offsetTop, $newWidth, $newHeight] = explode(',', $configuration['crop'], 4); - $options['crop'] = new Area((float)$offsetLeft, (float)$offsetTop, (float)$newWidth, (float)$newHeight); - } - if ($options['crop']->isEmpty()) { - unset($options['crop']); - } - } - } - if ($configuration['noScale'] ?? null) { - $options['noScale'] = $configuration['noScale']; - } - - return $options; + $result = ImageProcessingInstructions::fromProcessingTask($task); + return new self($result->width, $result->height); } } diff --git a/typo3/sysext/core/Classes/Imaging/ImageProcessingInstructions.php b/typo3/sysext/core/Classes/Imaging/ImageProcessingInstructions.php index 19f4eb87258f1f1fddb0b6aa50b8873eaf592e19..f6adc0c8464e1ad83ad5b571ed7648e469ff98a6 100644 --- a/typo3/sysext/core/Classes/Imaging/ImageProcessingInstructions.php +++ b/typo3/sysext/core/Classes/Imaging/ImageProcessingInstructions.php @@ -17,7 +17,10 @@ declare(strict_types=1); namespace TYPO3\CMS\Core\Imaging; +use TYPO3\CMS\Core\Imaging\Exception\ZeroImageDimensionException; use TYPO3\CMS\Core\Imaging\ImageManipulation\Area; +use TYPO3\CMS\Core\Resource\ProcessedFile; +use TYPO3\CMS\Core\Resource\Processing\TaskInterface; /** * A DTO representing all information needed to process an image, @@ -31,19 +34,42 @@ use TYPO3\CMS\Core\Imaging\ImageManipulation\Area; * * @internal This object is still internal as long as cropping isn't migrated yet to the Crop API. */ -class ImageProcessingInstructions +readonly class ImageProcessingInstructions { - /** @var int<0, max> */ - public int $originalWidth = 0; - /** @var int<0, max> */ - public int $originalHeight = 0; - /** @var int<0, max> */ - public int $width = 0; - /** @var int<0, max> */ - public int $height = 0; - public bool $useCropScaling = false; - public ?Area $cropArea = null; - public array $options = []; + /** + * @param int<0, max> $width + * @param int<0, max> $height + */ + public function __construct( + public int $width = 0, + public int $height = 0, + public ?Area $cropArea = null, + ) {} + + public static function fromProcessingTask(TaskInterface $task): ImageProcessingInstructions + { + $config = self::getConfigurationForImageCropScaleMask($task); + $processedFile = $task->getTargetFile(); + $isCropped = false; + if (($config['crop'] ?? null) instanceof Area) { + $isCropped = true; + $imageWidth = (int)round($config['crop']->getWidth()); + $imageHeight = (int)round($config['crop']->getHeight()); + } else { + $imageWidth = (int)$processedFile->getOriginalFile()->getProperty('width'); + $imageHeight = (int)$processedFile->getOriginalFile()->getProperty('height'); + } + if ($imageWidth <= 0 || $imageHeight <= 0) { + throw new ZeroImageDimensionException('Width and height of the image must be greater than zero.', 1597310560); + } + return ImageProcessingInstructions::fromCropScaleValues( + $imageWidth, + $imageHeight, + $config['width'] ?? '', + $config['height'] ?? '', + $config + ); + } /** * Get numbers for scaling the image based on input. @@ -85,147 +111,176 @@ class ImageProcessingInstructions * whereas $incomingWidth and $incomingHeight contain the target width and height that could be larger than originally requested. * * ---------------------------- - * @param int<0, max> $incomingWidth the width of an original image for example, can be "0" if there is no original image (thus, it will remain "0" in the "originalWidth") - * @param int<0, max> $incomingHeight the height of an original image for example, can be "0" if there is no original image (thus, it will remain "0" in the "origHeight") + * @param int<0, max> $incomingWidth the width of an original image for example, can be "0" if there is no original image + * @param int<0, max> $incomingHeight the height of an original image for example, can be "0" if there is no original image * @param int<0, max>|string $width "required" width that is requested, can be "" or "0" or a number of a magic "m" or "c" appended * @param int<0, max>|string $height "required" height that is requested, can be "" or "0" or a number of a magic "m" or "c" appended * @param array $options Options: Keys are like "maxW", "maxH", "minW", "minH" */ public static function fromCropScaleValues(int $incomingWidth, int $incomingHeight, int|string $width, int|string $height, array $options): self { - $cropOffsetHorizontal = 0; - $cropOffsetVertical = 0; $options = self::streamlineOptions($options); - $obj = new self(); + + if ($incomingWidth === 0 || $incomingHeight === 0) { + // @todo incomingWidth/Height makes no sense, we should ideally throw an exception here… + // this code is here to make existing unit tests happy and should be dropped + return new self( + width: 0, + height: 0, + cropArea: null + ); + } + + $cropArea = ($options['crop'] ?? null) instanceof Area ? $options['crop'] : new Area(0, 0, $incomingWidth, $incomingHeight); + // If both the width and the height are set and one of the numbers is appended by an m, the proportions will // be preserved and thus width and height are treated as maximum dimensions for the image. The image will be // scaled to fit into the rectangle of the dimensions width and height. $useWidthOrHeightAsMaximumLimits = str_contains($width . $height, 'm'); - if (($options['crop'] ?? null) instanceof Area) { - $obj->cropArea = $options['crop']; - unset($options['crop']); - } elseif (str_contains($width . $height, 'c')) { + $useCropScaling = str_contains($width . $height, 'c'); + + if ($useWidthOrHeightAsMaximumLimits && $useCropScaling) { + throw new \InvalidArgumentException('Cannot mix m and c modifiers for width/height', 1709840402); + } + + if ($useWidthOrHeightAsMaximumLimits) { + if (str_contains($width, 'm')) { + $options['maxWidth'] = min((int)$width, $options['maxWidth'] ?? PHP_INT_MAX); + // width: auto + $width = 0; + } + if (str_contains($height, 'm')) { + $options['maxHeight'] = min((int)$height, $options['maxHeight'] ?? PHP_INT_MAX); + // height: auto + $height = 0; + } + } + + if ((int)$width !== 0 && (int)$height !== 0 && $useCropScaling) { $cropOffsetHorizontal = (int)substr((string)strstr((string)$width, 'c'), 1); $cropOffsetVertical = (int)substr((string)strstr((string)$height, 'c'), 1); - $obj->useCropScaling = true; + $width = (int)$width; + $height = (int)$height; + + $cropArea = self::applyCropScaleToCropArea($cropArea, $width, $height, $cropOffsetVertical, $cropOffsetHorizontal); } + $width = (int)$width; $height = (int)$height; + + if ($width > 0 && $height === 0) { + $height = (int)round($cropArea->getHeight() * ($width / $cropArea->getWidth())); + } + if ($height > 0 && $width === 0) { + $width = (int)round($cropArea->getWidth() * ($height / $cropArea->getHeight())); + } + // If there are max-values... if (!empty($options['maxWidth'])) { - // If width is given... - if ($width > 0) { - if ($width > $options['maxWidth']) { - $width = $options['maxWidth']; - // Height should follow - $useWidthOrHeightAsMaximumLimits = true; - } - } else { - if ($incomingWidth > $options['maxWidth']) { - $width = $options['maxWidth']; - // Height should follow - $useWidthOrHeightAsMaximumLimits = true; - } + if ($width > $options['maxWidth'] || ($width === 0 && $cropArea->getWidth() > $options['maxWidth'])) { + $width = $options['maxWidth']; + $height = (int)round($cropArea->getHeight() * ($width / $cropArea->getWidth())); } } if (!empty($options['maxHeight'])) { - // If height is given... - if ($height > 0) { - if ($height > $options['maxHeight']) { - $height = $options['maxHeight']; - // Height should follow - $useWidthOrHeightAsMaximumLimits = true; - } - } else { - // Changed [0] to [1] 290801 - if ($incomingHeight > $options['maxHeight']) { - $height = $options['maxHeight']; - // Height should follow - $useWidthOrHeightAsMaximumLimits = true; - } + if ($height > $options['maxHeight'] || ($height === 0 && $cropArea->getHeight() > $options['maxHeight'])) { + $height = $options['maxHeight']; + $width = (int)round($cropArea->getWidth() * ($height / $cropArea->getHeight())); } } - $obj->originalWidth = $width; - $obj->originalHeight = $height; - if (!($GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_allowUpscaling'] ?? false)) { - if ($width > $incomingWidth) { - $width = $incomingWidth; - } - if ($height > $incomingHeight) { - $height = $incomingHeight; + + if (!empty($options['minWidth'])) { + if ($width < $options['minWidth'] || ($width === 0 && $cropArea->getWidth() < $options['minWidth'])) { + $width = $options['minWidth']; + $height = (int)round($cropArea->getHeight() * ($width / $cropArea->getWidth())); } } - // If scaling should be performed. Check that input "info" array will not cause division-by-zero - if (($width > 0 || $height > 0) && $incomingWidth && $incomingHeight) { - if ($width > 0 && $height === 0) { - $incomingHeight = (int)ceil($incomingHeight * ($width / $incomingWidth)); - $incomingWidth = $width; + if (!empty($options['minHeight'])) { + if ($height < $options['minHeight'] || ($height === 0 && $cropArea->getHeight() < $options['minHeight'])) { + $height = $options['minHeight']; + $width = (int)round($cropArea->getWidth() * ($height / $cropArea->getHeight())); } - if ((int)$width === 0 && $height > 0) { - $incomingWidth = (int)ceil($incomingWidth * ($height / $incomingHeight)); - $incomingHeight = $height; + } + + if ($width === 0 && $height === 0) { + $width = (int)round($cropArea->getWidth()); + $height = (int)round($cropArea->getHeight()); + } + if ($width === 0 || $height === 0) { + throw new \LogicException('Image processing instructions did not resolve into coherent positive width and height values. This is a bug. Please report.', 1709806820); + } + + if (!($GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_allowUpscaling'] ?? false)) { + if ($width > $cropArea->getWidth()) { + $width = (int)round($cropArea->getWidth()); + $height = (int)round($cropArea->getHeight() * ($width / $cropArea->getWidth())); } - if ($width !== 0 && $height !== 0) { - if ($useWidthOrHeightAsMaximumLimits) { - $ratio = $incomingWidth / $incomingHeight; - if ($height * $ratio > $width) { - $height = (int)round($width / $ratio); - } else { - $width = (int)round($height * $ratio); - } - } - if ($obj->useCropScaling) { - $ratio = $incomingWidth / $incomingHeight; - if ($height * $ratio < $width) { - $height = (int)round($width / $ratio); - } else { - $width = (int)round($height * $ratio); - } - } - $incomingWidth = $width; - $incomingHeight = $height; + if ($height > $cropArea->getHeight()) { + $height = (int)round($cropArea->getHeight()); + $width = (int)round($cropArea->getWidth() * ($height / $cropArea->getHeight())); } } - $resultWidth = $incomingWidth; - $resultHeight = $incomingHeight; - // Set minimum-measures! - if (isset($options['minWidth']) && $resultWidth < $options['minWidth']) { - if (($useWidthOrHeightAsMaximumLimits || $obj->useCropScaling) && $resultWidth) { - $resultHeight = (int)round($resultHeight * $options['minWidth'] / $resultWidth); - } - $resultWidth = $options['minWidth']; + + if ((int)$cropArea->getOffsetLeft() === 0 && + (int)$cropArea->getOffsetTop() === 0 && + (int)$cropArea->getWidth() === $incomingWidth && + (int)$cropArea->getHeight() === $incomingHeight) { + $cropArea = null; } - if (isset($options['minHeight']) && $resultHeight < $options['minHeight']) { - if (($useWidthOrHeightAsMaximumLimits || $obj->useCropScaling) && $resultHeight) { - $resultWidth = (int)round($resultWidth * $options['minHeight'] / $resultHeight); - } - $resultHeight = $options['minHeight']; - } - $obj->width = $resultWidth; - $obj->height = $resultHeight; - - // The incoming values are percentage values, and need to be calculated in - // the actual width and height of the target file size, see https://docs.typo3.org/m/typo3/reference-typoscript/main/en-us/Functions/Imgresource.html#width - // This needs a special calculation "magic", instead of using the "cropArea" feature. - // which TYPO3 uses since v8 which ships with a "cropArea" object right away. - if ($obj->useCropScaling && !$obj->cropArea) { - $cropWidth = $obj->originalWidth ?: $obj->width; - $cropHeight = $obj->originalHeight ?: $obj->height; - $offsetX = (float)(($obj->width - $obj->originalWidth) * ($cropOffsetHorizontal + 100) / 200); - $offsetY = (float)(($obj->height - $obj->originalHeight) * ($cropOffsetVertical + 100) / 200); - - $obj->cropArea = new Area( - $offsetX, - $offsetY, - (float)$cropWidth, - (float)$cropHeight, - ); + + return new self( + width: $width, + height: $height, + cropArea: $cropArea, + ); + } + + /** + * @param Area $cropArea with absolute crop data (not relative!) + * @param positive-int $width + * @param positive-int $height + * @param int<-100,100> $cropOffsetVertical + * @param int<-100,100> $cropOffsetHorizontal + */ + private static function applyCropScaleToCropArea( + Area $cropArea, + int $width, + int $height, + int $cropOffsetVertical, + int $cropOffsetHorizontal + ): Area { + // @phpstan-ignore-next-line + if (!($width > 0 && $height > 0 && $cropArea->getWidth() > 0 && $cropArea->getHeight() > 0)) { + throw new \InvalidArgumentException('Apply crop scale must use concrete width and height', 1709810881); } + $destRatio = $width / $height; + $cropRatio = $cropArea->getWidth() / $cropArea->getHeight(); - return $obj; + if ($destRatio > $cropRatio) { + $w = $cropArea->getWidth(); + $h = $cropArea->getWidth() / $destRatio; + $x = $cropArea->getOffsetLeft(); + $y = $cropArea->getOffsetTop() + (float)(($cropArea->getHeight() - $h) * ($cropOffsetVertical + 100) / 200); + } else { + $w = $cropArea->getHeight() * $destRatio; + $h = $cropArea->getHeight(); + $x = $cropArea->getOffsetLeft() + (float)(($cropArea->getWidth() - $w) * ($cropOffsetHorizontal + 100) / 200); + $y = $cropArea->getOffsetTop(); + } + + return new Area($x, $y, $w, $h); } - public static function streamlineOptions(array $options): array + /** + * @return array{ + * maxWidth?: int, + * maxHeight?: int, + * minWidth?: int, + * minHeight?: int, + * crop?: Area, + * } + */ + private static function streamlineOptions(array $options): array { if (isset($options['maxW'])) { $options['maxWidth'] = $options['maxW']; @@ -243,17 +298,36 @@ class ImageProcessingInstructions $options['minHeight'] = $options['minH']; unset($options['minH']); } + + if (($options['maxWidth'] ?? null) <= 0) { + unset($options['maxWidth']); + } + if (($options['maxHeight'] ?? null) <= 0) { + unset($options['maxHeight']); + } + if (($options['minWidth'] ?? null) <= 0) { + unset($options['minWidth']); + } + if (($options['minHeight'] ?? null) <= 0) { + unset($options['minHeight']); + } + if (isset($options['crop'])) { if (is_string($options['crop'])) { // check if it is a json object $cropData = json_decode($options['crop']); if ($cropData) { - $options['crop'] = new Area((float)$cropData->x, (float)$cropData->y, (float)$cropData->width, (float)$cropData->height); + // happens when $options['crop'] = '{"default":{"cropArea":{"x":0,"y":0,"width":1,"height":1},"selectedRatio":"NaN","focusArea":null}}' + if (!isset($cropData->x) || !isset($cropData->y) || !isset($cropData->width) || !isset($cropData->height)) { + unset($options['crop']); + } else { + $options['crop'] = new Area((float)$cropData->x, (float)$cropData->y, (float)$cropData->width, (float)$cropData->height); + } } else { [$offsetLeft, $offsetTop, $newWidth, $newHeight] = explode(',', $options['crop'], 4); $options['crop'] = new Area((float)$offsetLeft, (float)$offsetTop, (float)$newWidth, (float)$newHeight); } - if ($options['crop']->isEmpty()) { + if (isset($options['crop']) && $options['crop']->isEmpty()) { unset($options['crop']); } } elseif (!$options['crop'] instanceof Area) { @@ -262,4 +336,69 @@ class ImageProcessingInstructions } return $options; } + + /** + * @return array{ + * width?: int<0, max>|string, + * height?: int<0, max>|string, + * maxWidth?: int<0, max>, + * maxHeight?: int<0, max>, + * maxW?: int<0, max>, + * maxH?: int<0, max>, + * minW?: int<0, max>, + * minH?: int<0, max>, + * crop?: Area, + * noScale?: bool + * } + */ + private static function getConfigurationForImageCropScaleMask(TaskInterface $task): array + { + $configuration = $task->getConfiguration(); + + if ($task->getTargetFile()->getTaskIdentifier() === ProcessedFile::CONTEXT_IMAGEPREVIEW) { + $task->sanitizeConfiguration(); + // @todo: this transformation needs to happen in the PreviewTask, but if we do this, + // all preview images would be re-created, so we should be careful when to do this. + $configuration = $task->getConfiguration(); + $configuration['maxWidth'] = $configuration['width']; + unset($configuration['width']); + $configuration['maxHeight'] = $configuration['height']; + unset($configuration['height']); + } + + $options = $configuration; + if ($configuration['maxWidth'] ?? null) { + $options['maxW'] = $configuration['maxWidth']; + } + if ($configuration['maxHeight'] ?? null) { + $options['maxH'] = $configuration['maxHeight']; + } + if ($configuration['minWidth'] ?? null) { + $options['minW'] = $configuration['minWidth']; + } + if ($configuration['minHeight'] ?? null) { + $options['minH'] = $configuration['minHeight']; + } + if ($configuration['crop'] ?? null) { + $options['crop'] = $configuration['crop']; + if (is_string($configuration['crop'])) { + // check if it is a json object + $cropData = json_decode($configuration['crop']); + if ($cropData) { + $options['crop'] = new Area((float)$cropData->x, (float)$cropData->y, (float)$cropData->width, (float)$cropData->height); + } else { + [$offsetLeft, $offsetTop, $newWidth, $newHeight] = explode(',', $configuration['crop'], 4); + $options['crop'] = new Area((float)$offsetLeft, (float)$offsetTop, (float)$newWidth, (float)$newHeight); + } + if ($options['crop']->isEmpty()) { + unset($options['crop']); + } + } + } + if ($configuration['noScale'] ?? null) { + $options['noScale'] = $configuration['noScale']; + } + + return $options; + } } diff --git a/typo3/sysext/core/Classes/Resource/Processing/LocalCropScaleMaskHelper.php b/typo3/sysext/core/Classes/Resource/Processing/LocalCropScaleMaskHelper.php index 66a5ddb64515c00e673164d18e40b25fa76ceeab..10cf5fe87638382ecc94ff445961c56137a5e327 100644 --- a/typo3/sysext/core/Classes/Resource/Processing/LocalCropScaleMaskHelper.php +++ b/typo3/sysext/core/Classes/Resource/Processing/LocalCropScaleMaskHelper.php @@ -65,16 +65,6 @@ class LocalCropScaleMaskHelper $configuration = $targetFile->getProcessingConfiguration(); $configuration['additionalParameters'] ??= ''; - $croppedImage = null; - if (!empty($configuration['crop'])) { - $result = $imageOperations->crop($originalFileName, $targetFileExtension, $configuration['crop'], $configuration); - // @todo: in the future, we want this to be one crop call (together with the scale command) - unset($configuration['crop']); - if ($result !== null) { - $originalFileName = $croppedImage = $result->getRealPath(); - } - } - // Normal situation (no masking) - just scale the image if (!is_array($configuration['maskImages'] ?? null)) { // the result info is an array with 0=width,1=height,2=extension,3=filename @@ -85,8 +75,6 @@ class LocalCropScaleMaskHelper $configuration['height'] ?? '', $configuration['additionalParameters'], $configuration, - // in case file is in `/typo3temp/` from the crop operation above, it must create a result - $result !== null ); } else { $temporaryFileName = $this->getFilenameForImageCropScaleMask($task); @@ -133,7 +121,7 @@ class LocalCropScaleMaskHelper // check if the processing really generated a new file (scaled and/or cropped) if ($result !== null) { - if ($result->getRealPath() !== $originalFileName || $originalFileName === $croppedImage) { + if ($result->getRealPath() !== $originalFileName) { $result = [ 'width' => $result->getWidth(), 'height' => $result->getHeight(), @@ -145,11 +133,6 @@ class LocalCropScaleMaskHelper } } - // Cleanup temp file if it isn't used as result - if ($croppedImage && ($result === null || $croppedImage !== $result['filePath'])) { - GeneralUtility::unlink_tempfile($croppedImage); - } - // If noScale option is applied, we need to reset the width and height to ensure the scaled values // are used for the generated image tag even if the image itself is not scaled. This is needed, as // the result is discarded due to the fact that the original image is used. diff --git a/typo3/sysext/core/Tests/Unit/Imaging/ImageDimensionTest.php b/typo3/sysext/core/Tests/Unit/Imaging/ImageDimensionTest.php index 374d1c871c560e18e6f8adf2af9b55763934e958..e9b6a3b47ef342852a87fdc2f959d7999417d12f 100644 --- a/typo3/sysext/core/Tests/Unit/Imaging/ImageDimensionTest.php +++ b/typo3/sysext/core/Tests/Unit/Imaging/ImageDimensionTest.php @@ -105,7 +105,7 @@ final class ImageDimensionTest extends UnitTestCase new ImageDimension(1000, 500), 'jpg', ImageCropScaleMaskTask::class, - new ImageDimension(50, 25), + new ImageDimension(50, 50), ], 'width and height are applied as given' => [ [ @@ -155,7 +155,7 @@ final class ImageDimensionTest extends UnitTestCase new ImageDimension(1000, 500), 'svg', ImageCropScaleMaskTask::class, - new ImageDimension(100, 50), + new ImageDimension(100, 100), ], 'cropping is applied on SVGs' => [ [ diff --git a/typo3/sysext/core/Tests/Unit/Imaging/ImageProcessingInstructionsTest.php b/typo3/sysext/core/Tests/Unit/Imaging/ImageProcessingInstructionsTest.php index 451d1ed4bf4ba16b0d04b6b91785582dfe07360b..52c123db66facee4226fc4e86037ef69220ad3f7 100644 --- a/typo3/sysext/core/Tests/Unit/Imaging/ImageProcessingInstructionsTest.php +++ b/typo3/sysext/core/Tests/Unit/Imaging/ImageProcessingInstructionsTest.php @@ -33,11 +33,10 @@ final class ImageProcessingInstructionsTest extends UnitTestCase */ public static function fromCropScaleValuesImageDataProvider(): iterable { - $result = new ImageProcessingInstructions(); - $result->width = 150; - $result->height = 120; - $result->originalWidth = 150; - $result->originalHeight = 0; + $result = new ImageProcessingInstructions( + width: 150, + height: 120, + ); yield 'Get image scale for a width of 150px' => [ 170, 136, @@ -48,11 +47,10 @@ final class ImageProcessingInstructionsTest extends UnitTestCase $result, ]; - $result = new ImageProcessingInstructions(); - $result->width = 100; - $result->height = 80; - $result->originalWidth = 100; - $result->originalHeight = 0; + $result = new ImageProcessingInstructions( + width: 100, + height: 80, + ); yield 'Get image scale with a maximum width of 100px' => [ 170, 136, @@ -63,11 +61,10 @@ final class ImageProcessingInstructionsTest extends UnitTestCase $result, ]; - $result = new ImageProcessingInstructions(); - $result->width = 200; - $result->height = 136; - $result->originalWidth = 0; - $result->originalHeight = 0; + $result = new ImageProcessingInstructions( + width: 200, + height: 160, + ); yield 'Get image scale with a minimum width of 200px' => [ 170, 136, @@ -78,10 +75,10 @@ final class ImageProcessingInstructionsTest extends UnitTestCase $result, ]; - $result = new ImageProcessingInstructions(); - $result->originalWidth = 50; - $result->width = 0; - $result->height = 0; + $result = new ImageProcessingInstructions( + width: 0, + height: 0, + ); yield 'No PHP warning for zero in input dimensions when scaling' => [ 0, 0, @@ -92,10 +89,10 @@ final class ImageProcessingInstructionsTest extends UnitTestCase $result, ]; - $result = new ImageProcessingInstructions(); - $result->originalWidth = 50; - $result->width = 50; - $result->height = 450; + $result = new ImageProcessingInstructions( + width: 50, + height: 450, + ); yield 'width from original image and explicitly given scales an image down' => [ 100, 900, @@ -106,11 +103,10 @@ final class ImageProcessingInstructionsTest extends UnitTestCase $result, ]; - $result = new ImageProcessingInstructions(); - $result->originalWidth = 50; - $result->originalHeight = 300; - $result->width = 33; - $result->height = 300; + $result = new ImageProcessingInstructions( + width: 33, + height: 300, + ); yield 'width from original image with maxH set, also fills "origH" value' => [ 100, 900, @@ -121,11 +117,10 @@ final class ImageProcessingInstructionsTest extends UnitTestCase $result, ]; - $result = new ImageProcessingInstructions(); - $result->originalWidth = 150; - $result->originalHeight = 0; - $result->width = 150; - $result->height = 1350; + $result = new ImageProcessingInstructions( + width: 150, + height: 1350, + ); yield 'width from original image and explicitly given scales an image up' => [ 100, 900, @@ -136,11 +131,10 @@ final class ImageProcessingInstructionsTest extends UnitTestCase $result, ]; - $result = new ImageProcessingInstructions(); - $result->originalWidth = 150; - $result->originalHeight = 0; - $result->width = 100; - $result->height = 900; + $result = new ImageProcessingInstructions( + width: 100, + height: 900, + ); yield 'width from original image and explicitly given scales an image up but is disabled' => [ 100, 900, @@ -151,12 +145,11 @@ final class ImageProcessingInstructionsTest extends UnitTestCase $result, ]; - $result = new ImageProcessingInstructions(); - $result->originalWidth = 0; - $result->originalHeight = 0; - $result->width = 150; - $result->height = 900; - yield 'min width explicitly given scales an image up but is disabled resulting in do not keep aspect ratio' => [ + $result = new ImageProcessingInstructions( + width: 100, + height: 900, + ); + yield 'min width explicitly given scales an image up but is disabled' => [ 100, 900, '', @@ -166,11 +159,10 @@ final class ImageProcessingInstructionsTest extends UnitTestCase $result, ]; - $result = new ImageProcessingInstructions(); - $result->originalWidth = 50; - $result->originalHeight = 100; - $result->width = 0; - $result->height = 0; + $result = new ImageProcessingInstructions( + width: 0, + height: 0, + ); yield 'no orig image given monitors "origW"' => [ 0, 0, @@ -181,11 +173,10 @@ final class ImageProcessingInstructionsTest extends UnitTestCase $result, ]; - $result = new ImageProcessingInstructions(); - $result->originalWidth = 50; - $result->originalHeight = 800; - $result->width = 50; - $result->height = 450; + $result = new ImageProcessingInstructions( + width: 50, + height: 450, + ); yield 'Incoming instructions use "m" in width with given height' => [ 100, 900, @@ -196,11 +187,10 @@ final class ImageProcessingInstructionsTest extends UnitTestCase $result, ]; - $result = new ImageProcessingInstructions(); - $result->originalWidth = 50; - $result->originalHeight = 0; - $result->width = 50; - $result->height = 450; + $result = new ImageProcessingInstructions( + width: 50, + height: 450, + ); yield 'Incoming instructions use "m" in width without height' => [ 100, 900, @@ -211,13 +201,11 @@ final class ImageProcessingInstructionsTest extends UnitTestCase $result, ]; - $result = new ImageProcessingInstructions(); - $result->originalWidth = 50; - $result->originalHeight = 800; - $result->width = 89; - $result->height = 800; - $result->useCropScaling = true; - $result->cropArea = new Area(19.5, 0, 50, 800); + $result = new ImageProcessingInstructions( + width: 50, + height: 800, + cropArea: new Area(21.875, 0, 56.25 /* 900 / 800 * 50 */, 900), + ); yield 'Incoming instructions use "c" in width with given height' => [ 100, 900, @@ -228,13 +216,11 @@ final class ImageProcessingInstructionsTest extends UnitTestCase $result, ]; - $result = new ImageProcessingInstructions(); - $result->originalWidth = 50; - $result->originalHeight = 0; - $result->width = 50; - $result->height = 450; - $result->useCropScaling = true; - $result->cropArea = new Area(0, 225, 50, 450); + $result = new ImageProcessingInstructions( + width: 50, + height: 450, + cropArea: null, + ); yield 'Incoming instructions use "c" in width but without height' => [ 100, 900, @@ -245,13 +231,11 @@ final class ImageProcessingInstructionsTest extends UnitTestCase $result, ]; - $result = new ImageProcessingInstructions(); - $result->originalWidth = 50; - $result->originalHeight = 650; - $result->width = 72; - $result->height = 650; - $result->useCropScaling = true; - $result->cropArea = new Area(13.2, 0, 50, 650); + $result = new ImageProcessingInstructions( + width: 50, + height: 650, + cropArea: new Area(18.461538461538456, 0, 69.23076923076924 /* 900 / 650 * 50 */, 900), + ); yield 'Incoming instructions use "c" in width on both sides' => [ 100, 900, @@ -262,13 +246,11 @@ final class ImageProcessingInstructionsTest extends UnitTestCase $result, ]; - $result = new ImageProcessingInstructions(); - $result->originalWidth = 50; - $result->originalHeight = 800; - $result->width = 89; - $result->height = 800; - $result->useCropScaling = true; - $result->cropArea = new Area(23.4, 0, 50, 800); + $result = new ImageProcessingInstructions( + width: 50, + height: 800, + cropArea: new Area(26.25, 0, 56.25 /* 900 / 800 * 50 */, 900), + ); yield 'Incoming instructions use "c" in width on both sides with given height' => [ 100, 900, diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/ImageViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/ImageViewHelperTest.php index 8c9d5f6cc96bac0a9e5b4f8dcecd236862616fd1..11dd0e3dc60f6c6af1b2bbe7522d923fb35134e0 100644 --- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/ImageViewHelperTest.php +++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/ImageViewHelperTest.php @@ -198,22 +198,21 @@ final class ImageViewHelperTest extends FunctionalTestCase ]; yield 'inline-max width does not upscale' => [ '<f:image src="fileadmin/ImageViewHelperTest.jpg" width="500m" />', - '@^<img src="(fileadmin/_processed_/5/3/csm_ImageViewHelperTest_.*\.jpg)" width="500" height="375" alt="" />$@', - 500, - 375, + '@^<img src="(fileadmin/ImageViewHelperTest\.jpg)" width="400" height="300" alt="" />$@', + 400, + 300, ]; yield 'inline-max height does not upscale' => [ '<f:image src="fileadmin/ImageViewHelperTest.jpg" height="350m" />', - '@^<img src="(fileadmin/_processed_/5/3/csm_ImageViewHelperTest_.*\.jpg)" width="467" height="350" alt="" />$@', - 467, - 350, + '@^<img src="(fileadmin/ImageViewHelperTest\.jpg)" width="400" height="300" alt="" />$@', + 400, + 300, ]; - // would be 200x150, but image will be stretched (why!?) up to have a width of 250 yield 'min width' => [ '<f:image src="fileadmin/ImageViewHelperTest.jpg" height="150" minWidth="250" />', - '@^<img src="(fileadmin/_processed_/5/3/csm_ImageViewHelperTest_.*\.jpg)" width="250" height="150" alt="" />$@', + '@^<img src="(fileadmin/_processed_/5/3/csm_ImageViewHelperTest_.*\.jpg)" width="250" height="188" alt="" />$@', 250, - 150, + 188, ]; // would be 200x150, but image will be scaled down to have a width of 100 yield 'max width' => [ @@ -222,11 +221,10 @@ final class ImageViewHelperTest extends FunctionalTestCase 100, 75, ]; - // would be 200x150, but image will be stretched (why!?) up to have a height of 200 yield 'min height' => [ '<f:image src="fileadmin/ImageViewHelperTest.jpg" width="200" minHeight="200" />', - '@^<img src="(fileadmin/_processed_/5/3/csm_ImageViewHelperTest_.*\.jpg)" width="200" height="200" alt="" />$@', - 200, + '@^<img src="(fileadmin/_processed_/5/3/csm_ImageViewHelperTest_.*\.jpg)" width="267" height="200" alt="" />$@', + 267, 200, ]; // would be 200x150, but image will be scaled down to have a height of 75 diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/ImageViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/ImageViewHelperTest.php index 5cbaf0b79821731e184300e5a4a7397d56e5904f..e643a9901c37faad59dfd1618d258a68d7342fdb 100644 --- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/ImageViewHelperTest.php +++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/ImageViewHelperTest.php @@ -198,22 +198,21 @@ final class ImageViewHelperTest extends FunctionalTestCase ]; yield 'inline-max width does not upscale' => [ '<f:uri.image src="fileadmin/ImageViewHelperTest.jpg" width="500m" />', - '@^(fileadmin/_processed_/5/3/csm_ImageViewHelperTest_.*\.jpg)$@', - 500, - 375, + '@^(fileadmin/ImageViewHelperTest\.jpg)$@', + 400, + 300, ]; yield 'inline-max height does not upscale' => [ '<f:uri.image src="fileadmin/ImageViewHelperTest.jpg" height="350m" />', - '@^(fileadmin/_processed_/5/3/csm_ImageViewHelperTest_.*\.jpg)$@', - 467, - 350, + '@^(fileadmin/ImageViewHelperTest\.jpg)$@', + 400, + 300, ]; - // would be 200x150, but image will be stretched (why!?) up to have a width of 250 yield 'min width' => [ '<f:uri.image src="fileadmin/ImageViewHelperTest.jpg" height="150" minWidth="250" />', '@^(fileadmin/_processed_/5/3/csm_ImageViewHelperTest_.*\.jpg)$@', 250, - 150, + 188, ]; // would be 200x150, but image will be scaled down to have a width of 100 yield 'max width' => [ @@ -222,11 +221,10 @@ final class ImageViewHelperTest extends FunctionalTestCase 100, 75, ]; - // would be 200x150, but image will be stretched (why!?) up to have a height of 200 yield 'min height' => [ '<f:uri.image src="fileadmin/ImageViewHelperTest.jpg" width="200" minHeight="200" />', '@^(fileadmin/_processed_/5/3/csm_ImageViewHelperTest_.*\.jpg)$@', - 200, + 267, 200, ]; // would be 200x150, but image will be scaled down to have a height of 75 diff --git a/typo3/sysext/frontend/Tests/Functional/ContentObject/ContentObjectRendererTest.php b/typo3/sysext/frontend/Tests/Functional/ContentObject/ContentObjectRendererTest.php index 8224b81bb67cf1348159ed95f4e3e43ac884020d..55297841507b0d90fb3b080cf16e3fac1d28b552 100644 --- a/typo3/sysext/frontend/Tests/Functional/ContentObject/ContentObjectRendererTest.php +++ b/typo3/sysext/frontend/Tests/Functional/ContentObject/ContentObjectRendererTest.php @@ -1229,7 +1229,7 @@ And another one'; $result = $subject->getImgResource($fileReference, []); $expectedWidth = 512; - $expectedHeight = 341; + $expectedHeight = 342; self::assertEquals($expectedWidth, $result->getWidth()); self::assertEquals($expectedHeight, $result->getHeight()); diff --git a/typo3/sysext/seo/Tests/Functional/MetaTag/MetaTagGeneratorTest.php b/typo3/sysext/seo/Tests/Functional/MetaTag/MetaTagGeneratorTest.php index e487e79ee482daccc0c542ece6a4c9466bef7635..15b92a5639658bfeee19e44327c8bbacff041d9c 100644 --- a/typo3/sysext/seo/Tests/Functional/MetaTag/MetaTagGeneratorTest.php +++ b/typo3/sysext/seo/Tests/Functional/MetaTag/MetaTagGeneratorTest.php @@ -89,7 +89,8 @@ final class MetaTagGeneratorTest extends FunctionalTestCase yield 'social: 3000x600 enforced ratio (no up-scaling)' => [ true, ['width' => 3000, 'height' => 600], - ['width' => 1142, 'height' => 600], + // width = round(1200/630*600) + ['width' => 1143, 'height' => 600], ProcessedFile::class, ];