vendor/sulu/sulu/src/Sulu/Bundle/MediaBundle/Media/ImageConverter/ImagineImageConverter.php line 153

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of Sulu.
  4.  *
  5.  * (c) Sulu GmbH
  6.  *
  7.  * This source file is subject to the MIT license that is bundled
  8.  * with this source code in the file LICENSE.
  9.  */
  10. namespace Sulu\Bundle\MediaBundle\Media\ImageConverter;
  11. use Imagine\Exception\RuntimeException;
  12. use Imagine\Filter\Basic\Autorotate;
  13. use Imagine\Image\ImageInterface;
  14. use Imagine\Image\ImagineInterface;
  15. use Imagine\Image\Palette\RGB;
  16. use Sulu\Bundle\MediaBundle\Entity\FileVersion;
  17. use Sulu\Bundle\MediaBundle\Entity\FormatOptions;
  18. use Sulu\Bundle\MediaBundle\Media\Exception\ImageProxyInvalidFormatOptionsException;
  19. use Sulu\Bundle\MediaBundle\Media\Exception\ImageProxyInvalidImageFormat;
  20. use Sulu\Bundle\MediaBundle\Media\Exception\InvalidFileTypeException;
  21. use Sulu\Bundle\MediaBundle\Media\ImageConverter\Cropper\CropperInterface;
  22. use Sulu\Bundle\MediaBundle\Media\ImageConverter\Focus\FocusInterface;
  23. use Sulu\Bundle\MediaBundle\Media\ImageConverter\Scaler\ScalerInterface;
  24. use Sulu\Bundle\MediaBundle\Media\Storage\StorageInterface;
  25. /**
  26.  * Sulu imagine converter for media.
  27.  */
  28. class ImagineImageConverter implements ImageConverterInterface
  29. {
  30.     /**
  31.      * @var ImagineInterface
  32.      */
  33.     private $imagine;
  34.     /**
  35.      * @var ImagineInterface
  36.      */
  37.     private $svgImagine;
  38.     /**
  39.      * @var StorageInterface
  40.      */
  41.     private $storage;
  42.     /**
  43.      * @var MediaImageExtractorInterface
  44.      */
  45.     private $mediaImageExtractor;
  46.     /**
  47.      * @var TransformationPoolInterface
  48.      */
  49.     private $transformationPool;
  50.     /**
  51.      * @var FocusInterface
  52.      */
  53.     private $focus;
  54.     /**
  55.      * @var ScalerInterface
  56.      */
  57.     private $scaler;
  58.     /**
  59.      * @var CropperInterface
  60.      */
  61.     private $cropper;
  62.     /**
  63.      * @var array
  64.      */
  65.     private $formats;
  66.     /**
  67.      * @var array
  68.      */
  69.     private $supportedMimeTypes;
  70.     public function __construct(
  71.         ImagineInterface $imagine,
  72.         StorageInterface $storage,
  73.         MediaImageExtractorInterface $mediaImageExtractor,
  74.         TransformationPoolInterface $transformationPool,
  75.         FocusInterface $focus,
  76.         ScalerInterface $scaler,
  77.         CropperInterface $cropper,
  78.         array $formats,
  79.         array $supportedMimeTypes,
  80.         ?ImagineInterface $svgImagine null
  81.     ) {
  82.         $this->imagine $imagine;
  83.         $this->storage $storage;
  84.         $this->mediaImageExtractor $mediaImageExtractor;
  85.         $this->transformationPool $transformationPool;
  86.         $this->focus $focus;
  87.         $this->scaler $scaler;
  88.         $this->cropper $cropper;
  89.         $this->formats $formats;
  90.         $this->supportedMimeTypes $supportedMimeTypes;
  91.         $this->svgImagine $svgImagine;
  92.     }
  93.     public function getSupportedOutputImageFormats(?string $mimeType): array
  94.     {
  95.         if (!$mimeType) {
  96.             return [];
  97.         }
  98.         foreach ($this->supportedMimeTypes as $supportedMimeType) {
  99.             if (\fnmatch($supportedMimeType$mimeType)) {
  100.                 $preferredExtension 'jpg';
  101.                 switch ($mimeType) {
  102.                     case 'image/png':
  103.                         $preferredExtension 'png';
  104.                         break;
  105.                     case 'image/svg+xml':
  106.                     case 'image/svg':
  107.                         $preferredExtension 'png';
  108.                         if ($this->svgImagine) {
  109.                             $preferredExtension 'svg';
  110.                         }
  111.                         break;
  112.                     case 'image/webp':
  113.                         $preferredExtension 'webp';
  114.                         break;
  115.                     case 'image/gif':
  116.                         $preferredExtension 'gif';
  117.                         break;
  118.                 }
  119.                 return \array_unique([
  120.                     $preferredExtension,
  121.                     'jpg',
  122.                     'gif',
  123.                     'png',
  124.                     'webp',
  125.                 ]);
  126.             }
  127.         }
  128.         return [];
  129.     }
  130.     public function convert(FileVersion $fileVersion$formatKey$imageFormat)
  131.     {
  132.         $imageResource $this->mediaImageExtractor->extract(
  133.             $this->storage->load($fileVersion->getStorageOptions()),
  134.             $fileVersion->getMimeType()
  135.         );
  136.         $imagine $this->imagine;
  137.         if ('svg' === $imageFormat && $this->svgImagine) {
  138.             $imagine $this->svgImagine;
  139.         }
  140.         try {
  141.             $image $imagine->read($imageResource);
  142.         } catch (RuntimeException $e) {
  143.             throw new InvalidFileTypeException($e->getMessage(), $e);
  144.         }
  145.         $image $this->toRGB($image);
  146.         $image $this->autorotate($image);
  147.         $format $this->getFormat($formatKey);
  148.         $cropParameters $this->getCropParameters(
  149.             $image,
  150.             $fileVersion->getFormatOptions()->get($formatKey),
  151.             $this->formats[$formatKey]
  152.         );
  153.         if (isset($cropParameters)) {
  154.             $image $this->applyFormatCrop($image$cropParameters);
  155.         } elseif (isset($format['scale']) && ImageInterface::THUMBNAIL_INSET !== $format['scale']['mode']) {
  156.             $image $this->applyFocus($image$fileVersion$format['scale']);
  157.         }
  158.         if (isset($format['scale'])) {
  159.             $image $this->applyScale($image$format['scale']);
  160.         }
  161.         if (isset($format['transformations'])) {
  162.             $image $this->applyTransformations($image$format['transformations']);
  163.         }
  164.         $image->strip();
  165.         try {
  166.             // Set Interlacing to plane for smaller image size.
  167.             if (== \count($image->layers())) {
  168.                 $image->interlace(ImageInterface::INTERLACE_PLANE);
  169.             }
  170.         } catch (RuntimeException $exception) {
  171.             // ignore exceptions here (some imagine adapter does not implement this)
  172.         }
  173.         $imagineOptions $format['options'];
  174.         return $image->get(
  175.             $imageFormat,
  176.             $this->getOptionsFromImage($image$imageFormat$imagineOptions)
  177.         );
  178.     }
  179.     /**
  180.      * Applies an array of transformations on a passed image.
  181.      *
  182.      * @param array $tansformations
  183.      *
  184.      * @return ImageInterface The modified image
  185.      *
  186.      * @throws ImageProxyInvalidFormatOptionsException
  187.      */
  188.     private function applyTransformations(ImageInterface $image$tansformations)
  189.     {
  190.         foreach ($tansformations as $transformation) {
  191.             if (!isset($transformation['effect'])) {
  192.                 throw new ImageProxyInvalidFormatOptionsException('Effect not found');
  193.             }
  194.             $image $this->modifyAllLayers(
  195.                 $image,
  196.                 function(ImageInterface $layer) use ($transformation) {
  197.                     return $this->transformationPool->get($transformation['effect'])->execute(
  198.                         $layer,
  199.                         $transformation['parameters']
  200.                     );
  201.                 }
  202.             );
  203.         }
  204.         return $image;
  205.     }
  206.     /**
  207.      * Crops a given image according to given parameters.
  208.      *
  209.      * @param ImageInterface $image The image to crop
  210.      * @param array $cropParameters The parameters which define the area to crop
  211.      *
  212.      * @return ImageInterface The cropped image
  213.      */
  214.     private function applyFormatCrop(ImageInterface $image, array $cropParameters)
  215.     {
  216.         return $this->modifyAllLayers(
  217.             $image,
  218.             function(ImageInterface $layer) use ($cropParameters) {
  219.                 return $this->cropper->crop(
  220.                     $layer,
  221.                     $cropParameters['x'],
  222.                     $cropParameters['y'],
  223.                     $cropParameters['width'],
  224.                     $cropParameters['height']
  225.                 );
  226.             }
  227.         );
  228.     }
  229.     /**
  230.      * Crops the given image according to the focus point defined in the file version.
  231.      *
  232.      * @return ImageInterface
  233.      */
  234.     private function applyFocus(ImageInterface $imageFileVersion $fileVersion, array $scale)
  235.     {
  236.         return $this->modifyAllLayers(
  237.             $image,
  238.             function(ImageInterface $layer) use ($fileVersion$scale) {
  239.                 return $this->focus->focus(
  240.                     $layer,
  241.                     $fileVersion->getFocusPointX(),
  242.                     $fileVersion->getFocusPointY(),
  243.                     $scale['x'],
  244.                     $scale['y']
  245.                 );
  246.             }
  247.         );
  248.     }
  249.     /**
  250.      * Scales a given image according to the information passed as the second argument.
  251.      *
  252.      * @param array $scale
  253.      *
  254.      * @return ImageInterface
  255.      */
  256.     private function applyScale(ImageInterface $image$scale)
  257.     {
  258.         return $this->modifyAllLayers(
  259.             $image,
  260.             function(ImageInterface $layer) use ($scale) {
  261.                 return $this->scaler->scale(
  262.                     $layer,
  263.                     $scale['x'],
  264.                     $scale['y'],
  265.                     $scale['mode'],
  266.                     $scale['forceRatio'],
  267.                     $scale['retina']
  268.                 );
  269.             }
  270.         );
  271.     }
  272.     /**
  273.      * Ensures that the color mode of the passed image is RGB.
  274.      *
  275.      * @return ImageInterface $image The modified image
  276.      */
  277.     private function toRGB(ImageInterface $image)
  278.     {
  279.         if ('cmyk' == $image->palette()->name()) {
  280.             $image->usePalette(new RGB());
  281.         }
  282.         return $image;
  283.     }
  284.     /**
  285.      * Autorotate based on metadata of an image.
  286.      *
  287.      * @return ImageInterface
  288.      */
  289.     private function autorotate(ImageInterface $image)
  290.     {
  291.         $autorotateFilter = new Autorotate();
  292.         return $autorotateFilter->apply($image);
  293.     }
  294.     /**
  295.      * Constructs the parameters for the cropper. Returns null when
  296.      * the image should not be cropped.
  297.      *
  298.      * @param FormatOptions|null $formatOptions
  299.      *
  300.      * @return ?array
  301.      */
  302.     private function getCropParameters(ImageInterface $image$formatOptions, array $format)
  303.     {
  304.         if (isset($formatOptions)) {
  305.             $parameters = [
  306.                 'x' => $formatOptions->getCropX(),
  307.                 'y' => $formatOptions->getCropY(),
  308.                 'width' => $formatOptions->getCropWidth(),
  309.                 'height' => $formatOptions->getCropHeight(),
  310.             ];
  311.             if ($this->cropper->isValid(
  312.                 $image,
  313.                 $parameters['x'],
  314.                 $parameters['y'],
  315.                 $parameters['width'],
  316.                 $parameters['height'],
  317.                 $format
  318.             )) {
  319.                 return $parameters;
  320.             }
  321.         }
  322.         return null;
  323.     }
  324.     /**
  325.      * Applies a callback to every layer of an image and returns the resulting image.
  326.      *
  327.      * @param callable $modifier The callable to apply to all layers
  328.      *
  329.      * @return ImageInterface
  330.      */
  331.     private function modifyAllLayers(ImageInterface $image, callable $modifier)
  332.     {
  333.         try {
  334.             $layers $image->layers();
  335.         } catch (RuntimeException $exception) {
  336.             $layers = [];
  337.         }
  338.         if (\count($layers) > 1) {
  339.             $countLayer 0;
  340.             $image->layers()->coalesce();
  341.             /** @var ImageInterface $temporaryImage */
  342.             $temporaryImage null;
  343.             foreach ($image->layers() as $layer) {
  344.                 ++$countLayer;
  345.                 $layer \call_user_func($modifier$layer);
  346.                 if (=== $countLayer) {
  347.                     $temporaryImage = clone $layer// use first layer as main image
  348.                 } else {
  349.                     $temporaryImage->layers()->add($layer);
  350.                 }
  351.             }
  352.             $image $temporaryImage;
  353.         } else {
  354.             $image \call_user_func($modifier$image);
  355.         }
  356.         return $image;
  357.     }
  358.     /**
  359.      * Return the options for the given format.
  360.      *
  361.      * @param string $formatKey
  362.      *
  363.      * @return array
  364.      *
  365.      * @throws ImageProxyInvalidImageFormat
  366.      */
  367.     private function getFormat($formatKey)
  368.     {
  369.         if (!isset($this->formats[$formatKey])) {
  370.             throw new ImageProxyInvalidImageFormat('Format was not found');
  371.         }
  372.         return $this->formats[$formatKey];
  373.     }
  374.     /**
  375.      * @param string $imageExtension
  376.      * @param array $imagineOptions
  377.      *
  378.      * @return array
  379.      */
  380.     private function getOptionsFromImage(ImageInterface $image$imageExtension$imagineOptions)
  381.     {
  382.         $options = [];
  383.         if ('gif' == $imageExtension && \count($image->layers()) > 1) {
  384.             $options['animated'] = true;
  385.             $options['optimize'] = true;
  386.         }
  387.         return \array_merge($options$imagineOptions);
  388.     }
  389. }