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