vendor/pimcore/pimcore/models/Asset/Image/Thumbnail.php line 47

Open in your IDE?
  1. <?php
  2. /**
  3.  * Pimcore
  4.  *
  5.  * This source file is available under two different licenses:
  6.  * - GNU General Public License version 3 (GPLv3)
  7.  * - Pimcore Commercial License (PCL)
  8.  * Full copyright and license information is available in
  9.  * LICENSE.md which is distributed with this source code.
  10.  *
  11.  *  @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  12.  *  @license    http://www.pimcore.org/license     GPLv3 and PCL
  13.  */
  14. namespace Pimcore\Model\Asset\Image;
  15. use Pimcore\Event\AssetEvents;
  16. use Pimcore\Event\FrontendEvents;
  17. use Pimcore\Logger;
  18. use Pimcore\Model\Asset;
  19. use Pimcore\Model\Asset\Image;
  20. use Pimcore\Model\Asset\Image\Thumbnail\Config;
  21. use Pimcore\Model\Asset\Thumbnail\ImageThumbnailTrait;
  22. use Pimcore\Model\Exception\NotFoundException;
  23. use Pimcore\Model\Exception\ThumbnailFormatNotSupportedException;
  24. use Pimcore\Model\Exception\ThumbnailMaxScalingFactorException;
  25. use Pimcore\Tool;
  26. use Symfony\Component\EventDispatcher\GenericEvent;
  27. final class Thumbnail
  28. {
  29.     use ImageThumbnailTrait;
  30.     /**
  31.      * @internal
  32.      *
  33.      * @var bool[]
  34.      */
  35.     protected static $hasListenersCache = [];
  36.     /**
  37.      * @param Image $asset
  38.      * @param string|array|Thumbnail\Config|null $config
  39.      * @param bool $deferred
  40.      */
  41.     public function __construct($asset$config null$deferred true)
  42.     {
  43.         $this->asset $asset;
  44.         $this->deferred $deferred;
  45.         $this->config $this->createConfig($config);
  46.     }
  47.     /**
  48.      * TODO: Pimcore 11: Change method signature to getPath($args = [])
  49.      *
  50.      * @param mixed $args,...
  51.      *
  52.      * @return string
  53.      */
  54.     public function getPath(...$args)
  55.     {
  56.         // TODO: Pimcore 11: remove calling the covertArgsBcLayer() method
  57.         $args $this->convertArgsBcLayer($args);
  58.         // set defaults
  59.         $deferredAllowed $args['deferredAllowed'] ?? true;
  60.         $cacheBuster $args['cacheBuster'] ?? false;
  61.         $frontend $args['frontend'] ?? \Pimcore\Tool::isFrontend();
  62.         $pathReference null;
  63.         if ($this->getConfig()) {
  64.             if ($this->useOriginalFile($this->asset->getFilename()) && $this->getConfig()->isSvgTargetFormatPossible()) {
  65.                 // we still generate the raster image, to get the final size of the thumbnail
  66.                 // we use getRealFullPath() here, to avoid double encoding (getFullPath() returns already encoded path)
  67.                 $pathReference = [
  68.                     'src' => $this->asset->getRealFullPath(),
  69.                     'type' => 'asset',
  70.                 ];
  71.             }
  72.         }
  73.         if (!$pathReference) {
  74.             $pathReference $this->getPathReference($deferredAllowed);
  75.         }
  76.         $path $this->convertToWebPath($pathReference$frontend);
  77.         if ($cacheBuster) {
  78.             $path $this->addCacheBuster($path, ['cacheBuster' => true], $this->getAsset());
  79.         }
  80.         if ($this->hasListeners(FrontendEvents::ASSET_IMAGE_THUMBNAIL)) {
  81.             $event = new GenericEvent($this, [
  82.                 'pathReference' => $pathReference,
  83.                 'frontendPath' => $path,
  84.             ]);
  85.             \Pimcore::getEventDispatcher()->dispatch($eventFrontendEvents::ASSET_IMAGE_THUMBNAIL);
  86.             $path $event->getArgument('frontendPath');
  87.         }
  88.         return $path;
  89.     }
  90.     /**
  91.      * @param string $eventName
  92.      *
  93.      * @return bool
  94.      */
  95.     protected function hasListeners(string $eventName): bool
  96.     {
  97.         if (!isset(self::$hasListenersCache[$eventName])) {
  98.             self::$hasListenersCache[$eventName] = \Pimcore::getEventDispatcher()->hasListeners($eventName);
  99.         }
  100.         return self::$hasListenersCache[$eventName];
  101.     }
  102.     /**
  103.      * @param string $filename
  104.      *
  105.      * @return bool
  106.      */
  107.     protected function useOriginalFile($filename)
  108.     {
  109.         if ($this->getConfig()) {
  110.             if (!$this->getConfig()->isRasterizeSVG() && preg_match("@\.svgz?$@"$filename)) {
  111.                 return true;
  112.             }
  113.         }
  114.         return false;
  115.     }
  116.     /**
  117.      * @throws ThumbnailFormatNotSupportedException
  118.      * @throws ThumbnailMaxScalingFactorException
  119.      *
  120.      * @internal
  121.      *
  122.      * @param bool $deferredAllowed
  123.      */
  124.     public function generate($deferredAllowed true)
  125.     {
  126.         $this->validate();
  127.         $deferred false;
  128.         $generated false;
  129.         if ($this->asset && empty($this->pathReference)) {
  130.             // if no correct thumbnail config is given use the original image as thumbnail
  131.             if (!$this->config) {
  132.                 $this->pathReference = [
  133.                     'type' => 'asset',
  134.                     'src' => $this->asset->getRealFullPath(),
  135.                 ];
  136.             } else {
  137.                 try {
  138.                     $deferred $deferredAllowed && $this->deferred;
  139.                     $this->pathReference Thumbnail\Processor::process($this->asset$this->confignull$deferred$generated);
  140.                 } catch (\Exception $e) {
  141.                     Logger::error("Couldn't create thumbnail of image " $this->asset->getRealFullPath() . ': ' $e);
  142.                 }
  143.             }
  144.         }
  145.         if (empty($this->pathReference)) {
  146.             $this->pathReference = [
  147.                 'type' => 'error',
  148.                 'src' => '/bundles/pimcoreadmin/img/filetype-not-supported.svg',
  149.             ];
  150.         }
  151.         if ($this->hasListeners(AssetEvents::IMAGE_THUMBNAIL)) {
  152.             $event = new GenericEvent($this, [
  153.                 'deferred' => $deferred,
  154.                 'generated' => $generated,
  155.             ]);
  156.             \Pimcore::getEventDispatcher()->dispatch($eventAssetEvents::IMAGE_THUMBNAIL);
  157.         }
  158.     }
  159.     /**
  160.      * @return string Public path to thumbnail image.
  161.      */
  162.     public function __toString()
  163.     {
  164.         return $this->getPath();
  165.     }
  166.     /**
  167.      * @param string $path
  168.      * @param array $options
  169.      * @param Asset $asset
  170.      *
  171.      * @return string
  172.      */
  173.     private function addCacheBuster(string $path, array $optionsAsset $asset): string
  174.     {
  175.         if (isset($options['cacheBuster']) && $options['cacheBuster']) {
  176.             if (!str_starts_with($path'http')) {
  177.                 $path '/cache-buster-' $asset->getVersionCount() . $path;
  178.             }
  179.         }
  180.         return $path;
  181.     }
  182.     private function getSourceTagHtml(Config $thumbConfigstring $mediaQueryImage $image, array $options): string
  183.     {
  184.         $sourceTagAttributes = [];
  185.         $sourceTagAttributes['srcset'] = $this->getSrcset($thumbConfig$image$options$mediaQuery);
  186.         $thumb $image->getThumbnail($thumbConfigtrue);
  187.         if ($mediaQuery) {
  188.             $sourceTagAttributes['media'] = $mediaQuery;
  189.             $thumb->reset();
  190.         }
  191.         if (isset($options['previewDataUri'])) {
  192.             $sourceTagAttributes['data-srcset'] = $sourceTagAttributes['srcset'];
  193.             unset($sourceTagAttributes['srcset']);
  194.         }
  195.         if (!isset($options['disableWidthHeightAttributes'])) {
  196.             if ($thumb->getWidth()) {
  197.                 $sourceTagAttributes['width'] = $thumb->getWidth();
  198.             }
  199.             if ($thumb->getHeight()) {
  200.                 $sourceTagAttributes['height'] = $thumb->getHeight();
  201.             }
  202.         }
  203.         $sourceTagAttributes['type'] = $thumb->getMimeType();
  204.         $sourceCallback $options['sourceCallback'] ?? null;
  205.         if ($sourceCallback) {
  206.             $sourceTagAttributes $sourceCallback($sourceTagAttributes);
  207.         }
  208.         return '<source ' array_to_html_attribute_string($sourceTagAttributes) . ' />';
  209.     }
  210.     /**
  211.      * Get generated HTML for displaying the thumbnail image in a HTML document.
  212.      *
  213.      * @param array $options Custom configuration
  214.      *
  215.      * @return string
  216.      */
  217.     public function getHtml($options = [])
  218.     {
  219.         $emptyGif 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
  220.         /** @var Image $image */
  221.         $image $this->getAsset();
  222.         $thumbConfig $this->getConfig();
  223.         $pictureTagAttributes $options['pictureAttributes'] ?? []; // this is used for the html5 <picture> element
  224.         if (($options['lowQualityPlaceholder'] ?? false) && !Tool::isFrontendRequestByAdmin()) {
  225.             // this gets used in getImagTag() later, use a 1x1 transparent GIF as a fallback if no LQIP exists
  226.             $options['previewDataUri'] =  $image->getLowQualityPreviewDataUri() ?: $emptyGif;
  227.         }
  228.         $isAutoFormat $thumbConfig instanceof Config strtolower($thumbConfig->getFormat()) === 'source' false;
  229.         if ($isAutoFormat) {
  230.             // ensure the default image is not WebP
  231.             $this->pathReference = [];
  232.         }
  233.         $pictureCallback $options['pictureCallback'] ?? null;
  234.         if ($pictureCallback) {
  235.             $pictureTagAttributes $pictureCallback($pictureTagAttributes);
  236.         }
  237.         $html '<picture ' array_to_html_attribute_string($pictureTagAttributes) . '>' "\n";
  238.         if ($thumbConfig instanceof Config) {
  239.             $thumbConfigRes = clone $thumbConfig;
  240.             $html.= $this->getMediaConfigHtml($thumbConfigRes$image$options$isAutoFormat);
  241.         }
  242.         if (!($options['disableImgTag'] ?? null)) {
  243.             $html .= "\t" $this->getImageTag($options) . "\n";
  244.         }
  245.         $html .= '</picture>' "\n";
  246.         if ($options['useDataSrc'] ?? false) {
  247.             $html preg_replace('/ src(set)?=/i'' data-src$1='$html);
  248.         }
  249.         return $html;
  250.     }
  251.     protected function getMediaConfigHtml(Config $thumbConfigImage $image, array $optionsbool $isAutoFormat): string
  252.     {
  253.         $html '';
  254.         $mediaConfigs $thumbConfig->getMedias();
  255.         // currently only max-width is supported, the key of the media is WIDTHw (eg. 400w) according to the srcset specification
  256.         ksort($mediaConfigsSORT_NUMERIC);
  257.         array_push($mediaConfigs$thumbConfig->getItems()); //add the default config at the end - picturePolyfill v4
  258.         foreach ($mediaConfigs as $mediaQuery => $config) {
  259.             $thumbConfig->setItems($config);
  260.             $sourceHtml $this->getSourceTagHtml($thumbConfig$mediaQuery$image$options);
  261.             if (!empty($sourceHtml)) {
  262.                 if ($isAutoFormat) {
  263.                     foreach ($thumbConfig->getAutoFormatThumbnailConfigs() as $autoFormatConfig) {
  264.                         $autoFormatThumbnailHtml $this->getSourceTagHtml($autoFormatConfig$mediaQuery$image$options);
  265.                         if (!empty($autoFormatThumbnailHtml)) {
  266.                             $html .= "\t" $autoFormatThumbnailHtml "\n";
  267.                         }
  268.                     }
  269.                 }
  270.                 $html .= "\t" $sourceHtml "\n";
  271.             }
  272.         }
  273.         return $html;
  274.     }
  275.     /**
  276.      * @param array $options
  277.      * @param array $removeAttributes
  278.      *
  279.      * @return string
  280.      */
  281.     public function getImageTag(array $options = [], array $removeAttributes = []): string
  282.     {
  283.         /** @var Image $image */
  284.         $image $this->getAsset();
  285.         $attributes $options['imgAttributes'] ?? [];
  286.         $callback $options['imgCallback'] ?? null;
  287.         if (isset($options['previewDataUri'])) {
  288.             $attributes['src'] = $options['previewDataUri'];
  289.         } else {
  290.             $path $this->getPath();
  291.             $attributes['src'] = $this->addCacheBuster($path$options$image);
  292.         }
  293.         if (!isset($options['disableWidthHeightAttributes'])) {
  294.             if ($this->getWidth()) {
  295.                 $attributes['width'] = $this->getWidth();
  296.             }
  297.             if ($this->getHeight()) {
  298.                 $attributes['height'] = $this->getHeight();
  299.             }
  300.         }
  301.         $altText = !empty($options['alt']) ? $options['alt'] : (!empty($attributes['alt']) ? $attributes['alt'] : '');
  302.         $titleText = !empty($options['title']) ? $options['title'] : (!empty($attributes['title']) ? $attributes['title'] : '');
  303.         if (empty($titleText) && (!isset($options['disableAutoTitle']) || !$options['disableAutoTitle'])) {
  304.             if ($image->getMetadata('title')) {
  305.                 $titleText $image->getMetadata('title');
  306.             }
  307.         }
  308.         if (empty($altText) && (!isset($options['disableAutoAlt']) || !$options['disableAutoAlt'])) {
  309.             if ($image->getMetadata('alt')) {
  310.                 $altText $image->getMetadata('alt');
  311.             } elseif (isset($options['defaultalt'])) {
  312.                 $altText $options['defaultalt'];
  313.             } else {
  314.                 $altText $titleText;
  315.             }
  316.         }
  317.         // get copyright from asset
  318.         if ($image->getMetadata('copyright') && (!isset($options['disableAutoCopyright']) || !$options['disableAutoCopyright'])) {
  319.             if (!empty($altText)) {
  320.                 $altText .= ' | ';
  321.             }
  322.             if (!empty($titleText)) {
  323.                 $titleText .= ' | ';
  324.             }
  325.             $altText .= ('© ' $image->getMetadata('copyright'));
  326.             $titleText .= ('© ' $image->getMetadata('copyright'));
  327.         }
  328.         $attributes['alt'] = $altText;
  329.         if (!empty($titleText)) {
  330.             $attributes['title'] = $titleText;
  331.         }
  332.         if (!isset($attributes['loading'])) {
  333.             $attributes['loading'] = 'lazy';
  334.         }
  335.         foreach ($removeAttributes as $attribute) {
  336.             unset($attributes[$attribute]);
  337.         }
  338.         if ($callback) {
  339.             $attributes $callback($attributes);
  340.         }
  341.         $thumbConfig $this->getConfig();
  342.         if ($thumbConfig) {
  343.             $srcsetAttribute = isset($options['previewDataUri']) ? 'data-srcset' 'srcset';
  344.             $attributes[$srcsetAttribute] = $this->getSrcset($thumbConfig$image$options);
  345.         }
  346.         $htmlImgTag '';
  347.         if (!empty($attributes)) {
  348.             $htmlImgTag '<img ' array_to_html_attribute_string($attributes) . ' />';
  349.         }
  350.         return $htmlImgTag;
  351.     }
  352.     /**
  353.      * @param string $name
  354.      * @param int $highRes
  355.      *
  356.      * @return Thumbnail
  357.      *
  358.      * @throws \Exception
  359.      */
  360.     public function getMedia($name$highRes 1)
  361.     {
  362.         $thumbConfig $this->getConfig();
  363.         $mediaConfigs $thumbConfig->getMedias();
  364.         if (isset($mediaConfigs[$name])) {
  365.             $thumbConfigRes = clone $thumbConfig;
  366.             $thumbConfigRes->selectMedia($name);
  367.             $thumbConfigRes->setHighResolution($highRes);
  368.             $thumbConfigRes->setMedias([]);
  369.             /** @var Image $asset */
  370.             $asset $this->getAsset();
  371.             $thumb $asset->getThumbnail($thumbConfigRes);
  372.             return $thumb;
  373.         } else {
  374.             throw new \Exception("Media query '" $name "' doesn't exist in thumbnail configuration: " $thumbConfig->getName());
  375.         }
  376.     }
  377.     /**
  378.      * Get a thumbnail image configuration.
  379.      *
  380.      * @param string|array|Thumbnail\Config $selector Name, array or object describing a thumbnail configuration.
  381.      *
  382.      * @return Thumbnail\Config
  383.      *
  384.      * @throws NotFoundException
  385.      */
  386.     private function createConfig($selector)
  387.     {
  388.         $thumbnailConfig Thumbnail\Config::getByAutoDetect($selector);
  389.         if (!empty($selector) && $thumbnailConfig === null) {
  390.             throw new NotFoundException('Thumbnail definition "' . (is_string($selector) ? $selector '') . '" does not exist');
  391.         }
  392.         return $thumbnailConfig;
  393.     }
  394.     /**
  395.      * Get value that can be directly used ina srcset HTML attribute for images.
  396.      *
  397.      * @param Config $thumbConfig
  398.      * @param Image $image
  399.      * @param array $options
  400.      * @param string|null $mediaQuery Can be empty string if no media queries are defined.
  401.      *
  402.      * @return string Relative paths to different thunbnail images with 1x and 2x resolution
  403.      */
  404.     private function getSrcset(Config $thumbConfigImage $image, array $options, ?string $mediaQuery null): string
  405.     {
  406.         $srcSetValues = [];
  407.         foreach ([12] as $highRes) {
  408.             $thumbConfigRes = clone $thumbConfig;
  409.             if ($mediaQuery) {
  410.                 $thumbConfigRes->selectMedia($mediaQuery);
  411.             }
  412.             $thumbConfigRes->setHighResolution($highRes);
  413.             $thumb $image->getThumbnail($thumbConfigRestrue);
  414.             $descriptor $highRes 'x';
  415.             // encode comma in thumbnail path as srcset is a comma separated list
  416.             $srcSetValues[] = str_replace(',''%2C'$this->addCacheBuster($thumb ' ' $descriptor$options$image));
  417.             if ($this->useOriginalFile($this->asset->getFilename()) && $this->getConfig()->isSvgTargetFormatPossible()) {
  418.                 break;
  419.             }
  420.         }
  421.         return implode(', '$srcSetValues);
  422.     }
  423.     /**
  424.      * @throws ThumbnailFormatNotSupportedException
  425.      * @throws ThumbnailMaxScalingFactorException
  426.      */
  427.     private function validate(): void
  428.     {
  429.         if (!$this->asset || !$this->config) {
  430.             return;
  431.         }
  432.         if (!$this->checkAllowedFormats($this->config->getFormat(), $this->asset)) {
  433.             throw new ThumbnailFormatNotSupportedException();
  434.         }
  435.         if (!$this->checkMaxScalingFactor($this->config->getHighResolution())) {
  436.             throw new ThumbnailMaxScalingFactorException();
  437.         }
  438.     }
  439. }