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

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