<?php /*************************************************************** * Copyright notice * * (c) sgalinski Internet Services (https://www.sgalinski.de) * * All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is * free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * The GNU General Public License can be found at * http://www.gnu.org/copyleft/gpl.html. * * This script is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ namespace SGalinski\SgYoutube\ViewHelpers; use Closure; use InvalidArgumentException; use RuntimeException; use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException; use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException; use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Http\ApplicationType; use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection; use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Resource\FileInterface; use TYPO3\CMS\Core\Resource\FileReference; use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Configuration\ConfigurationManager; use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; use TYPO3\CMS\Extbase\Configuration\Exception\InvalidConfigurationTypeException; use TYPO3\CMS\Extbase\Service\ImageService; use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper; use TYPO3Fluid\Fluid\Core\ViewHelper\Exception; use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithContentArgumentAndRenderStatic; use UnexpectedValueException; /** * Helps with generating the picture-element according to the websites setup * * @see \FluidTYPO3\Vhs\ViewHelpers\Media\SourceViewHelper for inspiration * @link https://docs.typo3.org/m/typo3/reference-typoscript/main/en-us/Functions/Imgresource.html#width */ class PictureViewHelper extends AbstractViewHelper { use CompileWithContentArgumentAndRenderStatic; /** @var bool */ protected $escapeOutput = FALSE; /** @var ImageService */ protected static ImageService $imageService; /** @var ResourceFactory */ protected static ResourceFactory $resourceFactory; /** @var array */ protected static array $responsiveSettings = []; /** * Register the ViewHelper arguments */ public function initializeArguments(): void { $this->registerArgument( 'image', 'mixed', 'The reference of the image or a string like 1:/someimage.png with treatIdAsReference set', TRUE ); $this->registerArgument('class', 'string', 'The class attribute'); $this->registerArgument('pictureClass', 'string', 'The class attribute for the picture tag'); $this->registerArgument('id', 'string', 'The id attribute'); $this->registerArgument('alt', 'string', 'The alt attribute'); $this->registerArgument('title', 'string', 'The title attribute'); $this->registerArgument( 'width', 'string', 'Width of the image. This can be a numeric value representing the fixed width of the image in ' . 'pixels. But you can also perform simple calculations by adding "m" or "c" to the value. See ' . 'https://docs.typo3.org/m/typo3/reference-typoscript/main/en-us/Functions/Imgresource.html#width for ' . 'possible options.' ); $this->registerArgument( 'height', 'string', 'Height of the image. This can be a numeric value representing the fixed height of the image in ' . 'pixels. But you can also perform simple calculations by adding "m" or "c" to the value. See ' . 'https://docs.typo3.org/m/typo3/reference-typoscript/main/en-us/Functions/Imgresource.html#width for ' . 'possible options.' ); $this->registerArgument( 'responsiveHeights', 'array', 'Array of heights, see height description and ' . '/web/typo3conf/ext/project_base/Configuration/TypoScript/Configurations/Picture/Setup.typoscript for ' . 'the available keys' ); $this->registerArgument( 'responsiveImages', 'array', 'Array of alternative images to use per breakpoint, see ' . '/web/typo3conf/ext/project_base/Configuration/TypoScript/Configurations/Picture/Setup.typoscript for ' . 'the available keys' ); $this->registerArgument( 'maxWidth', 'integer', 'Maximum Width of the image. (no up-scaling)' ); $this->registerArgument( 'maxHeight', 'integer', 'Maximum Height of the image. (no up-scaling)' ); $this->registerArgument('minWidth', 'integer', 'Minimum Width of the image.'); $this->registerArgument('minHeight', 'integer', 'Minimum Height of the image.'); $this->registerArgument( 'treatIdAsReference', 'bool', 'Treat ID as Reference', FALSE, FALSE ); $this->registerArgument( 'disableWebp', 'bool', 'Disable webp converter', FALSE, FALSE ); $this->registerArgument( 'loadingDirective', 'string', 'Set the image loading to lazy, eager or none to let the browser decide', FALSE, 'lazy' ); } /** * Escapes special characters with their escaped counterparts as needed using PHPs htmlentities() function. * * @param array $arguments * @param Closure $renderChildrenClosure * @param RenderingContextInterface $renderingContext * @return string * @throws InvalidConfigurationTypeException * @throws ExtensionConfigurationExtensionNotConfiguredException * @throws ExtensionConfigurationPathDoesNotExistException */ public static function renderStatic( array $arguments, Closure $renderChildrenClosure, RenderingContextInterface $renderingContext ): string { /** @var FileReference|string $image */ $image = $arguments['image']; if (is_string($image)) { $image = str_replace('/fileadmin', '1:', $image); } // Also hide images which would be shown in the TYPO3 Backend without this check if ( $image instanceof FileReference && (int) $image->getReferenceProperties()['hidden'] === 1 && ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend() ) { return ''; } if (!isset(self::$imageService)) { self::$imageService = GeneralUtility::makeInstance(ImageService::class); $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class); self::$responsiveSettings = $configurationManager->getConfiguration( ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT )['plugin.']['tx_project_base.']['settings.']['responsiveImages.']; } $width = $arguments['width']; $maxWidth = (int) $arguments['maxWidth']; $minWidth = (int) $arguments['minWidth']; $height = $arguments['height']; $responsiveHeights = $arguments['responsiveHeights'] ?? []; $responsiveImages = $arguments['responsiveImages'] ?? []; $maxHeight = (int) $arguments['maxHeight']; $minHeight = (int) $arguments['minHeight']; $class = $arguments['class']; $pictureClass = $arguments['pictureClass'] ?? ''; $id = $arguments['id']; $title = $arguments['title'] ?? ''; $alt = $arguments['alt'] ?? ''; $disableWebp = $arguments['disableWebp'] ?? ''; try { if ($arguments['treatIdAsReference']) { if ($image instanceof FileReference) { /** @var FileReference $image */ $image = self::$imageService->getImage($image->getUid(), NULL, $arguments['treatIdAsReference']); } else { // Hidden feature: the id used for treatIdAsReference can be an int or a string (e.g. "1:/someimage.png") $image = self::$imageService->getImage($image, NULL, $arguments['treatIdAsReference']); } } else { /** @var FileReference $image */ $image = self::$imageService->getImage('', $image, $arguments['treatIdAsReference']); } } catch (\Exception $exception) { return ''; } $originalFile = $image; if (!($originalFile instanceof File) && method_exists($image, 'getOriginalFile')) { $originalFile = $image->getOriginalFile(); } if (!$originalFile) { return ''; } // Explanation: https://www.mediaevent.de/xhtml/picture.html // Also: We need to generate the other sources even if the image is small enough as we don't know if there // are editor changes to the crop variation. $sources = ''; $isNoSvg = !str_contains($originalFile->getProperties()['mime_type'], 'svg'); $sizes = [ 'width' => $width, 'height' => $height, 'minWidth' => $minWidth, 'minHeight' => $minHeight, 'maxWidth' => $maxWidth, 'maxHeight' => $maxHeight ]; $fileExtension = $originalFile->getProperty('extension'); $convertToWebp = !$disableWebp && ( $fileExtension === 'png' || $fileExtension === 'jpg' || $fileExtension === 'jpeg' || $fileExtension === 'webp' ); $imageUrl = self::getImage($image, $sizes, 'default', $convertToWebp); // Don't add a scaled version for SVG's. This is not necessary at all $determinedWidth = (int) $width; $determinedHeight = (int) $height; if ($isNoSvg && is_array(self::$responsiveSettings['breakpoints.'])) { $sorted = []; foreach (self::$responsiveSettings['breakpoints.'] as $sizeKey => $value) { $sorted[$sizeKey] = (int) self::$responsiveSettings['breakpoints.'][$sizeKey]['imageSize']; } asort($sorted); foreach ($sorted as $sizeKey => $breakpoint) { $imageSizeMaxWidth = (int) self::$responsiveSettings['breakpoints.'][$sizeKey]['imageSize']; if ((int) $width > $imageSizeMaxWidth) { $sizes = ['width' => $imageSizeMaxWidth]; $sizeKeyWithoutDot = str_replace('.', '', $sizeKey); if (is_array($responsiveHeights) && isset($responsiveHeights[$sizeKeyWithoutDot])) { $responsiveHeight = $responsiveHeights[$sizeKeyWithoutDot]; if ($responsiveHeight) { $sizes['height'] = $responsiveHeight; } } $responsiveImage = $image; if (is_array($responsiveImages) && isset($responsiveImages[$sizeKeyWithoutDot])) { $responsiveImage = $responsiveImages[$sizeKeyWithoutDot]; } $sourceImageUrl = self::getImage($responsiveImage, $sizes, $sizeKeyWithoutDot, $convertToWebp); $sources .= '<source media="(max-width: ' . $imageSizeMaxWidth . 'px)" srcset="' . $sourceImageUrl . '" />' . "\n"; } } if (is_file(Environment::getPublicPath() . '/' . $imageUrl)) { [$determinedWidth, $determinedHeight] = getimagesize( Environment::getPublicPath() . '/' . $imageUrl ); } } $idAsAttribute = $id ? ' id="' . $id . '"' : ''; $widthAsAttribute = $determinedWidth ? ' width="' . $determinedWidth . '"' : ''; $heightAsAttribute = $determinedHeight ? ' height="' . $determinedHeight . '"' : ''; $class .= ($isNoSvg ? ' image-no-svg' : ''); $alternative = $alt; if ($image instanceof FileReference) { if (!$alt) { $alternative = $image->hasProperty('alternative') ? $image->getAlternative() : ''; } if (!$title) { $title = $image->hasProperty('title') ? $image->getTitle() : ''; } } else { if (!$alt) { $alternative = $originalFile->getProperties()['alternative'] ?? ''; } if (!$title) { $title = $originalFile->getProperties()['title'] ?? ''; } } $titleAsAttribute = $title ? ' title="' . htmlspecialchars($title) . '"' : ''; $loadingDirective = ''; if (isset($arguments['loadingDirective']) && $arguments['loadingDirective'] !== 'none') { $arguments['loadingDirective'] = $arguments['loadingDirective'] !== '' ? $arguments['loadingDirective'] : 'lazy'; $loadingDirective = ' loading="' . $arguments['loadingDirective'] . '"'; } return '<picture' . ($pictureClass ? ' class="' . $pictureClass . '"' : '') . '>' . $sources . '<img ' . $idAsAttribute . ' class="image-embed-item ' . $class . '"' . $widthAsAttribute . $heightAsAttribute . $titleAsAttribute . ' alt="' . htmlspecialchars($alternative) . '" src="' . $imageUrl . '"' . $loadingDirective . ' />' . '</picture>'; } /** * Generates an image based on the FileReference * * @param FileInterface $image * @param array $sizes array consisting with one or more of width, height, minWidth, maxWidth, minHeight, maxHeight * @param string $cropVariant * @param bool $convertToWebp * @return string */ protected static function getImage( FileInterface $image, array $sizes = [], string $cropVariant = 'default', bool $convertToWebp = TRUE ): string { try { $cropString = ''; if ($image->hasProperty('crop') && $image->getProperty('crop')) { $cropString = $image->getProperty('crop'); } $cropVariantCollection = CropVariantCollection::create($cropString); $cropArea = $cropVariantCollection->getCropArea($cropVariant);# $allowedKeys = ['width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight']; $startingProcessingInstructions = []; foreach ($sizes as $key => $value) { if (in_array($key, $allowedKeys, TRUE)) { $startingProcessingInstructions[$key] = $value; } } $processingInstructions = array_merge( $startingProcessingInstructions, ['crop' => $cropArea->isEmpty() ? NULL : $cropArea->makeAbsoluteBasedOnFile($image)] ); if ($convertToWebp) { $processingInstructions['fileExtension'] = 'webp'; } $processedImage = self::$imageService->applyProcessingInstructions($image, $processingInstructions); return self::$imageService->getImageUri($processedImage) ?? ''; } catch (UnexpectedValueException $e) { // thrown if a file has been replaced with a folder throw new Exception($e->getMessage(), 1509741908, $e); } catch (RuntimeException $e) { // RuntimeException thrown if a file is outside a storage throw new Exception($e->getMessage(), 1509741909, $e); } catch (InvalidArgumentException $e) { // thrown if file storage does not exist throw new Exception($e->getMessage(), 1509741910, $e); } } }