xref: /webtrees/app/Factories/ImageFactory.php (revision d11be7027e34e3121be11cc025421873364403f9)
16577bfc3SGreg Roach<?php
26577bfc3SGreg Roach
36577bfc3SGreg Roach/**
46577bfc3SGreg Roach * webtrees: online genealogy
5*d11be702SGreg Roach * Copyright (C) 2023 webtrees development team
66577bfc3SGreg Roach * This program is free software: you can redistribute it and/or modify
76577bfc3SGreg Roach * it under the terms of the GNU General Public License as published by
86577bfc3SGreg Roach * the Free Software Foundation, either version 3 of the License, or
96577bfc3SGreg Roach * (at your option) any later version.
106577bfc3SGreg Roach * This program is distributed in the hope that it will be useful,
116577bfc3SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
126577bfc3SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
136577bfc3SGreg Roach * GNU General Public License for more details.
146577bfc3SGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
166577bfc3SGreg Roach */
176577bfc3SGreg Roach
186577bfc3SGreg Roachdeclare(strict_types=1);
196577bfc3SGreg Roach
206577bfc3SGreg Roachnamespace Fisharebest\Webtrees\Factories;
216577bfc3SGreg Roach
226577bfc3SGreg Roachuse Fig\Http\Message\StatusCodeInterface;
236577bfc3SGreg Roachuse Fisharebest\Webtrees\Auth;
246577bfc3SGreg Roachuse Fisharebest\Webtrees\Contracts\ImageFactoryInterface;
256577bfc3SGreg Roachuse Fisharebest\Webtrees\Contracts\UserInterface;
266577bfc3SGreg Roachuse Fisharebest\Webtrees\MediaFile;
27f0448b68SGreg Roachuse Fisharebest\Webtrees\Mime;
286b9cb339SGreg Roachuse Fisharebest\Webtrees\Registry;
296577bfc3SGreg Roachuse Fisharebest\Webtrees\Webtrees;
306577bfc3SGreg Roachuse Imagick;
316577bfc3SGreg Roachuse Intervention\Image\Constraint;
326577bfc3SGreg Roachuse Intervention\Image\Exception\NotReadableException;
336577bfc3SGreg Roachuse Intervention\Image\Exception\NotSupportedException;
346577bfc3SGreg Roachuse Intervention\Image\Image;
356577bfc3SGreg Roachuse Intervention\Image\ImageManager;
36f7cf8a15SGreg Roachuse League\Flysystem\FilesystemException;
37f7cf8a15SGreg Roachuse League\Flysystem\FilesystemOperator;
38f32d77e6SGreg Roachuse League\Flysystem\UnableToReadFile;
39f0448b68SGreg Roachuse League\Flysystem\UnableToRetrieveMetadata;
406577bfc3SGreg Roachuse Psr\Http\Message\ResponseInterface;
416577bfc3SGreg Roachuse RuntimeException;
426577bfc3SGreg Roachuse Throwable;
436577bfc3SGreg Roach
446577bfc3SGreg Roachuse function addcslashes;
456577bfc3SGreg Roachuse function basename;
466577bfc3SGreg Roachuse function extension_loaded;
47f32d77e6SGreg Roachuse function get_class;
487729532eSGreg Roachuse function implode;
496577bfc3SGreg Roachuse function pathinfo;
506577bfc3SGreg Roachuse function response;
517729532eSGreg Roachuse function str_contains;
526577bfc3SGreg Roachuse function view;
536577bfc3SGreg Roach
546577bfc3SGreg Roachuse const PATHINFO_EXTENSION;
556577bfc3SGreg Roach
566577bfc3SGreg Roach/**
576577bfc3SGreg Roach * Make an image (from another image).
586577bfc3SGreg Roach */
596577bfc3SGreg Roachclass ImageFactory implements ImageFactoryInterface
606577bfc3SGreg Roach{
616577bfc3SGreg Roach    // Imagick can detect the quality setting for images.  GD cannot.
626577bfc3SGreg Roach    protected const GD_DEFAULT_IMAGE_QUALITY     = 90;
636577bfc3SGreg Roach    protected const GD_DEFAULT_THUMBNAIL_QUALITY = 70;
646577bfc3SGreg Roach
656577bfc3SGreg Roach    protected const WATERMARK_FILE = 'resources/img/watermark.png';
666577bfc3SGreg Roach
676577bfc3SGreg Roach    protected const THUMBNAIL_CACHE_TTL = 8640000;
686577bfc3SGreg Roach
696577bfc3SGreg Roach    protected const INTERVENTION_DRIVERS = ['imagick', 'gd'];
706577bfc3SGreg Roach
7171166947SGreg Roach    public const SUPPORTED_FORMATS = [
726577bfc3SGreg Roach        'image/jpeg' => 'jpg',
736577bfc3SGreg Roach        'image/png'  => 'png',
746577bfc3SGreg Roach        'image/gif'  => 'gif',
756577bfc3SGreg Roach        'image/tiff' => 'tif',
766577bfc3SGreg Roach        'image/bmp'  => 'bmp',
776577bfc3SGreg Roach        'image/webp' => 'webp',
786577bfc3SGreg Roach    ];
796577bfc3SGreg Roach
806577bfc3SGreg Roach    /**
816577bfc3SGreg Roach     * Send the original file - either inline or as a download.
826577bfc3SGreg Roach     *
83f7cf8a15SGreg Roach     * @param FilesystemOperator $filesystem
846577bfc3SGreg Roach     * @param string             $path
856577bfc3SGreg Roach     * @param bool               $download
866577bfc3SGreg Roach     *
876577bfc3SGreg Roach     * @return ResponseInterface
886577bfc3SGreg Roach     */
89f7cf8a15SGreg Roach    public function fileResponse(FilesystemOperator $filesystem, string $path, bool $download): ResponseInterface
906577bfc3SGreg Roach    {
916577bfc3SGreg Roach        try {
92f0448b68SGreg Roach            try {
937729532eSGreg Roach                $mime_type = $filesystem->mimeType($path);
9428d026adSGreg Roach            } catch (UnableToRetrieveMetadata) {
957729532eSGreg Roach                $mime_type = Mime::DEFAULT_TYPE;
96f0448b68SGreg Roach            }
97f0448b68SGreg Roach
987729532eSGreg Roach            $filename = $download ? addcslashes(basename($path), '"') : '';
996577bfc3SGreg Roach
1007729532eSGreg Roach            return $this->imageResponse($filesystem->read($path), $mime_type, $filename);
101fc904122SGreg Roach        } catch (UnableToReadFile | FilesystemException $ex) {
1022a4e09d7SGreg Roach            return $this->replacementImageResponse((string) StatusCodeInterface::STATUS_NOT_FOUND)
1032a4e09d7SGreg Roach                ->withHeader('x-thumbnail-exception', get_class($ex) . ': ' . $ex->getMessage());
1046577bfc3SGreg Roach        }
1056577bfc3SGreg Roach    }
1066577bfc3SGreg Roach
1076577bfc3SGreg Roach    /**
1086577bfc3SGreg Roach     * Send a thumbnail.
1096577bfc3SGreg Roach     *
110f7cf8a15SGreg Roach     * @param FilesystemOperator $filesystem
1116577bfc3SGreg Roach     * @param string             $path
1126577bfc3SGreg Roach     * @param int                $width
1136577bfc3SGreg Roach     * @param int                $height
1146577bfc3SGreg Roach     * @param string             $fit
1156577bfc3SGreg Roach     *
1166577bfc3SGreg Roach     *
1176577bfc3SGreg Roach     * @return ResponseInterface
1186577bfc3SGreg Roach     */
1196577bfc3SGreg Roach    public function thumbnailResponse(
120f7cf8a15SGreg Roach        FilesystemOperator $filesystem,
1216577bfc3SGreg Roach        string $path,
1226577bfc3SGreg Roach        int $width,
1236577bfc3SGreg Roach        int $height,
1246577bfc3SGreg Roach        string $fit
1256577bfc3SGreg Roach    ): ResponseInterface {
1266577bfc3SGreg Roach        try {
1276577bfc3SGreg Roach            $image = $this->imageManager()->make($filesystem->readStream($path));
1286577bfc3SGreg Roach            $image = $this->autorotateImage($image);
1296577bfc3SGreg Roach            $image = $this->resizeImage($image, $width, $height, $fit);
1306577bfc3SGreg Roach
13171166947SGreg Roach            $format  = static::SUPPORTED_FORMATS[$image->mime()] ?? 'jpg';
1326577bfc3SGreg Roach            $quality = $this->extractImageQuality($image, static::GD_DEFAULT_THUMBNAIL_QUALITY);
1336577bfc3SGreg Roach            $data    = (string) $image->encode($format, $quality);
1346577bfc3SGreg Roach
1356577bfc3SGreg Roach            return $this->imageResponse($data, $image->mime(), '');
1366577bfc3SGreg Roach        } catch (NotReadableException $ex) {
137f0448b68SGreg Roach            return $this->replacementImageResponse('.' . pathinfo($path, PATHINFO_EXTENSION))
1386172e7f6SGreg Roach                ->withHeader('x-thumbnail-exception', get_class($ex) . ': ' . $ex->getMessage());
139f0448b68SGreg Roach        } catch (FilesystemException | UnableToReadFile $ex) {
1402a4e09d7SGreg Roach            return $this->replacementImageResponse((string) StatusCodeInterface::STATUS_NOT_FOUND)
1412a4e09d7SGreg Roach                ->withHeader('x-thumbnail-exception', get_class($ex) . ': ' . $ex->getMessage());
1426577bfc3SGreg Roach        } catch (Throwable $ex) {
1436577bfc3SGreg Roach            return $this->replacementImageResponse((string) StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR)
1446172e7f6SGreg Roach                ->withHeader('x-thumbnail-exception', get_class($ex) . ': ' . $ex->getMessage());
1456577bfc3SGreg Roach        }
1466577bfc3SGreg Roach    }
1476577bfc3SGreg Roach
1486577bfc3SGreg Roach    /**
1496577bfc3SGreg Roach     * Create a full-size version of an image.
1506577bfc3SGreg Roach     *
1516577bfc3SGreg Roach     * @param MediaFile $media_file
1526577bfc3SGreg Roach     * @param bool      $add_watermark
1536577bfc3SGreg Roach     * @param bool      $download
1546577bfc3SGreg Roach     *
1556577bfc3SGreg Roach     * @return ResponseInterface
1566577bfc3SGreg Roach     */
1576577bfc3SGreg Roach    public function mediaFileResponse(MediaFile $media_file, bool $add_watermark, bool $download): ResponseInterface
1586577bfc3SGreg Roach    {
1599458f20aSGreg Roach        $filesystem = $media_file->media()->tree()->mediaFilesystem();
1607729532eSGreg Roach        $path       = $media_file->filename();
1616577bfc3SGreg Roach
1626577bfc3SGreg Roach        if (!$add_watermark || !$media_file->isImage()) {
1637729532eSGreg Roach            return $this->fileResponse($filesystem, $path, $download);
1646577bfc3SGreg Roach        }
1656577bfc3SGreg Roach
1666577bfc3SGreg Roach        try {
1677729532eSGreg Roach            $image     = $this->imageManager()->make($filesystem->readStream($path));
1686577bfc3SGreg Roach            $image     = $this->autorotateImage($image);
1697729532eSGreg Roach            $watermark = $this->createWatermark($image->width(), $image->height(), $media_file);
1707729532eSGreg Roach            $image     = $this->addWatermark($image, $watermark);
1717729532eSGreg Roach            $filename  = $download ? basename($path) : '';
17271166947SGreg Roach            $format    = static::SUPPORTED_FORMATS[$image->mime()] ?? 'jpg';
1736577bfc3SGreg Roach            $quality   = $this->extractImageQuality($image, static::GD_DEFAULT_IMAGE_QUALITY);
1746577bfc3SGreg Roach            $data      = (string) $image->encode($format, $quality);
1756577bfc3SGreg Roach
1767729532eSGreg Roach            return $this->imageResponse($data, $image->mime(), $filename);
1776577bfc3SGreg Roach        } catch (NotReadableException $ex) {
1787729532eSGreg Roach            return $this->replacementImageResponse(pathinfo($path, PATHINFO_EXTENSION))
1796172e7f6SGreg Roach                ->withHeader('x-image-exception', $ex->getMessage());
180f0448b68SGreg Roach        } catch (FilesystemException | UnableToReadFile $ex) {
1812a4e09d7SGreg Roach            return $this->replacementImageResponse((string) StatusCodeInterface::STATUS_NOT_FOUND)
1822a4e09d7SGreg Roach                ->withHeader('x-thumbnail-exception', get_class($ex) . ': ' . $ex->getMessage());
1836577bfc3SGreg Roach        } catch (Throwable $ex) {
1846577bfc3SGreg Roach            return $this->replacementImageResponse((string) StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR)
1856172e7f6SGreg Roach                ->withHeader('x-image-exception', $ex->getMessage());
1866577bfc3SGreg Roach        }
1876577bfc3SGreg Roach    }
1886577bfc3SGreg Roach
1896577bfc3SGreg Roach    /**
1906577bfc3SGreg Roach     * Create a smaller version of an image.
1916577bfc3SGreg Roach     *
1926577bfc3SGreg Roach     * @param MediaFile $media_file
1936577bfc3SGreg Roach     * @param int       $width
1946577bfc3SGreg Roach     * @param int       $height
1956577bfc3SGreg Roach     * @param string    $fit
1966577bfc3SGreg Roach     * @param bool      $add_watermark
1976577bfc3SGreg Roach     *
1986577bfc3SGreg Roach     * @return ResponseInterface
1996577bfc3SGreg Roach     */
2006577bfc3SGreg Roach    public function mediaFileThumbnailResponse(
2016577bfc3SGreg Roach        MediaFile $media_file,
2026577bfc3SGreg Roach        int $width,
2036577bfc3SGreg Roach        int $height,
2046577bfc3SGreg Roach        string $fit,
2056577bfc3SGreg Roach        bool $add_watermark
2066577bfc3SGreg Roach    ): ResponseInterface {
2076577bfc3SGreg Roach        // Where are the images stored.
2089458f20aSGreg Roach        $filesystem = $media_file->media()->tree()->mediaFilesystem();
2096577bfc3SGreg Roach
2106577bfc3SGreg Roach        // Where is the image stored in the filesystem.
2116577bfc3SGreg Roach        $path = $media_file->filename();
2126577bfc3SGreg Roach
2136577bfc3SGreg Roach        try {
214f7cf8a15SGreg Roach            $mime_type = $filesystem->mimeType($path);
2156577bfc3SGreg Roach
2166577bfc3SGreg Roach            $key = implode(':', [
2176577bfc3SGreg Roach                $media_file->media()->tree()->name(),
2186577bfc3SGreg Roach                $path,
219f7cf8a15SGreg Roach                $filesystem->lastModified($path),
2206577bfc3SGreg Roach                (string) $width,
2216577bfc3SGreg Roach                (string) $height,
2226577bfc3SGreg Roach                $fit,
2236577bfc3SGreg Roach                (string) $add_watermark,
2246577bfc3SGreg Roach            ]);
2256577bfc3SGreg Roach
2266577bfc3SGreg Roach            $closure = function () use ($filesystem, $path, $width, $height, $fit, $add_watermark, $media_file): string {
2276577bfc3SGreg Roach                $image = $this->imageManager()->make($filesystem->readStream($path));
2286577bfc3SGreg Roach                $image = $this->autorotateImage($image);
2296577bfc3SGreg Roach                $image = $this->resizeImage($image, $width, $height, $fit);
2306577bfc3SGreg Roach
2316577bfc3SGreg Roach                if ($add_watermark) {
2326577bfc3SGreg Roach                    $watermark = $this->createWatermark($image->width(), $image->height(), $media_file);
2336577bfc3SGreg Roach                    $image     = $this->addWatermark($image, $watermark);
2346577bfc3SGreg Roach                }
2356577bfc3SGreg Roach
23671166947SGreg Roach                $format  = static::SUPPORTED_FORMATS[$image->mime()] ?? 'jpg';
2376577bfc3SGreg Roach                $quality = $this->extractImageQuality($image, static::GD_DEFAULT_THUMBNAIL_QUALITY);
2386577bfc3SGreg Roach
2396577bfc3SGreg Roach                return (string) $image->encode($format, $quality);
2406577bfc3SGreg Roach            };
2416577bfc3SGreg Roach
2426577bfc3SGreg Roach            // Images and Responses both contain resources - which cannot be serialized.
2436577bfc3SGreg Roach            // So cache the raw image data.
2446b9cb339SGreg Roach            $data = Registry::cache()->file()->remember($key, $closure, static::THUMBNAIL_CACHE_TTL);
2456577bfc3SGreg Roach
2466577bfc3SGreg Roach            return $this->imageResponse($data, $mime_type, '');
2476577bfc3SGreg Roach        } catch (NotReadableException $ex) {
248f0448b68SGreg Roach            return $this->replacementImageResponse('.' . pathinfo($path, PATHINFO_EXTENSION))
2496172e7f6SGreg Roach                ->withHeader('x-thumbnail-exception', get_class($ex) . ': ' . $ex->getMessage());
250f0448b68SGreg Roach        } catch (FilesystemException | UnableToReadFile $ex) {
2512a4e09d7SGreg Roach            return $this->replacementImageResponse((string) StatusCodeInterface::STATUS_NOT_FOUND)
2522a4e09d7SGreg Roach                ->withHeader('x-thumbnail-exception', get_class($ex) . ': ' . $ex->getMessage());
2536577bfc3SGreg Roach        } catch (Throwable $ex) {
2546577bfc3SGreg Roach            return $this->replacementImageResponse((string) StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR)
2556172e7f6SGreg Roach                ->withHeader('x-thumbnail-exception', get_class($ex) . ': ' . $ex->getMessage());
2566577bfc3SGreg Roach        }
2576577bfc3SGreg Roach    }
2586577bfc3SGreg Roach
2596577bfc3SGreg Roach    /**
2606577bfc3SGreg Roach     * Does a full-sized image need a watermark?
2616577bfc3SGreg Roach     *
2626577bfc3SGreg Roach     * @param MediaFile     $media_file
2636577bfc3SGreg Roach     * @param UserInterface $user
2646577bfc3SGreg Roach     *
2656577bfc3SGreg Roach     * @return bool
2666577bfc3SGreg Roach     */
2676577bfc3SGreg Roach    public function fileNeedsWatermark(MediaFile $media_file, UserInterface $user): bool
2686577bfc3SGreg Roach    {
2696577bfc3SGreg Roach        $tree = $media_file->media()->tree();
2706577bfc3SGreg Roach
271f56b86d2SGreg Roach        return Auth::accessLevel($tree, $user) > (int) $tree->getPreference('SHOW_NO_WATERMARK');
2726577bfc3SGreg Roach    }
2736577bfc3SGreg Roach
2746577bfc3SGreg Roach    /**
2756577bfc3SGreg Roach     * Does a thumbnail image need a watermark?
2766577bfc3SGreg Roach     *
2776577bfc3SGreg Roach     * @param MediaFile     $media_file
2786577bfc3SGreg Roach     * @param UserInterface $user
2796577bfc3SGreg Roach     *
2806577bfc3SGreg Roach     * @return bool
2816577bfc3SGreg Roach     */
2826577bfc3SGreg Roach    public function thumbnailNeedsWatermark(MediaFile $media_file, UserInterface $user): bool
2836577bfc3SGreg Roach    {
2846577bfc3SGreg Roach        return $this->fileNeedsWatermark($media_file, $user);
2856577bfc3SGreg Roach    }
2866577bfc3SGreg Roach
2876577bfc3SGreg Roach    /**
2886577bfc3SGreg Roach     * Create a watermark image, perhaps specific to a media-file.
2896577bfc3SGreg Roach     *
2906577bfc3SGreg Roach     * @param int       $width
2916577bfc3SGreg Roach     * @param int       $height
2926577bfc3SGreg Roach     * @param MediaFile $media_file
2936577bfc3SGreg Roach     *
2946577bfc3SGreg Roach     * @return Image
2956577bfc3SGreg Roach     */
2966577bfc3SGreg Roach    public function createWatermark(int $width, int $height, MediaFile $media_file): Image
2976577bfc3SGreg Roach    {
2986577bfc3SGreg Roach        return $this->imageManager()
2996577bfc3SGreg Roach            ->make(Webtrees::ROOT_DIR . static::WATERMARK_FILE)
3006577bfc3SGreg Roach            ->resize($width, $height, static function (Constraint $constraint) {
3016577bfc3SGreg Roach                $constraint->aspectRatio();
3026577bfc3SGreg Roach            });
3036577bfc3SGreg Roach    }
3046577bfc3SGreg Roach
3056577bfc3SGreg Roach    /**
3066577bfc3SGreg Roach     * Add a watermark to an image.
3076577bfc3SGreg Roach     *
3086577bfc3SGreg Roach     * @param Image $image
3096577bfc3SGreg Roach     * @param Image $watermark
3106577bfc3SGreg Roach     *
3116577bfc3SGreg Roach     * @return Image
3126577bfc3SGreg Roach     */
3136577bfc3SGreg Roach    public function addWatermark(Image $image, Image $watermark): Image
3146577bfc3SGreg Roach    {
3156577bfc3SGreg Roach        return $image->insert($watermark, 'center');
3166577bfc3SGreg Roach    }
3176577bfc3SGreg Roach
3186577bfc3SGreg Roach    /**
3196577bfc3SGreg Roach     * Send a replacement image, to replace one that could not be found or created.
3206577bfc3SGreg Roach     *
3216577bfc3SGreg Roach     * @param string $text HTTP status code or file extension
3226577bfc3SGreg Roach     *
3236577bfc3SGreg Roach     * @return ResponseInterface
3246577bfc3SGreg Roach     */
3256577bfc3SGreg Roach    public function replacementImageResponse(string $text): ResponseInterface
3266577bfc3SGreg Roach    {
3276577bfc3SGreg Roach        // We can't create a PNG/BMP/JPEG image, as the GD/IMAGICK libraries may be missing.
3286577bfc3SGreg Roach        $svg = view('errors/image-svg', ['status' => $text]);
3296577bfc3SGreg Roach
3306577bfc3SGreg Roach        // We can't send the actual status code, as browsers won't show images with 4xx/5xx.
3316577bfc3SGreg Roach        return response($svg, StatusCodeInterface::STATUS_OK, [
3327729532eSGreg Roach            'content-type' => 'image/svg+xml',
3336577bfc3SGreg Roach        ]);
3346577bfc3SGreg Roach    }
3356577bfc3SGreg Roach
3366577bfc3SGreg Roach    /**
3376577bfc3SGreg Roach     * @param string $data
3386577bfc3SGreg Roach     * @param string $mime_type
3396577bfc3SGreg Roach     * @param string $filename
3406577bfc3SGreg Roach     *
3416577bfc3SGreg Roach     * @return ResponseInterface
3426577bfc3SGreg Roach     */
3436577bfc3SGreg Roach    protected function imageResponse(string $data, string $mime_type, string $filename): ResponseInterface
3446577bfc3SGreg Roach    {
3457729532eSGreg Roach        if ($mime_type === 'image/svg+xml' && str_contains($data, '<script')) {
3467729532eSGreg Roach            return $this->replacementImageResponse('XSS')
3476172e7f6SGreg Roach                ->withHeader('x-image-exception', 'SVG image blocked due to XSS.');
3486577bfc3SGreg Roach        }
3496577bfc3SGreg Roach
3508340714cSGreg Roach        // HTML files may contain javascript and iframes, so use content-security-policy to disable them.
3517729532eSGreg Roach        $response = response($data)
352fc904122SGreg Roach            ->withHeader('content-type', $mime_type)
3538340714cSGreg Roach            ->withHeader('content-security-policy', 'script-src none;frame-src none');
3547729532eSGreg Roach
3557729532eSGreg Roach        if ($filename === '') {
3567729532eSGreg Roach            return $response;
3577729532eSGreg Roach        }
3587729532eSGreg Roach
3597729532eSGreg Roach        return $response
3607729532eSGreg Roach            ->withHeader('content-disposition', 'attachment; filename="' . addcslashes(basename($filename), '"'));
3616577bfc3SGreg Roach    }
3626577bfc3SGreg Roach
3636577bfc3SGreg Roach    /**
3646577bfc3SGreg Roach     * @return ImageManager
3656577bfc3SGreg Roach     * @throws RuntimeException
3666577bfc3SGreg Roach     */
3676577bfc3SGreg Roach    protected function imageManager(): ImageManager
3686577bfc3SGreg Roach    {
3696577bfc3SGreg Roach        foreach (static::INTERVENTION_DRIVERS as $driver) {
3706577bfc3SGreg Roach            if (extension_loaded($driver)) {
3716577bfc3SGreg Roach                return new ImageManager(['driver' => $driver]);
3726577bfc3SGreg Roach            }
3736577bfc3SGreg Roach        }
3746577bfc3SGreg Roach
3756577bfc3SGreg Roach        throw new RuntimeException('No PHP graphics library is installed.  Need Imagick or GD');
3766577bfc3SGreg Roach    }
3776577bfc3SGreg Roach
3786577bfc3SGreg Roach    /**
3796577bfc3SGreg Roach     * Apply EXIF rotation to an image.
3806577bfc3SGreg Roach     *
3816577bfc3SGreg Roach     * @param Image $image
3826577bfc3SGreg Roach     *
3836577bfc3SGreg Roach     * @return Image
3846577bfc3SGreg Roach     */
3856577bfc3SGreg Roach    protected function autorotateImage(Image $image): Image
3866577bfc3SGreg Roach    {
3876577bfc3SGreg Roach        try {
3886577bfc3SGreg Roach            // Auto-rotate using EXIF information.
3896577bfc3SGreg Roach            return $image->orientate();
39028d026adSGreg Roach        } catch (NotSupportedException) {
3916577bfc3SGreg Roach            // If we can't auto-rotate the image, then don't.
3926577bfc3SGreg Roach            return $image;
3936577bfc3SGreg Roach        }
3946577bfc3SGreg Roach    }
3956577bfc3SGreg Roach
3966577bfc3SGreg Roach    /**
3976577bfc3SGreg Roach     * Resize an image.
3986577bfc3SGreg Roach     *
3996577bfc3SGreg Roach     * @param Image  $image
4006577bfc3SGreg Roach     * @param int    $width
4016577bfc3SGreg Roach     * @param int    $height
4026577bfc3SGreg Roach     * @param string $fit
4036577bfc3SGreg Roach     *
4046577bfc3SGreg Roach     * @return Image
4056577bfc3SGreg Roach     */
4066577bfc3SGreg Roach    protected function resizeImage(Image $image, int $width, int $height, string $fit): Image
4076577bfc3SGreg Roach    {
4086577bfc3SGreg Roach        switch ($fit) {
4096577bfc3SGreg Roach            case 'crop':
4106577bfc3SGreg Roach                return $image->fit($width, $height);
4116577bfc3SGreg Roach            case 'contain':
4126577bfc3SGreg Roach                return $image->resize($width, $height, static function (Constraint $constraint) {
4136577bfc3SGreg Roach                    $constraint->aspectRatio();
4146577bfc3SGreg Roach                    $constraint->upsize();
4156577bfc3SGreg Roach                });
4166577bfc3SGreg Roach        }
4176577bfc3SGreg Roach
4186577bfc3SGreg Roach        return $image;
4196577bfc3SGreg Roach    }
4206577bfc3SGreg Roach
4216577bfc3SGreg Roach    /**
4226577bfc3SGreg Roach     * Extract the quality/compression parameter from an image.
4236577bfc3SGreg Roach     *
4246577bfc3SGreg Roach     * @param Image $image
4256577bfc3SGreg Roach     * @param int   $default
4266577bfc3SGreg Roach     *
4276577bfc3SGreg Roach     * @return int
4286577bfc3SGreg Roach     */
4296577bfc3SGreg Roach    protected function extractImageQuality(Image $image, int $default): int
4306577bfc3SGreg Roach    {
4316577bfc3SGreg Roach        $core = $image->getCore();
4326577bfc3SGreg Roach
4336577bfc3SGreg Roach        if ($core instanceof Imagick) {
434bf26501bSGreg Roach            return $core->getImageCompressionQuality() ?: $default;
4356577bfc3SGreg Roach        }
4366577bfc3SGreg Roach
4376577bfc3SGreg Roach        return $default;
4386577bfc3SGreg Roach    }
4396577bfc3SGreg Roach}
440