├── LICENSE ├── README.md ├── composer.json └── src ├── Api ├── Api.php ├── ApiInterface.php └── Encoder.php ├── Filesystem ├── FileNotFoundException.php └── FilesystemException.php ├── Manipulators ├── Background.php ├── BaseManipulator.php ├── Blur.php ├── Border.php ├── Brightness.php ├── Contrast.php ├── Crop.php ├── Filter.php ├── Flip.php ├── Gamma.php ├── Helpers │ ├── Color.php │ └── Dimension.php ├── ManipulatorInterface.php ├── Orientation.php ├── Pixelate.php ├── Sharpen.php ├── Size.php └── Watermark.php ├── Responses ├── PsrResponseFactory.php └── ResponseFactoryInterface.php ├── Server.php ├── ServerFactory.php ├── Signatures ├── Signature.php ├── SignatureException.php ├── SignatureFactory.php └── SignatureInterface.php └── Urls ├── UrlBuilder.php └── UrlBuilderFactory.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jonathan Reinink 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Glide 2 | 3 | [![Latest Version](https://img.shields.io/github/release/thephpleague/glide.svg?style=flat-square)](https://github.com/thephpleague/glide/releases) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](https://github.com/thephpleague/glide/blob/master/LICENSE) 5 | [![Build Status](https://img.shields.io/github/actions/workflow/status/thephpleague/glide/test.yaml?style=flat-square&branch=master)](https://github.com/thephpleague/glide/actions/workflows/test.yaml?query=branch%3Amaster++) 6 | [![Code Coverage](https://img.shields.io/codecov/c/github/thephpleague/glide/master?style=flat-square)](https://app.codecov.io/gh/thephpleague/glide/) 7 | [![Total Downloads](https://img.shields.io/packagist/dt/league/glide.svg?style=flat-square)](https://packagist.org/packages/league/glide) 8 | [![Source Code](http://img.shields.io/badge/source-thephpleague/glide-blue.svg?style=flat-square)](https://github.com/thephpleague/glide) 9 | [![Author](http://img.shields.io/badge/author-@reinink-blue.svg?style=flat-square)](https://twitter.com/reinink) 10 | [![Author](http://img.shields.io/badge/author-@titouangalopin-blue.svg?style=flat-square)](https://twitter.com/titouangalopin) 11 | 12 | Glide is a wonderfully easy on-demand image manipulation library written in PHP. Its straightforward API is exposed via HTTP, similar to cloud image processing services like [Imgix](http://www.imgix.com/) and [Cloudinary](http://cloudinary.com/). Glide leverages powerful libraries like [Intervention Image](http://image.intervention.io/) (for image handling and manipulation) and [Flysystem](http://flysystem.thephpleague.com/) (for file system abstraction). 13 | 14 | [![© Photo Joel Reynolds](https://glide.herokuapp.com/1.0/kayaks.jpg?w=1000)](https://glide.herokuapp.com/1.0/kayaks.jpg?w=1000) 15 | > © Photo Joel Reynolds 16 | 17 | ## Highlights 18 | 19 | - Adjust, resize and add effects to images using a simple HTTP based API. 20 | - Manipulated images are automatically cached and served with far-future expires headers. 21 | - Create your own image processing server or integrate Glide directly into your app. 22 | - Supports the [GD](http://php.net/manual/en/book.image.php) library, the [Imagick](http://php.net/manual/en/book.imagick.php) PHP extension and [libvips](https://github.com/libvips/php-vips) PHP extension. 23 | - Supports many response methods, including PSR-7, HttpFoundation and more. 24 | - Ability to secure image URLs using HTTP signatures. 25 | - Works with many different file systems, thanks to the [Flysystem](http://flysystem.thephpleague.com/) library. 26 | - Powered by the battle tested [Intervention Image](http://image.intervention.io/) image handling and manipulation library. 27 | - Framework-agnostic, will work with any project. 28 | - Composer ready and PSR-2 compliant. 29 | 30 | ## Documentation 31 | 32 | Full documentation can be found at [glide.thephpleague.com](http://glide.thephpleague.com). 33 | 34 | ## Installation 35 | 36 | Glide is available via Composer: 37 | 38 | ```bash 39 | $ composer require league/glide 40 | ``` 41 | 42 | ## Testing 43 | 44 | Glide has a [PHPUnit](https://phpunit.de/) test suite. To run the tests, run the following command from the project folder: 45 | 46 | ```bash 47 | $ phpunit 48 | ``` 49 | ## Contributing 50 | 51 | Contributions are welcome and will be fully credited. Please see [CONTRIBUTING](https://github.com/thephpleague/glide/blob/master/CONTRIBUTING.md) for details. 52 | 53 | ## Security 54 | 55 | If you discover any security related issues, please email jonathan@reinink.ca instead of using the issue tracker. 56 | 57 | ## Credits 58 | 59 | - [Jonathan Reinink](https://github.com/reinink) 60 | - [All Contributors](https://github.com/thephpleague/glide/contributors) 61 | 62 | ## License 63 | 64 | The MIT License (MIT). Please see [LICENSE](https://github.com/thephpleague/glide/blob/master/LICENSE) for more information. 65 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "league/glide", 3 | "description": "Wonderfully easy on-demand image manipulation library with an HTTP based API.", 4 | "keywords": [ 5 | "league", 6 | "image", 7 | "processing", 8 | "manipulation", 9 | "editing", 10 | "gd", 11 | "imagemagick", 12 | "imagick" 13 | ], 14 | "homepage": "http://glide.thephpleague.com", 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Jonathan Reinink", 19 | "email": "jonathan@reinink.ca", 20 | "homepage": "http://reinink.ca" 21 | }, 22 | { 23 | "name": "Titouan Galopin", 24 | "email": "galopintitouan@gmail.com", 25 | "homepage": "https://titouangalopin.com" 26 | } 27 | ], 28 | "require": { 29 | "php": "^8.1", 30 | "intervention/image": "^3.9.1", 31 | "league/flysystem": "^3.0", 32 | "psr/http-message": "^1.0|^2.0" 33 | }, 34 | "require-dev": { 35 | "mockery/mockery": "^1.6", 36 | "phpunit/phpunit": "^10.5 || ^11.0", 37 | "friendsofphp/php-cs-fixer": "^3.48", 38 | "phpstan/phpstan": "^2.0" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "League\\Glide\\": "src/" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "League\\Glide\\": "tests/" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Api/Api.php: -------------------------------------------------------------------------------- 1 | 37 | */ 38 | protected array $apiParams; 39 | 40 | /** 41 | * Create API instance. 42 | * 43 | * @param ImageManager $imageManager Intervention image manager. 44 | * @param array $manipulators Collection of manipulators. 45 | */ 46 | public function __construct(ImageManager $imageManager, array $manipulators) 47 | { 48 | $this->setImageManager($imageManager); 49 | $this->setManipulators($manipulators); 50 | $this->setApiParams(); 51 | } 52 | 53 | /** 54 | * Set the image manager. 55 | * 56 | * @param ImageManager $imageManager Intervention image manager. 57 | */ 58 | public function setImageManager(ImageManager $imageManager): void 59 | { 60 | $this->imageManager = $imageManager; 61 | } 62 | 63 | /** 64 | * Get the image manager. 65 | * 66 | * @return ImageManager Intervention image manager. 67 | */ 68 | public function getImageManager(): ImageManager 69 | { 70 | return $this->imageManager; 71 | } 72 | 73 | /** 74 | * Set the manipulators. 75 | * 76 | * @param array $manipulators Collection of manipulators. 77 | */ 78 | public function setManipulators(array $manipulators): void 79 | { 80 | foreach ($manipulators as $manipulator) { 81 | if (!($manipulator instanceof ManipulatorInterface)) { 82 | throw new \InvalidArgumentException('Not a valid manipulator.'); 83 | } 84 | } 85 | 86 | $this->manipulators = $manipulators; 87 | } 88 | 89 | /** 90 | * Get the manipulators. 91 | * 92 | * @return array Collection of manipulators. 93 | */ 94 | public function getManipulators(): array 95 | { 96 | return $this->manipulators; 97 | } 98 | 99 | /** 100 | * Perform image manipulations. 101 | * 102 | * @param string $source Source image binary data. 103 | * @param array $params The manipulation params. 104 | * 105 | * @return string Manipulated image binary data. 106 | */ 107 | public function run(string $source, array $params): string 108 | { 109 | $image = $this->imageManager->read($source, BinaryImageDecoder::class); 110 | 111 | foreach ($this->manipulators as $manipulator) { 112 | $manipulator->setParams($params); 113 | $image = $manipulator->run($image); 114 | } 115 | 116 | return $this->encode($image, $params); 117 | } 118 | 119 | /** 120 | * Perform image encoding to a given format. 121 | * 122 | * @param ImageInterface $image Image object 123 | * @param array $params the manipulator params 124 | * 125 | * @return string Manipulated image binary data 126 | */ 127 | public function encode(ImageInterface $image, array $params): string 128 | { 129 | $encoder = new Encoder($params); 130 | $encoded = $encoder->run($image); 131 | 132 | return $encoded->toString(); 133 | } 134 | 135 | /** 136 | * Sets the API parameters for all manipulators. 137 | * 138 | * @return list 139 | */ 140 | public function setApiParams(): array 141 | { 142 | $this->apiParams = self::GLOBAL_API_PARAMS; 143 | 144 | foreach ($this->manipulators as $manipulator) { 145 | $this->apiParams = array_merge($this->apiParams, $manipulator->getApiParams()); 146 | } 147 | 148 | return $this->apiParams = array_values(array_unique($this->apiParams)); 149 | } 150 | 151 | /** 152 | * Retun the list of API params. 153 | * 154 | * @return list 155 | */ 156 | public function getApiParams(): array 157 | { 158 | return $this->apiParams; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Api/ApiInterface.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public function getApiParams(): array; 25 | } 26 | -------------------------------------------------------------------------------- /src/Api/Encoder.php: -------------------------------------------------------------------------------- 1 | params = $params; 29 | } 30 | 31 | /** 32 | * Set the manipulation params. 33 | * 34 | * @param array $params The manipulation params. 35 | * 36 | * @return $this 37 | */ 38 | public function setParams(array $params) 39 | { 40 | $this->params = $params; 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * Get a specific manipulation param. 47 | */ 48 | public function getParam(string $name): mixed 49 | { 50 | return array_key_exists($name, $this->params) 51 | ? $this->params[$name] 52 | : null; 53 | } 54 | 55 | /** 56 | * Perform output image manipulation. 57 | * 58 | * @param ImageInterface $image The source image. 59 | * 60 | * @return EncodedImageInterface The encoded image. 61 | */ 62 | public function run(ImageInterface $image): EncodedImageInterface 63 | { 64 | $format = $this->getFormat($image); 65 | $quality = $this->getQuality(); 66 | $shouldInterlace = filter_var($this->getParam('interlace'), FILTER_VALIDATE_BOOLEAN); 67 | 68 | if ('pjpg' === $format) { 69 | $shouldInterlace = true; 70 | $format = 'jpg'; 71 | } 72 | 73 | $encoderOptions = []; 74 | switch ($format) { 75 | case 'avif': 76 | case 'heic': 77 | case 'tiff': 78 | case 'webp': 79 | $encoderOptions['quality'] = $quality; 80 | break; 81 | case 'jpg': 82 | $encoderOptions['quality'] = $quality; 83 | $encoderOptions['progressive'] = $shouldInterlace; 84 | break; 85 | case 'gif': 86 | case 'png': 87 | $encoderOptions['interlaced'] = $shouldInterlace; 88 | break; 89 | default: 90 | throw new \Exception("Invalid format provided: {$format}"); 91 | } 92 | 93 | return $image->encodeByExtension($format, ...$encoderOptions); 94 | } 95 | 96 | /** 97 | * Resolve format. 98 | * 99 | * @param ImageInterface $image The source image. 100 | * 101 | * @return string The resolved format. 102 | */ 103 | public function getFormat(ImageInterface $image): string 104 | { 105 | $fm = (string) $this->getParam('fm'); 106 | if ($fm) { 107 | return array_key_exists($fm, static::supportedFormats()) ? $fm : 'jpg'; 108 | } 109 | 110 | $mediaType = MediaType::tryFrom($image->origin()->mediaType()); 111 | if (null === $mediaType) { 112 | return 'jpg'; 113 | } 114 | 115 | $fm = $mediaType->format()->fileExtension()->value; 116 | 117 | return array_key_exists($fm, static::supportedFormats()) ? $fm : 'jpg'; 118 | } 119 | 120 | /** 121 | * Get a list of supported image formats and MIME types. 122 | * 123 | * @return array 124 | */ 125 | public static function supportedFormats(): array 126 | { 127 | return [ 128 | 'avif' => 'image/avif', 129 | 'gif' => 'image/gif', 130 | 'jpg' => 'image/jpeg', 131 | 'pjpg' => 'image/jpeg', 132 | 'png' => 'image/png', 133 | 'webp' => 'image/webp', 134 | 'tiff' => 'image/tiff', 135 | 'heic' => 'image/heic', 136 | ]; 137 | } 138 | 139 | /** 140 | * Resolve quality. 141 | * 142 | * @return int The resolved quality. 143 | */ 144 | public function getQuality(): int 145 | { 146 | $default = 85; 147 | $q = $this->getParam('q'); 148 | 149 | if ( 150 | !is_numeric($q) 151 | || $q < 0 152 | || $q > 100 153 | ) { 154 | return $default; 155 | } 156 | 157 | return (int) $q; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Filesystem/FileNotFoundException.php: -------------------------------------------------------------------------------- 1 | getParam('bg'); 28 | 29 | if ('' === $bg) { 30 | return $image; 31 | } 32 | 33 | $color = (new Color($bg))->formatted(); 34 | 35 | return $image->driver()->createImage($image->width(), $image->height()) 36 | ->fill($color) 37 | ->place($image, 'top-left', 0, 0) 38 | ->setOrigin( 39 | new Origin($image->origin()->mediaType()) 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Manipulators/BaseManipulator.php: -------------------------------------------------------------------------------- 1 | params = array_filter($params, fn (string $key): bool => in_array($key, $this->getApiParams()), ARRAY_FILTER_USE_KEY); 24 | 25 | return $this; 26 | } 27 | 28 | /** 29 | * Get a specific manipulation param. 30 | */ 31 | public function getParam(string $name): mixed 32 | { 33 | return $this->params[$name] ?? null; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Manipulators/Blur.php: -------------------------------------------------------------------------------- 1 | getBlur(); 26 | 27 | if (null !== $blur) { 28 | $image->blur($blur); 29 | } 30 | 31 | return $image; 32 | } 33 | 34 | /** 35 | * Resolve blur amount. 36 | * 37 | * @return int|null The resolved blur amount. 38 | */ 39 | public function getBlur(): ?int 40 | { 41 | $blur = $this->getParam('blur'); 42 | 43 | if (!is_numeric($blur) 44 | || $blur < 0 45 | || $blur > 100 46 | ) { 47 | return null; 48 | } 49 | 50 | return (int) $blur; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Manipulators/Border.php: -------------------------------------------------------------------------------- 1 | getBorder($image); 29 | 30 | if ($border) { 31 | [$width, $color, $method] = $border; 32 | 33 | return $this->{'run'.$method}($image, $width, $color); 34 | } 35 | 36 | return $image; 37 | } 38 | 39 | /** 40 | * Resolve border amount. 41 | * 42 | * @param ImageInterface $image The source image. 43 | * 44 | * @return (float|string)[]|null The resolved border amount. 45 | * 46 | * @psalm-return array{0: float, 1: string, 2: string}|null 47 | */ 48 | public function getBorder(ImageInterface $image): ?array 49 | { 50 | $border = (string) $this->getParam('border'); 51 | if ('' === $border) { 52 | return null; 53 | } 54 | 55 | $values = explode(',', $border); 56 | 57 | $width = $this->getWidth($image, $this->getDpr(), $values[0]); 58 | $color = $this->getColor($values[1] ?? 'ffffff'); 59 | $method = $this->getMethod($values[2] ?? 'overlay'); 60 | 61 | if (null !== $width) { 62 | return [$width, $color, $method]; 63 | } 64 | 65 | return null; 66 | } 67 | 68 | /** 69 | * Get border width. 70 | * 71 | * @param ImageInterface $image The source image. 72 | * @param float $dpr The device pixel ratio. 73 | * @param string $width The border width. 74 | * 75 | * @return float|null The resolved border width. 76 | */ 77 | public function getWidth(ImageInterface $image, float $dpr, string $width): ?float 78 | { 79 | return (new Dimension($image, $dpr))->get($width); 80 | } 81 | 82 | /** 83 | * Get formatted color. 84 | * 85 | * @param string $color The color. 86 | * 87 | * @return string The formatted color. 88 | */ 89 | public function getColor(string $color): string 90 | { 91 | return (new Color($color))->formatted(); 92 | } 93 | 94 | /** 95 | * Resolve the border method. 96 | * 97 | * @param string $method The raw border method. 98 | * 99 | * @return string The resolved border method. 100 | */ 101 | public function getMethod(string $method): string 102 | { 103 | return match ($method) { 104 | 'expand' => 'expand', 105 | 'shrink' => 'shrink', 106 | default => 'overlay', 107 | }; 108 | } 109 | 110 | /** 111 | * Resolve the device pixel ratio. 112 | * 113 | * @return float The device pixel ratio. 114 | */ 115 | public function getDpr(): float 116 | { 117 | $dpr = $this->getParam('dpr'); 118 | 119 | if (!is_numeric($dpr) 120 | || $dpr < 0 121 | || $dpr > 8 122 | ) { 123 | return 1.0; 124 | } 125 | 126 | return (float) $dpr; 127 | } 128 | 129 | /** 130 | * Run the overlay border method. 131 | * 132 | * @param ImageInterface $image The source image. 133 | * @param float $width The border width. 134 | * @param string $color The border color. 135 | * 136 | * @return ImageInterface The manipulated image. 137 | */ 138 | public function runOverlay(ImageInterface $image, float $width, string $color): ImageInterface 139 | { 140 | return $image->drawRectangle( 141 | (int) round($width / 2), 142 | (int) round($width / 2), 143 | function (RectangleFactory $rectangle) use ($image, $width, $color) { 144 | $rectangle->size( 145 | (int) round($image->width() - $width), 146 | (int) round($image->height() - $width), 147 | ); 148 | $rectangle->border($color, intval($width)); 149 | } 150 | ); 151 | } 152 | 153 | /** 154 | * Run the shrink border method. 155 | * 156 | * @param ImageInterface $image The source image. 157 | * @param float $width The border width. 158 | * @param string $color The border color. 159 | * 160 | * @return ImageInterface The manipulated image. 161 | */ 162 | public function runShrink(ImageInterface $image, float $width, string $color): ImageInterface 163 | { 164 | return $image 165 | ->resize( 166 | (int) round($image->width() - ($width * 2)), 167 | (int) round($image->height() - ($width * 2)) 168 | ) 169 | ->resizeCanvasRelative( 170 | (int) round($width * 2), 171 | (int) round($width * 2), 172 | $color, 173 | 'center', 174 | ); 175 | } 176 | 177 | /** 178 | * Run the expand border method. 179 | * 180 | * @param ImageInterface $image The source image. 181 | * @param float $width The border width. 182 | * @param string $color The border color. 183 | * 184 | * @return ImageInterface The manipulated image. 185 | */ 186 | public function runExpand(ImageInterface $image, float $width, string $color): ImageInterface 187 | { 188 | return $image->resizeCanvasRelative( 189 | (int) round($width * 2), 190 | (int) round($width * 2), 191 | $color, 192 | 'center', 193 | ); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/Manipulators/Brightness.php: -------------------------------------------------------------------------------- 1 | getBrightness(); 26 | 27 | if (null !== $brightness) { 28 | $image->brightness($brightness); 29 | } 30 | 31 | return $image; 32 | } 33 | 34 | /** 35 | * Resolve brightness amount. 36 | * 37 | * @return int|null The resolved brightness amount. 38 | */ 39 | public function getBrightness(): ?int 40 | { 41 | $bri = (string) $this->getParam('bri'); 42 | 43 | if ('' === $bri 44 | || !preg_match('/^-*[0-9]+$/', $bri) 45 | || $bri < -100 46 | || $bri > 100 47 | ) { 48 | return null; 49 | } 50 | 51 | return (int) $bri; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Manipulators/Contrast.php: -------------------------------------------------------------------------------- 1 | getContrast(); 26 | 27 | if (null !== $contrast) { 28 | $image->contrast($contrast); 29 | } 30 | 31 | return $image; 32 | } 33 | 34 | /** 35 | * Resolve contrast amount. 36 | * 37 | * @return int|null The resolved contrast amount. 38 | */ 39 | public function getContrast(): ?int 40 | { 41 | $con = (string) $this->getParam('con'); 42 | 43 | if ('' === $con 44 | || !preg_match('/^-*[0-9]+$/', $con) 45 | || $con < -100 46 | || $con > 100 47 | ) { 48 | return null; 49 | } 50 | 51 | return (int) $con; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Manipulators/Crop.php: -------------------------------------------------------------------------------- 1 | getCoordinates($image); 26 | 27 | if ($coordinates) { 28 | $coordinates = $this->limitToImageBoundaries($image, $coordinates); 29 | 30 | $image->crop( 31 | $coordinates[0], 32 | $coordinates[1], 33 | $coordinates[2], 34 | $coordinates[3] 35 | ); 36 | } 37 | 38 | return $image; 39 | } 40 | 41 | /** 42 | * Resolve coordinates. 43 | * 44 | * @param ImageInterface $image The source image. 45 | * 46 | * @return int[]|null The resolved coordinates. 47 | * 48 | * @psalm-return array{0: int, 1: int, 2: int, 3: int}|null 49 | */ 50 | public function getCoordinates(ImageInterface $image): ?array 51 | { 52 | $crop = (string) $this->getParam('crop'); 53 | 54 | if ('' === $crop) { 55 | return null; 56 | } 57 | 58 | $coordinates = explode(',', $crop); 59 | 60 | if (4 !== count($coordinates) 61 | || (!is_numeric($coordinates[0])) 62 | || (!is_numeric($coordinates[1])) 63 | || (!is_numeric($coordinates[2])) 64 | || (!is_numeric($coordinates[3])) 65 | || ($coordinates[0] <= 0) 66 | || ($coordinates[1] <= 0) 67 | || ($coordinates[2] < 0) 68 | || ($coordinates[3] < 0) 69 | || ($coordinates[2] >= $image->width()) 70 | || ($coordinates[3] >= $image->height())) { 71 | return null; 72 | } 73 | 74 | return [ 75 | (int) $coordinates[0], 76 | (int) $coordinates[1], 77 | (int) $coordinates[2], 78 | (int) $coordinates[3], 79 | ]; 80 | } 81 | 82 | /** 83 | * Limit coordinates to image boundaries. 84 | * 85 | * @param ImageInterface $image The source image. 86 | * @param int[] $coordinates The coordinates. 87 | * 88 | * @return int[] The limited coordinates. 89 | */ 90 | public function limitToImageBoundaries(ImageInterface $image, array $coordinates): array 91 | { 92 | if ($coordinates[0] > ($image->width() - $coordinates[2])) { 93 | $coordinates[0] = $image->width() - $coordinates[2]; 94 | } 95 | 96 | if ($coordinates[1] > ($image->height() - $coordinates[3])) { 97 | $coordinates[1] = $image->height() - $coordinates[3]; 98 | } 99 | 100 | return $coordinates; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Manipulators/Filter.php: -------------------------------------------------------------------------------- 1 | getParam('filt')) { 26 | 'greyscale' => $this->runGreyscaleFilter($image), 27 | 'sepia' => $this->runSepiaFilter($image), 28 | default => $image, 29 | }; 30 | } 31 | 32 | /** 33 | * Perform greyscale manipulation. 34 | * 35 | * @param ImageInterface $image The source image. 36 | * 37 | * @return ImageInterface The manipulated image. 38 | */ 39 | public function runGreyscaleFilter(ImageInterface $image): ImageInterface 40 | { 41 | return $image->greyscale(); 42 | } 43 | 44 | /** 45 | * Perform sepia manipulation. 46 | * 47 | * @param ImageInterface $image The source image. 48 | * 49 | * @return ImageInterface The manipulated image. 50 | */ 51 | public function runSepiaFilter(ImageInterface $image): ImageInterface 52 | { 53 | $image->greyscale() 54 | ->brightness(-10) 55 | ->contrast(10) 56 | ->colorize(38, 27, 12) 57 | ->brightness(-10) 58 | ->contrast(10); 59 | 60 | return $image; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Manipulators/Flip.php: -------------------------------------------------------------------------------- 1 | getFlip(); 26 | 27 | if (null !== $flip) { 28 | return match ($flip) { 29 | 'both' => $image->flip()->flop(), 30 | 'v' => $image->flip(), 31 | 'h' => $image->flop(), 32 | default => $image, 33 | }; 34 | } 35 | 36 | return $image; 37 | } 38 | 39 | /** 40 | * Resolve flip. 41 | * 42 | * @return string|null The resolved flip. 43 | */ 44 | public function getFlip(): ?string 45 | { 46 | $flip = $this->getParam('flip'); 47 | 48 | if (in_array($flip, ['h', 'v', 'both'], true)) { 49 | return $flip; 50 | } 51 | 52 | return null; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Manipulators/Gamma.php: -------------------------------------------------------------------------------- 1 | getGamma(); 26 | 27 | if (null !== $gamma) { 28 | $image->gamma($gamma); 29 | } 30 | 31 | return $image; 32 | } 33 | 34 | /** 35 | * Resolve gamma amount. 36 | * 37 | * @return float|null The resolved gamma amount. 38 | */ 39 | public function getGamma(): ?float 40 | { 41 | $gam = (string) $this->getParam('gam'); 42 | 43 | if ('' === $gam 44 | || !preg_match('/^[0-9]\.*[0-9]*$/', $gam) 45 | || $gam < 0.1 46 | || $gam > 9.99 47 | ) { 48 | return null; 49 | } 50 | 51 | return (float) $gam; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Manipulators/Helpers/Color.php: -------------------------------------------------------------------------------- 1 | getHexFromColorName($value); 58 | if (null !== $hex) { 59 | $rgba = $this->parseHex($hex); 60 | $alpha = 1; 61 | break; 62 | } 63 | 64 | if (preg_match(self::SHORT_RGB, $value)) { 65 | $rgba = $this->parseHex($value.$value); 66 | $alpha = 1; 67 | break; 68 | } 69 | 70 | if (preg_match(self::SHORT_ARGB, $value)) { 71 | $rgba = $this->parseHex(substr($value, 1).substr($value, 1)); 72 | $alpha = (float) substr($value, 0, 1) / 10; 73 | break; 74 | } 75 | 76 | if (preg_match(self::LONG_RGB, $value)) { 77 | $rgba = $this->parseHex($value); 78 | $alpha = 1; 79 | break; 80 | } 81 | 82 | if (preg_match(self::LONG_ARGB, $value)) { 83 | $rgba = $this->parseHex(substr($value, 2)); 84 | $alpha = (float) substr($value, 0, 2) / 100; 85 | break; 86 | } 87 | 88 | $rgba = [255, 255, 255]; 89 | $alpha = 0; 90 | } while (false); // @phpstan-ignore-line 91 | 92 | $this->red = $rgba[0]; 93 | $this->green = $rgba[1]; 94 | $this->blue = $rgba[2]; 95 | $this->alpha = $alpha; 96 | } 97 | 98 | /** 99 | * Parse hex color to RGB values. 100 | * 101 | * @param string $hex The hex value. 102 | * 103 | * @return array The RGB values. 104 | */ 105 | public function parseHex(string $hex): array 106 | { 107 | return array_map('hexdec', str_split($hex, 2)); 108 | } 109 | 110 | /** 111 | * Format color for consumption. 112 | * 113 | * @return string The formatted color. 114 | */ 115 | public function formatted(): string 116 | { 117 | return 'rgba('.$this->red.', '.$this->green.', '.$this->blue.', '.$this->alpha.')'; 118 | } 119 | 120 | /** 121 | * Get hex code by color name. 122 | * 123 | * @param string $name The color name. 124 | * 125 | * @return string|null The hex code. 126 | */ 127 | public function getHexFromColorName(string $name): ?string 128 | { 129 | $colors = [ 130 | 'aliceblue' => 'F0F8FF', 131 | 'antiquewhite' => 'FAEBD7', 132 | 'aqua' => '00FFFF', 133 | 'aquamarine' => '7FFFD4', 134 | 'azure' => 'F0FFFF', 135 | 'beige' => 'F5F5DC', 136 | 'bisque' => 'FFE4C4', 137 | 'black' => '000000', 138 | 'blanchedalmond' => 'FFEBCD', 139 | 'blue' => '0000FF', 140 | 'blueviolet' => '8A2BE2', 141 | 'brown' => 'A52A2A', 142 | 'burlywood' => 'DEB887', 143 | 'cadetblue' => '5F9EA0', 144 | 'chartreuse' => '7FFF00', 145 | 'chocolate' => 'D2691E', 146 | 'coral' => 'FF7F50', 147 | 'cornflowerblue' => '6495ED', 148 | 'cornsilk' => 'FFF8DC', 149 | 'crimson' => 'DC143C', 150 | 'cyan' => '00FFFF', 151 | 'darkblue' => '00008B', 152 | 'darkcyan' => '008B8B', 153 | 'darkgoldenrod' => 'B8860B', 154 | 'darkgray' => 'A9A9A9', 155 | 'darkgreen' => '006400', 156 | 'darkkhaki' => 'BDB76B', 157 | 'darkmagenta' => '8B008B', 158 | 'darkolivegreen' => '556B2F', 159 | 'darkorange' => 'FF8C00', 160 | 'darkorchid' => '9932CC', 161 | 'darkred' => '8B0000', 162 | 'darksalmon' => 'E9967A', 163 | 'darkseagreen' => '8FBC8F', 164 | 'darkslateblue' => '483D8B', 165 | 'darkslategray' => '2F4F4F', 166 | 'darkturquoise' => '00CED1', 167 | 'darkviolet' => '9400D3', 168 | 'deeppink' => 'FF1493', 169 | 'deepskyblue' => '00BFFF', 170 | 'dimgray' => '696969', 171 | 'dodgerblue' => '1E90FF', 172 | 'firebrick' => 'B22222', 173 | 'floralwhite' => 'FFFAF0', 174 | 'forestgreen' => '228B22', 175 | 'fuchsia' => 'FF00FF', 176 | 'gainsboro' => 'DCDCDC', 177 | 'ghostwhite' => 'F8F8FF', 178 | 'gold' => 'FFD700', 179 | 'goldenrod' => 'DAA520', 180 | 'gray' => '808080', 181 | 'green' => '008000', 182 | 'greenyellow' => 'ADFF2F', 183 | 'honeydew' => 'F0FFF0', 184 | 'hotpink' => 'FF69B4', 185 | 'indianred' => 'CD5C5C', 186 | 'indigo' => '4B0082', 187 | 'ivory' => 'FFFFF0', 188 | 'khaki' => 'F0E68C', 189 | 'lavender' => 'E6E6FA', 190 | 'lavenderblush' => 'FFF0F5', 191 | 'lawngreen' => '7CFC00', 192 | 'lemonchiffon' => 'FFFACD', 193 | 'lightblue' => 'ADD8E6', 194 | 'lightcoral' => 'F08080', 195 | 'lightcyan' => 'E0FFFF', 196 | 'lightgoldenrodyellow' => 'FAFAD2', 197 | 'lightgray' => 'D3D3D3', 198 | 'lightgreen' => '90EE90', 199 | 'lightpink' => 'FFB6C1', 200 | 'lightsalmon' => 'FFA07A', 201 | 'lightseagreen' => '20B2AA', 202 | 'lightskyblue' => '87CEFA', 203 | 'lightslategray' => '778899', 204 | 'lightsteelblue' => 'B0C4DE', 205 | 'lightyellow' => 'FFFFE0', 206 | 'lime' => '00FF00', 207 | 'limegreen' => '32CD32', 208 | 'linen' => 'FAF0E6', 209 | 'magenta' => 'FF00FF', 210 | 'maroon' => '800000', 211 | 'mediumaquamarine' => '66CDAA', 212 | 'mediumblue' => '0000CD', 213 | 'mediumorchid' => 'BA55D3', 214 | 'mediumpurple' => '9370DB', 215 | 'mediumseagreen' => '3CB371', 216 | 'mediumslateblue' => '7B68EE', 217 | 'mediumspringgreen' => '00FA9A', 218 | 'mediumturquoise' => '48D1CC', 219 | 'mediumvioletred' => 'C71585', 220 | 'midnightblue' => '191970', 221 | 'mintcream' => 'F5FFFA', 222 | 'mistyrose' => 'FFE4E1', 223 | 'moccasin' => 'FFE4B5', 224 | 'navajowhite' => 'FFDEAD', 225 | 'navy' => '000080', 226 | 'oldlace' => 'FDF5E6', 227 | 'olive' => '808000', 228 | 'olivedrab' => '6B8E23', 229 | 'orange' => 'FFA500', 230 | 'orangered' => 'FF4500', 231 | 'orchid' => 'DA70D6', 232 | 'palegoldenrod' => 'EEE8AA', 233 | 'palegreen' => '98FB98', 234 | 'paleturquoise' => 'AFEEEE', 235 | 'palevioletred' => 'DB7093', 236 | 'papayawhip' => 'FFEFD5', 237 | 'peachpuff' => 'FFDAB9', 238 | 'peru' => 'CD853F', 239 | 'pink' => 'FFC0CB', 240 | 'plum' => 'DDA0DD', 241 | 'powderblue' => 'B0E0E6', 242 | 'purple' => '800080', 243 | 'rebeccapurple' => '663399', 244 | 'red' => 'FF0000', 245 | 'rosybrown' => 'BC8F8F', 246 | 'royalblue' => '4169E1', 247 | 'saddlebrown' => '8B4513', 248 | 'salmon' => 'FA8072', 249 | 'sandybrown' => 'F4A460', 250 | 'seagreen' => '2E8B57', 251 | 'seashell' => 'FFF5EE', 252 | 'sienna' => 'A0522D', 253 | 'silver' => 'C0C0C0', 254 | 'skyblue' => '87CEEB', 255 | 'slateblue' => '6A5ACD', 256 | 'slategray' => '708090', 257 | 'snow' => 'FFFAFA', 258 | 'springgreen' => '00FF7F', 259 | 'steelblue' => '4682B4', 260 | 'tan' => 'D2B48C', 261 | 'teal' => '008080', 262 | 'thistle' => 'D8BFD8', 263 | 'tomato' => 'FF6347', 264 | 'turquoise' => '40E0D0', 265 | 'violet' => 'EE82EE', 266 | 'wheat' => 'F5DEB3', 267 | 'white' => 'FFFFFF', 268 | 'whitesmoke' => 'F5F5F5', 269 | 'yellow' => 'FFFF00', 270 | 'yellowgreen' => '9ACD32', 271 | ]; 272 | 273 | $name = strtolower($name); 274 | 275 | if (array_key_exists($name, $colors)) { 276 | return $colors[$name]; 277 | } 278 | 279 | return null; 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/Manipulators/Helpers/Dimension.php: -------------------------------------------------------------------------------- 1 | image = $image; 30 | $this->dpr = $dpr; 31 | } 32 | 33 | /** 34 | * Resolve the dimension. 35 | * 36 | * @param string $value The dimension value. 37 | * 38 | * @return float|null The resolved dimension. 39 | */ 40 | public function get(string $value): ?float 41 | { 42 | if (is_numeric($value) and $value > 0) { 43 | return (float) $value * $this->dpr; 44 | } 45 | 46 | if (preg_match('/^(\d{1,2}(?!\d)|100)(w|h)$/', $value, $matches)) { 47 | if ('h' === $matches[2]) { 48 | return (float) $this->image->height() * ((float) $matches[1] / 100); 49 | } 50 | 51 | return (float) $this->image->width() * ((float) $matches[1] / 100); 52 | } 53 | 54 | return null; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Manipulators/ManipulatorInterface.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | public function getApiParams(): array; 31 | 32 | /** 33 | * Perform the image manipulation. 34 | * 35 | * @param ImageInterface $image The source image. 36 | * 37 | * @return ImageInterface The manipulated image. 38 | */ 39 | public function run(ImageInterface $image): ImageInterface; 40 | } 41 | -------------------------------------------------------------------------------- /src/Manipulators/Orientation.php: -------------------------------------------------------------------------------- 1 | getOrientation(); 26 | 27 | if ('auto' === $orientation) { 28 | return match ($image->exif('Orientation')) { 29 | 2 => $image->flip(), 30 | 3 => $image->rotate(180), 31 | 4 => $image->rotate(180)->flip(), 32 | 5 => $image->rotate(270)->flip(), 33 | 6 => $image->rotate(270), 34 | 7 => $image->rotate(90)->flip(), 35 | 8 => $image->rotate(90), 36 | default => $image, 37 | }; 38 | } 39 | 40 | return $image->rotate((float) $orientation); 41 | } 42 | 43 | /** 44 | * Resolve orientation. 45 | * 46 | * @return string The resolved orientation. 47 | */ 48 | public function getOrientation(): string 49 | { 50 | $or = (string) $this->getParam('or'); 51 | 52 | if (in_array($or, ['0', '90', '180', '270'], true)) { 53 | return $or; 54 | } 55 | 56 | return 'auto'; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Manipulators/Pixelate.php: -------------------------------------------------------------------------------- 1 | getPixelate(); 26 | 27 | if (null !== $pixelate) { 28 | $image->pixelate($pixelate); 29 | } 30 | 31 | return $image; 32 | } 33 | 34 | /** 35 | * Resolve pixelate amount. 36 | * 37 | * @return int|null The resolved pixelate amount. 38 | */ 39 | public function getPixelate(): ?int 40 | { 41 | $pixel = $this->getParam('pixel'); 42 | 43 | if (!is_numeric($pixel) 44 | || $pixel < 0 45 | || $pixel > 1000 46 | ) { 47 | return null; 48 | } 49 | 50 | return (int) $pixel; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Manipulators/Sharpen.php: -------------------------------------------------------------------------------- 1 | getSharpen(); 26 | 27 | if (null !== $sharpen) { 28 | $image->sharpen($sharpen); 29 | } 30 | 31 | return $image; 32 | } 33 | 34 | /** 35 | * Resolve sharpen amount. 36 | * 37 | * @return int|null The resolved sharpen amount. 38 | */ 39 | public function getSharpen(): ?int 40 | { 41 | $sharp = $this->getParam('sharp'); 42 | 43 | if (!is_numeric($sharp) 44 | || $sharp < 0 45 | || $sharp > 100 46 | ) { 47 | return null; 48 | } 49 | 50 | return (int) $sharp; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Manipulators/Size.php: -------------------------------------------------------------------------------- 1 | maxImageSize = $maxImageSize; 25 | } 26 | 27 | public function getApiParams(): array 28 | { 29 | return ['w', 'h', 'fit', 'dpr']; 30 | } 31 | 32 | /** 33 | * Set the maximum image size. 34 | * 35 | * @param int|null $maxImageSize Maximum image size in pixels. 36 | */ 37 | public function setMaxImageSize(?int $maxImageSize = null): void 38 | { 39 | $this->maxImageSize = $maxImageSize; 40 | } 41 | 42 | /** 43 | * Get the maximum image size. 44 | * 45 | * @return int|null Maximum image size in pixels. 46 | */ 47 | public function getMaxImageSize(): ?int 48 | { 49 | return $this->maxImageSize; 50 | } 51 | 52 | /** 53 | * Perform size image manipulation. 54 | * 55 | * @param ImageInterface $image The source image. 56 | * 57 | * @return ImageInterface The manipulated image. 58 | */ 59 | public function run(ImageInterface $image): ImageInterface 60 | { 61 | $width = $this->getWidth(); 62 | $height = $this->getHeight(); 63 | $fit = $this->getFit(); 64 | $dpr = $this->getDpr(); 65 | 66 | [$width, $height] = $this->resolveMissingDimensions($image, $width, $height); 67 | [$width, $height] = $this->applyDpr($width, $height, $dpr); 68 | [$width, $height] = $this->limitImageSize($width, $height); 69 | 70 | if ($width !== $image->width() || $height !== $image->height() || 1.0 !== $this->getCrop()[2]) { 71 | $image = $this->runResize($image, $fit, $width, $height); 72 | } 73 | 74 | return $image; 75 | } 76 | 77 | /** 78 | * Resolve width. 79 | * 80 | * @return int|null The resolved width. 81 | */ 82 | public function getWidth(): ?int 83 | { 84 | $w = (int) $this->getParam('w'); 85 | 86 | return $w <= 0 ? null : $w; 87 | } 88 | 89 | /** 90 | * Resolve height. 91 | * 92 | * @return int|null The resolved height. 93 | */ 94 | public function getHeight(): ?int 95 | { 96 | $h = (int) $this->getParam('h'); 97 | 98 | return $h <= 0 ? null : $h; 99 | } 100 | 101 | /** 102 | * Resolve fit. 103 | * 104 | * @return string The resolved fit. 105 | */ 106 | public function getFit(): string 107 | { 108 | $fit = (string) $this->getParam('fit'); 109 | 110 | if (in_array($fit, ['contain', 'fill', 'max', 'stretch', 'fill-max', 'cover'], true)) { 111 | return $fit; 112 | } 113 | 114 | if (preg_match('/^(crop|cover)(-top-left|-top|-top-right|-left|-center|-right|-bottom-left|-bottom|-bottom-right?)*$/', $fit)) { 115 | return 'cover'; 116 | } 117 | 118 | if (preg_match('/^(crop)(-[\d]{1,3}-[\d]{1,3}(?:-[\d]{1,3}(?:\.\d+)?)?)*$/', $fit)) { 119 | return 'crop'; 120 | } 121 | 122 | return 'contain'; 123 | } 124 | 125 | /** 126 | * Resolve the device pixel ratio. 127 | * 128 | * @return float The device pixel ratio. 129 | */ 130 | public function getDpr(): float 131 | { 132 | $dpr = $this->getParam('dpr'); 133 | 134 | if (!is_numeric($dpr)) { 135 | return 1.0; 136 | } 137 | 138 | if ($dpr < 0 || $dpr > 8) { 139 | return 1.0; 140 | } 141 | 142 | return (float) $dpr; 143 | } 144 | 145 | /** 146 | * Resolve missing image dimensions. 147 | * 148 | * @param ImageInterface $image The source image. 149 | * @param int|null $width The image width. 150 | * @param int|null $height The image height. 151 | * 152 | * @return int[] The resolved width and height. 153 | */ 154 | public function resolveMissingDimensions(ImageInterface $image, ?int $width = null, ?int $height = null): array 155 | { 156 | if (is_null($width) and is_null($height)) { 157 | $width = $image->width(); 158 | $height = $image->height(); 159 | } 160 | 161 | if (is_null($width) || is_null($height)) { 162 | $size = (new Rectangle($image->width(), $image->height())) 163 | ->scale($width, $height); 164 | 165 | $width = $size->width(); 166 | $height = $size->height(); 167 | } 168 | 169 | return [ 170 | $width, 171 | $height, 172 | ]; 173 | } 174 | 175 | /** 176 | * Apply the device pixel ratio. 177 | * 178 | * @param int $width The target image width. 179 | * @param int $height The target image height. 180 | * @param float $dpr The device pixel ratio. 181 | * 182 | * @return int[] The modified width and height. 183 | */ 184 | public function applyDpr(int $width, int $height, float $dpr): array 185 | { 186 | $width = $width * $dpr; 187 | $height = $height * $dpr; 188 | 189 | return [ 190 | (int) round($width), 191 | (int) round($height), 192 | ]; 193 | } 194 | 195 | /** 196 | * Limit image size to maximum allowed image size. 197 | * 198 | * @param int $width The image width. 199 | * @param int $height The image height. 200 | * 201 | * @return int[] The limited width and height. 202 | */ 203 | public function limitImageSize(int $width, int $height): array 204 | { 205 | if (null !== $this->maxImageSize) { 206 | $imageSize = $width * $height; 207 | 208 | if ($imageSize > $this->maxImageSize) { 209 | $width = $width / sqrt($imageSize / $this->maxImageSize); 210 | $height = $height / sqrt($imageSize / $this->maxImageSize); 211 | } 212 | } 213 | 214 | return [ 215 | (int) $width, 216 | (int) $height, 217 | ]; 218 | } 219 | 220 | /** 221 | * Perform resize image manipulation. 222 | * 223 | * @param ImageInterface $image The source image. 224 | * @param string $fit The fit. 225 | * @param int $width The width. 226 | * @param int $height The height. 227 | * 228 | * @return ImageInterface The manipulated image. 229 | */ 230 | public function runResize(ImageInterface $image, string $fit, int $width, int $height): ImageInterface 231 | { 232 | if ('contain' === $fit) { 233 | return $this->runContainResize($image, $width, $height); 234 | } 235 | 236 | if ('fill' === $fit) { 237 | return $this->runFillResize($image, $width, $height); 238 | } 239 | 240 | if ('fill-max' === $fit) { 241 | return $this->runFillMaxResize($image, $width, $height); 242 | } 243 | 244 | if ('max' === $fit) { 245 | return $this->runMaxResize($image, $width, $height); 246 | } 247 | 248 | if ('stretch' === $fit) { 249 | return $this->runStretchResize($image, $width, $height); 250 | } 251 | 252 | if ('cover' === $fit) { 253 | return $this->runCoverResize($image, $width, $height); 254 | } 255 | 256 | if ('crop' === $fit) { 257 | return $this->runCropResize($image, $width, $height); 258 | } 259 | 260 | return $image; 261 | } 262 | 263 | /** 264 | * Perform contain resize image manipulation. 265 | * 266 | * @param ImageInterface $image The source image. 267 | * @param int $width The width. 268 | * @param int $height The height. 269 | * 270 | * @return ImageInterface The manipulated image. 271 | */ 272 | public function runContainResize(ImageInterface $image, int $width, int $height): ImageInterface 273 | { 274 | return $image->scale($width, $height); 275 | } 276 | 277 | /** 278 | * Perform max resize image manipulation. 279 | * 280 | * @param ImageInterface $image The source image. 281 | * @param int $width The width. 282 | * @param int $height The height. 283 | * 284 | * @return ImageInterface The manipulated image. 285 | */ 286 | public function runMaxResize(ImageInterface $image, int $width, int $height): ImageInterface 287 | { 288 | return $image->scaleDown($width, $height); 289 | } 290 | 291 | /** 292 | * Perform fill resize image manipulation. 293 | * 294 | * @param ImageInterface $image The source image. 295 | * @param int $width The width. 296 | * @param int $height The height. 297 | * 298 | * @return ImageInterface The manipulated image. 299 | */ 300 | public function runFillResize(ImageInterface $image, int $width, int $height): ImageInterface 301 | { 302 | return $image->pad($width, $height, 'transparent'); 303 | } 304 | 305 | /** 306 | * Perform fill-max resize image manipulation. 307 | * 308 | * @param ImageInterface $image The source image. 309 | * @param int $width The width. 310 | * @param int $height The height. 311 | * 312 | * @return ImageInterface The manipulated image. 313 | */ 314 | public function runFillMaxResize(ImageInterface $image, int $width, int $height): ImageInterface 315 | { 316 | return $image->contain($width, $height, 'transparent'); 317 | } 318 | 319 | /** 320 | * Perform stretch resize image manipulation. 321 | * 322 | * @param ImageInterface $image The source image. 323 | * @param int $width The width. 324 | * @param int $height The height. 325 | * 326 | * @return ImageInterface The manipulated image. 327 | */ 328 | public function runStretchResize(ImageInterface $image, int $width, int $height): ImageInterface 329 | { 330 | return $image->resize($width, $height); 331 | } 332 | 333 | /** 334 | * Perform crop resize image manipulation. 335 | * 336 | * @param ImageInterface $image The source image. 337 | * @param int $width The width. 338 | * @param int $height The height. 339 | * 340 | * @return ImageInterface The manipulated image. 341 | */ 342 | public function runCropResize(ImageInterface $image, int $width, int $height): ImageInterface 343 | { 344 | [$resize_width, $resize_height] = $this->resolveCropResizeDimensions($image, $width, $height); 345 | 346 | $zoom = $this->getCrop()[2]; 347 | 348 | $image->scale((int) round($resize_width * $zoom), (int) round($resize_height * $zoom)); 349 | 350 | [$offset_x, $offset_y] = $this->resolveCropOffset($image, $width, $height); 351 | 352 | return $image->crop($width, $height, $offset_x, $offset_y, 'transparent'); 353 | } 354 | 355 | /** 356 | * Perform crop resize image manipulation. 357 | * 358 | * @param ImageInterface $image The source image. 359 | * @param int $width The width. 360 | * @param int $height The height. 361 | * @param ?string $position The position of the crop 362 | * 363 | * @return ImageInterface The manipulated image. 364 | */ 365 | public function runCoverResize(ImageInterface $image, int $width, int $height, ?string $position = null): ImageInterface 366 | { 367 | $position ??= str_replace(['crop-', 'cover-'], '', (string) $this->getParam('fit')); 368 | 369 | $position = empty($position) || in_array($position, ['crop', 'cover']) ? 'center' : $position; 370 | 371 | return $image->cover($width, $height, $position); 372 | } 373 | 374 | /** 375 | * Resolve the crop resize dimensions. 376 | * 377 | * @param ImageInterface $image The source image. 378 | * @param int $width The width. 379 | * @param int $height The height. 380 | * 381 | * @return array The resize dimensions. 382 | */ 383 | public function resolveCropResizeDimensions(ImageInterface $image, int $width, int $height): array 384 | { 385 | if ($height > $width * ($image->height() / $image->width())) { 386 | return [$height * ($image->width() / $image->height()), $height]; 387 | } 388 | 389 | return [$width, $width * ($image->height() / $image->width())]; 390 | } 391 | 392 | /** 393 | * Resolve the crop offset. 394 | * 395 | * @param ImageInterface $image The source image. 396 | * @param int $width The width. 397 | * @param int $height The height. 398 | * 399 | * @return array The crop offset. 400 | */ 401 | public function resolveCropOffset(ImageInterface $image, int $width, int $height): array 402 | { 403 | [$offset_percentage_x, $offset_percentage_y] = $this->getCrop(); 404 | 405 | $offset_x = (int) (($image->width() * $offset_percentage_x / 100) - ($width / 2)); 406 | $offset_y = (int) (($image->height() * $offset_percentage_y / 100) - ($height / 2)); 407 | 408 | $max_offset_x = $image->width() - $width; 409 | $max_offset_y = $image->height() - $height; 410 | 411 | if ($offset_x < 0) { 412 | $offset_x = 0; 413 | } 414 | 415 | if ($offset_y < 0) { 416 | $offset_y = 0; 417 | } 418 | 419 | if ($offset_x > $max_offset_x) { 420 | $offset_x = $max_offset_x; 421 | } 422 | 423 | if ($offset_y > $max_offset_y) { 424 | $offset_y = $max_offset_y; 425 | } 426 | 427 | return [$offset_x, $offset_y]; 428 | } 429 | 430 | /** 431 | * Resolve crop with zoom. 432 | * 433 | * @return (float|int)[] The resolved crop. 434 | * 435 | * @psalm-return array{0: int, 1: int, 2: float} 436 | */ 437 | public function getCrop(): array 438 | { 439 | $cropMethods = [ 440 | 'crop-top-left' => [0, 0, 1.0], 441 | 'crop-top' => [50, 0, 1.0], 442 | 'crop-top-right' => [100, 0, 1.0], 443 | 'crop-left' => [0, 50, 1.0], 444 | 'crop-center' => [50, 50, 1.0], 445 | 'crop-right' => [100, 50, 1.0], 446 | 'crop-bottom-left' => [0, 100, 1.0], 447 | 'crop-bottom' => [50, 100, 1.0], 448 | 'crop-bottom-right' => [100, 100, 1.0], 449 | ]; 450 | 451 | $fit = (string) $this->getParam('fit'); 452 | 453 | if ('' === $fit) { 454 | return [50, 50, 1.0]; 455 | } 456 | 457 | if (array_key_exists($fit, $cropMethods)) { 458 | return $cropMethods[$fit]; 459 | } 460 | 461 | if (preg_match('/^crop-([\d]{1,3})-([\d]{1,3})(?:-([\d]{1,3}(?:\.\d+)?))*$/', $fit, $matches)) { 462 | $matches[3] = $matches[3] ?? 1; 463 | 464 | if ($matches[1] > 100 || $matches[2] > 100 || $matches[3] > 100) { 465 | return [50, 50, 1.0]; 466 | } 467 | 468 | return [ 469 | (int) $matches[1], 470 | (int) $matches[2], 471 | (float) $matches[3], 472 | ]; 473 | } 474 | 475 | return [50, 50, 1.0]; 476 | } 477 | } 478 | -------------------------------------------------------------------------------- /src/Manipulators/Watermark.php: -------------------------------------------------------------------------------- 1 | setWatermarks($watermarks); 33 | $this->setWatermarksPathPrefix($watermarksPathPrefix); 34 | } 35 | 36 | public function getApiParams(): array 37 | { 38 | return ['mark', 'markw', 'markh', 'markx', 'marky', 'markpad', 'markfit', 'markpos', 'markalpha', 'dpr', 'w', 'h']; 39 | } 40 | 41 | /** 42 | * Set the watermarks file system. 43 | * 44 | * @param FilesystemOperator $watermarks The watermarks file system. 45 | */ 46 | public function setWatermarks(?FilesystemOperator $watermarks = null): void 47 | { 48 | $this->watermarks = $watermarks; 49 | } 50 | 51 | /** 52 | * Get the watermarks file system. 53 | * 54 | * @return FilesystemOperator|null The watermarks file system. 55 | */ 56 | public function getWatermarks(): ?FilesystemOperator 57 | { 58 | return $this->watermarks; 59 | } 60 | 61 | /** 62 | * Set the watermarks path prefix. 63 | * 64 | * @param string $watermarksPathPrefix The watermarks path prefix. 65 | */ 66 | public function setWatermarksPathPrefix(string $watermarksPathPrefix = ''): void 67 | { 68 | $this->watermarksPathPrefix = trim($watermarksPathPrefix, '/'); 69 | } 70 | 71 | /** 72 | * Get the watermarks path prefix. 73 | * 74 | * @return string The watermarks path prefix. 75 | */ 76 | public function getWatermarksPathPrefix(): string 77 | { 78 | return $this->watermarksPathPrefix; 79 | } 80 | 81 | /** 82 | * Perform watermark image manipulation. 83 | * 84 | * @param ImageInterface $image The source image. 85 | * 86 | * @return ImageInterface The manipulated image. 87 | */ 88 | public function run(ImageInterface $image): ImageInterface 89 | { 90 | $watermark = $this->getImage($image); 91 | 92 | if (null === $watermark) { 93 | return $image; 94 | } 95 | 96 | $markw = $this->getDimension($image, 'markw'); 97 | $markh = $this->getDimension($image, 'markh'); 98 | $markx = $this->getDimension($image, 'markx'); 99 | $marky = $this->getDimension($image, 'marky'); 100 | $markpad = $this->getDimension($image, 'markpad'); 101 | $markfit = $this->getFit(); 102 | $markpos = $this->getPosition(); 103 | $markalpha = $this->getAlpha(); 104 | 105 | if (null !== $markpad) { 106 | $markx = $marky = $markpad; 107 | } 108 | 109 | $size = new Size(); 110 | $size->setParams([ 111 | 'w' => $markw, 112 | 'h' => $markh, 113 | 'fit' => $markfit, 114 | ]); 115 | $watermark = $size->run($watermark); 116 | 117 | return $image->place($watermark, $markpos, intval($markx), intval($marky), $markalpha); 118 | } 119 | 120 | /** 121 | * Get the watermark image. 122 | * 123 | * @param ImageInterface $image The source image. 124 | * 125 | * @return ImageInterface|null The watermark image. 126 | */ 127 | public function getImage(ImageInterface $image): ?ImageInterface 128 | { 129 | if (null === $this->watermarks) { 130 | return null; 131 | } 132 | 133 | $path = (string) $this->getParam('mark'); 134 | 135 | if ('' === $path) { 136 | return null; 137 | } 138 | 139 | if ($this->watermarksPathPrefix) { 140 | $path = $this->watermarksPathPrefix.'/'.$path; 141 | } 142 | 143 | $mark = null; 144 | try { 145 | if ($this->watermarks->fileExists($path)) { 146 | $source = $this->watermarks->read($path); 147 | 148 | $mark = $image->driver()->handleInput($source); 149 | } 150 | } catch (FilesystemV2Exception $exception) { 151 | throw new FilesystemException('Could not read the image `'.$path.'`.'); 152 | } 153 | 154 | if ($mark instanceof ImageInterface) { 155 | return $mark; 156 | } 157 | 158 | return null; 159 | } 160 | 161 | /** 162 | * Get a dimension. 163 | * 164 | * @param ImageInterface $image The source image. 165 | * @param string $field The requested field. 166 | * 167 | * @return float|null The dimension. 168 | */ 169 | public function getDimension(ImageInterface $image, string $field): ?float 170 | { 171 | $dim = $this->getParam($field); 172 | 173 | if ($dim) { 174 | return (new Dimension($image, $this->getDpr()))->get((string) $dim); 175 | } 176 | 177 | return null; 178 | } 179 | 180 | /** 181 | * Resolve the device pixel ratio. 182 | * 183 | * @return float The device pixel ratio. 184 | */ 185 | public function getDpr(): float 186 | { 187 | $dpr = $this->getParam('dpr'); 188 | 189 | if (!is_numeric($dpr)) { 190 | return 1.0; 191 | } 192 | 193 | if ($dpr < 0 || $dpr > 8) { 194 | return 1.0; 195 | } 196 | 197 | return (float) $dpr; 198 | } 199 | 200 | /** 201 | * Get the fit. 202 | * 203 | * @return string|null The fit. 204 | */ 205 | public function getFit(): ?string 206 | { 207 | $fitMethods = [ 208 | 'contain', 209 | 'max', 210 | 'stretch', 211 | 'crop', 212 | 'crop-top-left', 213 | 'crop-top', 214 | 'crop-top-right', 215 | 'crop-left', 216 | 'crop-center', 217 | 'crop-right', 218 | 'crop-bottom-left', 219 | 'crop-bottom', 220 | 'crop-bottom-right', 221 | ]; 222 | 223 | $markfit = $this->getParam('markfit'); 224 | 225 | if (in_array($markfit, $fitMethods, true)) { 226 | return $markfit; 227 | } 228 | 229 | return null; 230 | } 231 | 232 | /** 233 | * Get the position. 234 | * 235 | * @return string The position. 236 | */ 237 | public function getPosition(): string 238 | { 239 | $positions = [ 240 | 'top-left', 241 | 'top', 242 | 'top-right', 243 | 'left', 244 | 'center', 245 | 'right', 246 | 'bottom-left', 247 | 'bottom', 248 | 'bottom-right', 249 | ]; 250 | 251 | $markpos = $this->getParam('markpos'); 252 | 253 | if (in_array($markpos, $positions, true)) { 254 | return $markpos; 255 | } 256 | 257 | return 'bottom-right'; 258 | } 259 | 260 | /** 261 | * Get the alpha channel. 262 | * 263 | * @return int The alpha. 264 | */ 265 | public function getAlpha(): int 266 | { 267 | $markalpha = $this->getParam('markalpha'); 268 | 269 | if (!is_numeric($markalpha)) { 270 | return 100; 271 | } 272 | 273 | if ($markalpha < 0 || $markalpha > 100) { 274 | return 100; 275 | } 276 | 277 | return (int) $markalpha; 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/Responses/PsrResponseFactory.php: -------------------------------------------------------------------------------- 1 | response = $response; 31 | $this->streamCallback = $streamCallback; 32 | } 33 | 34 | /** 35 | * Create response. 36 | * 37 | * @param FilesystemOperator $cache Cache file system. 38 | * @param string $path Cached file path. 39 | * 40 | * @return ResponseInterface Response object. 41 | */ 42 | public function create(FilesystemOperator $cache, string $path): ResponseInterface 43 | { 44 | $stream = $this->streamCallback->__invoke( 45 | $cache->readStream($path) 46 | ); 47 | 48 | $contentType = $cache->mimeType($path); 49 | $contentLength = (string) $cache->fileSize($path); 50 | $cacheControl = 'max-age=31536000, public'; 51 | $expires = date_create('+1 years')->format('D, d M Y H:i:s').' GMT'; 52 | 53 | return $this->response->withBody($stream) 54 | ->withHeader('Content-Type', $contentType) 55 | ->withHeader('Content-Length', $contentLength) 56 | ->withHeader('Cache-Control', $cacheControl) 57 | ->withHeader('Expires', $expires); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Responses/ResponseFactoryInterface.php: -------------------------------------------------------------------------------- 1 | setSource($source); 91 | $this->setCache($cache); 92 | $this->setApi($api); 93 | $this->tempDir = sys_get_temp_dir(); 94 | } 95 | 96 | /** 97 | * Set source file system. 98 | * 99 | * @param FilesystemOperator $source Source file system. 100 | */ 101 | public function setSource(FilesystemOperator $source): void 102 | { 103 | $this->source = $source; 104 | } 105 | 106 | /** 107 | * Get source file system. 108 | * 109 | * @return FilesystemOperator Source file system. 110 | */ 111 | public function getSource(): FilesystemOperator 112 | { 113 | return $this->source; 114 | } 115 | 116 | /** 117 | * Set source path prefix. 118 | * 119 | * @param string $sourcePathPrefix Source path prefix. 120 | */ 121 | public function setSourcePathPrefix(string $sourcePathPrefix): void 122 | { 123 | $this->sourcePathPrefix = trim($sourcePathPrefix, '/'); 124 | } 125 | 126 | /** 127 | * Get source path prefix. 128 | * 129 | * @return string Source path prefix. 130 | */ 131 | public function getSourcePathPrefix(): string 132 | { 133 | return $this->sourcePathPrefix; 134 | } 135 | 136 | /** 137 | * Get source path. 138 | * 139 | * @param string $path Image path. 140 | * 141 | * @return string The source path. 142 | * 143 | * @throws FileNotFoundException 144 | */ 145 | public function getSourcePath(string $path): string 146 | { 147 | $path = trim($path, '/'); 148 | 149 | $baseUrl = $this->baseUrl.'/'; 150 | 151 | if (substr($path, 0, strlen($baseUrl)) === $baseUrl) { 152 | $path = trim(substr($path, strlen($baseUrl)), '/'); 153 | } 154 | 155 | if ('' === $path) { 156 | throw new FileNotFoundException('Image path missing.'); 157 | } 158 | 159 | if ($this->sourcePathPrefix) { 160 | $path = $this->sourcePathPrefix.'/'.$path; 161 | } 162 | 163 | return rawurldecode($path); 164 | } 165 | 166 | /** 167 | * Check if a source file exists. 168 | * 169 | * @param string $path Image path. 170 | * 171 | * @return bool Whether the source file exists. 172 | * 173 | * @throws FileNotFoundException 174 | */ 175 | public function sourceFileExists(string $path): bool 176 | { 177 | try { 178 | return $this->source->fileExists($this->getSourcePath($path)); 179 | } catch (FilesystemV2Exception $exception) { 180 | return false; 181 | } 182 | } 183 | 184 | /** 185 | * Set base URL. 186 | * 187 | * @param string $baseUrl Base URL. 188 | */ 189 | public function setBaseUrl(string $baseUrl): void 190 | { 191 | $this->baseUrl = trim($baseUrl, '/'); 192 | } 193 | 194 | /** 195 | * Get base URL. 196 | * 197 | * @return string Base URL. 198 | */ 199 | public function getBaseUrl(): string 200 | { 201 | return $this->baseUrl; 202 | } 203 | 204 | /** 205 | * Set cache file system. 206 | * 207 | * @param FilesystemOperator $cache Cache file system. 208 | */ 209 | public function setCache(FilesystemOperator $cache): void 210 | { 211 | $this->cache = $cache; 212 | } 213 | 214 | /** 215 | * Get cache file system. 216 | * 217 | * @return FilesystemOperator Cache file system. 218 | */ 219 | public function getCache(): FilesystemOperator 220 | { 221 | return $this->cache; 222 | } 223 | 224 | /** 225 | * Set cache path prefix. 226 | * 227 | * @param string $cachePathPrefix Cache path prefix. 228 | */ 229 | public function setCachePathPrefix(string $cachePathPrefix): void 230 | { 231 | $this->cachePathPrefix = trim($cachePathPrefix, '/'); 232 | } 233 | 234 | /** 235 | * Get cache path prefix. 236 | * 237 | * @return string Cache path prefix. 238 | */ 239 | public function getCachePathPrefix(): string 240 | { 241 | return $this->cachePathPrefix; 242 | } 243 | 244 | /** 245 | * Get temporary EXIF data directory. 246 | */ 247 | public function getTempDir(): string 248 | { 249 | return $this->tempDir; 250 | } 251 | 252 | /** 253 | * Set temporary EXIF data directory. This directory must be a local path and exists on the filesystem. 254 | * 255 | * @throws \InvalidArgumentException 256 | */ 257 | public function setTempDir(string $tempDir): void 258 | { 259 | if (!$tempDir || !is_dir($tempDir)) { 260 | throw new \InvalidArgumentException(sprintf('Invalid temp dir provided: "%s" does not exist.', $tempDir)); 261 | } 262 | 263 | $this->tempDir = rtrim($tempDir, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; 264 | } 265 | 266 | /** 267 | * Set the group cache in folders setting. 268 | * 269 | * @param bool $groupCacheInFolders Whether to group cache in folders. 270 | */ 271 | public function setGroupCacheInFolders(bool $groupCacheInFolders): void 272 | { 273 | $this->groupCacheInFolders = $groupCacheInFolders; 274 | } 275 | 276 | /** 277 | * Get the group cache in folders setting. 278 | * 279 | * @return bool Whether to group cache in folders. 280 | */ 281 | public function getGroupCacheInFolders(): bool 282 | { 283 | return $this->groupCacheInFolders; 284 | } 285 | 286 | /** 287 | * Set the cache with file extensions setting. 288 | * 289 | * @param bool $cacheWithFileExtensions Whether to cache with file extensions. 290 | */ 291 | public function setCacheWithFileExtensions(bool $cacheWithFileExtensions): void 292 | { 293 | $this->cacheWithFileExtensions = $cacheWithFileExtensions; 294 | } 295 | 296 | /** 297 | * Get the cache with file extensions setting. 298 | * 299 | * @return bool Whether to cache with file extensions. 300 | */ 301 | public function getCacheWithFileExtensions(): bool 302 | { 303 | return $this->cacheWithFileExtensions; 304 | } 305 | 306 | /** 307 | * Set a custom cachePathCallable. 308 | * 309 | * @param \Closure|null $cachePathCallable The custom cache path callable. It receives the same arguments as @see getCachePath 310 | */ 311 | public function setCachePathCallable(?\Closure $cachePathCallable): void 312 | { 313 | $this->cachePathCallable = $cachePathCallable; 314 | } 315 | 316 | /** 317 | * Gets the custom cachePathCallable. 318 | * 319 | * @return \Closure|null The custom cache path callable. It receives the same arguments as @see getCachePath 320 | */ 321 | public function getCachePathCallable(): ?\Closure 322 | { 323 | return $this->cachePathCallable; 324 | } 325 | 326 | /** 327 | * Get cache path. 328 | * 329 | * @param string $path Image path. 330 | * @param array $params Image manipulation params. 331 | * 332 | * @return string Cache path. 333 | * 334 | * @throws FileNotFoundException 335 | */ 336 | public function getCachePath(string $path, array $params = []): string 337 | { 338 | $customCallable = $this->getCachePathCallable(); 339 | if (null !== $customCallable) { 340 | $boundCallable = \Closure::bind($customCallable, $this, static::class); 341 | if (null === $boundCallable) { 342 | throw new \UnexpectedValueException('Invalid cache path callable'); 343 | } 344 | 345 | return $boundCallable($path, $params); 346 | } 347 | 348 | $sourcePath = $this->getSourcePath($path); 349 | 350 | if ($this->sourcePathPrefix) { 351 | $sourcePath = substr($sourcePath, strlen($this->sourcePathPrefix) + 1); 352 | } 353 | 354 | $params = $this->getAllParams($params); 355 | unset($params['s'], $params['p']); 356 | ksort($params); 357 | 358 | $cachedPath = hash('xxh3', $sourcePath.'?'.http_build_query($params)); 359 | 360 | if ($this->groupCacheInFolders) { 361 | $cachedPath = $sourcePath.'/'.$cachedPath; 362 | } 363 | 364 | if ($this->cachePathPrefix) { 365 | $cachedPath = $this->cachePathPrefix.'/'.$cachedPath; 366 | } 367 | 368 | if ($this->cacheWithFileExtensions) { 369 | $ext = $params['fm'] ?? pathinfo($path, PATHINFO_EXTENSION); 370 | $ext = 'pjpg' === $ext ? 'jpg' : $ext; 371 | $cachedPath .= '.'.$ext; 372 | } 373 | 374 | return $cachedPath; 375 | } 376 | 377 | /** 378 | * Check if a cache file exists. 379 | * 380 | * @param string $path Image path. 381 | * @param array $params Image manipulation params. 382 | * 383 | * @return bool Whether the cache file exists. 384 | */ 385 | public function cacheFileExists(string $path, array $params): bool 386 | { 387 | try { 388 | return $this->cache->fileExists( 389 | $this->getCachePath($path, $params) 390 | ); 391 | } catch (FilesystemV2Exception $exception) { 392 | return false; 393 | } 394 | } 395 | 396 | /** 397 | * Delete cached manipulations for an image. 398 | * 399 | * @param string $path Image path. 400 | * 401 | * @return bool Whether the delete succeeded. 402 | */ 403 | public function deleteCache(string $path): bool 404 | { 405 | if (!$this->groupCacheInFolders) { 406 | throw new \InvalidArgumentException('Deleting cached image manipulations is not possible when grouping cache into folders is disabled.'); 407 | } 408 | 409 | try { 410 | $this->cache->deleteDirectory( 411 | dirname($this->getCachePath($path)) 412 | ); 413 | 414 | return true; 415 | } catch (FilesystemV2Exception $exception) { 416 | return false; 417 | } 418 | } 419 | 420 | /** 421 | * Set image manipulation API. 422 | * 423 | * @param ApiInterface $api Image manipulation API. 424 | */ 425 | public function setApi(ApiInterface $api): void 426 | { 427 | $this->api = $api; 428 | } 429 | 430 | /** 431 | * Get image manipulation API. 432 | * 433 | * @return ApiInterface Image manipulation API. 434 | */ 435 | public function getApi(): ApiInterface 436 | { 437 | return $this->api; 438 | } 439 | 440 | /** 441 | * Set default image manipulations. 442 | * 443 | * @param array $defaults Default image manipulations. 444 | */ 445 | public function setDefaults(array $defaults): void 446 | { 447 | $this->defaults = $defaults; 448 | } 449 | 450 | /** 451 | * Get default image manipulations. 452 | * 453 | * @return array Default image manipulations. 454 | */ 455 | public function getDefaults(): array 456 | { 457 | return $this->defaults; 458 | } 459 | 460 | /** 461 | * Set preset image manipulations. 462 | * 463 | * @param array $presets Preset image manipulations. 464 | */ 465 | public function setPresets(array $presets): void 466 | { 467 | $this->presets = $presets; 468 | } 469 | 470 | /** 471 | * Get preset image manipulations. 472 | * 473 | * @return array Preset image manipulations. 474 | */ 475 | public function getPresets(): array 476 | { 477 | return $this->presets; 478 | } 479 | 480 | /** 481 | * Get all image manipulations params, including defaults and presets. 482 | * 483 | * @param array $params Image manipulation params. 484 | * 485 | * @return array All image manipulation params. 486 | */ 487 | public function getAllParams(array $params): array 488 | { 489 | $all = $this->defaults; 490 | 491 | if (isset($params['p'])) { 492 | foreach (explode(',', (string) $params['p']) as $preset) { 493 | if (isset($this->presets[$preset])) { 494 | $all = array_merge($all, $this->presets[$preset]); 495 | } 496 | } 497 | } 498 | 499 | return array_filter(array_merge($all, $params), fn ($key) => in_array($key, $this->api->getApiParams(), true), ARRAY_FILTER_USE_KEY); 500 | } 501 | 502 | /** 503 | * Set response factory. 504 | * 505 | * @param ?ResponseFactoryInterface $responseFactory Response factory. 506 | */ 507 | public function setResponseFactory(?ResponseFactoryInterface $responseFactory = null): void 508 | { 509 | $this->responseFactory = $responseFactory; 510 | } 511 | 512 | /** 513 | * Get response factory. 514 | * 515 | * @return ResponseFactoryInterface|null Response factory. 516 | */ 517 | public function getResponseFactory(): ?ResponseFactoryInterface 518 | { 519 | return $this->responseFactory; 520 | } 521 | 522 | /** 523 | * Generate and return image response. 524 | * 525 | * @param string $path Image path. 526 | * @param array $params Image manipulation params. 527 | * 528 | * @return mixed Image response. 529 | * 530 | * @throws \InvalidArgumentException 531 | * @throws FileNotFoundException 532 | * @throws FilesystemException 533 | */ 534 | public function getImageResponse(string $path, array $params): mixed 535 | { 536 | if (is_null($this->responseFactory)) { 537 | throw new \InvalidArgumentException('Unable to get image response, no response factory defined.'); 538 | } 539 | 540 | $path = $this->makeImage($path, $params); 541 | 542 | return $this->responseFactory->create($this->cache, $path); 543 | } 544 | 545 | /** 546 | * Generate and return Base64 encoded image. 547 | * 548 | * @param string $path Image path. 549 | * @param array $params Image manipulation params. 550 | * 551 | * @return string Base64 encoded image. 552 | * 553 | * @throws FileNotFoundException 554 | * @throws FilesystemException 555 | */ 556 | public function getImageAsBase64(string $path, array $params): string 557 | { 558 | $path = $this->makeImage($path, $params); 559 | 560 | try { 561 | $source = $this->cache->read($path); 562 | 563 | return 'data:'.$this->cache->mimeType($path).';base64,'.base64_encode($source); 564 | } catch (FilesystemV2Exception $exception) { 565 | throw new FilesystemException('Could not read the image `'.$path.'`.'); 566 | } 567 | } 568 | 569 | /** 570 | * Generate and output image. 571 | * 572 | * @param string $path Image path. 573 | * @param array $params Image manipulation params. 574 | * 575 | * @throws \InvalidArgumentException 576 | * @throws FileNotFoundException 577 | * @throws FilesystemException 578 | */ 579 | public function outputImage(string $path, array $params): void 580 | { 581 | $path = $this->makeImage($path, $params); 582 | 583 | try { 584 | header('Content-Type:'.$this->cache->mimeType($path)); 585 | header('Content-Length:'.$this->cache->fileSize($path)); 586 | header('Cache-Control:max-age=31536000, public'); 587 | header('Expires:'.date_create('+1 years')->format('D, d M Y H:i:s').' GMT'); 588 | 589 | $stream = $this->cache->readStream($path); 590 | 591 | if (0 !== ftell($stream)) { 592 | rewind($stream); 593 | } 594 | fpassthru($stream); 595 | fclose($stream); 596 | } catch (FilesystemV2Exception $exception) { 597 | throw new FilesystemException('Could not read the image `'.$path.'`.'); 598 | } 599 | } 600 | 601 | /** 602 | * Generate manipulated image. 603 | * 604 | * @param string $path Image path. 605 | * @param array $params Image manipulation params. 606 | * 607 | * @return string Cache path. 608 | * 609 | * @throws FileNotFoundException 610 | * @throws FilesystemException 611 | */ 612 | public function makeImage(string $path, array $params): string 613 | { 614 | $sourcePath = $this->getSourcePath($path); 615 | $cachedPath = $this->getCachePath($path, $params); 616 | 617 | if (true === $this->cacheFileExists($path, $params)) { 618 | return $cachedPath; 619 | } 620 | 621 | if (false === $this->sourceFileExists($path)) { 622 | throw new FileNotFoundException('Could not find the image `'.$sourcePath.'`.'); 623 | } 624 | 625 | try { 626 | $source = $this->source->read( 627 | $sourcePath 628 | ); 629 | } catch (FilesystemV2Exception $exception) { 630 | throw new FilesystemException('Could not read the image `'.$sourcePath.'`.', 0, $exception); 631 | } 632 | 633 | try { 634 | $this->cache->write( 635 | $cachedPath, 636 | $this->api->run($source, $this->getAllParams($params)) 637 | ); 638 | } catch (FilesystemV2Exception $exception) { 639 | throw new FilesystemException('Could not write the image `'.$cachedPath.'`.', 0, $exception); 640 | } 641 | 642 | return $cachedPath; 643 | } 644 | } 645 | -------------------------------------------------------------------------------- /src/ServerFactory.php: -------------------------------------------------------------------------------- 1 | config = $config; 43 | } 44 | 45 | /** 46 | * Get configured server. 47 | * 48 | * @return Server Configured Glide server. 49 | */ 50 | public function getServer(): Server 51 | { 52 | $server = new Server( 53 | $this->getSource(), 54 | $this->getCache(), 55 | $this->getApi() 56 | ); 57 | 58 | $server->setSourcePathPrefix($this->getSourcePathPrefix() ?? ''); 59 | $server->setCachePathPrefix($this->getCachePathPrefix() ?? ''); 60 | $server->setGroupCacheInFolders($this->getGroupCacheInFolders()); 61 | $server->setCacheWithFileExtensions($this->getCacheWithFileExtensions()); 62 | $server->setDefaults($this->getDefaults()); 63 | $server->setPresets($this->getPresets()); 64 | $server->setBaseUrl($this->getBaseUrl() ?? ''); 65 | $server->setResponseFactory($this->getResponseFactory()); 66 | $server->setCachePathCallable($this->getCachePathCallable()); 67 | 68 | $tempDir = $this->getTempDir(); 69 | if (null !== $tempDir) { 70 | $server->setTempDir($tempDir); 71 | } 72 | 73 | return $server; 74 | } 75 | 76 | /** 77 | * Get source file system. 78 | * 79 | * @return FilesystemOperator Source file system. 80 | */ 81 | public function getSource(): FilesystemOperator 82 | { 83 | if (!isset($this->config['source'])) { 84 | throw new \InvalidArgumentException('A "source" file system must be set.'); 85 | } 86 | 87 | if (is_string($this->config['source'])) { 88 | return new Filesystem( 89 | new LocalFilesystemAdapter($this->config['source']) 90 | ); 91 | } 92 | 93 | return $this->config['source']; 94 | } 95 | 96 | /** 97 | * Get source path prefix. 98 | * 99 | * @return string|null Source path prefix. 100 | */ 101 | public function getSourcePathPrefix(): ?string 102 | { 103 | return $this->config['source_path_prefix'] ?? null; 104 | } 105 | 106 | /** 107 | * Get cache file system. 108 | * 109 | * @return FilesystemOperator Cache file system. 110 | */ 111 | public function getCache(): FilesystemOperator 112 | { 113 | if (!isset($this->config['cache'])) { 114 | throw new \InvalidArgumentException('A "cache" file system must be set.'); 115 | } 116 | 117 | if (is_string($this->config['cache'])) { 118 | return new Filesystem( 119 | new LocalFilesystemAdapter($this->config['cache']) 120 | ); 121 | } 122 | 123 | return $this->config['cache']; 124 | } 125 | 126 | /** 127 | * Get cache path prefix. 128 | * 129 | * @return string|null Cache path prefix. 130 | */ 131 | public function getCachePathPrefix(): ?string 132 | { 133 | return $this->config['cache_path_prefix'] ?? null; 134 | } 135 | 136 | /** 137 | * Get temporary EXIF data directory. 138 | */ 139 | public function getTempDir(): ?string 140 | { 141 | return $this->config['temp_dir'] ?? null; 142 | } 143 | 144 | /** 145 | * Get cache path callable. 146 | * 147 | * @return \Closure|null Cache path callable. 148 | */ 149 | public function getCachePathCallable(): ?\Closure 150 | { 151 | return $this->config['cache_path_callable'] ?? null; 152 | } 153 | 154 | /** 155 | * Get the group cache in folders setting. 156 | * 157 | * @return bool Whether to group cache in folders. 158 | */ 159 | public function getGroupCacheInFolders(): bool 160 | { 161 | return $this->config['group_cache_in_folders'] ?? true; 162 | } 163 | 164 | /** 165 | * Get the cache with file extensions setting. 166 | * 167 | * @return bool Whether to cache with file extensions. 168 | */ 169 | public function getCacheWithFileExtensions(): bool 170 | { 171 | return $this->config['cache_with_file_extensions'] ?? false; 172 | } 173 | 174 | /** 175 | * Get watermarks file system. 176 | * 177 | * @return FilesystemOperator|null Watermarks file system. 178 | */ 179 | public function getWatermarks(): ?FilesystemOperator 180 | { 181 | if (!isset($this->config['watermarks'])) { 182 | return null; 183 | } 184 | 185 | if (is_string($this->config['watermarks'])) { 186 | return new Filesystem( 187 | new LocalFilesystemAdapter($this->config['watermarks']) 188 | ); 189 | } 190 | 191 | return $this->config['watermarks']; 192 | } 193 | 194 | /** 195 | * Get watermarks path prefix. 196 | * 197 | * @return string|null Watermarks path prefix. 198 | */ 199 | public function getWatermarksPathPrefix(): ?string 200 | { 201 | return $this->config['watermarks_path_prefix'] ?? null; 202 | } 203 | 204 | /** 205 | * Get image manipulation API. 206 | * 207 | * @return Api Image manipulation API. 208 | */ 209 | public function getApi(): Api 210 | { 211 | return new Api( 212 | $this->getImageManager(), 213 | $this->getManipulators() 214 | ); 215 | } 216 | 217 | /** 218 | * Get Intervention image manager. 219 | * 220 | * @return ImageManager Intervention image manager. 221 | */ 222 | public function getImageManager(): ImageManager 223 | { 224 | $driver = 'gd'; 225 | 226 | if (isset($this->config['driver'])) { 227 | $driver = $this->config['driver']; 228 | } 229 | 230 | return match ($driver) { 231 | 'gd' => ImageManager::gd(), 232 | 'imagick' => ImageManager::imagick(), 233 | default => ImageManager::withDriver($driver), 234 | }; 235 | } 236 | 237 | /** 238 | * Get image manipulators. 239 | * 240 | * @return array Image manipulators. 241 | */ 242 | public function getManipulators(): array 243 | { 244 | return [ 245 | new Orientation(), 246 | new Crop(), 247 | new Size($this->getMaxImageSize()), 248 | new Brightness(), 249 | new Contrast(), 250 | new Gamma(), 251 | new Sharpen(), 252 | new Filter(), 253 | new Flip(), 254 | new Blur(), 255 | new Pixelate(), 256 | new Watermark($this->getWatermarks(), $this->getWatermarksPathPrefix() ?? ''), 257 | new Background(), 258 | new Border(), 259 | ]; 260 | } 261 | 262 | /** 263 | * Get maximum image size. 264 | * 265 | * @return int|null Maximum image size. 266 | */ 267 | public function getMaxImageSize(): ?int 268 | { 269 | return $this->config['max_image_size'] ?? null; 270 | } 271 | 272 | /** 273 | * Get default image manipulations. 274 | * 275 | * @return array Default image manipulations. 276 | */ 277 | public function getDefaults(): array 278 | { 279 | return $this->config['defaults'] ?? []; 280 | } 281 | 282 | /** 283 | * Get preset image manipulations. 284 | * 285 | * @return array Preset image manipulations. 286 | */ 287 | public function getPresets(): array 288 | { 289 | return $this->config['presets'] ?? []; 290 | } 291 | 292 | /** 293 | * Get base URL. 294 | * 295 | * @return string|null Base URL. 296 | */ 297 | public function getBaseUrl(): ?string 298 | { 299 | return $this->config['base_url'] ?? null; 300 | } 301 | 302 | /** 303 | * Get response factory. 304 | * 305 | * @return ResponseFactoryInterface|null Response factory. 306 | */ 307 | public function getResponseFactory(): ?ResponseFactoryInterface 308 | { 309 | return $this->config['response'] ?? null; 310 | } 311 | 312 | /** 313 | * Create configured server. 314 | * 315 | * @param array $config Configuration parameters. 316 | * 317 | * @return Server Configured server. 318 | */ 319 | public static function create(array $config = []): Server 320 | { 321 | return (new self($config))->getServer(); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/Signatures/Signature.php: -------------------------------------------------------------------------------- 1 | signKey = $signKey; 22 | } 23 | 24 | /** 25 | * Add an HTTP signature to manipulation parameters. 26 | * 27 | * @param string $path The resource path. 28 | * @param array $params The manipulation parameters. 29 | * 30 | * @return array The updated manipulation parameters. 31 | */ 32 | public function addSignature(string $path, array $params): array 33 | { 34 | return array_merge($params, ['s' => $this->generateSignature($path, $params)]); 35 | } 36 | 37 | /** 38 | * Validate a request signature. 39 | * 40 | * @param string $path The resource path. 41 | * @param array $params The manipulation params. 42 | * 43 | * @throws SignatureException 44 | */ 45 | public function validateRequest(string $path, array $params): void 46 | { 47 | if (!isset($params['s'])) { 48 | throw new SignatureException('Signature is missing.'); 49 | } 50 | 51 | if ($params['s'] !== $this->generateSignature($path, $params)) { 52 | throw new SignatureException('Signature is not valid.'); 53 | } 54 | } 55 | 56 | /** 57 | * Generate an HTTP signature. 58 | * 59 | * @param string $path The resource path. 60 | * @param array $params The manipulation parameters. 61 | * 62 | * @return string The generated HTTP signature. 63 | */ 64 | public function generateSignature(string $path, array $params): string 65 | { 66 | unset($params['s']); 67 | ksort($params); 68 | 69 | return md5($this->signKey.':'.ltrim($path, '/').'?'.http_build_query($params)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Signatures/SignatureException.php: -------------------------------------------------------------------------------- 1 | setBaseUrl($baseUrl); 35 | $this->setSignature($signature); 36 | } 37 | 38 | /** 39 | * Set the base URL. 40 | * 41 | * @param string $baseUrl The base URL. 42 | */ 43 | public function setBaseUrl(string $baseUrl): void 44 | { 45 | if ('//' === substr($baseUrl, 0, 2)) { 46 | $baseUrl = 'http:'.$baseUrl; 47 | $this->isRelativeDomain = true; 48 | } 49 | 50 | $this->baseUrl = rtrim($baseUrl, '/'); 51 | } 52 | 53 | /** 54 | * Set the HTTP signature. 55 | * 56 | * @param SignatureInterface|null $signature The HTTP signature used to sign URLs. 57 | */ 58 | public function setSignature(?SignatureInterface $signature = null): void 59 | { 60 | $this->signature = $signature; 61 | } 62 | 63 | /** 64 | * Get the URL. 65 | * 66 | * @param string $path The resource path. 67 | * @param array $params The manipulation parameters. 68 | * 69 | * @return string The URL. 70 | */ 71 | public function getUrl(string $path, array $params = []): string 72 | { 73 | $parts = parse_url($this->baseUrl.'/'.trim($path, '/')); 74 | 75 | if (false === $parts) { 76 | throw new \InvalidArgumentException('Not a valid path.'); 77 | } 78 | 79 | /** 80 | * @psalm-suppress PossiblyNullArgument, PossiblyUndefinedArrayOffset 81 | * 82 | * @phpstan-ignore-next-line 83 | */ 84 | $parts['path'] = '/'.trim($parts['path'], '/'); 85 | 86 | if ($this->signature) { 87 | $params = $this->signature->addSignature($parts['path'], $params); 88 | } 89 | 90 | return $this->buildUrl($parts, $params); 91 | } 92 | 93 | /** 94 | * Build the URL. 95 | * 96 | * @param array $parts The URL parts. 97 | * @param array $params The manipulation parameters. 98 | * 99 | * @return string The built URL. 100 | */ 101 | protected function buildUrl(array $parts, array $params): string 102 | { 103 | $url = ''; 104 | 105 | if (isset($parts['host'])) { 106 | if ($this->isRelativeDomain) { 107 | $url .= '//'.$parts['host']; 108 | } else { 109 | $url .= $parts['scheme'].'://'.$parts['host']; 110 | } 111 | 112 | if (isset($parts['port'])) { 113 | $url .= ':'.$parts['port']; 114 | } 115 | } 116 | 117 | $url .= $parts['path']; 118 | 119 | if (count($params)) { 120 | $url .= '?'.http_build_query($params); 121 | } 122 | 123 | return $url; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Urls/UrlBuilderFactory.php: -------------------------------------------------------------------------------- 1 |