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

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