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

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