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

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 Enterprise License (PEL)
  8.  * Full copyright and license information is available in
  9.  * LICENSE.md which is distributed with this source code.
  10.  *
  11.  * @category   Pimcore
  12.  * @package    Asset
  13.  *
  14.  * @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  15.  * @license    http://www.pimcore.org/license     GPLv3 and PEL
  16.  */
  17. namespace Pimcore\Model\Asset\Image;
  18. use Pimcore\Event\AssetEvents;
  19. use Pimcore\Event\FrontendEvents;
  20. use Pimcore\Logger;
  21. use Pimcore\Model\Asset\Image;
  22. use Pimcore\Model\Asset\Thumbnail\ImageThumbnailTrait;
  23. use Pimcore\Tool;
  24. use Symfony\Component\EventDispatcher\GenericEvent;
  25. class Thumbnail
  26. {
  27.     use ImageThumbnailTrait;
  28.     /**
  29.      * @var bool[]
  30.      */
  31.     protected static $hasListenersCache = [];
  32.     /**
  33.      * @param Image $asset
  34.      * @param string|array|Thumbnail\Config $config
  35.      * @param bool $deferred
  36.      */
  37.     public function __construct($asset$config null$deferred true)
  38.     {
  39.         $this->asset $asset;
  40.         $this->deferred $deferred;
  41.         $this->config $this->createConfig($config);
  42.     }
  43.     /**
  44.      * @param bool $deferredAllowed
  45.      *
  46.      * @return string
  47.      */
  48.     public function getPath($deferredAllowed true)
  49.     {
  50.         $fsPath $this->getFileSystemPath($deferredAllowed);
  51.         if ($this->getConfig()) {
  52.             if ($this->useOriginalFile($this->asset->getFilename()) && $this->getConfig()->isSvgTargetFormatPossible()) {
  53.                 // we still generate the raster image, to get the final size of the thumbnail
  54.                 // we use getRealFullPath() here, to avoid double encoding (getFullPath() returns already encoded path)
  55.                 $fsPath $this->asset->getRealFullPath();
  56.             }
  57.         }
  58.         $path $this->convertToWebPath($fsPath);
  59.         if ($this->hasListeners(FrontendEvents::ASSET_IMAGE_THUMBNAIL)) {
  60.             $event = new GenericEvent($this, [
  61.                 'filesystemPath' => $fsPath,
  62.                 'frontendPath' => $path,
  63.             ]);
  64.             \Pimcore::getEventDispatcher()->dispatch(FrontendEvents::ASSET_IMAGE_THUMBNAIL$event);
  65.             $path $event->getArgument('frontendPath');
  66.         }
  67.         return $path;
  68.     }
  69.     /**
  70.      * @param string $eventName
  71.      *
  72.      * @return bool
  73.      */
  74.     protected function hasListeners(string $eventName): bool
  75.     {
  76.         if (!isset(self::$hasListenersCache[$eventName])) {
  77.             self::$hasListenersCache[$eventName] = \Pimcore::getEventDispatcher()->hasListeners($eventName);
  78.         }
  79.         return self::$hasListenersCache[$eventName];
  80.     }
  81.     /**
  82.      * @param string $filename
  83.      *
  84.      * @return bool
  85.      */
  86.     protected function useOriginalFile($filename)
  87.     {
  88.         if ($this->getConfig()) {
  89.             if (!$this->getConfig()->isRasterizeSVG() && preg_match("@\.svgz?$@"$filename)) {
  90.                 return true;
  91.             }
  92.         }
  93.         return false;
  94.     }
  95.     /**
  96.      * @param bool $deferredAllowed
  97.      */
  98.     public function generate($deferredAllowed true)
  99.     {
  100.         $errorImage PIMCORE_WEB_ROOT '/bundles/pimcoreadmin/img/filetype-not-supported.svg';
  101.         $deferred false;
  102.         $generated false;
  103.         if (!$this->asset) {
  104.             $this->filesystemPath $errorImage;
  105.         } elseif (!$this->filesystemPath) {
  106.             // if no correct thumbnail config is given use the original image as thumbnail
  107.             if (!$this->config) {
  108.                 $this->filesystemPath $this->asset->getRealFullPath();
  109.             } else {
  110.                 try {
  111.                     $deferred $deferredAllowed && $this->deferred;
  112.                     $this->filesystemPath Thumbnail\Processor::process($this->asset$this->confignull$deferredtrue$generated);
  113.                 } catch (\Exception $e) {
  114.                     $this->filesystemPath $errorImage;
  115.                     Logger::error("Couldn't create thumbnail of image " $this->asset->getRealFullPath());
  116.                     Logger::error($e);
  117.                 }
  118.             }
  119.         }
  120.         if ($this->hasListeners(AssetEvents::IMAGE_THUMBNAIL)) {
  121.             \Pimcore::getEventDispatcher()->dispatch(AssetEvents::IMAGE_THUMBNAIL, new GenericEvent($this, [
  122.                 'deferred' => $deferred,
  123.                 'generated' => $generated,
  124.             ]));
  125.         }
  126.     }
  127.     /**
  128.      * Get the public path to the thumbnail image.
  129.      * This method is here for backwards compatility.
  130.      * Up to Pimcore 1.4.8 a thumbnail was returned as a path to an image.
  131.      *
  132.      * @return string Public path to thumbnail image.
  133.      */
  134.     public function __toString()
  135.     {
  136.         return $this->getPath(true);
  137.     }
  138.     /**
  139.      * @param string $path
  140.      * @param array $options
  141.      * @param Image $asset
  142.      *
  143.      * @return string
  144.      */
  145.     protected function addCacheBuster(string $path, array $optionsImage $asset): string
  146.     {
  147.         if (isset($options['cacheBuster']) && $options['cacheBuster']) {
  148.             $path '/cache-buster-' $asset->getModificationDate() . $path;
  149.         }
  150.         return $path;
  151.     }
  152.     /**
  153.      * Get generated HTML for displaying the thumbnail image in a HTML document. (XHTML compatible).
  154.      * Attributes can be added as a parameter. Attributes containing illegal characters are ignored.
  155.      * Width and Height attribute can be overridden. SRC-attribute not.
  156.      * Values of attributes are escaped.
  157.      *
  158.      * @param array $options Custom configurations and HTML attributes.
  159.      * @param array $removeAttributes Listof key-value pairs of HTML attributes that should be removed
  160.      *
  161.      * @return string IMG-element with at least the attributes src, width, height, alt.
  162.      */
  163.     public function getHtml($options = [], $removeAttributes = [])
  164.     {
  165.         /** @var Image $image */
  166.         $image $this->getAsset();
  167.         $attributes = [];
  168.         $pictureAttribs $options['pictureAttributes'] ?? []; // this is used for the html5 <picture> element
  169.         // re-add support for disableWidthHeightAttributes
  170.         if (isset($options['disableWidthHeightAttributes']) && $options['disableWidthHeightAttributes']) {
  171.             // make sure the attributes are removed
  172.             $removeAttributes array_merge($removeAttributes, ['width''height']);
  173.         } else {
  174.             if ($this->getWidth()) {
  175.                 $attributes['width'] = $this->getWidth();
  176.             }
  177.             if ($this->getHeight()) {
  178.                 $attributes['height'] = $this->getHeight();
  179.             }
  180.         }
  181.         $w3cImgAttributes = ['alt''align''border''height''hspace''ismap''longdesc''usemap',
  182.             'vspace''width''class''dir''id''lang''style''title''xml:lang''onmouseover',
  183.             'onabort''onclick''ondblclick''onmousedown''onmousemove''onmouseout''onmouseup',
  184.             'onkeydown''onkeypress''onkeyup''itemprop''itemscope''itemtype', ];
  185.         $customAttributes = [];
  186.         if (isset($options['attributes']) && is_array($options['attributes'])) {
  187.             $customAttributes $options['attributes'];
  188.         }
  189.         $altText '';
  190.         $titleText '';
  191.         if (isset($options['alt'])) {
  192.             $altText $options['alt'];
  193.         }
  194.         if (isset($options['title'])) {
  195.             $titleText $options['title'];
  196.         }
  197.         if (empty($titleText) && (!isset($options['disableAutoTitle']) || !$options['disableAutoTitle'])) {
  198.             if ($image->getMetadata('title')) {
  199.                 $titleText $image->getMetadata('title');
  200.             }
  201.         }
  202.         if (empty($altText) && (!isset($options['disableAutoAlt']) || !$options['disableAutoAlt'])) {
  203.             if ($image->getMetadata('alt')) {
  204.                 $altText $image->getMetadata('alt');
  205.             } elseif (isset($options['defaultalt'])) {
  206.                 $altText $options['defaultalt'];
  207.             } else {
  208.                 $altText $titleText;
  209.             }
  210.         }
  211.         // get copyright from asset
  212.         if ($image->getMetadata('copyright') && (!isset($options['disableAutoCopyright']) || !$options['disableAutoCopyright'])) {
  213.             if (!empty($altText)) {
  214.                 $altText .= ' | ';
  215.             }
  216.             if (!empty($titleText)) {
  217.                 $titleText .= ' | ';
  218.             }
  219.             $altText .= ('© ' $image->getMetadata('copyright'));
  220.             $titleText .= ('© ' $image->getMetadata('copyright'));
  221.         }
  222.         $options['alt'] = $altText;
  223.         if (!empty($titleText)) {
  224.             $options['title'] = $titleText;
  225.         }
  226.         $attributesRaw array_merge($options$customAttributes);
  227.         foreach ($attributesRaw as $key => $value) {
  228.             if (!(is_string($value) || is_numeric($value) || is_bool($value))) {
  229.                 continue;
  230.             }
  231.             if (!(in_array($key$w3cImgAttributes) || isset($customAttributes[$key]) || strpos($key'data-') === 0)) {
  232.                 continue;
  233.             }
  234.             //only include attributes with characters a-z and dashes in their name.
  235.             if (preg_match('/^[a-z-]+$/i'$key)) {
  236.                 $attributes[$key] = $value;
  237.                 // some attributes need to be added also as data- attribute, this is specific to picturePolyfill
  238.                 if (in_array($key, ['alt'])) {
  239.                     $pictureAttribs['data-' $key] = $value;
  240.                 }
  241.             }
  242.         }
  243.         $path $this->getPath(true);
  244.         $attributes['src'] = $this->addCacheBuster($path$options$image);
  245.         $thumbConfig $this->getConfig();
  246.         if ($this->getConfig() && !$this->getConfig()->hasMedias() && !$this->useOriginalFile($path)) {
  247.             // generate the srcset
  248.             $srcSetValues = [];
  249.             foreach ([12] as $highRes) {
  250.                 $thumbConfigRes = clone $thumbConfig;
  251.                 $thumbConfigRes->setHighResolution($highRes);
  252.                 $srcsetEntry $image->getThumbnail($thumbConfigRestrue) . ' ' $highRes 'x';
  253.                 $srcSetValues[] = $this->addCacheBuster($srcsetEntry$options$image);
  254.             }
  255.             $attributes['srcset'] = implode(', '$srcSetValues);
  256.         }
  257.         foreach ($removeAttributes as $attribute) {
  258.             unset($attributes[$attribute]);
  259.             unset($pictureAttribs[$attribute]);
  260.         }
  261.         $isLowQualityPreview false;
  262.         if ((isset($options['lowQualityPlaceholder']) && $options['lowQualityPlaceholder']) && !Tool::isFrontendRequestByAdmin()) {
  263.             $previewDataUri $this->getAsset()->getLowQualityPreviewDataUri();
  264.             if (!$previewDataUri) {
  265.                 // use a 1x1 transparent GIF as a fallback if no LQIP exists
  266.                 $previewDataUri 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
  267.             }
  268.             $isLowQualityPreview true;
  269.             $attributes['data-src'] = $attributes['src'];
  270.             if (isset($attributes['srcset'])) {
  271.                 $attributes['data-srcset'] = $attributes['srcset'];
  272.             }
  273.             $attributes['src'] = $previewDataUri;
  274.             unset($attributes['srcset']);
  275.         }
  276.         // build html tag
  277.         $htmlImgTag '<img ' array_to_html_attribute_string($attributes) . ' />';
  278.         // $this->getConfig() can be empty, the original image is returned
  279.         if ($this->getConfig() && ($this->getConfig()->hasMedias() || $this->getConfig()->getForcePictureTag())) {
  280.             // output the <picture> - element
  281.             $config = \Pimcore\Config::getSystemConfiguration('assets');
  282.             $isWebPAutoSupport $config['image']['thumbnails']['webp_auto_support'] ?? false;
  283.             $isAutoFormat = ($isWebPAutoSupport && strtolower($this->getConfig()->getFormat()) === 'source') ? true false;
  284.             $webpSupportBackup null;
  285.             if ($isAutoFormat) {
  286.                 $webpSupportBackup Image\Thumbnail\Processor::setHasWebpSupport(false);
  287.                 // ensure the default image is not WebP
  288.                 $this->filesystemPath null;
  289.                 $path $this->getPath(true);
  290.             }
  291.             $html '<picture ' array_to_html_attribute_string($pictureAttribs) . ' data-default-src="' $this->addCacheBuster($path$options$image) . '">' "\n";
  292.             $mediaConfigs $thumbConfig->getMedias();
  293.             // currently only max-width is supported, the key of the media is WIDTHw (eg. 400w) according to the srcset specification
  294.             ksort($mediaConfigsSORT_NUMERIC);
  295.             array_push($mediaConfigs$thumbConfig->getItems()); //add the default config at the end - picturePolyfill v4
  296.             foreach ($mediaConfigs as $mediaQuery => $config) {
  297.                 $srcSetValues = [];
  298.                 $sourceTagAttributes = [];
  299.                 $thumb null;
  300.                 foreach ([12] as $highRes) {
  301.                     $thumbConfigRes = clone $thumbConfig;
  302.                     $thumbConfigRes->selectMedia($mediaQuery);
  303.                     $thumbConfigRes->setHighResolution($highRes);
  304.                     $thumb $image->getThumbnail($thumbConfigRestrue);
  305.                     $srcSetValues[] = $this->addCacheBuster($thumb ' ' $highRes 'x'$options$image);
  306.                     if ($this->useOriginalFile($this->asset->getFilename()) && $this->getConfig()->isSvgTargetFormatPossible()) {
  307.                         break;
  308.                     }
  309.                     if ($isAutoFormat) {
  310.                         $thumbConfigWebP = clone $thumbConfigRes;
  311.                         $thumbConfigWebP->setFormat('webp');
  312.                         $image->getThumbnail($thumbConfigWebPtrue)->getPath();
  313.                     }
  314.                 }
  315.                 if ($thumb) {
  316.                     $sourceTagAttributes['srcset'] = implode(', '$srcSetValues);
  317.                     if ($mediaQuery) {
  318.                         if (preg_match('/^[\d]+w$/'$mediaQuery)) {
  319.                             // we replace the width indicator (400w) out of the name and build a proper media query for max width
  320.                             $maxWidth str_replace('w'''$mediaQuery);
  321.                             $sourceTagAttributes['media'] = '(max-width: ' $maxWidth 'px)';
  322.                         } else {
  323.                             // new style custom media queries
  324.                             $sourceTagAttributes['media'] = $mediaQuery;
  325.                         }
  326.                         $thumb->reset();
  327.                     }
  328.                     if ($isLowQualityPreview) {
  329.                         $sourceTagAttributes['data-srcset'] = $sourceTagAttributes['srcset'];
  330.                         unset($sourceTagAttributes['srcset']);
  331.                     }
  332.                     $sourceTagAttributes['type'] = $thumb->getMimeType();
  333.                     $sourceHtml '<source ' array_to_html_attribute_string($sourceTagAttributes) . ' />';
  334.                     if ($isAutoFormat) {
  335.                         $sourceHtmlWebP preg_replace(['@(\.)(jpg|png)( \dx)@''@(/)(jpeg|png)(")@'], '$1webp$3'$sourceHtml);
  336.                         if ($sourceHtmlWebP != $sourceHtml) {
  337.                             $html .= "\t" $sourceHtmlWebP "\n";
  338.                         }
  339.                     }
  340.                     $html .= "\t" $sourceHtml "\n";
  341.                 }
  342.             }
  343.             $attrCleanedForPicture $attributes;
  344.             $attrCleanedForPicture['src'] = $this->addCacheBuster($path$options$image);
  345.             unset($attrCleanedForPicture['width']);
  346.             unset($attrCleanedForPicture['height']);
  347.             if (isset($attrCleanedForPicture['srcset'])) {
  348.                 unset($attrCleanedForPicture['srcset']);
  349.             }
  350.             if ($isLowQualityPreview) {
  351.                 unset($attrCleanedForPicture['data-src']);
  352.                 unset($attrCleanedForPicture['data-srcset']);
  353.                 $attrCleanedForPicture['data-src'] = $attrCleanedForPicture['src'];
  354.                 $attrCleanedForPicture['src'] = $attributes['src'];
  355.             }
  356.             $htmlImgTagForpicture "\t" '<img ' array_to_html_attribute_string($attrCleanedForPicture) .' />';
  357.             $html .= $htmlImgTagForpicture "\n";
  358.             $html .= '</picture>' "\n";
  359.             $htmlImgTag $html;
  360.             if ($isAutoFormat) {
  361.                 Image\Thumbnail\Processor::setHasWebpSupport($webpSupportBackup);
  362.             }
  363.         }
  364.         if (isset($options['useDataSrc']) && $options['useDataSrc']) {
  365.             $htmlImgTag preg_replace('/ src(set)?=/i'' data-src$1='$htmlImgTag);
  366.         }
  367.         return $htmlImgTag;
  368.     }
  369.     /**
  370.      * @param string $name
  371.      * @param int $highRes
  372.      *
  373.      * @return Thumbnail
  374.      *
  375.      * @throws \Exception
  376.      */
  377.     public function getMedia($name$highRes 1)
  378.     {
  379.         $thumbConfig $this->getConfig();
  380.         $mediaConfigs $thumbConfig->getMedias();
  381.         if (isset($mediaConfigs[$name])) {
  382.             $thumbConfigRes = clone $thumbConfig;
  383.             $thumbConfigRes->selectMedia($name);
  384.             $thumbConfigRes->setHighResolution($highRes);
  385.             $thumbConfigRes->setMedias([]);
  386.             $thumb $this->getAsset()->getThumbnail($thumbConfigRes);
  387.             return $thumb;
  388.         } else {
  389.             throw new \Exception("Media query '" $name "' doesn't exist in thumbnail configuration: " $thumbConfig->getName());
  390.         }
  391.     }
  392.     /**
  393.      * Get a thumbnail image configuration.
  394.      *
  395.      * @param string|array|Thumbnail\Config $selector Name, array or object describing a thumbnail configuration.
  396.      *
  397.      * @return Thumbnail\Config
  398.      */
  399.     protected function createConfig($selector)
  400.     {
  401.         return Thumbnail\Config::getByAutoDetect($selector);
  402.     }
  403. }