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

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