<?php
declare(strict_types=1);
/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/
namespace Contao\Image;
use Imagine\Exception\RuntimeException as ImagineRuntimeException;
use Imagine\Filter\Basic\Autorotate;
use Imagine\Image\Palette\RGB;
use Symfony\Component\Filesystem\Filesystem;
use Webmozart\PathUtil\Path;
class Resizer implements ResizerInterface
{
/**
* @var Filesystem
*
* @internal
*/
protected $filesystem;
/**
* @var string
*
* @internal
*/
protected $cacheDir;
/**
* @var ResizeCalculator
*/
private $calculator;
public function __construct(string $cacheDir, ResizeCalculator $calculator = null, Filesystem $filesystem = null)
{
if (null === $calculator) {
$calculator = new ResizeCalculator();
}
if (null === $filesystem) {
$filesystem = new Filesystem();
}
$this->cacheDir = $cacheDir;
$this->calculator = $calculator;
$this->filesystem = $filesystem;
}
/**
* {@inheritdoc}
*/
public function resize(ImageInterface $image, ResizeConfiguration $config, ResizeOptions $options): ImageInterface
{
if (
$image->getDimensions()->isUndefined()
|| ($config->isEmpty() && $this->canSkipResize($image, $options))
) {
$image = $this->createImage($image, $image->getPath());
} else {
$image = $this->processResize($image, $config, $options);
}
if (null !== $options->getTargetPath()) {
$this->filesystem->copy($image->getPath(), $options->getTargetPath(), true);
$image = $this->createImage($image, $options->getTargetPath());
}
return $image;
}
/**
* Executes the resize operation via Imagine.
*
* @internal Do not call this method in your code; it will be made private in a future version
*/
protected function executeResize(ImageInterface $image, ResizeCoordinates $coordinates, string $path, ResizeOptions $options): ImageInterface
{
$dir = \dirname($path);
if (!$this->filesystem->exists($dir)) {
$this->filesystem->mkdir($dir);
}
$imagineOptions = $options->getImagineOptions();
$imagineImage = $image->getImagine()->open($image->getPath());
if (ImageDimensions::ORIENTATION_NORMAL !== $image->getDimensions()->getOrientation()) {
(new Autorotate())->apply($imagineImage);
}
$imagineImage
->resize($coordinates->getSize())
->crop($coordinates->getCropStart(), $coordinates->getCropSize())
->usePalette(new RGB())
->strip()
;
if (isset($imagineOptions['interlace'])) {
try {
$imagineImage->interlace($imagineOptions['interlace']);
} catch (ImagineRuntimeException $e) {
// Ignore failed interlacing
}
}
if (!isset($imagineOptions['format'])) {
$imagineOptions['format'] = strtolower(pathinfo($path, PATHINFO_EXTENSION));
}
// Fix bug with undefined index notice in Imagine
if ('webp' === $imagineOptions['format'] && !isset($imagineOptions['webp_quality'])) {
$imagineOptions['webp_quality'] = 80;
}
// Atomic write operation
$tmpPath = $this->filesystem->tempnam($dir, 'img');
$this->filesystem->chmod($tmpPath, 0666, umask());
$imagineImage->save($tmpPath, $imagineOptions);
$this->filesystem->rename($tmpPath, $path, true);
return $this->createImage($image, $path);
}
/**
* Creates a new image instance for the specified path.
*
* @internal Do not call this method in your code; it will be made private in a future version
*/
protected function createImage(ImageInterface $image, string $path): ImageInterface
{
return new Image($path, $image->getImagine(), $this->filesystem);
}
/**
* Processes the resize and executes it if not already cached.
*
* @internal
*/
protected function processResize(ImageInterface $image, ResizeConfiguration $config, ResizeOptions $options): ImageInterface
{
$coordinates = $this->calculator->calculate($config, $image->getDimensions(), $image->getImportantPart());
// Skip resizing if it would have no effect
if (
$this->canSkipResize($image, $options)
&& !$image->getDimensions()->isRelative()
&& $coordinates->isEqualTo($image->getDimensions()->getSize())
) {
return $this->createImage($image, $image->getPath());
}
$cachePath = $this->cacheDir.'/'.$this->createCachePath($image->getPath(), $coordinates, $options);
if ($this->filesystem->exists($cachePath) && !$options->getBypassCache()) {
return $this->createImage($image, $cachePath);
}
return $this->executeResize($image, $coordinates, $cachePath, $options);
}
private function canSkipResize(ImageInterface $image, ResizeOptions $options): bool
{
if (!$options->getSkipIfDimensionsMatch()) {
return false;
}
if (ImageDimensions::ORIENTATION_NORMAL !== $image->getDimensions()->getOrientation()) {
return false;
}
if (
isset($options->getImagineOptions()['format'])
&& $options->getImagineOptions()['format'] !== strtolower(pathinfo($image->getPath(), PATHINFO_EXTENSION))
) {
return false;
}
return true;
}
/**
* Returns the relative target cache path.
*/
private function createCachePath(string $path, ResizeCoordinates $coordinates, ResizeOptions $options): string
{
$imagineOptions = $options->getImagineOptions();
ksort($imagineOptions);
$hashData = array_merge(
[
Path::makeRelative($path, $this->cacheDir),
filemtime($path),
$coordinates->getHash(),
],
array_keys($imagineOptions),
array_map(
static function ($value) {
return \is_array($value) ? implode(',', $value) : $value;
},
array_values($imagineOptions)
)
);
$hash = substr(md5(implode('|', $hashData)), 0, 9);
$pathinfo = pathinfo($path);
$extension = $options->getImagineOptions()['format'] ?? strtolower($pathinfo['extension']);
return $hash[0].'/'.$pathinfo['filename'].'-'.substr($hash, 1).'.'.$extension;
}
}