vendor/contao/image/src/Image.php line 120

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4.  * This file is part of Contao.
  5.  *
  6.  * (c) Leo Feyer
  7.  *
  8.  * @license LGPL-3.0-or-later
  9.  */
  10. namespace Contao\Image;
  11. use Contao\Image\Exception\FileNotExistsException;
  12. use Contao\Image\Exception\InvalidArgumentException;
  13. use Contao\ImagineSvg\Image as SvgImage;
  14. use Contao\ImagineSvg\Imagine as SvgImagine;
  15. use DOMDocument;
  16. use Imagine\Image\Box;
  17. use Imagine\Image\BoxInterface;
  18. use Imagine\Image\ImagineInterface;
  19. use Imagine\Image\Metadata\MetadataBag;
  20. use Symfony\Component\Filesystem\Filesystem;
  21. use Webmozart\PathUtil\Path;
  22. use XMLReader;
  23. class Image implements ImageInterface
  24. {
  25.     /**
  26.      * @var string
  27.      *
  28.      * @internal
  29.      */
  30.     protected $path;
  31.     /**
  32.      * @var ImageDimensions
  33.      *
  34.      * @internal
  35.      */
  36.     protected $dimensions;
  37.     /**
  38.      * @var ImagineInterface
  39.      *
  40.      * @internal
  41.      */
  42.     protected $imagine;
  43.     /**
  44.      * @var ImportantPart
  45.      */
  46.     private $importantPart;
  47.     public function __construct(string $pathImagineInterface $imagineFilesystem $filesystem null)
  48.     {
  49.         if (null === $filesystem) {
  50.             $filesystem = new Filesystem();
  51.         }
  52.         if (!$filesystem->exists($path)) {
  53.             throw new FileNotExistsException($path.' does not exist');
  54.         }
  55.         if (is_dir($path)) {
  56.             throw new FileNotExistsException($path.' is a directory');
  57.         }
  58.         $this->path $path;
  59.         $this->imagine $imagine;
  60.     }
  61.     /**
  62.      * {@inheritdoc}
  63.      */
  64.     public function getImagine(): ImagineInterface
  65.     {
  66.         return $this->imagine;
  67.     }
  68.     /**
  69.      * {@inheritdoc}
  70.      */
  71.     public function getPath(): string
  72.     {
  73.         return $this->path;
  74.     }
  75.     /**
  76.      * {@inheritdoc}
  77.      */
  78.     public function getUrl(string $rootDirstring $prefix ''): string
  79.     {
  80.         if (!Path::isBasePath($rootDir$this->path)) {
  81.             throw new InvalidArgumentException(sprintf('Path "%s" is not inside root directory "%s"'$this->path$rootDir));
  82.         }
  83.         $url Path::makeRelative($this->path$rootDir);
  84.         $url str_replace('%2F''/'rawurlencode($url));
  85.         return $prefix.$url;
  86.     }
  87.     /**
  88.      * {@inheritdoc}
  89.      */
  90.     public function getDimensions(): ImageDimensions
  91.     {
  92.         if (null === $this->dimensions) {
  93.             // Try getSvgSize() or native exif_read_data()/getimagesize() for better performance
  94.             if ($this->imagine instanceof SvgImagine) {
  95.                 $size $this->getSvgSize();
  96.                 if (null !== $size) {
  97.                     $this->dimensions = new ImageDimensions($size);
  98.                 }
  99.             } elseif (
  100.                 \function_exists('exif_read_data')
  101.                 && ($exif = @exif_read_data($this->path'COMPUTED,IFD0'))
  102.                 && !empty($exif['COMPUTED']['Width'])
  103.                 && !empty($exif['COMPUTED']['Height'])
  104.             ) {
  105.                 $orientation $this->fixOrientation($exif['Orientation'] ?? null);
  106.                 $size $this->fixSizeOrientation(new Box($exif['COMPUTED']['Width'], $exif['COMPUTED']['Height']), $orientation);
  107.                 $this->dimensions = new ImageDimensions($sizenullnull$orientation);
  108.             } elseif (
  109.                 ($size = @getimagesize($this->path))
  110.                 && !empty($size[0]) && !empty($size[1])
  111.             ) {
  112.                 $this->dimensions = new ImageDimensions(new Box($size[0], $size[1]));
  113.             }
  114.             // Fall back to Imagine
  115.             if (null === $this->dimensions) {
  116.                 $imagineImage $this->imagine->open($this->path);
  117.                 $orientation $this->fixOrientation($imagineImage->metadata()->get('ifd0.Orientation'));
  118.                 $size $this->fixSizeOrientation($imagineImage->getSize(), $orientation);
  119.                 $this->dimensions = new ImageDimensions($sizenullnull$orientation);
  120.             }
  121.         }
  122.         return $this->dimensions;
  123.     }
  124.     /**
  125.      * {@inheritdoc}
  126.      */
  127.     public function getImportantPart(): ImportantPart
  128.     {
  129.         return $this->importantPart ?? new ImportantPart();
  130.     }
  131.     /**
  132.      * {@inheritdoc}
  133.      */
  134.     public function setImportantPart(ImportantPart $importantPart null): ImageInterface
  135.     {
  136.         $this->importantPart $importantPart;
  137.         return $this;
  138.     }
  139.     /**
  140.      * Corrects invalid EXIF orientation values.
  141.      */
  142.     private function fixOrientation($orientation): int
  143.     {
  144.         $orientation = (int) $orientation;
  145.         if ($orientation || $orientation 8) {
  146.             return ImageDimensions::ORIENTATION_NORMAL;
  147.         }
  148.         return $orientation;
  149.     }
  150.     /**
  151.      * Swaps width and height for (-/+)90 degree rotated orientations.
  152.      */
  153.     private function fixSizeOrientation(BoxInterface $sizeint $orientation): BoxInterface
  154.     {
  155.         if (
  156.             \in_array(
  157.                 $orientation,
  158.                 [
  159.                     ImageDimensions::ORIENTATION_90,
  160.                     ImageDimensions::ORIENTATION_270,
  161.                     ImageDimensions::ORIENTATION_MIRROR_90,
  162.                     ImageDimensions::ORIENTATION_MIRROR_270,
  163.                 ],
  164.                 true
  165.             )
  166.         ) {
  167.             return new Box($size->getHeight(), $size->getWidth());
  168.         }
  169.         return $size;
  170.     }
  171.     /**
  172.      * Reads the SVG image file partially and returns the size of it.
  173.      *
  174.      * This is faster than reading and parsing the whole SVG file just to get
  175.      * the size of it, especially for large files.
  176.      */
  177.     private function getSvgSize(): ?BoxInterface
  178.     {
  179.         if (!class_exists(SvgImage::class) || !class_exists(XMLReader::class) || !class_exists(DOMDocument::class)) {
  180.             return null;
  181.         }
  182.         static $zlibSupport;
  183.         if (null === $zlibSupport) {
  184.             $reader = new XMLReader();
  185.             $zlibSupport = \in_array('compress.zlib'stream_get_wrappers(), true)
  186.                 && true === @$reader->open('compress.zlib://data:text/xml,<x/>')
  187.                 && true === @$reader->read()
  188.                 && true === @$reader->close();
  189.         }
  190.         $size null;
  191.         $reader = new XMLReader();
  192.         $path $this->path;
  193.         if ($zlibSupport) {
  194.             $path 'compress.zlib://'.$path;
  195.         }
  196.         $disableEntities null;
  197.         if (LIBXML_VERSION 20900) {
  198.             // Enable the entity loader at first to make XMLReader::open() work
  199.             // see https://bugs.php.net/bug.php?id=73328
  200.             $disableEntities libxml_disable_entity_loader(false);
  201.         }
  202.         $internalErrors libxml_use_internal_errors(true);
  203.         if ($reader->open($pathnullLIBXML_NONET)) {
  204.             if (LIBXML_VERSION 20900) {
  205.                 // After opening the file disable the entity loader for security reasons
  206.                 libxml_disable_entity_loader();
  207.             }
  208.             $size $this->getSvgSizeFromReader($reader);
  209.             $reader->close();
  210.         }
  211.         if (LIBXML_VERSION 20900) {
  212.             libxml_disable_entity_loader($disableEntities);
  213.         }
  214.         libxml_use_internal_errors($internalErrors);
  215.         libxml_clear_errors();
  216.         return $size;
  217.     }
  218.     /**
  219.      * Extracts the SVG image size from the given XMLReader object.
  220.      */
  221.     private function getSvgSizeFromReader(XMLReader $reader): ?BoxInterface
  222.     {
  223.         // Move the pointer to the first element in the document
  224.         while ($reader->read() && XMLReader::ELEMENT !== $reader->nodeType);
  225.         if (XMLReader::ELEMENT !== $reader->nodeType || 'svg' !== $reader->name) {
  226.             return null;
  227.         }
  228.         $document = new DOMDocument();
  229.         $svg $document->createElement('svg');
  230.         $document->appendChild($svg);
  231.         foreach (['width''height''viewBox'] as $key) {
  232.             if ($value $reader->getAttribute($key)) {
  233.                 $svg->setAttribute($key$value);
  234.             }
  235.         }
  236.         $image = new SvgImage($document, new MetadataBag());
  237.         return $image->getSize();
  238.     }
  239. }