'uid_local', 'allowedExtensions' => null, // default: $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] 'cropVariants' => [ 'default' => [ 'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.crop_variant.default', 'allowedAspectRatios' => [ '16:9' => [ 'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.16_9', 'value' => 16 / 9, ], '3:2' => [ 'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.3_2', 'value' => 3 / 2, ], '4:3' => [ 'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.4_3', 'value' => 4 / 3, ], '1:1' => [ 'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.1_1', 'value' => 1.0, ], 'NaN' => [ 'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.free', 'value' => 0.0, ], ], 'excludeFromSync' => false, 'selectedRatio' => 'NaN', 'cropArea' => [ 'x' => 0.0, 'y' => 0.0, 'width' => 1.0, 'height' => 1.0, ], ], ], ]; /** * Default field wizards enabled for this element. * * @var array */ protected $defaultFieldWizard = [ 'localizationStateSelector' => [ 'renderType' => 'localizationStateSelector', ], 'otherLanguageThumbnails' => [ 'renderType' => 'otherLanguageThumbnails', 'after' => [ 'localizationStateSelector', ], ], 'defaultLanguageDifferences' => [ 'renderType' => 'defaultLanguageDifferences', 'after' => [ 'otherLanguageThumbnails', ], ], ]; public function __construct( private readonly BackendViewFactory $backendViewFactory, private readonly UriBuilder $uriBuilder, private readonly EventDispatcherInterface $eventDispatcher, private readonly ResourceFactory $resourceFactory, private readonly HashService $hashService, ) {} /** * This will render an imageManipulation field * * @return array As defined in initializeResultArray() of AbstractNode * @throws InvalidConfigurationException */ public function render(): array { $resultArray = $this->initializeResultArray(); $parameterArray = $this->data['parameterArray']; $config = $this->populateConfiguration($parameterArray['fieldConf']['config']); $file = $this->getFile($this->data['databaseRow'], $config['file_field']); if (!$file) { // Early return in case we do not find a file return $resultArray; } $config = $this->processConfiguration($config, $parameterArray['itemFormElValue'], $file); $fieldInformationResult = $this->renderFieldInformation(); $fieldInformationHtml = $fieldInformationResult['html']; $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false); $fieldControlResult = $this->renderFieldControl(); $fieldControlHtml = $fieldControlResult['html']; $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false); $fieldWizardResult = $this->renderFieldWizard(); $fieldWizardHtml = $fieldWizardResult['html']; $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false); $arguments = [ 'fieldInformation' => $fieldInformationHtml, 'fieldControl' => $fieldControlHtml, 'fieldWizard' => $fieldWizardHtml, 'isAllowedFileExtension' => in_array(strtolower($file->getExtension()), GeneralUtility::trimExplode(',', strtolower($config['allowedExtensions'])), true), 'image' => $file, 'formEngine' => [ 'field' => [ 'value' => $parameterArray['itemFormElValue'], 'name' => $parameterArray['itemFormElName'], ], 'validation' => '[]', ], 'config' => $config, 'wizardUri' => $this->getWizardUri(), 'wizardPayload' => json_encode($this->getWizardPayload($config['cropVariants'], $file)), 'previewUrl' => $this->eventDispatcher->dispatch( new ModifyImageManipulationPreviewUrlEvent($this->data['databaseRow'], $config, $file) )->getPreviewUrl(), ]; if ($arguments['isAllowedFileExtension']) { $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create( '@typo3/backend/image-manipulation.js' )->invoke('initializeTrigger'); $arguments['formEngine']['field']['id'] = StringUtility::getUniqueId('formengine-image-manipulation-'); if ($config['required'] ?? false) { $arguments['formEngine']['validation'] = $this->getValidationDataAsJsonString(['required' => true]); } } $view = $this->backendViewFactory->create($this->data['request']); $view->assignMultiple($arguments); $resultArray['html'] = $this->wrapWithFieldsetAndLegend($view->render('Form/ImageManipulationElement')); return $resultArray; } /** * Get file object * * @param string $fieldName * @return File|null */ protected function getFile(array $row, $fieldName) { $file = null; $fileUid = !empty($row[$fieldName]) ? $row[$fieldName] : null; if (is_array($fileUid) && isset($fileUid[0]['uid'])) { $fileUid = $fileUid[0]['uid']; } if (MathUtility::canBeInterpretedAsInteger($fileUid)) { try { $file = $this->resourceFactory->getFileObject($fileUid); } catch (FileDoesNotExistException|\InvalidArgumentException) { } } return $file; } /** * @return array * @throws InvalidConfigurationException */ protected function populateConfiguration(array $baseConfiguration) { $defaultConfig = self::$defaultConfig; // If ratios are set do not add default options if (isset($baseConfiguration['cropVariants'])) { unset($defaultConfig['cropVariants']); } $config = array_replace_recursive($defaultConfig, $baseConfiguration); if (!is_array($config['cropVariants'])) { throw new InvalidConfigurationException('Crop variants configuration must be an array', 1485377267); } $cropVariants = []; foreach ($config['cropVariants'] as $id => $cropVariant) { // Filter allowed aspect ratios $cropVariant['allowedAspectRatios'] = array_filter($cropVariant['allowedAspectRatios'] ?? [], static function (array $aspectRatio): bool { return !(bool)($aspectRatio['disabled'] ?? false); }); // Aspect ratios may not contain a "." character, see Ratio::__construct() // To match them again properly, same replacement is required here. $preparedAllowedAspectRatios = []; foreach ($cropVariant['allowedAspectRatios'] as $aspectRatio => $aspectRatioDefinition) { $preparedAllowedAspectRatios[Ratio::prepareAspectRatioId($aspectRatio)] = $aspectRatioDefinition; } $cropVariant['allowedAspectRatios'] = $preparedAllowedAspectRatios; // Ignore disabled crop variants if (!empty($cropVariant['disabled'])) { continue; } if (empty($cropVariant['allowedAspectRatios'])) { throw new InvalidConfigurationException('Crop variants configuration ' . $id . ' contains no allowed aspect ratios', 1620147893); } // Enforce a crop area (default is full image) if (empty($cropVariant['cropArea'])) { $cropVariant['cropArea'] = Area::createEmpty()->asArray(); } $cropVariants[$id] = $cropVariant; } $config['cropVariants'] = $cropVariants; // By default we allow all image extensions that can be handled by the GFX functionality $config['allowedExtensions'] ??= $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']; return $config; } /** * @return array * @throws InvalidConfigurationException */ protected function processConfiguration(array $config, string &$elementValue, File $file) { $cropVariantsAreEqual = $this->checkIfCropVariantsAreEqual($config['cropVariants']); $cropVariantCollection = CropVariantCollection::create($elementValue, $config['cropVariants']); if (empty($config['readOnly']) && !empty($file->getProperty('width'))) { $cropVariantCollection = $cropVariantCollection->applyRatioRestrictionToSelectedCropArea($file); $elementValue = (string)$cropVariantCollection; } $config['cropVariants'] = $cropVariantCollection->asArray(); $config['allowedExtensions'] = implode(', ', GeneralUtility::trimExplode(',', $config['allowedExtensions'], true)); $config['syncAvailable'] = $cropVariantsAreEqual; return $config; } protected function getWizardUri(): string { return (string)$this->uriBuilder->buildUriFromRoute($this->wizardRouteName); } protected function getWizardPayload(array $cropVariants, File $image): array { $uriArguments = []; $arguments = [ 'cropVariants' => $cropVariants, 'image' => $image->getUid(), ]; $uriArguments['arguments'] = json_encode($arguments); $uriArguments['signature'] = $this->hashService->hmac((string)$uriArguments['arguments'], $this->wizardRouteName); return $uriArguments; } protected function checkIfCropVariantsAreEqual(array $cropVariants): bool { $validVariants = array_filter( $cropVariants, static fn($variant) => ! ($variant['excludeFromSync'] ?? false) ); if (count($validVariants) <= 1) { return true; } $processedVariants = array_map(static function (array $variant) { // Remove the title for comparison, because it must be different and does not disturb the feature unset($variant['title']); $variant = ArrayUtility::sortByKeyRecursive($variant); return ArrayUtility::flatten($variant); }, $validVariants); $first = array_shift($processedVariants); return array_reduce( $processedVariants, static fn(bool $allEqualSoFar, array $currentVariant) => $allEqualSoFar && ($currentVariant === $first), true ); } }