diff --git a/Classes/Service/ImageDataProvider.php b/Classes/Service/ImageDataProvider.php new file mode 100644 index 0000000..1230a3c --- /dev/null +++ b/Classes/Service/ImageDataProvider.php @@ -0,0 +1,177 @@ +imageService = $imageService; + } + + public function getImageVariantData(FileReference $fileReference, string $variant) + { + $cropConfiguration = json_decode((string)$fileReference->getProperty('crop'), true); + // filter for all crop configurations that match the chosen image variant + $matchingCropConfiguration = array_filter($cropConfiguration, function ($key) use ($variant) { + return strpos($key, $variant . '__') === 0; + }, ARRAY_FILTER_USE_KEY); + $cropVariants = CropVariantCollection::create(json_encode($matchingCropConfiguration)); + $cropVariantIds = array_keys($matchingCropConfiguration); + unset($matchingCropConfiguration); + + $sources = []; + $pixelDensities = $this->getPixelDensitiesFromTypoScript(); + foreach ($cropVariantIds as $cropVariantId) { + $srcset = []; + $sizeConfiguration = $this->getSizeConfiguration($fileReference, $cropVariantId); + + foreach ($pixelDensities as $pixelDensity) { + $imageUri = $this->processImage( + $fileReference, + (int)round(($sizeConfiguration['width'] * $pixelDensity)), + (int)round(($sizeConfiguration['height'] * $pixelDensity)), + $cropVariants->getCropArea($cropVariantId) + ); + $srcset[] = $imageUri . ' ' . $pixelDensity . 'x'; + } + + $mediaQuery = $this->getMediaQueryFromSizeConfig($sizeConfiguration); + + $sources[] = [ + 'srcsets' => $srcset, + 'mediaQuery' => $mediaQuery + ]; + } + + $lastCropVariantId = end($cropVariantIds); + $sizeConfiguration = $this->getSizeConfiguration($fileReference, $lastCropVariantId); + $defaultImageUri = $imageUri = $this->processImage( + $fileReference, + (int)$sizeConfiguration['width'], + (int)$sizeConfiguration['height'], + $cropVariants->getCropArea($lastCropVariantId) + ); + + return [ + 'sources' => $sources, + 'fallbackImageSrc' => $defaultImageUri, + ]; + } + + protected function processImage( + FileReference $fileReference, + int $width, + int $height, + $cropArea = null + ): string { + if ($cropArea instanceof Area && !$cropArea->isEmpty()) { + $cropArea = $cropArea->makeAbsoluteBasedOnFile($fileReference); + } else { + $cropArea = null; + } + $processingInstructions = [ + 'width' => $width, + 'height' => $height, + 'crop' => $cropArea, + ]; + $processedImage = $this->imageService->applyProcessingInstructions($fileReference, $processingInstructions); + return $this->imageService->getImageUri($processedImage); + } + + protected function getSizeConfiguration(FileReference $fileReference, string $cropVariantId): array + { + list($variantIdentifier, $sizeIdentifier) = explode('__', $cropVariantId); + return $this->getMelonImagesConfigForFileReference($fileReference)['variants'][$variantIdentifier]['sizes'][$sizeIdentifier]; + } + + protected function getBreakpointsFromTypoScript(): array + { + return (array)$this->getTypoScriptSettings()['breakpoints']; + } + + protected function getPixelDensitiesFromTypoScript(): array + { + return GeneralUtility::trimExplode(',', (string)$this->getTypoScriptSettings()['pixelDensities'] ?? '1'); + } + + protected function getTypoScriptSettings(): array + { + static $typoScriptSettings; + if (!is_array($typoScriptSettings)) { + $objectManager = GeneralUtility::makeInstance(ObjectManager::class); + $configurationManager = $objectManager->get(ConfigurationManagerInterface::class); + $typoscript = $configurationManager->getConfiguration( + ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT + ); + $typoScriptSettings = GeneralUtility::makeInstance(TypoScriptService::class)->convertTypoScriptArrayToPlainArray( + $typoscript['package.']['Smichaelsen\MelonImages.'] ?? [] + ); + } + return $typoScriptSettings; + } + + protected function getMediaQueryFromSizeConfig(array $sizeConfiguration): string + { + $breakpointsConfig = $this->getBreakpointsFromTypoScript(); + $breakpoints = []; + foreach (GeneralUtility::trimExplode(',', $sizeConfiguration['breakpoints']) as $breakpointName) { + $constraints = []; + if ($breakpointsConfig[$breakpointName]['from']) { + $constraints[] = '(min-width: ' . $breakpointsConfig[$breakpointName]['from'] . 'px)'; + } + if ($breakpointsConfig[$breakpointName]['to']) { + $constraints[] = '(max-width: ' . $breakpointsConfig[$breakpointName]['to'] . 'px)'; + } + if (empty($constraints)) { + continue; + } + $breakpoints[] = implode(' and ', $constraints); + } + if (empty($breakpoints)) { + return ''; + } + return implode(', ', $breakpoints); + } + + protected function getMelonImagesConfigForFileReference(FileReference $fileReference): array + { + static $melonConfigPerFileReferenceUid = []; + if (!isset($melonConfigPerFileReferenceUid[$fileReference->getUid()])) { + $melonConfigPerFileReferenceUid[$fileReference->getUid()] = (function () use ($fileReference) { + $typoScriptSettings = $this->getTypoScriptSettings(); + $table = $fileReference->getProperty('tablenames'); + $tableSettings = $typoScriptSettings['croppingConfiguration'][$table]; + unset($typoScriptSettings); + $fieldName = $fileReference->getProperty('fieldname'); + $typeField = $GLOBALS['TCA'][$table]['ctrl']['type']; + if ($typeField) { + $record = BackendUtility::getRecord($table, $fileReference->getProperty('uid_foreign'), $typeField); + $type = $record[$typeField]; + if ($tableSettings[$type]) { + return $tableSettings[$type][$fieldName]; + } + } + return $tableSettings['_all'][$fieldName]; + })(); + } + return $melonConfigPerFileReferenceUid[$fileReference->getUid()]; + } +} diff --git a/Classes/ViewHelpers/CroppedImageUriViewHelper.php b/Classes/ViewHelpers/CroppedImageUriViewHelper.php new file mode 100644 index 0000000..68e993f --- /dev/null +++ b/Classes/ViewHelpers/CroppedImageUriViewHelper.php @@ -0,0 +1,47 @@ +imageDataProvider = $imageDataProvider; + } + + public function initializeArguments() + { + parent::initializeArguments(); + $this->registerArgument('fileReference', 'mixed', 'File reference to render', true); + $this->registerArgument('variant', 'string', 'Name of the image variant to use', true); + } + + public function render(): string + { + $fileReference = $this->arguments['fileReference']; + if ($fileReference instanceof ExtbaseFileReferenceModel) { + $fileReference = $fileReference->getOriginalResource(); + } + if (!$fileReference instanceof FileReference) { + return ''; + } + + $variant = $this->arguments['variant']; + $variantData = $this->imageDataProvider->getImageVariantData($fileReference, $variant); + + return $variantData['fallbackImageSrc']; + } + +} diff --git a/Classes/ViewHelpers/CroppedImageViewHelper.php b/Classes/ViewHelpers/CroppedImageViewHelper.php new file mode 100644 index 0000000..18ebcfb --- /dev/null +++ b/Classes/ViewHelpers/CroppedImageViewHelper.php @@ -0,0 +1,55 @@ +imageDataProvider = $imageDataProvider; + } + + public function initializeArguments() + { + parent::initializeArguments(); + $this->registerUniversalTagAttributes(); + $this->registerArgument('fileReference', 'mixed', 'File reference to render', true); + $this->registerArgument('variant', 'string', 'Name of the image variant to use', true); + } + + public function render(): string + { + $fileReference = $this->arguments['fileReference']; + if ($fileReference instanceof ExtbaseFileReferenceModel) { + $fileReference = $fileReference->getOriginalResource(); + } + if (!$fileReference instanceof FileReference) { + return ''; + } + + $variant = $this->arguments['variant']; + $variantData = $this->imageDataProvider->getImageVariantData($fileReference, $variant); + + $this->tag->addAttribute('src', $variantData['fallbackImageSrc']); + $this->tag->addAttribute('alt', (string)$fileReference->getAlternative()); + if ($fileReference->getTitle()) { + $this->tag->addAttribute('title', (string)$fileReference->getTitle()); + } + + return $this->tag->render(); + } +} diff --git a/Classes/ViewHelpers/ResponsivePictureViewHelper.php b/Classes/ViewHelpers/ResponsivePictureViewHelper.php index d5a747e..d2e0150 100644 --- a/Classes/ViewHelpers/ResponsivePictureViewHelper.php +++ b/Classes/ViewHelpers/ResponsivePictureViewHelper.php @@ -2,16 +2,9 @@ declare(strict_types=1); namespace Smichaelsen\MelonImages\ViewHelpers; -use TYPO3\CMS\Backend\Utility\BackendUtility; -use TYPO3\CMS\Core\Imaging\ImageManipulation\Area; -use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection; +use Smichaelsen\MelonImages\Service\ImageDataProvider; use TYPO3\CMS\Core\Resource\FileReference; -use TYPO3\CMS\Core\TypoScript\TypoScriptService; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; use TYPO3\CMS\Extbase\Domain\Model\FileReference as ExtbaseFileReferenceModel; -use TYPO3\CMS\Extbase\Object\ObjectManager; -use TYPO3\CMS\Extbase\Service\ImageService; use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper; class ResponsivePictureViewHelper extends AbstractTagBasedViewHelper @@ -19,13 +12,13 @@ class ResponsivePictureViewHelper extends AbstractTagBasedViewHelper protected $tagName = 'picture'; /** - * @var ImageService + * @var ImageDataProvider */ - protected $imageService; + protected $imageDataProvider; - public function injectImageService(ImageService $imageService) + public function injectImageDataProvider(ImageDataProvider $imageDataProvider) { - $this->imageService = $imageService; + $this->imageDataProvider = $imageDataProvider; } public function initializeArguments() @@ -47,155 +40,26 @@ public function render(): string } $variant = $this->arguments['variant']; + $variantData = $this->imageDataProvider->getImageVariantData($fileReference, $variant); - $cropConfiguration = json_decode((string)$fileReference->getProperty('crop'), true); - // filter for all crop configurations that match the chosen image variant - $matchingCropConfiguration = array_filter($cropConfiguration, function ($key) use ($variant) { - return strpos($key, $variant . '__') === 0; - }, ARRAY_FILTER_USE_KEY); - $cropVariants = CropVariantCollection::create(json_encode($matchingCropConfiguration)); - $cropVariantIds = array_keys($matchingCropConfiguration); - unset($matchingCropConfiguration); - - $sourceMarkups = []; - $pixelDensities = $this->getPixelDensitiesFromTypoScript(); - foreach ($cropVariantIds as $cropVariantId) { - $srcset = []; - $sizeConfiguration = $this->getSizeConfiguration($fileReference, $cropVariantId); - - foreach ($pixelDensities as $pixelDensity) { - $imageUri = $this->processImage( - $fileReference, - (int)round(($sizeConfiguration['width'] * $pixelDensity)), - (int)round(($sizeConfiguration['height'] * $pixelDensity)), - $cropVariants->getCropArea($cropVariantId) - ); - $srcset[] = $imageUri . ' ' . $pixelDensity . 'x'; - } - - $mediaQuery = $this->getMediaQueryFromSizeConfig($sizeConfiguration); + $tagContent = ''; + foreach ($variantData['sources'] as $source) { + $mediaQuery = $source['mediaQuery']; if (!empty($mediaQuery)) { $mediaQuery = ' media="' . $mediaQuery . '"'; } - $sourceMarkups[] = ''; + $tagContent .= '' . "\n"; } - // the last crop variant is used as fallback - $lastCropVariantId = end($cropVariantIds); - $sizeConfiguration = $this->getSizeConfiguration($fileReference, $lastCropVariantId); - $defaultImageUri = $imageUri = $this->processImage( - $fileReference, - (int)$sizeConfiguration['width'], - (int)$sizeConfiguration['height'] - ); - $imgTitle = $fileReference->getTitle() ? 'title="' . htmlspecialchars($fileReference->getTitle()) . '"' : ''; - $sourceMarkups[] = sprintf( + $title = $fileReference->getTitle() ? 'title="' . htmlspecialchars($fileReference->getTitle()) . '"' : ''; + $tagContent .= sprintf( '%s', - $defaultImageUri, - $fileReference->getAlternative(), - htmlspecialchars($imgTitle) + $variantData['fallbackImageSrc'], + htmlspecialchars((string)$fileReference->getAlternative()), + $title ); - $this->tag->setContent(implode("\n", $sourceMarkups)); + $this->tag->setContent($tagContent); return $this->tag->render(); } - - protected function processImage( - FileReference $fileReference, - int $width, - int $height, - $cropArea = null - ): string { - if ($cropArea instanceof Area && !$cropArea->isEmpty()) { - $cropArea = $cropArea->makeAbsoluteBasedOnFile($fileReference); - } else { - $cropArea = null; - } - $processingInstructions = [ - 'width' => $width, - 'height' => $height, - 'crop' => $cropArea, - ]; - $processedImage = $this->imageService->applyProcessingInstructions($fileReference, $processingInstructions); - return $this->imageService->getImageUri($processedImage, $this->arguments['absolute']); - } - - protected function getBreakpointsFromTypoScript(): array - { - return (array)$this->getTypoScriptSettings()['breakpoints']; - } - - protected function getPixelDensitiesFromTypoScript(): array - { - return GeneralUtility::trimExplode(',', (string)$this->getTypoScriptSettings()['pixelDensities'] ?? '1'); - } - - protected function getTypoScriptSettings(): array - { - static $typoScriptSettings; - if (!is_array($typoScriptSettings)) { - $objectManager = GeneralUtility::makeInstance(ObjectManager::class); - $configurationManager = $objectManager->get(ConfigurationManagerInterface::class); - $typoscript = $configurationManager->getConfiguration( - ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT - ); - $typoScriptSettings = GeneralUtility::makeInstance(TypoScriptService::class)->convertTypoScriptArrayToPlainArray( - $typoscript['package.']['Smichaelsen\MelonImages.'] ?? [] - ); - } - return $typoScriptSettings; - } - - protected function getSizeConfiguration(FileReference $fileReference, string $cropVariantId): array - { - list($variantIdentifier, $sizeIdentifier) = explode('__', $cropVariantId); - return $this->getMelonImagesConfigForFileReference($fileReference)['variants'][$variantIdentifier]['sizes'][$sizeIdentifier]; - } - - protected function getMediaQueryFromSizeConfig(array $sizeConfiguration): string - { - $breakpointsConfig = $this->getBreakpointsFromTypoScript(); - $breakpoints = []; - foreach (GeneralUtility::trimExplode(',', $sizeConfiguration['breakpoints']) as $breakpointName) { - $constraints = []; - if ($breakpointsConfig[$breakpointName]['from']) { - $constraints[] = '(min-width: ' . $breakpointsConfig[$breakpointName]['from'] . 'px)'; - } - if ($breakpointsConfig[$breakpointName]['to']) { - $constraints[] = '(max-width: ' . $breakpointsConfig[$breakpointName]['to'] . 'px)'; - } - if (empty($constraints)) { - continue; - } - $breakpoints[] = implode(' and ', $constraints); - } - if (empty($breakpoints)) { - return ''; - } - return implode(', ', $breakpoints); - } - - protected function getMelonImagesConfigForFileReference(FileReference $fileReference): array - { - static $melonConfigPerFileReferenceUid = []; - if (!isset($melonConfigPerFileReferenceUid[$fileReference->getUid()])) { - $melonConfigPerFileReferenceUid[$fileReference->getUid()] = (function () use ($fileReference) { - $typoScriptSettings = $this->getTypoScriptSettings(); - $table = $fileReference->getProperty('tablenames'); - $tableSettings = $typoScriptSettings['croppingConfiguration'][$table]; - unset($typoScriptSettings); - $fieldName = $fileReference->getProperty('fieldname'); - $typeField = $GLOBALS['TCA'][$table]['ctrl']['type']; - if ($typeField) { - $record = BackendUtility::getRecord($table, $fileReference->getProperty('uid_foreign'), $typeField); - $type = $record[$typeField]; - if ($tableSettings[$type]) { - return $tableSettings[$type][$fieldName]; - } - } - return $tableSettings['_all'][$fieldName]; - })(); - } - return $melonConfigPerFileReferenceUid[$fileReference->getUid()]; - } } diff --git a/Readme.md b/Readme.md index 7dd02e9..95cd7d3 100644 --- a/Readme.md +++ b/Readme.md @@ -84,6 +84,16 @@ package.Smichaelsen\MelonImages { } } } + + square { + title = Square for Open Graph + sizes { + all { + width = 512 + height = 512 + } + } + } } } } @@ -95,6 +105,8 @@ package.Smichaelsen\MelonImages { ## Rendering +### Responsive Image + To render the reponsive image with the correct cropping use the provided ViewHelper: ``` @@ -119,3 +131,31 @@ The rendering (with the above TypoScript config) looks something like this: ``` + +### Cropped Image and cropped image url + +The rendering as responsive `` tag is not always desirable. You can also just apply the cropping and ignore the sizes and breakpoints. + +``` + + + + + + + + +``` + +The rendering looks something like this: + +``` + + + +```