vendor/contao/image/src/Resizer.php line 77

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\InvalidArgumentException;
  12. use Contao\Image\Metadata\ImageMetadata;
  13. use Contao\Image\Metadata\MetadataReaderWriter;
  14. use Contao\ImagineSvg\Image as SvgImage;
  15. use Imagine\Exception\InvalidArgumentException as ImagineInvalidArgumentException;
  16. use Imagine\Exception\RuntimeException as ImagineRuntimeException;
  17. use Imagine\Filter\Basic\Autorotate;
  18. use Imagine\Gd\Image as GdImage;
  19. use Imagine\Image\ImageInterface as ImagineImageInterface;
  20. use Imagine\Image\Palette\RGB;
  21. use Symfony\Component\Filesystem\Filesystem;
  22. use Symfony\Component\Filesystem\Path;
  23. /**
  24.  * @method __construct(string $cacheDir, string $secret, ResizeCalculator $calculator = null, Filesystem $filesystem = null, MetadataReaderWriter $metadataReaderWriter = null)
  25.  */
  26. class Resizer implements ResizerInterface
  27. {
  28.     /**
  29.      * @var Filesystem
  30.      *
  31.      * @internal
  32.      */
  33.     protected $filesystem;
  34.     /**
  35.      * @var string
  36.      *
  37.      * @internal
  38.      */
  39.     protected $cacheDir;
  40.     /**
  41.      * @var ResizeCalculator
  42.      */
  43.     private $calculator;
  44.     /**
  45.      * @var MetadataReaderWriter
  46.      */
  47.     private $metadataReaderWriter;
  48.     /**
  49.      * @var string|null
  50.      */
  51.     private $secret;
  52.     /**
  53.      * @param string                    $cacheDir
  54.      * @param string                    $secret
  55.      * @param ResizeCalculator|null     $calculator
  56.      * @param Filesystem|null           $filesystem
  57.      * @param MetadataReaderWriter|null $metadataReaderWriter
  58.      */
  59.     public function __construct(string $cacheDir/*, string $secret, ResizeCalculator $calculator = null, Filesystem $filesystem = null, MetadataReaderWriter $metadataReaderWriter = null*/)
  60.     {
  61.         if (\func_num_args() > && \is_string(func_get_arg(1))) {
  62.             $secret func_get_arg(1);
  63.             $calculator \func_num_args() > func_get_arg(2) : null;
  64.             $filesystem \func_num_args() > func_get_arg(3) : null;
  65.             $metadataReaderWriter \func_num_args() > func_get_arg(4) : null;
  66.         } else {
  67.             trigger_deprecation('contao/image''1.2''Not passing a secret to "%s()" has been deprecated and will no longer work in version 2.0.'__METHOD__);
  68.             $secret null;
  69.             $calculator \func_num_args() > func_get_arg(1) : null;
  70.             $filesystem \func_num_args() > func_get_arg(2) : null;
  71.             $metadataReaderWriter \func_num_args() > func_get_arg(3) : null;
  72.         }
  73.         if (null === $calculator) {
  74.             $calculator = new ResizeCalculator();
  75.         }
  76.         if (null === $filesystem) {
  77.             $filesystem = new Filesystem();
  78.         }
  79.         if (null === $metadataReaderWriter) {
  80.             $metadataReaderWriter = new MetadataReaderWriter();
  81.         }
  82.         if (!$calculator instanceof ResizeCalculator) {
  83.             $type \is_object($calculator) ? \get_class($calculator) : \gettype($calculator);
  84.             throw new \TypeError(sprintf('%s(): Argument #3 ($calculator) must be of type ResizeCalculator|null, %s given'__METHOD__$type));
  85.         }
  86.         if (!$filesystem instanceof Filesystem) {
  87.             $type \is_object($filesystem) ? \get_class($filesystem) : \gettype($filesystem);
  88.             throw new \TypeError(sprintf('%s(): Argument #4 ($filesystem) must be of type ResizeCalculator|null, %s given'__METHOD__$type));
  89.         }
  90.         if (!$metadataReaderWriter instanceof MetadataReaderWriter) {
  91.             $type \is_object($metadataReaderWriter) ? \get_class($metadataReaderWriter) : \gettype($metadataReaderWriter);
  92.             throw new \TypeError(sprintf('%s(): Argument #5 ($metadataReaderWriter) must be of type MetadataReaderWriter|null, %s given'__METHOD__$type));
  93.         }
  94.         if ('' === $secret) {
  95.             throw new InvalidArgumentException('$secret must not be empty');
  96.         }
  97.         $this->cacheDir $cacheDir;
  98.         $this->calculator $calculator;
  99.         $this->filesystem $filesystem;
  100.         $this->metadataReaderWriter $metadataReaderWriter;
  101.         $this->secret $secret;
  102.     }
  103.     /**
  104.      * {@inheritdoc}
  105.      */
  106.     public function resize(ImageInterface $imageResizeConfiguration $configResizeOptions $options): ImageInterface
  107.     {
  108.         if (
  109.             $image->getDimensions()->isUndefined()
  110.             || ($config->isEmpty() && $this->canSkipResize($image$options))
  111.         ) {
  112.             $image $this->createImage($image$image->getPath());
  113.         } else {
  114.             $image $this->processResize($image$config$options);
  115.         }
  116.         if (null !== $options->getTargetPath()) {
  117.             $this->filesystem->copy($image->getPath(), $options->getTargetPath(), true);
  118.             $image $this->createImage($image$options->getTargetPath());
  119.         }
  120.         return $image;
  121.     }
  122.     /**
  123.      * Executes the resize operation via Imagine.
  124.      *
  125.      * @internal Do not call this method in your code; it will be made private in a future version
  126.      */
  127.     protected function executeResize(ImageInterface $imageResizeCoordinates $coordinatesstring $pathResizeOptions $options): ImageInterface
  128.     {
  129.         $dir \dirname($path);
  130.         if (!$this->filesystem->exists($dir)) {
  131.             $this->filesystem->mkdir($dir);
  132.         }
  133.         $imagineOptions $options->getImagineOptions();
  134.         $imagineImage $image->getImagine()->open($image->getPath());
  135.         if (ImageDimensions::ORIENTATION_NORMAL !== $image->getDimensions()->getOrientation()) {
  136.             (new Autorotate())->apply($imagineImage);
  137.         }
  138.         if ($imagineImage instanceof SvgImage || $imagineImage instanceof GdImage) {
  139.             // Backwards compatibility with imagine/imagine <= 1.2.4 and contao/imagine-svg <= 1.0.3
  140.             $filter ImagineImageInterface::FILTER_UNDEFINED;
  141.         } else {
  142.             $filter $imagineOptions['resampling-filter'] ?? ImagineImageInterface::FILTER_LANCZOS;
  143.         }
  144.         $imagineImage
  145.             ->resize($coordinates->getSize(), $filter)
  146.             ->crop($coordinates->getCropStart(), $coordinates->getCropSize())
  147.             ->usePalette(new RGB())
  148.             ->strip()
  149.         ;
  150.         if (isset($imagineOptions['interlace'])) {
  151.             try {
  152.                 $imagineImage->interlace($imagineOptions['interlace']);
  153.             } catch (ImagineInvalidArgumentException|ImagineRuntimeException $e) {
  154.                 // Ignore failed interlacing
  155.             }
  156.         }
  157.         if (!isset($imagineOptions['format'])) {
  158.             $imagineOptions['format'] = strtolower(pathinfo($pathPATHINFO_EXTENSION));
  159.         }
  160.         // Fix bug with undefined index notice in Imagine
  161.         if ('webp' === $imagineOptions['format'] && !isset($imagineOptions['webp_quality'])) {
  162.             $imagineOptions['webp_quality'] = 80;
  163.         }
  164.         $tmpPath1 $this->filesystem->tempnam($dir'img');
  165.         $tmpPath2 $this->filesystem->tempnam($dir'img');
  166.         $this->filesystem->chmod([$tmpPath1$tmpPath2], 0666umask());
  167.         if ($options->getPreserveCopyrightMetadata() && ($metadata $this->getMetadata($image))->getAll()) {
  168.             $imagineImage->save($tmpPath1$imagineOptions);
  169.             try {
  170.                 $this->metadataReaderWriter->applyCopyrightToFile($tmpPath1$tmpPath2$metadata$options->getPreserveCopyrightMetadata());
  171.             } catch (\Throwable $exception) {
  172.                 $this->filesystem->rename($tmpPath1$tmpPath2true);
  173.             }
  174.         } else {
  175.             $imagineImage->save($tmpPath2$imagineOptions);
  176.         }
  177.         $this->filesystem->remove($tmpPath1);
  178.         // Atomic write operation
  179.         $this->filesystem->rename($tmpPath2$pathtrue);
  180.         return $this->createImage($image$path);
  181.     }
  182.     /**
  183.      * Creates a new image instance for the specified path.
  184.      *
  185.      * @internal Do not call this method in your code; it will be made private in a future version
  186.      */
  187.     protected function createImage(ImageInterface $imagestring $path): ImageInterface
  188.     {
  189.         return new Image($path$image->getImagine(), $this->filesystem);
  190.     }
  191.     /**
  192.      * Processes the resize and executes it if not already cached.
  193.      *
  194.      * @internal
  195.      */
  196.     protected function processResize(ImageInterface $imageResizeConfiguration $configResizeOptions $options): ImageInterface
  197.     {
  198.         $coordinates $this->calculator->calculate($config$image->getDimensions(), $image->getImportantPart());
  199.         // Skip resizing if it would have no effect
  200.         if (
  201.             $this->canSkipResize($image$options)
  202.             && !$image->getDimensions()->isRelative()
  203.             && $coordinates->isEqualTo($image->getDimensions()->getSize())
  204.         ) {
  205.             return $this->createImage($image$image->getPath());
  206.         }
  207.         $cachePath Path::join($this->cacheDir$this->createCachePath($image->getPath(), $coordinates$optionsfalse));
  208.         if (!$options->getBypassCache()) {
  209.             if ($this->filesystem->exists($cachePath)) {
  210.                 return $this->createImage($image$cachePath);
  211.             }
  212.             $legacyCachePath Path::join($this->cacheDir$this->createCachePath($image->getPath(), $coordinates$optionstrue));
  213.             if ($this->filesystem->exists($legacyCachePath)) {
  214.                 trigger_deprecation('contao/image''1.2''Reusing old cached images like "%s" from version 1.1 has been deprecated and will no longer work in version 2.0. Clear the image cache directory "%s" and regenerate all images to get rid of this message.'$legacyCachePath$this->cacheDir);
  215.                 return $this->createImage($image$legacyCachePath);
  216.             }
  217.         }
  218.         return $this->executeResize($image$coordinates$cachePath$options);
  219.     }
  220.     private function canSkipResize(ImageInterface $imageResizeOptions $options): bool
  221.     {
  222.         if (!$options->getSkipIfDimensionsMatch()) {
  223.             return false;
  224.         }
  225.         if (ImageDimensions::ORIENTATION_NORMAL !== $image->getDimensions()->getOrientation()) {
  226.             return false;
  227.         }
  228.         if (
  229.             isset($options->getImagineOptions()['format'])
  230.             && $options->getImagineOptions()['format'] !== strtolower(pathinfo($image->getPath(), PATHINFO_EXTENSION))
  231.         ) {
  232.             return false;
  233.         }
  234.         return true;
  235.     }
  236.     /**
  237.      * Returns the relative target cache path.
  238.      */
  239.     private function createCachePath(string $pathResizeCoordinates $coordinatesResizeOptions $optionsbool $useLegacyHash): string
  240.     {
  241.         $imagineOptions $options->getImagineOptions();
  242.         ksort($imagineOptions);
  243.         $hashData array_merge(
  244.             [
  245.                 Path::makeRelative($path$this->cacheDir),
  246.                 filemtime($path),
  247.                 $coordinates->getHash(),
  248.             ],
  249.             array_keys($imagineOptions),
  250.             array_map(
  251.                 static function ($value) {
  252.                     return \is_array($value) ? implode(','$value) : $value;
  253.                 },
  254.                 array_values($imagineOptions)
  255.             )
  256.         );
  257.         $preserveMeta $options->getPreserveCopyrightMetadata();
  258.         if ($preserveMeta !== (new ResizeOptions())->getPreserveCopyrightMetadata()) {
  259.             ksort($preserveMetaSORT_STRING);
  260.             $hashData[] = json_encode($preserveMeta);
  261.         }
  262.         if ($useLegacyHash || null === $this->secret) {
  263.             $hash substr(md5(implode('|'$hashData)), 09);
  264.         } else {
  265.             $hash hash_hmac('sha256'implode('|'$hashData), $this->secrettrue);
  266.             $hash substr($this->encodeBase32($hash), 016);
  267.         }
  268.         $pathinfo pathinfo($path);
  269.         $extension $options->getImagineOptions()['format'] ?? strtolower($pathinfo['extension']);
  270.         return Path::join($hash[0], $pathinfo['filename'].'-'.substr($hash1).'.'.$extension);
  271.     }
  272.     private function getMetadata(ImageInterface $image): ImageMetadata
  273.     {
  274.         try {
  275.             return $this->metadataReaderWriter->parse($image->getPath());
  276.         } catch (\Throwable $exception) {
  277.             return new ImageMetadata([]);
  278.         }
  279.     }
  280.     /**
  281.      * Encode a string with Crockford’s Base32 in lowercase
  282.      * (0123456789abcdefghjkmnpqrstvwxyz).
  283.      */
  284.     private function encodeBase32(string $bytes): string
  285.     {
  286.         $result = [];
  287.         foreach (str_split($bytes5) as $chunk) {
  288.             $result[] = substr(
  289.                 str_pad(
  290.                     strtr(
  291.                         base_convert(bin2hex(str_pad($chunk5"\0")), 1632),
  292.                         'ijklmnopqrstuv',
  293.                         'jkmnpqrstvwxyz' // Crockford's Base32
  294.                     ),
  295.                     8,
  296.                     '0',
  297.                     STR_PAD_LEFT
  298.                 ),
  299.                 0,
  300.                 (int) ceil(\strlen($chunk) * 5)
  301.             );
  302.         }
  303.         return implode(''$result);
  304.     }
  305. }