vendor/pimcore/pimcore/lib/Image/Adapter/Imagick.php line 598

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\Image\Adapter;
  15. use Pimcore\Cache;
  16. use Pimcore\Config;
  17. use Pimcore\File;
  18. use Pimcore\Image\Adapter;
  19. use Pimcore\Logger;
  20. use Pimcore\Model\Asset;
  21. class Imagick extends Adapter
  22. {
  23.     /**
  24.      * @var string|null
  25.      */
  26.     protected static $RGBColorProfile;
  27.     /**
  28.      * @var string|null
  29.      */
  30.     protected static $CMYKColorProfile;
  31.     /**
  32.      * @var \Imagick|null
  33.      */
  34.     protected $resource;
  35.     /**
  36.      * @var string
  37.      */
  38.     protected $imagePath;
  39.     /**
  40.      * @var array<string, bool>
  41.      */
  42.     protected static $supportedFormatsCache = [];
  43.     private ?array $initalOptions null;
  44.     /**
  45.      * {@inheritdoc}
  46.      */
  47.     public function load($imagePath$options = [])
  48.     {
  49.         $this->initalOptions ??= $options;
  50.         if (isset($options['preserveColor'])) {
  51.             // set this option to TRUE to skip all color transformations during the loading process
  52.             // this can massively improve performance if the color information doesn't matter, ...
  53.             // eg. when using this function to obtain dimensions from an image
  54.             $this->setPreserveColor($options['preserveColor']);
  55.         }
  56.         if (isset($options['asset']) && preg_match('@\.svgz?$@'$imagePath) && preg_match('@[^a-zA-Z0-9\-\.~_/]+@'$imagePath)) {
  57.             // Imagick/Inkscape delegate has problems with special characters in the file path, eg. "ß" causes
  58.             // Inkscape to completely go crazy -> Debian 8.10, Inkscape 0.48.5 r10040, Imagick 6.8.9-9 Q16, Imagick 3.4.3
  59.             // we create a local temp file, to workaround this problem
  60.             $imagePath $options['asset']->getTemporaryFile();
  61.             $this->tmpFiles[] = $imagePath;
  62.         }
  63.         if ($this->resource) {
  64.             unset($this->resource);
  65.             $this->resource null;
  66.         }
  67.         try {
  68.             $i = new \Imagick();
  69.             $this->imagePath $imagePath;
  70.             if (isset($options['resolution'])) {
  71.                 $i->setResolution($options['resolution']['x'], $options['resolution']['y']);
  72.             }
  73.             $imagePathLoad $imagePath;
  74.             $imagePathLoad $imagePathLoad '[0]';
  75.             if (!$i->readImage($imagePathLoad) || !@filesize($imagePath)) {
  76.                 return false;
  77.             }
  78.             $this->resource $i;
  79.             if (!$this->reinitializing && !$this->isPreserveColor()) {
  80.                 if (method_exists($i'setColorspace')) {
  81.                     $i->setColorspace(\Imagick::COLORSPACE_SRGB);
  82.                 }
  83.                 if ($this->isVectorGraphic($imagePath)) {
  84.                     // only for vector graphics
  85.                     // the below causes problems with PSDs when target format is PNG32 (nobody knows why ;-))
  86.                     $i->setBackgroundColor(new \ImagickPixel('transparent'));
  87.                     //for certain edge-cases simply setting the background-color to transparent does not seem to work
  88.                     //workaround by using transparentPaintImage (somehow even works without setting a target. no clue why)
  89.                     $i->transparentPaintImage(''10false);
  90.                 }
  91.                 $this->setColorspaceToRGB();
  92.             }
  93.             // set dimensions
  94.             $dimensions $this->getDimensions();
  95.             $this->setWidth($dimensions['width']);
  96.             $this->setHeight($dimensions['height']);
  97.             if (!$this->sourceImageFormat) {
  98.                 $this->sourceImageFormat $i->getImageFormat();
  99.             }
  100.             // check if image can have alpha channel
  101.             if (!$this->reinitializing) {
  102.                 $alphaChannel $i->getImageAlphaChannel();
  103.                 if ($alphaChannel) {
  104.                     $this->setIsAlphaPossible(true);
  105.                 }
  106.             }
  107.             if ($this->checkPreserveAnimation($i->getImageFormat(), $ifalse)) {
  108.                 if (!$this->resource->readImage($imagePath) || !filesize($imagePath)) {
  109.                     return false;
  110.                 }
  111.                 $this->resource $this->resource->coalesceImages();
  112.             }
  113.             $isClipAutoSupport \Pimcore::getContainer()->getParameter('pimcore.config')['assets']['image']['thumbnails']['clip_auto_support'];
  114.             if ($isClipAutoSupport && !$this->reinitializing && $this->has8BIMClippingPath()) {
  115.                 // the following way of determining a clipping path is very resource intensive (using Imagick),
  116.                 // so we try with the approach in has8BIMClippingPath() instead
  117.                 // check for the existence of an embedded clipping path (8BIM / Adobe profile meta data)
  118.                 //$identifyRaw = $i->identifyImage(true)['rawOutput'];
  119.                 //if (strpos($identifyRaw, 'Clipping path') && strpos($identifyRaw, '<svg')) {
  120.                 // if there's a clipping path embedded, apply the first one
  121.                 try {
  122.                     $i->setImageAlphaChannel(\Imagick::ALPHACHANNEL_TRANSPARENT);
  123.                     $i->clipImage();
  124.                     $i->setImageAlphaChannel(\Imagick::ALPHACHANNEL_OPAQUE);
  125.                 } catch (\Exception $e) {
  126.                     Logger::info(sprintf('Although automatic clipping support is enabled, your current ImageMagick / Imagick version does not support this operation on the image %s'$imagePath));
  127.                 }
  128.                 //}
  129.             }
  130.         } catch (\Exception $e) {
  131.             Logger::error('Unable to load image ' $imagePath ': ' $e);
  132.             return false;
  133.         }
  134.         $this->setModified(false);
  135.         return $this;
  136.     }
  137.     private function has8BIMClippingPath(): bool
  138.     {
  139.         $handle fopen($this->imagePath'rb');
  140.         $chunk fread($handle1024*1000); // read the first 1MB
  141.         fclose($handle);
  142.         // according to 8BIM format: https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037504
  143.         // we're looking for the resource id 'Name of clipping path' which is 8BIM 2999 (decimal) or 0x0BB7 in hex
  144.         if (preg_match('/8BIM\x0b\xb7/'$chunk)) {
  145.             return true;
  146.         }
  147.         return false;
  148.     }
  149.     /**
  150.      * {@inheritdoc}
  151.      */
  152.     public function getContentOptimizedFormat()
  153.     {
  154.         $format 'pjpeg';
  155.         if ($this->hasAlphaChannel()) {
  156.             $format 'png32';
  157.         }
  158.         return $format;
  159.     }
  160.     /**
  161.      * {@inheritdoc}
  162.      */
  163.     public function save($path$format null$quality null)
  164.     {
  165.         if (!$format) {
  166.             $format 'png32';
  167.         }
  168.         if ($format == 'original') {
  169.             $format $this->sourceImageFormat;
  170.         }
  171.         $format strtolower($format);
  172.         if ($format == 'png') {
  173.             // we need to force imagick to create png32 images, otherwise this can cause some strange effects
  174.             // when used with gray-scale images
  175.             $format 'png32';
  176.         }
  177.         $originalFilename null;
  178.         $i $this->resource// this is because of HHVM which has problems with $this->resource->writeImage();
  179.         if (in_array($format, ['jpeg''pjpeg''jpg']) && $this->isAlphaPossible) {
  180.             // set white background for transparent pixels
  181.             $i->setImageBackgroundColor('#ffffff');
  182.             if ($i->getImageAlphaChannel() !== 0) { // Note: returns (int) 0 if there's no AlphaChannel, PHP Docs are wrong. See: https://www.imagemagick.org/api/channel.php
  183.                 // Imagick version compatibility
  184.                 $alphaChannel 11// This works at least as far back as version 3.1.0~rc1-1
  185.                 if (defined('Imagick::ALPHACHANNEL_REMOVE')) {
  186.                     // Imagick::ALPHACHANNEL_REMOVE has been added in 3.2.0b2
  187.                     $alphaChannel \Imagick::ALPHACHANNEL_REMOVE;
  188.                 }
  189.                 $i->setImageAlphaChannel($alphaChannel);
  190.             }
  191.             $i->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN);
  192.         }
  193.         if (!$this->isPreserveMetaData()) {
  194.             $i->stripImage();
  195.             if ($format == 'png32') {
  196.                 // do not include any meta-data
  197.                 // this is due a bug in -strip, therefore we have to use this custom option
  198.                 // see also: https://github.com/ImageMagick/ImageMagick/issues/156
  199.                 $i->setOption('png:include-chunk''none');
  200.             }
  201.         }
  202.         if (!$this->isPreserveColor()) {
  203.             $i->profileImage('*''');
  204.         }
  205.         if ($quality && !$this->isPreserveColor()) {
  206.             $i->setCompressionQuality((int) $quality);
  207.             $i->setImageCompressionQuality((int) $quality);
  208.         }
  209.         if ($format == 'tiff') {
  210.             $i->setCompression(\Imagick::COMPRESSION_LZW);
  211.         }
  212.         // force progressive JPEG if filesize >= 10k
  213.         // normally jpeg images are bigger than 10k so we avoid the double compression (baseline => filesize check => if necessary progressive)
  214.         // and check the dimensions here instead to faster generate the image
  215.         // progressive JPEG - better compression, smaller filesize, especially for web optimization
  216.         if ($format == 'jpeg' && !$this->isPreserveColor()) {
  217.             if (($this->getWidth() * $this->getHeight()) > 35000) {
  218.                 $i->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
  219.             }
  220.         }
  221.         // Imagick isn't able to work with custom stream wrappers, so we make a workaround
  222.         $realTargetPath null;
  223.         if (!stream_is_local($path)) {
  224.             $realTargetPath $path;
  225.             $path PIMCORE_SYSTEM_TEMP_DIRECTORY '/imagick-tmp-' uniqid() . '.' File::getFileExtension($path);
  226.         }
  227.         if (!stream_is_local($path)) {
  228.             $i->setImageFormat($format);
  229.             $success File::put($path$i->getImageBlob());
  230.         } else {
  231.             if ($this->checkPreserveAnimation($format$i)) {
  232.                 $success $i->writeImages('GIF:' $pathtrue);
  233.             } else {
  234.                 $success $i->writeImage($format ':' $path);
  235.             }
  236.         }
  237.         if (!$success) {
  238.             throw new \Exception('Unable to write image: ' $path);
  239.         }
  240.         if ($realTargetPath) {
  241.             File::rename($path$realTargetPath);
  242.         }
  243.         return $this;
  244.     }
  245.     /**
  246.      * @param string $format
  247.      * @param \Imagick|null $i
  248.      * @param bool $checkNumberOfImages
  249.      *
  250.      * @return bool
  251.      */
  252.     private function checkPreserveAnimation(string $format ''\Imagick $i nullbool $checkNumberOfImages true)
  253.     {
  254.         if (!$this->isPreserveAnimation()) {
  255.             return false;
  256.         }
  257.         if (!$i) {
  258.             $i $this->resource;
  259.         }
  260.         if ($i && $checkNumberOfImages && $i->getNumberImages() <= 1) {
  261.             return false;
  262.         }
  263.         if ($format && !in_array(strtolower($format), ['gif''original''auto'])) {
  264.             return false;
  265.         }
  266.         return true;
  267.     }
  268.     /**
  269.      * {@inheritdoc}
  270.      */
  271.     protected function destroy()
  272.     {
  273.         if ($this->resource) {
  274.             $this->resource->clear();
  275.             $this->resource->destroy();
  276.             $this->resource null;
  277.         }
  278.     }
  279.     /**
  280.      * @return bool
  281.      */
  282.     private function hasAlphaChannel()
  283.     {
  284.         if ($this->isAlphaPossible) {
  285.             $width $this->resource->getImageWidth(); // Get the width of the image
  286.             $height $this->resource->getImageHeight(); // Get the height of the image
  287.             // We run the image pixel by pixel and as soon as we find a transparent pixel we stop and return true.
  288.             for ($i 0$i $width$i++) {
  289.                 for ($j 0$j $height$j++) {
  290.                     $pixel $this->resource->getImagePixelColor($i$j);
  291.                     $color $pixel->getColor(1); // get the real alpha not just 1/0
  292.                     if ($color['a'] < 1) { // if there's an alpha pixel, return true
  293.                         return true;
  294.                     }
  295.                 }
  296.             }
  297.         }
  298.         // If we dont find any pixel the function will return false.
  299.         return false;
  300.     }
  301.     /**
  302.      * @return $this
  303.      */
  304.     private function setColorspaceToRGB()
  305.     {
  306.         $imageColorspace $this->resource->getImageColorspace();
  307.         if (in_array($imageColorspace, [\Imagick::COLORSPACE_RGB\Imagick::COLORSPACE_SRGB])) {
  308.             // no need to process (s)RGB images
  309.             return $this;
  310.         }
  311.         $profiles $this->resource->getImageProfiles('icc'true);
  312.         if (isset($profiles['icc'])) {
  313.             if (strpos($profiles['icc'], 'RGB') !== false) {
  314.                 // no need to process (s)RGB images
  315.                 return $this;
  316.             }
  317.             // Workaround for ImageMagick (e.g. 6.9.10-23) bug, that let's it crash immediately if the tagged colorspace is
  318.             // different from the colorspace of the embedded icc color profile
  319.             // If that is the case we just ignore the color profiles
  320.             if (strpos($profiles['icc'], 'CMYK') !== false && $imageColorspace !== \Imagick::COLORSPACE_CMYK) {
  321.                 return $this;
  322.             }
  323.         }
  324.         if ($imageColorspace == \Imagick::COLORSPACE_CMYK) {
  325.             if (self::getCMYKColorProfile() && self::getRGBColorProfile()) {
  326.                 // if it doesn't have a CMYK ICC profile, we add one
  327.                 if (!isset($profiles['icc'])) {
  328.                     $this->resource->profileImage('icc'self::getCMYKColorProfile());
  329.                 }
  330.                 // then we add an RGB profile
  331.                 $this->resource->profileImage('icc'self::getRGBColorProfile());
  332.                 $this->resource->setImageColorspace(\Imagick::COLORSPACE_SRGB); // we have to use SRGB here, no clue why but it works
  333.             } else {
  334.                 $this->resource->setImageColorspace(\Imagick::COLORSPACE_SRGB);
  335.             }
  336.         } elseif ($imageColorspace == \Imagick::COLORSPACE_GRAY) {
  337.             $this->resource->setImageColorspace(\Imagick::COLORSPACE_SRGB);
  338.         } elseif (!in_array($imageColorspace, [\Imagick::COLORSPACE_RGB\Imagick::COLORSPACE_SRGB])) {
  339.             $this->resource->setImageColorspace(\Imagick::COLORSPACE_SRGB);
  340.         } else {
  341.             // this is to handle all other embedded icc profiles
  342.             if (isset($profiles['icc'])) {
  343.                 try {
  344.                     // if getImageColorspace() says SRGB but the embedded icc profile is CMYK profileImage() will throw an exception
  345.                     $this->resource->profileImage('icc'self::getRGBColorProfile());
  346.                     $this->resource->setImageColorspace(\Imagick::COLORSPACE_SRGB);
  347.                 } catch (\Exception $e) {
  348.                     Logger::warn((string) $e);
  349.                 }
  350.             }
  351.         }
  352.         // this is a HACK to force grayscale images to be real RGB - truecolor, this is important if you want to use
  353.         // thumbnails in PDF's because they do not support "real" grayscale JPEGs or PNGs
  354.         // problem is described here: http://imagemagick.org/Usage/basics/#type
  355.         // and here: http://www.imagemagick.org/discourse-server/viewtopic.php?f=2&t=6888#p31891
  356.         // 20.7.2018: this seems to cause new issues with newer Imagick/PHP versions, so we take it out for now ...
  357.         //  not sure if this workaround is actually still necessary (wouldn't assume so).
  358.         /*$currentLocale = setlocale(LC_ALL, '0'); // this locale hack thing is also a hack for imagick
  359.         setlocale(LC_ALL, 'en'); // Set locale to "en" for ImagickDraw::point() to ensure the involved tostring() methods keep the decimal point
  360.         $draw = new \ImagickDraw();
  361.         $draw->setFillColor('#ff0000');
  362.         $draw->setfillopacity(.01);
  363.         $draw->point(floor($this->getWidth() / 2), floor($this->getHeight() / 2)); // place it in the middle of the image
  364.         $this->resource->drawImage($draw);
  365.         setlocale(LC_ALL, $currentLocale); // see setlocale() above, for details ;-)
  366.         */
  367.         return $this;
  368.     }
  369.     /**
  370.      * @internal
  371.      *
  372.      * @param string $CMYKColorProfile
  373.      */
  374.     public static function setCMYKColorProfile($CMYKColorProfile)
  375.     {
  376.         self::$CMYKColorProfile $CMYKColorProfile;
  377.     }
  378.     /**
  379.      * @internal
  380.      *
  381.      * @return string
  382.      */
  383.     public static function getCMYKColorProfile()
  384.     {
  385.         if (!self::$CMYKColorProfile) {
  386.             $path Config::getSystemConfiguration('assets')['icc_cmyk_profile'] ?? null;
  387.             if (!$path || !file_exists($path)) {
  388.                 $path __DIR__ '/../icc-profiles/ISOcoated_v2_eci.icc'// default profile
  389.             }
  390.             if (file_exists($path)) {
  391.                 self::$CMYKColorProfile file_get_contents($path);
  392.             }
  393.         }
  394.         return self::$CMYKColorProfile;
  395.     }
  396.     /**
  397.      * @internal
  398.      *
  399.      * @param string $RGBColorProfile
  400.      */
  401.     public static function setRGBColorProfile($RGBColorProfile)
  402.     {
  403.         self::$RGBColorProfile $RGBColorProfile;
  404.     }
  405.     /**
  406.      * @internal
  407.      *
  408.      * @return string
  409.      */
  410.     public static function getRGBColorProfile()
  411.     {
  412.         if (!self::$RGBColorProfile) {
  413.             $path Config::getSystemConfiguration('assets')['icc_rgb_profile'] ?? null;
  414.             if (!$path || !file_exists($path)) {
  415.                 $path __DIR__ '/../icc-profiles/sRGB_IEC61966-2-1_black_scaled.icc'// default profile
  416.             }
  417.             if (file_exists($path)) {
  418.                 self::$RGBColorProfile file_get_contents($path);
  419.             }
  420.         }
  421.         return self::$RGBColorProfile;
  422.     }
  423.     /**
  424.      * {@inheritdoc}
  425.      */
  426.     public function resize($width$height)
  427.     {
  428.         $this->preModify();
  429.         // this is the check for vector formats because they need to have a resolution set
  430.         // this does only work if "resize" is the first step in the image-pipeline
  431.         if ($this->isVectorGraphic()) {
  432.             // the resolution has to be set before loading the image, that's why we have to destroy the instance and load it again
  433.             $res $this->resource->getImageResolution();
  434.             if ($res['x'] && $res['y']) {
  435.                 $x_ratio $res['x'] / $this->getWidth();
  436.                 $y_ratio $res['y'] / $this->getHeight();
  437.                 $this->resource->removeImage();
  438.                 $newRes = ['x' => $width $x_ratio'y' => $height $y_ratio];
  439.                 // only use the calculated resolution if we need a higher one that the one we got from the metadata (getImageResolution)
  440.                 // this is because sometimes the quality is much better when using the "native" resolution from the metadata
  441.                 if ($newRes['x'] > $res['x'] && $newRes['y'] > $res['y']) {
  442.                     $res $newRes;
  443.                 }
  444.             } else {
  445.                 // this is mostly for SVGs, it seems that getImageResolution() doesn't return a value anymore for SVGs
  446.                 // so we calculate the density ourselves, Inkscape/ImageMagick seem to use 96ppi, so that's how we get
  447.                 // the right values for -density (setResolution)
  448.                 $res = [
  449.                     'x' => ($width $this->getWidth()) * 96,
  450.                     'y' => ($height $this->getHeight()) * 96,
  451.                 ];
  452.             }
  453.             $this->resource->setResolution($res['x'], $res['y']);
  454.             $this->resource->readImage($this->imagePath);
  455.             if (!$this->isPreserveColor()) {
  456.                 $this->setColorspaceToRGB();
  457.             }
  458.         }
  459.         $width = (int)$width;
  460.         $height = (int)$height;
  461.         if ($this->getWidth() !== $width || $this->getHeight() !== $height) {
  462.             if ($this->checkPreserveAnimation()) {
  463.                 foreach ($this->resource as $i => $frame) {
  464.                     $frame->resizeimage($width$height\Imagick::FILTER_UNDEFINED1false);
  465.                 }
  466.             } else {
  467.                 $this->resource->resizeimage($width$height\Imagick::FILTER_UNDEFINED1false);
  468.             }
  469.             $this->setWidth($width);
  470.             $this->setHeight($height);
  471.         }
  472.         $this->postModify();
  473.         return $this;
  474.     }
  475.     /**
  476.      * {@inheritdoc}
  477.      */
  478.     public function crop($x$y$width$height)
  479.     {
  480.         $this->preModify();
  481.         $this->resource->cropImage($width$height$x$y);
  482.         $this->resource->setImagePage($width$height00);
  483.         $this->setWidth($width);
  484.         $this->setHeight($height);
  485.         $this->postModify();
  486.         return $this;
  487.     }
  488.     /**
  489.      * {@inheritdoc}
  490.      */
  491.     public function frame($width$height$forceResize false)
  492.     {
  493.         $this->preModify();
  494.         $this->contain($width$height$forceResize);
  495.         $x = ($width $this->getWidth()) / 2;
  496.         $y = ($height $this->getHeight()) / 2;
  497.         $newImage $this->createCompositeImageFromResource($width$height$x$y);
  498.         $this->resource $newImage;
  499.         $this->setWidth($width);
  500.         $this->setHeight($height);
  501.         $this->postModify();
  502.         $this->setIsAlphaPossible(true);
  503.         return $this;
  504.     }
  505.     /**
  506.      * {@inheritdoc}
  507.      */
  508.     public function trim($tolerance)
  509.     {
  510.         $this->preModify();
  511.         $this->resource->trimimage($tolerance);
  512.         $dimensions $this->getDimensions();
  513.         $this->setWidth($dimensions['width']);
  514.         $this->setHeight($dimensions['height']);
  515.         $this->postModify();
  516.         return $this;
  517.     }
  518.     /**
  519.      * {@inheritdoc}
  520.      */
  521.     public function setBackgroundColor($color)
  522.     {
  523.         $this->preModify();
  524.         $newImage $this->createCompositeImageFromResource($this->getWidth(), $this->getHeight(), 00$color);
  525.         $this->resource $newImage;
  526.         $this->postModify();
  527.         $this->setIsAlphaPossible(false);
  528.         return $this;
  529.     }
  530.     /**
  531.      * @param int $width
  532.      * @param int $height
  533.      * @param string $color
  534.      *
  535.      * @return \Imagick
  536.      */
  537.     private function createImage($width$height$color 'transparent')
  538.     {
  539.         $newImage = new \Imagick();
  540.         $newImage->newimage($width$height$color);
  541.         $newImage->setImageFormat($this->resource->getImageFormat());
  542.         return $newImage;
  543.     }
  544.     /**
  545.      * @param int $width
  546.      * @param int $height
  547.      * @param int $x
  548.      * @param int $y
  549.      * @param string $color
  550.      * @param int $composite
  551.      *
  552.      * @return \Imagick
  553.      */
  554.     private function createCompositeImageFromResource($width$height$x$y$color 'transparent'$composite \Imagick::COMPOSITE_DEFAULT)
  555.     {
  556.         $newImage null;
  557.         if ($this->checkPreserveAnimation()) {
  558.             foreach ($this->resource as $i => $frame) {
  559.                 $imageFrame $this->createImage($width$height$color);
  560.                 $imageFrame->compositeImage($frame$composite$x$y);
  561.                 if (!$newImage) {
  562.                     $newImage $imageFrame;
  563.                 } else {
  564.                     $newImage->addImage($imageFrame);
  565.                 }
  566.             }
  567.         } else {
  568.             $newImage $this->createImage($width$height$color);
  569.             $newImage->compositeImage($this->resource$composite$x$y);
  570.         }
  571.         return $newImage;
  572.     }
  573.     /**
  574.      * {@inheritdoc}
  575.      */
  576.     public function rotate($angle)
  577.     {
  578.         $this->preModify();
  579.         $this->resource->rotateImage(new \ImagickPixel('none'), $angle);
  580.         $this->setWidth($this->resource->getimagewidth());
  581.         $this->setHeight($this->resource->getimageheight());
  582.         $this->postModify();
  583.         $this->setIsAlphaPossible(true);
  584.         return $this;
  585.     }
  586.     /**
  587.      * {@inheritdoc}
  588.      */
  589.     public function roundCorners($width$height)
  590.     {
  591.         $this->preModify();
  592.         $this->internalRoundCorners($width$height);
  593.         $this->postModify();
  594.         $this->setIsAlphaPossible(true);
  595.         return $this;
  596.     }
  597.     /**
  598.      * Workaround for Imagick PHP extension v3.4.4 which removed Imagick::roundCorners
  599.      *
  600.      * @param int $width
  601.      * @param int $height
  602.      */
  603.     private function internalRoundCorners($width$height)
  604.     {
  605.         $imageWidth $this->resource->getImageWidth();
  606.         $imageHeight $this->resource->getImageHeight();
  607.         $rectangle = new \ImagickDraw();
  608.         $rectangle->setFillColor(new \ImagickPixel('black'));
  609.         $rectangle->roundRectangle(00$imageWidth 1$imageHeight 1$width$height);
  610.         $mask = new \Imagick();
  611.         $mask->newImage($imageWidth$imageHeight, new \ImagickPixel('transparent'), 'png');
  612.         $mask->drawImage($rectangle);
  613.         $this->resource->compositeImage($mask\Imagick::COMPOSITE_DSTIN00);
  614.     }
  615.     /**
  616.      * {@inheritdoc}
  617.      */
  618.     public function setBackgroundImage($image$mode null)
  619.     {
  620.         $this->preModify();
  621.         $image ltrim($image'/');
  622.         $image PIMCORE_WEB_ROOT '/' $image;
  623.         if (is_file($image)) {
  624.             $newImage = new \Imagick();
  625.             if ($mode == 'asTexture') {
  626.                 $newImage->newImage($this->getWidth(), $this->getHeight(), new \ImagickPixel());
  627.                 $texture = new \Imagick($image);
  628.                 $newImage $newImage->textureImage($texture);
  629.             } else {
  630.                 $newImage->readimage($image);
  631.                 if ($mode == 'cropTopLeft') {
  632.                     $newImage->cropImage($this->getWidth(), $this->getHeight(), 00);
  633.                 } else {
  634.                     // default behavior (fit)
  635.                     $newImage->resizeimage($this->getWidth(), $this->getHeight(), \Imagick::FILTER_UNDEFINED1false);
  636.                 }
  637.             }
  638.             $newImage->compositeImage($this->resource\Imagick::COMPOSITE_DEFAULT00);
  639.             $this->resource $newImage;
  640.         }
  641.         $this->postModify();
  642.         return $this;
  643.     }
  644.     /**
  645.      * {@inheritdoc}
  646.      */
  647.     public function addOverlay($image$x 0$y 0$alpha 100$composite 'COMPOSITE_DEFAULT'$origin 'top-left')
  648.     {
  649.         $this->preModify();
  650.         // 100 alpha is default
  651.         if (empty($alpha)) {
  652.             $alpha 100;
  653.         }
  654.         $alpha round($alpha 1001);
  655.         //Make sure the composite constant exists.
  656.         if (is_null(constant('Imagick::' $composite))) {
  657.             $composite 'COMPOSITE_DEFAULT';
  658.         }
  659.         $newImage null;
  660.         if (is_string($image)) {
  661.             $asset Asset\Image::getByPath($image);
  662.             if ($asset instanceof Asset\Image) {
  663.                 $image $asset->getTemporaryFile();
  664.             } else {
  665.                 trigger_deprecation(
  666.                     'pimcore/pimcore',
  667.                     '10.3',
  668.                     'Using relative path for Image Thumbnail overlay is deprecated, use Asset Image path.'
  669.                 );
  670.                 $image ltrim($image'/');
  671.                 $image PIMCORE_PROJECT_ROOT '/' $image;
  672.             }
  673.             $newImage = new \Imagick();
  674.             $newImage->readimage($image);
  675.         } elseif ($image instanceof \Imagick) {
  676.             $newImage $image;
  677.         }
  678.         if ($newImage) {
  679.             if ($origin === 'top-right') {
  680.                 $x $this->resource->getImageWidth() - $newImage->getImageWidth() - $x;
  681.             } elseif ($origin === 'bottom-left') {
  682.                 $y $this->resource->getImageHeight() - $newImage->getImageHeight() - $y;
  683.             } elseif ($origin === 'bottom-right') {
  684.                 $x $this->resource->getImageWidth() - $newImage->getImageWidth() - $x;
  685.                 $y $this->resource->getImageHeight() - $newImage->getImageHeight() - $y;
  686.             } elseif ($origin === 'center') {
  687.                 $x round($this->resource->getImageWidth() / 2) - round($newImage->getImageWidth() / 2) + $x;
  688.                 $y round($this->resource->getImageHeight() / 2) - round($newImage->getImageHeight() / 2) + $y;
  689.             }
  690.             $newImage->evaluateImage(\Imagick::EVALUATE_MULTIPLY$alpha\Imagick::CHANNEL_ALPHA);
  691.             $this->resource->compositeImage($newImageconstant('Imagick::' $composite), (int)$x, (int)$y);
  692.         }
  693.         $this->postModify();
  694.         return $this;
  695.     }
  696.     /**
  697.      * {@inheritdoc}
  698.      */
  699.     public function addOverlayFit($image$composite 'COMPOSITE_DEFAULT')
  700.     {
  701.         $asset Asset\Image::getByPath($image);
  702.         if ($asset instanceof Asset\Image) {
  703.             $image $asset->getTemporaryFile();
  704.         } else {
  705.             trigger_deprecation(
  706.                 'pimcore/pimcore',
  707.                 '10.3',
  708.                 'Using relative path for Image Thumbnail overlay is deprecated, use Asset Image path.'
  709.             );
  710.             $image ltrim($image'/');
  711.             $image PIMCORE_PROJECT_ROOT '/' $image;
  712.         }
  713.         $newImage = new \Imagick();
  714.         $newImage->readimage($image);
  715.         $newImage->resizeimage($this->getWidth(), $this->getHeight(), \Imagick::FILTER_UNDEFINED1false);
  716.         $this->addOverlay($newImage00100$composite);
  717.         return $this;
  718.     }
  719.     /**
  720.      * {@inheritdoc}
  721.      */
  722.     public function applyMask($image)
  723.     {
  724.         $this->preModify();
  725.         $image ltrim($image'/');
  726.         $image PIMCORE_PROJECT_ROOT '/' $image;
  727.         if (is_file($image)) {
  728.             $this->resource->setImageMatte(true);
  729.             $newImage = new \Imagick();
  730.             $newImage->readimage($image);
  731.             $newImage->resizeimage($this->getWidth(), $this->getHeight(), \Imagick::FILTER_UNDEFINED1false);
  732.             $this->resource->compositeImage($newImage\Imagick::COMPOSITE_COPYOPACITY00\Imagick::CHANNEL_ALPHA);
  733.         }
  734.         $this->postModify();
  735.         $this->setIsAlphaPossible(true);
  736.         return $this;
  737.     }
  738.     /**
  739.      * {@inheritdoc}
  740.      */
  741.     public function grayscale()
  742.     {
  743.         $this->preModify();
  744.         $this->resource->setImageType(\Imagick::IMGTYPE_GRAYSCALEMATTE);
  745.         $this->postModify();
  746.         return $this;
  747.     }
  748.     /**
  749.      * {@inheritdoc}
  750.      */
  751.     public function sepia()
  752.     {
  753.         $this->preModify();
  754.         $this->resource->sepiatoneimage(85);
  755.         $this->postModify();
  756.         return $this;
  757.     }
  758.     /**
  759.      * @param float $radius
  760.      * @param float $sigma
  761.      * @param float $amount
  762.      * @param float $threshold
  763.      *
  764.      * @return $this
  765.      */
  766.     public function sharpen($radius 0$sigma 1.0$amount 1.0$threshold 0.05)
  767.     {
  768.         $this->preModify();
  769.         $this->resource->normalizeImage();
  770.         $this->resource->unsharpMaskImage($radius$sigma$amount$threshold);
  771.         $this->postModify();
  772.         return $this;
  773.     }
  774.     /**
  775.      * {@inheritdoc}
  776.      */
  777.     public function gaussianBlur($radius 0$sigma 1.0)
  778.     {
  779.         $this->preModify();
  780.         $this->resource->gaussianBlurImage($radius$sigma);
  781.         $this->postModify();
  782.         return $this;
  783.     }
  784.     /**
  785.      * {@inheritdoc}
  786.      */
  787.     public function brightnessSaturation($brightness 100$saturation 100$hue 100)
  788.     {
  789.         $this->preModify();
  790.         $this->resource->modulateImage($brightness$saturation$hue);
  791.         $this->postModify();
  792.         return $this;
  793.     }
  794.     /**
  795.      * {@inheritdoc}
  796.      */
  797.     public function mirror($mode)
  798.     {
  799.         $this->preModify();
  800.         if ($mode == 'vertical') {
  801.             $this->resource->flipImage();
  802.         } elseif ($mode == 'horizontal') {
  803.             $this->resource->flopImage();
  804.         }
  805.         $this->postModify();
  806.         return $this;
  807.     }
  808.     /**
  809.      * @param string $imagePath
  810.      *
  811.      * @return bool
  812.      */
  813.     public function isVectorGraphic($imagePath null)
  814.     {
  815.         if (!$imagePath) {
  816.             $imagePath $this->imagePath;
  817.         }
  818.         // we need to do this check first, because ImageMagick using the inkscape delegate returns "PNG" when calling
  819.         // getimageformat() onto SVG graphics, this is a workaround to avoid problems
  820.         if (preg_match("@\.(svgz?|eps|pdf|ps|ai|indd)$@i"$imagePath)) {
  821.             return true;
  822.         }
  823.         try {
  824.             if ($this->resource) {
  825.                 $type $this->resource->getimageformat();
  826.                 $vectorTypes = [
  827.                     'EPT',
  828.                     'EPDF',
  829.                     'EPI',
  830.                     'EPS',
  831.                     'EPS2',
  832.                     'EPS3',
  833.                     'EPSF',
  834.                     'EPSI',
  835.                     'EPT',
  836.                     'PDF',
  837.                     'PFA',
  838.                     'PFB',
  839.                     'PFM',
  840.                     'PS',
  841.                     'PS2',
  842.                     'PS3',
  843.                     'SVG',
  844.                     'SVGZ',
  845.                     'MVG',
  846.                 ];
  847.                 if (in_array(strtoupper($type), $vectorTypes)) {
  848.                     return true;
  849.                 }
  850.             }
  851.         } catch (\Exception $e) {
  852.             Logger::err((string) $e);
  853.         }
  854.         return false;
  855.     }
  856.     /**
  857.      * @return array
  858.      */
  859.     private function getDimensions()
  860.     {
  861.         if ($vectorDimensions $this->getVectorFormatEmbeddedRasterDimensions()) {
  862.             return $vectorDimensions;
  863.         }
  864.         return [
  865.             'width' => $this->resource->getImageWidth(),
  866.             'height' => $this->resource->getImageHeight(),
  867.         ];
  868.     }
  869.     /**
  870.      * @return array|null
  871.      */
  872.     private function getVectorFormatEmbeddedRasterDimensions()
  873.     {
  874.         if (in_array($this->resource->getimageformat(), ['EPT''EPDF''EPI''EPS''EPS2''EPS3''EPSF''EPSI''EPT''PDF''PFA''PFB''PFM''PS''PS2''PS3'])) {
  875.             // we need a special handling for PhotoShop EPS
  876.             $i 0;
  877.             $epsFile fopen($this->imagePath'r');
  878.             while (($eps_line fgets($epsFile)) && ($i 100)) {
  879.                 if (preg_match('/%ImageData: ([0-9]+) ([0-9]+)/i'$eps_line$matches)) {
  880.                     return [
  881.                         'width' => $matches[1],
  882.                         'height' => $matches[2],
  883.                     ];
  884.                 }
  885.                 $i++;
  886.             }
  887.         }
  888.         return null;
  889.     }
  890.     /**
  891.      * {@inheritdoc}
  892.      */
  893.     protected function getVectorRasterDimensions()
  894.     {
  895.         if ($vectorDimensions $this->getVectorFormatEmbeddedRasterDimensions()) {
  896.             return $vectorDimensions;
  897.         }
  898.         return parent::getVectorRasterDimensions();
  899.     }
  900.     /**
  901.      * {@inheritdoc}
  902.      */
  903.     public function supportsFormat(string $formatbool $force false)
  904.     {
  905.         if ($force) {
  906.             return $this->checkFormatSupport($format);
  907.         }
  908.         if (!isset(self::$supportedFormatsCache[$format])) {
  909.             // since determining if an image format is supported is quite expensive we use two-tiered caching
  910.             // in-process caching (static variable) and the shared cache
  911.             $cacheKey 'imagick_format_' $format;
  912.             if (($cachedValue Cache::load($cacheKey)) !== false) {
  913.                 self::$supportedFormatsCache[$format] = (bool) $cachedValue;
  914.             } else {
  915.                 self::$supportedFormatsCache[$format] = $this->checkFormatSupport($format);
  916.                 // we cache the status as an int, so that we know if the status was cached or not, with bool that wouldn't be possible, since load() returns false if item doesn't exists
  917.                 Cache::save((int) self::$supportedFormatsCache[$format], $cacheKey, [], null999true);
  918.             }
  919.         }
  920.         return self::$supportedFormatsCache[$format];
  921.     }
  922.     /**
  923.      * @param string $format
  924.      *
  925.      * @return bool
  926.      */
  927.     private function checkFormatSupport(string $format): bool
  928.     {
  929.         try {
  930.             // we can't use \Imagick::queryFormats() here, because this doesn't consider configured delegates
  931.             $tmpFile PIMCORE_SYSTEM_TEMP_DIRECTORY '/imagick-format-support-detection-' uniqid() . '.' $format;
  932.             $image = new \Imagick();
  933.             $image->newImage(11, new \ImagickPixel('red'));
  934.             $image->writeImage($format ':' $tmpFile);
  935.             unlink($tmpFile);
  936.             return true;
  937.         } catch (\Exception $e) {
  938.             return false;
  939.         }
  940.     }
  941. }