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

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