<?php
declare(strict_types=1);
/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/
namespace Contao\Image;
use Contao\Image\Exception\FileNotExistsException;
use Contao\Image\Exception\InvalidArgumentException;
use Contao\ImagineSvg\Image as SvgImage;
use Contao\ImagineSvg\Imagine as SvgImagine;
use DOMDocument;
use Imagine\Image\Box;
use Imagine\Image\BoxInterface;
use Imagine\Image\ImagineInterface;
use Imagine\Image\Metadata\MetadataBag;
use Symfony\Component\Filesystem\Filesystem;
use Webmozart\PathUtil\Path;
use XMLReader;
class Image implements ImageInterface
{
/**
* @var string
*
* @internal
*/
protected $path;
/**
* @var ImageDimensions
*
* @internal
*/
protected $dimensions;
/**
* @var ImagineInterface
*
* @internal
*/
protected $imagine;
/**
* @var ImportantPart
*/
private $importantPart;
public function __construct(string $path, ImagineInterface $imagine, Filesystem $filesystem = null)
{
if (null === $filesystem) {
$filesystem = new Filesystem();
}
if (!$filesystem->exists($path)) {
throw new FileNotExistsException($path.' does not exist');
}
if (is_dir($path)) {
throw new FileNotExistsException($path.' is a directory');
}
$this->path = $path;
$this->imagine = $imagine;
}
/**
* {@inheritdoc}
*/
public function getImagine(): ImagineInterface
{
return $this->imagine;
}
/**
* {@inheritdoc}
*/
public function getPath(): string
{
return $this->path;
}
/**
* {@inheritdoc}
*/
public function getUrl(string $rootDir, string $prefix = ''): string
{
if (!Path::isBasePath($rootDir, $this->path)) {
throw new InvalidArgumentException(sprintf('Path "%s" is not inside root directory "%s"', $this->path, $rootDir));
}
$url = Path::makeRelative($this->path, $rootDir);
$url = str_replace('%2F', '/', rawurlencode($url));
return $prefix.$url;
}
/**
* {@inheritdoc}
*/
public function getDimensions(): ImageDimensions
{
if (null === $this->dimensions) {
// Try getSvgSize() or native exif_read_data()/getimagesize() for better performance
if ($this->imagine instanceof SvgImagine) {
$size = $this->getSvgSize();
if (null !== $size) {
$this->dimensions = new ImageDimensions($size);
}
} elseif (
\function_exists('exif_read_data')
&& ($exif = @exif_read_data($this->path, 'COMPUTED,IFD0'))
&& !empty($exif['COMPUTED']['Width'])
&& !empty($exif['COMPUTED']['Height'])
) {
$orientation = $this->fixOrientation($exif['Orientation'] ?? null);
$size = $this->fixSizeOrientation(new Box($exif['COMPUTED']['Width'], $exif['COMPUTED']['Height']), $orientation);
$this->dimensions = new ImageDimensions($size, null, null, $orientation);
} elseif (
($size = @getimagesize($this->path))
&& !empty($size[0]) && !empty($size[1])
) {
$this->dimensions = new ImageDimensions(new Box($size[0], $size[1]));
}
// Fall back to Imagine
if (null === $this->dimensions) {
$imagineImage = $this->imagine->open($this->path);
$orientation = $this->fixOrientation($imagineImage->metadata()->get('ifd0.Orientation'));
$size = $this->fixSizeOrientation($imagineImage->getSize(), $orientation);
$this->dimensions = new ImageDimensions($size, null, null, $orientation);
}
}
return $this->dimensions;
}
/**
* {@inheritdoc}
*/
public function getImportantPart(): ImportantPart
{
return $this->importantPart ?? new ImportantPart();
}
/**
* {@inheritdoc}
*/
public function setImportantPart(ImportantPart $importantPart = null): ImageInterface
{
$this->importantPart = $importantPart;
return $this;
}
/**
* Corrects invalid EXIF orientation values.
*/
private function fixOrientation($orientation): int
{
$orientation = (int) $orientation;
if ($orientation < 1 || $orientation > 8) {
return ImageDimensions::ORIENTATION_NORMAL;
}
return $orientation;
}
/**
* Swaps width and height for (-/+)90 degree rotated orientations.
*/
private function fixSizeOrientation(BoxInterface $size, int $orientation): BoxInterface
{
if (
\in_array(
$orientation,
[
ImageDimensions::ORIENTATION_90,
ImageDimensions::ORIENTATION_270,
ImageDimensions::ORIENTATION_MIRROR_90,
ImageDimensions::ORIENTATION_MIRROR_270,
],
true
)
) {
return new Box($size->getHeight(), $size->getWidth());
}
return $size;
}
/**
* Reads the SVG image file partially and returns the size of it.
*
* This is faster than reading and parsing the whole SVG file just to get
* the size of it, especially for large files.
*/
private function getSvgSize(): ?BoxInterface
{
if (!class_exists(SvgImage::class) || !class_exists(XMLReader::class) || !class_exists(DOMDocument::class)) {
return null;
}
static $zlibSupport;
if (null === $zlibSupport) {
$reader = new XMLReader();
$zlibSupport = \in_array('compress.zlib', stream_get_wrappers(), true)
&& true === @$reader->open('compress.zlib://data:text/xml,<x/>')
&& true === @$reader->read()
&& true === @$reader->close();
}
$size = null;
$reader = new XMLReader();
$path = $this->path;
if ($zlibSupport) {
$path = 'compress.zlib://'.$path;
}
$disableEntities = null;
if (LIBXML_VERSION < 20900) {
// Enable the entity loader at first to make XMLReader::open() work
// see https://bugs.php.net/bug.php?id=73328
$disableEntities = libxml_disable_entity_loader(false);
}
$internalErrors = libxml_use_internal_errors(true);
if ($reader->open($path, null, LIBXML_NONET)) {
if (LIBXML_VERSION < 20900) {
// After opening the file disable the entity loader for security reasons
libxml_disable_entity_loader();
}
$size = $this->getSvgSizeFromReader($reader);
$reader->close();
}
if (LIBXML_VERSION < 20900) {
libxml_disable_entity_loader($disableEntities);
}
libxml_use_internal_errors($internalErrors);
libxml_clear_errors();
return $size;
}
/**
* Extracts the SVG image size from the given XMLReader object.
*/
private function getSvgSizeFromReader(XMLReader $reader): ?BoxInterface
{
// Move the pointer to the first element in the document
while ($reader->read() && XMLReader::ELEMENT !== $reader->nodeType);
if (XMLReader::ELEMENT !== $reader->nodeType || 'svg' !== $reader->name) {
return null;
}
$document = new DOMDocument();
$svg = $document->createElement('svg');
$document->appendChild($svg);
foreach (['width', 'height', 'viewBox'] as $key) {
if ($value = $reader->getAttribute($key)) {
$svg->setAttribute($key, $value);
}
}
$image = new SvgImage($document, new MetadataBag());
return $image->getSize();
}
}