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

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