Skip to content
Snippets Groups Projects
PictureViewHelper.php 14 KiB
Newer Older
<?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'
Georgi's avatar
Georgi committed
				|| $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);
		}
	}
}