vendor/pimcore/pimcore/models/Asset/Image/Thumbnail/Processor.php line 103

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\Thumbnail;
  15. use League\Flysystem\FilesystemException;
  16. use Pimcore\Config as PimcoreConfig;
  17. use Pimcore\File;
  18. use Pimcore\Helper\TemporaryFileHelperTrait;
  19. use Pimcore\Image\Adapter;
  20. use Pimcore\Logger;
  21. use Pimcore\Messenger\OptimizeImageMessage;
  22. use Pimcore\Model\Asset;
  23. use Pimcore\Model\Tool\TmpStore;
  24. use Pimcore\Tool\Storage;
  25. use Symfony\Component\Lock\LockFactory;
  26. /**
  27.  * @internal
  28.  */
  29. class Processor
  30. {
  31.     use TemporaryFileHelperTrait;
  32.     /**
  33.      * @var array
  34.      */
  35.     protected static $argumentMapping = [
  36.         'resize' => ['width''height'],
  37.         'scaleByWidth' => ['width''forceResize'],
  38.         'scaleByHeight' => ['height''forceResize'],
  39.         'contain' => ['width''height''forceResize'],
  40.         'cover' => ['width''height''positioning''forceResize'],
  41.         'frame' => ['width''height''forceResize'],
  42.         'trim' => ['tolerance'],
  43.         'rotate' => ['angle'],
  44.         'crop' => ['x''y''width''height'],
  45.         'setBackgroundColor' => ['color'],
  46.         'roundCorners' => ['width''height'],
  47.         'setBackgroundImage' => ['path''mode'],
  48.         'addOverlay' => ['path''x''y''alpha''composite''origin'],
  49.         'addOverlayFit' => ['path''composite'],
  50.         'applyMask' => ['path'],
  51.         'cropPercent' => ['width''height''x''y'],
  52.         'grayscale' => [],
  53.         'sepia' => [],
  54.         'sharpen' => ['radius''sigma''amount''threshold'],
  55.         'gaussianBlur' => ['radius''sigma'],
  56.         'brightnessSaturation' => ['brightness''saturation''hue'],
  57.         'mirror' => ['mode'],
  58.     ];
  59.     /**
  60.      * @param string $format
  61.      * @param array $allowed
  62.      * @param string $fallback
  63.      *
  64.      * @return string
  65.      */
  66.     private static function getAllowedFormat($format$allowed = [], $fallback 'png')
  67.     {
  68.         $typeMappings = [
  69.             'jpg' => 'jpeg',
  70.             'tif' => 'tiff',
  71.         ];
  72.         if (isset($typeMappings[$format])) {
  73.             $format $typeMappings[$format];
  74.         }
  75.         if (in_array($format$allowed)) {
  76.             $target $format;
  77.         } else {
  78.             $target $fallback;
  79.         }
  80.         return $target;
  81.     }
  82.     /**
  83.      * @param Asset $asset
  84.      * @param Config $config
  85.      * @param string|resource|null $fileSystemPath
  86.      * @param bool $deferred deferred means that the image will be generated on-the-fly (details see below)
  87.      * @param bool $generated
  88.      *
  89.      * @return array
  90.      *
  91.      * @throws \Exception
  92.      */
  93.     public static function process(
  94.         Asset $asset,
  95.         Config $config,
  96.         $fileSystemPath null,
  97.         $deferred false,
  98.         &$generated false
  99.     ) {
  100.         $generated false;
  101.         $format strtolower($config->getFormat());
  102.         // Optimize if allowed to strip info.
  103.         $optimizeContent = (!$config->isPreserveColor() && !$config->isPreserveMetaData());
  104.         $optimizedFormat false;
  105.         if (self::containsTransformationType($config'1x1_pixel')) {
  106.             return [
  107.                 'src' => 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
  108.                 'type' => 'data-uri',
  109.             ];
  110.         }
  111.         $fileExt File::getFileExtension($asset->getFilename());
  112.         // simple detection for source type if SOURCE is selected
  113.         if ($format == 'source' || empty($format)) {
  114.             $optimizedFormat true;
  115.             $format self::getAllowedFormat($fileExt, ['pjpeg''jpeg''gif''png'], 'png');
  116.             if ($format === 'jpeg') {
  117.                 $format 'pjpeg';
  118.             }
  119.         }
  120.         if ($format == 'print') {
  121.             // Don't optimize images for print as we assume we want images as
  122.             // untouched as possible.
  123.             $optimizedFormat $optimizeContent false;
  124.             $format self::getAllowedFormat($fileExt, ['svg''jpeg''png''tiff'], 'png');
  125.             if (($format == 'tiff') && \Pimcore\Tool::isFrontendRequestByAdmin()) {
  126.                 // return a webformat in admin -> tiff cannot be displayed in browser
  127.                 $format 'png';
  128.                 $deferred false// deferred is default, but it's not possible when using isFrontendRequestByAdmin()
  129.             } elseif (
  130.                 ($format == 'tiff' && self::containsTransformationType($config'tifforiginal'))
  131.                 || $format == 'svg'
  132.             ) {
  133.                 return [
  134.                     'src' => $asset->getRealFullPath(),
  135.                     'type' => 'asset',
  136.                 ];
  137.             }
  138.         } elseif ($format == 'tiff') {
  139.             $optimizedFormat $optimizeContent false;
  140.             if (\Pimcore\Tool::isFrontendRequestByAdmin()) {
  141.                 // return a webformat in admin -> tiff cannot be displayed in browser
  142.                 $format 'png';
  143.                 $deferred false// deferred is default, but it's not possible when using isFrontendRequestByAdmin()
  144.             }
  145.         }
  146.         $image Asset\Image::getImageTransformInstance();
  147.         $thumbDir rtrim($asset->getRealPath(), '/').'/'.$asset->getId().'/image-thumb__'.$asset->getId().'__'.$config->getName();
  148.         $filename preg_replace("/\." preg_quote(File::getFileExtension($asset->getFilename()), '/') . '$/i'''$asset->getFilename());
  149.         // add custom suffix if available
  150.         if ($config->getFilenameSuffix()) {
  151.             $filename .= '~-~' $config->getFilenameSuffix();
  152.         }
  153.         // add high-resolution modifier suffix to the filename
  154.         if ($config->getHighResolution() > 1) {
  155.             $filename .= '@' $config->getHighResolution() . 'x';
  156.         }
  157.         $fileExtension $format;
  158.         if ($format == 'original') {
  159.             $fileExtension $fileExt;
  160.         } elseif ($format === 'pjpeg' || $format === 'jpeg') {
  161.             $fileExtension 'jpg';
  162.         }
  163.         $filename .= '.' $fileExtension;
  164.         $storagePath $thumbDir '/' $filename;
  165.         $storage Storage::get('thumbnail');
  166.         // check for existing and still valid thumbnail
  167.         $modificationDate null;
  168.         $statusCacheEnabled \Pimcore::getContainer()->getParameter('pimcore.config')['assets']['image']['thumbnails']['status_cache'];
  169.         if ($statusCacheEnabled && $deferred) {
  170.             $modificationDate $asset->getDao()->getCachedThumbnailModificationDate($config->getName(), $filename);
  171.         } else {
  172.             try {
  173.                 $modificationDate $storage->lastModified($storagePath);
  174.             } catch (FilesystemException $e) {
  175.                 // nothing to do
  176.             }
  177.         }
  178.         if ($modificationDate) {
  179.             try {
  180.                 if ($modificationDate >= $asset->getDataModificationDate()) {
  181.                     return [
  182.                         'src' => $storagePath,
  183.                         'type' => 'thumbnail',
  184.                         'storagePath' => $storagePath,
  185.                     ];
  186.                 } else {
  187.                     // delete the file if it's not valid anymore, otherwise writing the actual data from
  188.                     // the local tmp-file to the real storage a bit further down doesn't work, as it has a
  189.                     // check for race-conditions & locking, so it needs to check for the existence of the thumbnail
  190.                     $storage->delete($storagePath);
  191.                     // refresh the thumbnail cache, if the asset modification date is modified
  192.                     // this is necessary because the thumbnail cache is not cleared automatically
  193.                     // when the original asset is modified
  194.                     $asset->getDao()->deleteFromThumbnailCache($config->getName());
  195.                 }
  196.             } catch (FilesystemException $e) {
  197.                 // nothing to do
  198.             }
  199.         }
  200.         // deferred means that the image will be generated on-the-fly (when requested by the browser)
  201.         // the configuration is saved for later use in
  202.         // \Pimcore\Bundle\CoreBundle\Controller\PublicServicesController::thumbnailAction()
  203.         // so that it can be used also with dynamic configurations
  204.         if ($deferred) {
  205.             // only add the config to the TmpStore if necessary (e.g. if the config is auto-generated)
  206.             if (!Config::exists($config->getName())) {
  207.                 $configId 'thumb_' $asset->getId() . '__' md5($storagePath);
  208.                 TmpStore::add($configId$config'thumbnail_deferred');
  209.             }
  210.             return [
  211.                 'src' => $storagePath,
  212.                 'type' => 'deferred',
  213.                 'storagePath' => $storagePath,
  214.             ];
  215.         }
  216.         // transform image
  217.         $image->setPreserveColor($config->isPreserveColor());
  218.         $image->setPreserveMetaData($config->isPreserveMetaData());
  219.         $image->setPreserveAnimation($config->getPreserveAnimation());
  220.         $fileExists false;
  221.         try {
  222.             // check if file is already on the file-system and if it is still valid
  223.             $modificationDate $storage->lastModified($storagePath);
  224.             if ($modificationDate $asset->getModificationDate()) {
  225.                 $storage->delete($storagePath);
  226.             } else {
  227.                 $fileExists true;
  228.             }
  229.         } catch (\Exception $e) {
  230.             Logger::debug($e->getMessage());
  231.         }
  232.         if ($fileExists === false) {
  233.             $lockKey 'image_thumbnail_' $asset->getId() . '_' md5($storagePath);
  234.             $lock \Pimcore::getContainer()->get(LockFactory::class)->createLock($lockKey);
  235.             $lock->acquire(true);
  236.             $startTime microtime(true);
  237.             // after we got the lock, check again if the image exists in the meantime - if not - generate it
  238.             if (!$storage->fileExists($storagePath)) {
  239.                 // all checks on the file system should be below the deferred part for performance reasons (remote file systems)
  240.                 if (!$fileSystemPath) {
  241.                     $fileSystemPath $asset->getLocalFile();
  242.                 }
  243.                 if (is_resource($fileSystemPath)) {
  244.                     $fileSystemPathStream $fileSystemPath;
  245.                     $fileSystemPath self::getLocalFileFromStream($fileSystemPath);
  246.                     @fclose($fileSystemPathStream);
  247.                 }
  248.                 if (!file_exists($fileSystemPath)) {
  249.                     throw new \Exception(sprintf('Source file %s does not exist!'$fileSystemPath));
  250.                 }
  251.                 if (!$image->load($fileSystemPath, ['asset' => $asset])) {
  252.                     throw new \Exception(sprintf('Unable to generate thumbnail for asset %s from source image %s'$asset->getId(), $fileSystemPath));
  253.                 }
  254.                 $transformations $config->getItems();
  255.                 // check if the original image has an orientation exif flag
  256.                 // if so add a transformation at the beginning that rotates and/or mirrors the image
  257.                 if (function_exists('exif_read_data')) {
  258.                     $exif = @exif_read_data($fileSystemPath);
  259.                     if (is_array($exif)) {
  260.                         if (array_key_exists('Orientation'$exif)) {
  261.                             $orientation = (int)$exif['Orientation'];
  262.                             if ($orientation 1) {
  263.                                 $angleMappings = [
  264.                                     => 180,
  265.                                     => 180,
  266.                                     => 180,
  267.                                     => 90,
  268.                                     => 90,
  269.                                     => 90,
  270.                                     => 270,
  271.                                 ];
  272.                                 if (array_key_exists($orientation$angleMappings)) {
  273.                                     array_unshift($transformations, [
  274.                                         'method' => 'rotate',
  275.                                         'arguments' => [
  276.                                             'angle' => $angleMappings[$orientation],
  277.                                         ],
  278.                                     ]);
  279.                                 }
  280.                                 // values that have to be mirrored, this is not very common, but should be covered anyway
  281.                                 $mirrorMappings = [
  282.                                     => 'vertical',
  283.                                     => 'horizontal',
  284.                                     => 'vertical',
  285.                                     => 'horizontal',
  286.                                 ];
  287.                                 if (array_key_exists($orientation$mirrorMappings)) {
  288.                                     array_unshift($transformations, [
  289.                                         'method' => 'mirror',
  290.                                         'arguments' => [
  291.                                             'mode' => $mirrorMappings[$orientation],
  292.                                         ],
  293.                                     ]);
  294.                                 }
  295.                             }
  296.                         }
  297.                     }
  298.                 }
  299.                 self::applyTransformations($image$asset$config$transformations);
  300.                 if ($optimizedFormat) {
  301.                     $format $image->getContentOptimizedFormat();
  302.                 }
  303.                 $tmpFsPath File::getLocalTempFilePath($fileExtension);
  304.                 $image->save($tmpFsPath$format$config->getQuality());
  305.                 $stream fopen($tmpFsPath'rb');
  306.                 $storage->writeStream($storagePath$stream);
  307.                 if (is_resource($stream)) {
  308.                     fclose($stream);
  309.                 }
  310.                 if ($statusCacheEnabled && $asset instanceof Asset\Image) {
  311.                     //update thumbnail dimensions to cache
  312.                     $asset->addThumbnailFileToCache($tmpFsPath$filename$config);
  313.                 }
  314.                 $generated true;
  315.                 $pimcoreAssetsConfig PimcoreConfig::getSystemConfiguration('assets');
  316.                 $isImageOptimizersEnabled $pimcoreAssetsConfig['image']['thumbnails']['image_optimizers']['enabled'];
  317.                 if ($optimizedFormat && $optimizeContent && $isImageOptimizersEnabled) {
  318.                     \Pimcore::getContainer()->get('messenger.bus.pimcore-core')->dispatch(
  319.                         new OptimizeImageMessage($storagePath)
  320.                     );
  321.                 }
  322.                 Logger::debug('Thumbnail ' $storagePath ' generated in ' .
  323.                     (microtime(true) - $startTime) . ' seconds');
  324.             } else {
  325.                 Logger::debug('Thumbnail ' $storagePath ' already generated, waiting on lock for ' .
  326.                     (microtime(true) - $startTime) . ' seconds');
  327.             }
  328.             $lock->release();
  329.         }
  330.         // quick bugfix / workaround,
  331.         //it seems that imagemagick / image optimizers creates sometimes empty PNG chunks (total size 33 bytes)
  332.         // no clue why it does so as this is not continuous reproducible, and this is the only fix we can do for now
  333.         // if the file is corrupted the file will be created on the fly
  334.         //when requested by the browser (because it's deleted here)
  335.         if ($storage->fileExists($storagePath) && $storage->fileSize($storagePath) < 50) {
  336.             $storage->delete($storagePath);
  337.             $asset->getDao()->deleteFromThumbnailCache($config->getName(), $filename);
  338.             return [
  339.                 'src' => $storagePath,
  340.                 'type' => 'deferred',
  341.             ];
  342.         }
  343.         return [
  344.             'src' => $storagePath,
  345.             'type' => 'thumbnail',
  346.             'storagePath' => $storagePath,
  347.         ];
  348.     }
  349.     /**
  350.      * @param Adapter $image
  351.      * @param Asset $asset
  352.      * @param Config $config
  353.      * @param array|null $transformations
  354.      *
  355.      * @return void
  356.      */
  357.     private static function applyTransformations(Adapter $imageAsset $assetConfig $config, ?array $transformations): void
  358.     {
  359.         if (is_array($transformations) && !empty($transformations)) {
  360.             $sourceImageWidth PHP_INT_MAX;
  361.             $sourceImageHeight PHP_INT_MAX;
  362.             if ($asset instanceof Asset\Image) {
  363.                 $sourceImageWidth $asset->getWidth();
  364.                 $sourceImageHeight $asset->getHeight();
  365.             }
  366.             $highResFactor $config->getHighResolution();
  367.             $calculateMaxFactor = function ($factor$original$new) {
  368.                 $newFactor $factor $original $new;
  369.                 if ($newFactor 1) {
  370.                     // don't go below factor 1
  371.                     $newFactor 1;
  372.                 }
  373.                 return $newFactor;
  374.             };
  375.             // sorry for the goto/label - but in this case it makes life really easier and the code more readable
  376.             prepareTransformations:
  377.             foreach ($transformations as &$transformation) {
  378.                 if (!empty($transformation) && !isset($transformation['isApplied'])) {
  379.                     $arguments = [];
  380.                     if (is_string($transformation['method'])) {
  381.                         $mapping self::$argumentMapping[$transformation['method']];
  382.                         if (is_array($transformation['arguments'])) {
  383.                             foreach ($transformation['arguments'] as $key => $value) {
  384.                                 $position array_search($key$mapping);
  385.                                 if ($position !== false) {
  386.                                     // high res calculations if enabled
  387.                                     if (!in_array($transformation['method'], ['cropPercent']) && in_array($key,
  388.                                         ['width''height''x''y'])) {
  389.                                         if ($highResFactor && $highResFactor 1) {
  390.                                             $value *= $highResFactor;
  391.                                             $value = (int)ceil($value);
  392.                                             if (!isset($transformation['arguments']['forceResize']) ||
  393.                                                 !$transformation['arguments']['forceResize']) {
  394.                                                 // check if source image is big enough otherwise adjust high-res factor
  395.                                                 if (in_array($key, ['width''x'])) {
  396.                                                     if ($sourceImageWidth $value) {
  397.                                                         $highResFactor $calculateMaxFactor(
  398.                                                             $highResFactor,
  399.                                                             $sourceImageWidth,
  400.                                                             $value
  401.                                                         );
  402.                                                         goto prepareTransformations;
  403.                                                     }
  404.                                                 } elseif (in_array($key, ['height''y'])) {
  405.                                                     if ($sourceImageHeight $value) {
  406.                                                         $highResFactor $calculateMaxFactor(
  407.                                                             $highResFactor,
  408.                                                             $sourceImageHeight,
  409.                                                             $value
  410.                                                         );
  411.                                                         goto prepareTransformations;
  412.                                                     }
  413.                                                 }
  414.                                             }
  415.                                         }
  416.                                     }
  417.                                     // inject the focal point
  418.                                     if ($transformation['method'] == 'cover' &&
  419.                                         $key == 'positioning' &&
  420.                                         $asset->getCustomSetting('focalPointX')) {
  421.                                         $value = [
  422.                                             'x' => $asset->getCustomSetting('focalPointX'),
  423.                                             'y' => $asset->getCustomSetting('focalPointY'),
  424.                                         ];
  425.                                     }
  426.                                     $arguments[$position] = $value;
  427.                                 }
  428.                             }
  429.                         }
  430.                     }
  431.                     ksort($arguments);
  432.                     if (!is_string($transformation['method']) && is_callable($transformation['method'])) {
  433.                         trigger_deprecation(
  434.                             'pimcore/pimcore',
  435.                             '10.6',
  436.                             'Using Callable in thumbnail transformations is deprecated and will not work on Pimcore 11.'
  437.                         );
  438.                         $transformation['method']($image);
  439.                     } elseif (method_exists($image$transformation['method'])) {
  440.                         call_user_func_array([$image$transformation['method']], $arguments);
  441.                     }
  442.                     $transformation['isApplied'] = true;
  443.                 }
  444.             }
  445.         }
  446.     }
  447.     /**
  448.      * @param Config $config
  449.      * @param string $transformationType
  450.      *
  451.      * @return bool
  452.      */
  453.     private static function containsTransformationType(Config $configstring $transformationType): bool
  454.     {
  455.         $transformations $config->getItems();
  456.         if (is_array($transformations) && count($transformations) > 0) {
  457.             foreach ($transformations as $transformation) {
  458.                 if (!empty($transformation)) {
  459.                     if ($transformation['method'] == $transformationType) {
  460.                         return true;
  461.                     }
  462.                 }
  463.             }
  464.         }
  465.         return false;
  466.     }
  467. }