├── LICENSE ├── composer.json ├── readme.md └── src ├── Analyzers ├── ColorspaceAnalyzer.php ├── HeightAnalyzer.php ├── PixelColorAnalyzer.php ├── PixelColorsAnalyzer.php ├── ProfileAnalyzer.php ├── ResolutionAnalyzer.php └── WidthAnalyzer.php ├── ColorProcessor.php ├── Core.php ├── Decoders ├── Base64ImageDecoder.php ├── BinaryImageDecoder.php ├── DataUriImageDecoder.php ├── EncodedImageObjectDecoder.php ├── FilePathImageDecoder.php ├── FilePointerImageDecoder.php ├── NativeObjectDecoder.php └── SplFileInfoImageDecoder.php ├── Driver.php ├── Encoders ├── AvifEncoder.php ├── BmpEncoder.php ├── GifEncoder.php ├── HeicEncoder.php ├── Jpeg2000Encoder.php ├── JpegEncoder.php ├── PngEncoder.php ├── TiffEncoder.php └── WebpEncoder.php ├── FontProcessor.php ├── Frame.php ├── LoaderDetector.php ├── Modifiers ├── AlignRotationModifier.php ├── BlendTransparencyModifier.php ├── BlurModifier.php ├── BrightnessModifier.php ├── ColorizeModifier.php ├── ColorspaceModifier.php ├── ContainModifier.php ├── ContrastModifier.php ├── CoverDownModifier.php ├── CoverModifier.php ├── CropModifier.php ├── DrawBezierModifier.php ├── DrawEllipseModifier.php ├── DrawLineModifier.php ├── DrawPixelModifier.php ├── DrawPolygonModifier.php ├── DrawRectangleModifier.php ├── FillModifier.php ├── FlipModifier.php ├── FlopModifier.php ├── GammaModifier.php ├── GreyscaleModifier.php ├── InvertModifier.php ├── PadModifier.php ├── PixelateModifier.php ├── PlaceModifier.php ├── ProfileModifier.php ├── ProfileRemovalModifier.php ├── RemoveAnimationModifier.php ├── ResizeCanvasModifier.php ├── ResizeCanvasRelativeModifier.php ├── ResizeDownModifier.php ├── ResizeModifier.php ├── ResolutionModifier.php ├── RotateModifier.php ├── ScaleDownModifier.php ├── ScaleModifier.php ├── SharpenModifier.php ├── SliceAnimationModifier.php ├── StripMetaModifier.php ├── TextModifier.php └── TrimModifier.php └── TrueTypeFont.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024-present Oliver Vogel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "intervention/image-driver-vips", 3 | "description": "libvips driver for Intervention Image", 4 | "homepage": "https://image.intervention.io/", 5 | "keywords": [ 6 | "image", 7 | "vips", 8 | "libvips" 9 | ], 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Oliver Vogel", 14 | "email": "oliver@intervention.io", 15 | "homepage": "https://intervention.io/" 16 | }, 17 | { 18 | "name": "Thomas Picquet", 19 | "email": "thomas@sctr.net" 20 | } 21 | ], 22 | "require": { 23 | "php": "^8.1", 24 | "intervention/image": "^3.11.0", 25 | "jcupitt/vips": "^2.4" 26 | }, 27 | "require-dev": { 28 | "ext-fileinfo": "*", 29 | "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", 30 | "phpstan/phpstan": "^2", 31 | "squizlabs/php_codesniffer": "^3.8", 32 | "slevomat/coding-standard": "~8.0" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Intervention\\Image\\Drivers\\Vips\\": "src" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "Intervention\\Image\\Drivers\\Vips\\Tests\\": "tests" 42 | } 43 | }, 44 | "scripts": { 45 | "test": [ 46 | "@phpunit", 47 | "@phpcs", 48 | "@phpstan" 49 | ], 50 | "phpstan": "phpstan analyse --ansi", 51 | "phpunit": "phpunit", 52 | "phpcs": "phpcs" 53 | }, 54 | "config": { 55 | "allow-plugins": { 56 | "dealerdirect/phpcodesniffer-composer-installer": true 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # libvips driver for Intervention Image 3 2 | 3 | [![Latest Version](https://img.shields.io/packagist/v/intervention/image-driver-vips.svg)](https://packagist.org/packages/intervention/image-driver-vips) 4 | [![Build Status](https://github.com/Intervention/image-driver-vips/actions/workflows/run-tests.yml/badge.svg)](https://github.com/Intervention/image-driver-vips/actions) 5 | [![Monthly Downloads](https://img.shields.io/packagist/dm/intervention/image-driver-vips.svg)](https://packagist.org/packages/intervention/image-driver-vips/stats) 6 | [![Support me on Ko-fi](https://raw.githubusercontent.com/Intervention/image-driver-vips/develop/.github/images/support.svg)](https://ko-fi.com/interventionphp) 7 | 8 | Intervention Image's official driver to use [Intervention Image](https://github.com/Intervention/image) with 9 | [libvips](https://github.com/libvips/libvips). libvips is a fast, low-memory 10 | image processing library that outperforms the standard PHP image extensions GD 11 | and Imagick. This package makes it easy to utilize the power of libvips in your 12 | project while taking advantage of Intervention Image's user-friendly and 13 | easy-to-use API. 14 | 15 | ## Installation 16 | 17 | You can easily install this library using [Composer](https://getcomposer.org). 18 | Simply request the package with the following command: 19 | 20 | ```bash 21 | composer require intervention/image-driver-vips 22 | ``` 23 | 24 | ## Getting Started 25 | 26 | The public [API](https://image.intervention.io/v3) of Intervention Image can be 27 | used unchanged. The only [configuration](https://image.intervention.io/v3/basics/image-manager) that needs to be done is to ensure that 28 | `Intervention\Image\Drivers\Vips\Driver` by this library is used by `Intervention\Image\ImageManager`. 29 | 30 | ## Code Examples 31 | 32 | ```php 33 | use Intervention\Image\ImageManager; 34 | use Intervention\Image\Drivers\Vips\Driver as VipsDriver; 35 | 36 | // create image manager with vips driver 37 | $manager = ImageManager::withDriver(VipsDriver::class); 38 | 39 | // open an image file 40 | $image = $manager->read('images/example.gif'); 41 | 42 | // resize image instance 43 | $image->resize(height: 300); 44 | 45 | // insert a watermark 46 | $image->place('images/watermark.png'); 47 | 48 | // encode edited image 49 | $encoded = $image->toJpg(); 50 | 51 | // save encoded image 52 | $encoded->save('images/example.jpg'); 53 | ``` 54 | 55 | ## Requirements 56 | 57 | - PHP >= 8.1 58 | 59 | ## Caveats 60 | 61 | - Due to the technical characteristics of libvips, it is currently **not possible** 62 | to implement color quantization via `ImageInterface::reduceColors()` as 63 | intended. However, there is a [pull request in 64 | libvips](https://github.com/libvips/php-vips/issues/256#issuecomment-2575872401) 65 | that enables this feature and it may be integrated here the future as well. 66 | 67 | - With PHP on macOS, font files are not recognized in the 68 | `ImageInterface::text()` call by default because Quartz as a rendering engine 69 | does not allow font files to be loaded at runtime via the fontconfig API. 70 | However, setting the environment variable `PANGOCAIRO_BACKEND` to 71 | `fontconfig` helps here. 72 | 73 | ## Authors 74 | 75 | This library was developed by [Oliver Vogel](https://intervention.io) and Thomas Picquet. 76 | 77 | ## License 78 | 79 | Intervention Image Driver Vips is licensed under the [MIT License](LICENSE). 80 | -------------------------------------------------------------------------------- /src/Analyzers/ColorspaceAnalyzer.php: -------------------------------------------------------------------------------- 1 | core()->native()->interpretation); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Analyzers/HeightAnalyzer.php: -------------------------------------------------------------------------------- 1 | core()->native(); 21 | 22 | return $vipsImage->getType('page-height') === 0 ? $vipsImage->height : $vipsImage->get('page-height'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Analyzers/PixelColorAnalyzer.php: -------------------------------------------------------------------------------- 1 | colorAt( 30 | $image->colorspace(), 31 | $image->core(), 32 | $this->x, 33 | $this->y, 34 | ); 35 | } 36 | 37 | /** 38 | * Detects color at given position and returns it as ColorInterface 39 | * 40 | * @throws ColorException|AnimationException 41 | */ 42 | protected function colorAt(ColorspaceInterface $colorspace, CoreInterface $core, int $x, int $y): ColorInterface 43 | { 44 | $core = Core::ensureInMemory($core); 45 | 46 | return $this->driver() 47 | ->colorProcessor($colorspace) 48 | ->nativeToColor(array_map( 49 | fn(int|float $value): int => (int) max(min($value, 255), 0), 50 | $core->native()->getpoint($x, $y) 51 | )); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Analyzers/PixelColorsAnalyzer.php: -------------------------------------------------------------------------------- 1 | height(); 26 | 27 | for ($i = 0; $i < $image->count(); $i++) { 28 | $colors->push( 29 | $this->colorAt( 30 | $image->colorspace(), 31 | $image->core(), 32 | $this->x, 33 | $this->y + $i * $height 34 | ) 35 | ); 36 | } 37 | 38 | return $colors; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Analyzers/ProfileAnalyzer.php: -------------------------------------------------------------------------------- 1 | core()->native()->get('icc-profile-data'); 25 | } catch (VipsException) { 26 | throw new ColorException('No ICC profile found in image.'); 27 | } 28 | 29 | return new Profile($profiles); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Analyzers/ResolutionAnalyzer.php: -------------------------------------------------------------------------------- 1 | core()->native(); 22 | 23 | return new Resolution( 24 | round($vipsImage->xres * 25.4), 25 | round($vipsImage->yres * 25.4) 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Analyzers/WidthAnalyzer.php: -------------------------------------------------------------------------------- 1 | core()->native()->width; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ColorProcessor.php: -------------------------------------------------------------------------------- 1 | $value * 255, $color->normalize()); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | * 41 | * @see ColorProcessorInterface::nativeToColor() 42 | */ 43 | public function nativeToColor(mixed $native): ColorInterface 44 | { 45 | if (!is_array($native)) { 46 | throw new ColorException('Vips driver can only decode colors in array format.'); 47 | } 48 | 49 | return match ($this->colorspace::class) { 50 | CmykColorspace::class => new CmykColor(...$native), 51 | default => new RgbColor(...$native), 52 | }; 53 | } 54 | 55 | /** 56 | * Transform vips interpretation into colorspace object 57 | * 58 | * @throws ColorException 59 | */ 60 | public static function interpretationToColorspace(string $interpretation): ColorspaceInterface 61 | { 62 | return match ($interpretation) { 63 | Interpretation::MULTIBAND => new RgbColorspace(), 64 | Interpretation::B_W => new RgbColorspace(), 65 | Interpretation::HISTOGRAM => new RgbColorspace(), 66 | Interpretation::FOURIER => new RgbColorspace(), 67 | Interpretation::XYZ => new RgbColorspace(), 68 | Interpretation::LAB => new RgbColorspace(), 69 | Interpretation::CMYK => new CmykColorspace(), 70 | Interpretation::LABQ => new RgbColorspace(), 71 | Interpretation::RGB => new RgbColorspace(), 72 | Interpretation::CMC => new RgbColorspace(), 73 | Interpretation::LCH => new RgbColorspace(), 74 | Interpretation::LABS => new RgbColorspace(), 75 | Interpretation::SRGB => new RgbColorspace(), 76 | Interpretation::HSV => new HsvColorspace(), 77 | Interpretation::SCRGB => new RgbColorspace(), 78 | Interpretation::XYZ => new RgbColorspace(), 79 | Interpretation::RGB16 => new RgbColorspace(), 80 | Interpretation::GREY16 => new RgbColorspace(), 81 | Interpretation::MATRIX => new RgbColorspace(), 82 | default => throw new ColorException( 83 | 'Unable to transform interpretation "' . $interpretation . '" to colorspace.', 84 | ), 85 | }; 86 | } 87 | 88 | /** 89 | * Transform colorspace into vips interpretation 90 | */ 91 | public static function colorspaceToInterpretation(string|ColorspaceInterface $colorspace): string 92 | { 93 | $classname = is_string($colorspace) ? $colorspace : $colorspace::class; 94 | 95 | return match ($classname) { 96 | RgbColorspace::class => Interpretation::SRGB, 97 | CmykColorspace::class => Interpretation::CMYK, 98 | HsvColorspace::class => Interpretation::HSV, 99 | default => Interpretation::SRGB, 100 | }; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Core.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class Core implements CoreInterface, Iterator 22 | { 23 | protected int $iteratorIndex = 0; 24 | 25 | /** 26 | * Create new core instance 27 | * 28 | * @return void 29 | */ 30 | public function __construct(protected VipsImage $vipsImage) 31 | { 32 | // 33 | } 34 | 35 | /** 36 | * @param list $frames 37 | * @throws VipsException 38 | */ 39 | public static function createFromFrames(array $frames, int $loops = 0): self 40 | { 41 | $natives = []; 42 | $delay = []; 43 | 44 | foreach ($frames as $frame) { 45 | $delay[] = intval($frame->delay() * 1000); 46 | $natives[] = $frame->native(); 47 | } 48 | 49 | $image = VipsImage::arrayjoin($natives, ['across' => 1]); 50 | $image->set('delay', $delay); 51 | $image->set('loop', $loops); 52 | $image->set('page-height', $natives[0]->height); 53 | $image->set('n-pages', count($frames)); 54 | 55 | return new self($image); 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | * 61 | * @see CoreInterface::native() 62 | */ 63 | public function native(): mixed 64 | { 65 | return $this->vipsImage; 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | * 71 | * @see CoreInterface::setNative() 72 | */ 73 | public function setNative(mixed $native): CoreInterface 74 | { 75 | $this->vipsImage = $native; 76 | 77 | return $this; 78 | } 79 | 80 | /** 81 | * Renders vips image of given core into memory and serves any downstream 82 | * requests from the memory area 83 | */ 84 | public static function ensureInMemory(CoreInterface $core): CoreInterface 85 | { 86 | try { 87 | if (!in_array('vips-sequential', $core->native()->getFields())) { 88 | return $core; 89 | } 90 | 91 | if (false === (bool) $core->native()->get('vips-sequential')) { 92 | return $core; 93 | } 94 | 95 | $core->setNative($core->native()->copyMemory()); 96 | } catch (AnimationException) { 97 | // 98 | } 99 | 100 | return $core; 101 | } 102 | 103 | /** 104 | * {@inheritdoc} 105 | * 106 | * @see CoreInterface::count() 107 | * 108 | * @throws VipsException 109 | */ 110 | public function count(): int 111 | { 112 | return $this->vipsImage->getType('n-pages') === 0 ? 1 : $this->vipsImage->get('n-pages'); 113 | } 114 | 115 | /** 116 | * @param list $frames 117 | * @throws VipsException|AnimationException 118 | */ 119 | public static function replaceFrames(VipsImage $vipsImage, array $frames): VipsImage 120 | { 121 | $loops = in_array('loop', $vipsImage->getFields()) ? $vipsImage->get('loop') : 0; 122 | 123 | return self::createFromFrames($frames, $loops)->native(); 124 | } 125 | 126 | /** 127 | * {@inheritdoc} 128 | * 129 | * @see CoreInterface::frame() 130 | * 131 | * @throws AnimationException|VipsException 132 | */ 133 | public function frame(int $position): FrameInterface 134 | { 135 | $count = $this->count(); 136 | 137 | if ($position > ($count - 1)) { 138 | throw new AnimationException('Frame #' . $position . ' could not be found in the image.'); 139 | } 140 | 141 | if ($count === 1) { 142 | return new Frame($this->vipsImage); 143 | } 144 | 145 | $sequential = in_array('vips-sequential', $this->vipsImage->getFields()) ? 146 | $this->vipsImage->get('vips-sequential') : null; 147 | 148 | if ($sequential) { 149 | $this->vipsImage = $this->vipsImage->copyMemory(); 150 | } 151 | 152 | $delay = in_array('delay', $this->vipsImage->getFields()) ? 153 | ($this->vipsImage->get('delay')[$position] ?? 0) : null; 154 | 155 | try { 156 | $height = $this->vipsImage->getType('page-height') === 0 ? 157 | $this->vipsImage->height : $this->vipsImage->get('page-height'); 158 | 159 | // extract only certain frame 160 | $vipsImage = $this->vipsImage->extract_area( 161 | 0, 162 | $height * $position, 163 | $this->vipsImage->width, 164 | $height 165 | ); 166 | 167 | $vipsImage->set('n-pages', 1); 168 | if (!is_null($delay)) { 169 | $vipsImage->set('delay', $delay); 170 | 171 | return new Frame($vipsImage, $delay / 1000); 172 | } 173 | 174 | return new Frame($vipsImage); 175 | } catch (VipsException) { 176 | throw new AnimationException('Frame #' . $position . ' could not be found in the image.'); 177 | } 178 | } 179 | 180 | /** 181 | * {@inheritdoc} 182 | * 183 | * @see CoreInterface::add() 184 | * 185 | * @throws AnimationException|VipsException 186 | */ 187 | public function add(FrameInterface $frame): self 188 | { 189 | $frames = $this->toArray(); 190 | $frames[] = $frame; 191 | 192 | $this->setNative(self::replaceFrames($this->vipsImage, $frames)); 193 | 194 | return $this; 195 | } 196 | 197 | /** 198 | * {@inheritdoc} 199 | * 200 | * @see CoreInterface::loops() 201 | * 202 | * @throws VipsException 203 | */ 204 | public function loops(): int 205 | { 206 | return (int) $this->vipsImage->get('loop'); 207 | } 208 | 209 | /** 210 | * {@inheritdoc} 211 | * 212 | * @see CoreInterface::setLoops() 213 | * 214 | * @throws VipsException 215 | */ 216 | public function setLoops(int $loops): CoreInterface 217 | { 218 | $this->vipsImage->set('loop', $loops); 219 | 220 | return $this; 221 | } 222 | 223 | /** 224 | * {@inheritdoc} 225 | * 226 | * @see CollectionInterface::first() 227 | * 228 | * @throws AnimationException|VipsException 229 | */ 230 | public function first(): FrameInterface 231 | { 232 | return $this->frame(0); 233 | } 234 | 235 | /** 236 | * {@inheritdoc} 237 | * 238 | * @see CollectableInterface::last() 239 | * 240 | * @throws AnimationException|VipsException 241 | */ 242 | public function last(): FrameInterface 243 | { 244 | return $this->frame($this->count() - 1); 245 | } 246 | 247 | /** 248 | * {@inheritdoc} 249 | * 250 | * @see CollectionInterface::has() 251 | */ 252 | public function has(int|string $key): bool 253 | { 254 | try { 255 | return (bool) $this->frame($key); 256 | } catch (VipsException | AnimationException) { 257 | return false; 258 | } 259 | } 260 | 261 | /** 262 | * {@inheritdoc} 263 | * 264 | * @see CollectionInterface::push() 265 | * 266 | * @throws AnimationException|VipsException 267 | */ 268 | public function push($item): CollectionInterface 269 | { 270 | return $this->add($item); 271 | } 272 | 273 | /** 274 | * {@inheritdoc} 275 | * 276 | * @see CollectionInterface::get() 277 | */ 278 | public function get(int|string $key, $default = null): mixed 279 | { 280 | try { 281 | return $this->frame($key); 282 | } catch (VipsException | AnimationException) { 283 | return $default; 284 | } 285 | } 286 | 287 | /** 288 | * {@inheritdoc} 289 | * 290 | * @see CollectionInterface::getAtPosition() 291 | * 292 | * @throws Exception 293 | */ 294 | public function getAtPosition(int $key = 0, $default = null): mixed 295 | { 296 | return $this->get($key, $default); 297 | } 298 | 299 | /** 300 | * {@inheritdoc} 301 | * 302 | * @see CollectionInterface::empty() 303 | */ 304 | public function empty(): CollectionInterface 305 | { 306 | $this->vipsImage = VipsImage::black(1, 1)->cast($this->vipsImage->format); 307 | 308 | return $this; 309 | } 310 | 311 | /** 312 | * @throws AnimationException|VipsException 313 | * @return list 314 | */ 315 | public function toArray(): array 316 | { 317 | $frames = []; 318 | 319 | for ($i = 0; $i < $this->count(); $i++) { 320 | $frames[] = $this->frame($i); 321 | } 322 | 323 | return $frames; 324 | } 325 | 326 | /** 327 | * {@inheritdoc} 328 | * 329 | * @see CollectionInterface::slice() 330 | * 331 | * @throws AnimationException|VipsException 332 | */ 333 | public function slice(int $offset, ?int $length = 0): CollectionInterface 334 | { 335 | $frames = $this->toArray(); 336 | 337 | $frames = array_slice($frames, $offset, $length); 338 | $this->setNative(self::replaceFrames($this->vipsImage, $frames)); 339 | 340 | return $this; 341 | } 342 | 343 | /** 344 | * Implementation of IteratorAggregate 345 | * 346 | * @return Traversable 347 | */ 348 | public function getIterator(): Traversable 349 | { 350 | return new ArrayIterator($this); // @phpstan-ignore-line 351 | } 352 | 353 | /** 354 | * {@inheritdoc} 355 | * 356 | * @see Iterator::valid() 357 | */ 358 | public function valid(): bool 359 | { 360 | return $this->has($this->iteratorIndex); 361 | } 362 | 363 | /** 364 | * {@inheritdoc} 365 | * 366 | * @see Iterator::current() 367 | */ 368 | public function current(): mixed 369 | { 370 | return $this->get($this->iteratorIndex); 371 | } 372 | 373 | /** 374 | * {@inheritdoc} 375 | * 376 | * @see Iterator::next() 377 | */ 378 | public function next(): void 379 | { 380 | $this->iteratorIndex += 1; 381 | } 382 | 383 | /** 384 | * {@inheritdoc} 385 | * 386 | * @see Iterator::key() 387 | */ 388 | public function key(): mixed 389 | { 390 | return $this->iteratorIndex; 391 | } 392 | 393 | /** 394 | * {@inheritdoc} 395 | * 396 | * @see Iterator::rewind() 397 | */ 398 | public function rewind(): void 399 | { 400 | $this->iteratorIndex = 0; 401 | } 402 | 403 | /** 404 | * Show debug info for the current image 405 | * 406 | * @throws VipsException 407 | * @return array 408 | */ 409 | public function __debugInfo(): array 410 | { 411 | $debug = []; 412 | 413 | foreach ($this->vipsImage->getFields() as $name) { 414 | $value = $this->vipsImage->get($name); 415 | 416 | if (str_ends_with($name, "-data")) { 417 | $len = strlen($value); 418 | $value = "<$len bytes of binary data>"; 419 | } 420 | 421 | $debug[$name] = is_array($value) ? implode(", ", $value) : (string) $value; 422 | } 423 | 424 | return $debug; 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /src/Decoders/Base64ImageDecoder.php: -------------------------------------------------------------------------------- 1 | isValidBase64($input)) { 21 | throw new DecoderException('Unable to decode input'); 22 | } 23 | 24 | return parent::decode(base64_decode($input)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Decoders/BinaryImageDecoder.php: -------------------------------------------------------------------------------- 1 | stringOptions(), [ 29 | 'access' => Vips\Access::SEQUENTIAL, 30 | ]); 31 | } catch (Exception) { 32 | throw new DecoderException('Unable to decode input'); 33 | } 34 | 35 | $image = parent::decode($vipsImage); 36 | 37 | // get media type enum from string media type 38 | $format = Format::tryCreate($image->origin()->mediaType()); 39 | 40 | // extract exif data for appropriate formats 41 | if (in_array($format, [Format::JPEG, Format::TIFF])) { 42 | $image->setExif($this->extractExifData($input)); 43 | } 44 | 45 | return $image; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Decoders/DataUriImageDecoder.php: -------------------------------------------------------------------------------- 1 | parseDataUri($input); 25 | 26 | if (!$uri->isValid()) { 27 | throw new DecoderException('Unable to decode input'); 28 | } 29 | 30 | if ($uri->isBase64Encoded()) { 31 | return parent::decode(base64_decode($uri->data())); 32 | } 33 | 34 | return parent::decode(urldecode($uri->data())); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Decoders/EncodedImageObjectDecoder.php: -------------------------------------------------------------------------------- 1 | toString()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Decoders/FilePathImageDecoder.php: -------------------------------------------------------------------------------- 1 | isFile($input)) { 24 | throw new DecoderException('Unable to decode input'); 25 | } 26 | 27 | try { 28 | $vipsImage = Vips\Image::newFromFile($input . '[' . $this->stringOptions() . ']', [ 29 | 'access' => Vips\Access::SEQUENTIAL, 30 | ]); 31 | } catch (Exception) { 32 | throw new DecoderException('Unable to decode input'); 33 | } 34 | 35 | $image = parent::decode($vipsImage); 36 | 37 | // set file path on origin 38 | $image->origin()->setFilePath($input); 39 | 40 | // extract exif data for the appropriate formats 41 | if (in_array($this->vipsMediaType($vipsImage)?->format(), [Format::JPEG, Format::TIFF])) { 42 | $image->setExif($this->extractExifData($input)); 43 | } 44 | 45 | return $image; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Decoders/FilePointerImageDecoder.php: -------------------------------------------------------------------------------- 1 | driver()->config()->autoOrientation === true) { 40 | $input = $input->autorot(); 41 | } 42 | 43 | // build image instance 44 | $image = new Image( 45 | $this->driver(), 46 | new Core($input) 47 | ); 48 | 49 | // set media type on origin 50 | if ($mediaType = $this->vipsMediaType($input)) { 51 | $image->origin()->setMediaType($mediaType); 52 | } 53 | 54 | return $image; 55 | } 56 | 57 | /** 58 | * Get options for vips library according to current configuration 59 | */ 60 | protected function stringOptions(): string 61 | { 62 | $options = ''; 63 | 64 | if ($this->driver()->config()->decodeAnimation === true) { 65 | $options = 'n=-1'; 66 | } 67 | 68 | return $options; 69 | } 70 | 71 | /** 72 | * Return media type of given vips image instance 73 | */ 74 | protected function vipsMediaType(VipsImage $vips): ?MediaType 75 | { 76 | try { 77 | $loader = $vips->get('vips-loader'); 78 | } catch (VipsException) { 79 | return null; 80 | } 81 | 82 | $result = preg_match("/^(?P.+)load(_.+)?$/", $loader, $matches); 83 | 84 | if ($result !== 1) { 85 | return null; 86 | } 87 | 88 | return match ($matches['loader']) { 89 | 'gif' => MediaType::IMAGE_GIF, 90 | 'heif' => MediaType::IMAGE_HEIF, 91 | 'jp2k' => MediaType::IMAGE_JP2, 92 | 'jpeg' => MediaType::IMAGE_JPEG, 93 | 'png' => MediaType::IMAGE_PNG, 94 | 'tiff' => MediaType::IMAGE_TIFF, 95 | 'webp' => MediaType::IMAGE_WEBP, 96 | default => null 97 | }; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Decoders/SplFileInfoImageDecoder.php: -------------------------------------------------------------------------------- 1 | getRealPath()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Driver.php: -------------------------------------------------------------------------------- 1 | add(255) // add red channel 51 | ->cast(BandFormat::UCHAR) // cast to format 52 | ->embed(0, 0, $width, $height, ['extend' => Extend::COPY]) // extend to given width/height 53 | ->copy([ 54 | 'interpretation' => Interpretation::SRGB, 55 | 'xres' => 96 / 25.4, 56 | 'yres' => 96 / 25.4, 57 | ]) // srgb 58 | ->bandjoin([ 59 | 255, // green 60 | 255, // blue 61 | 0, // alpha 62 | ]); 63 | 64 | return new Image($this, new Core($vipsImage)); 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | * 70 | * @see DriverInterface::createAnimation() 71 | * 72 | * @throws RuntimeException|VipsException 73 | */ 74 | public function createAnimation(callable $init): ImageInterface 75 | { 76 | $animation = new class ($this) 77 | { 78 | /** 79 | * @var list 80 | */ 81 | protected array $frames = []; 82 | 83 | public function __construct( 84 | protected DriverInterface $driver, 85 | ) { 86 | // 87 | } 88 | 89 | /** 90 | * @throws RuntimeException 91 | */ 92 | public function add(mixed $source, float $delay = 1): self 93 | { 94 | $this->frames[] = $this->driver->handleInput($source)->core()->first()->setDelay($delay); 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * @throws RuntimeException|VipsException 101 | */ 102 | public function __invoke(): ImageInterface 103 | { 104 | return new Image( 105 | $this->driver, 106 | Core::createFromFrames($this->frames) 107 | ); 108 | } 109 | }; 110 | 111 | $init($animation); 112 | 113 | return call_user_func($animation); 114 | } 115 | 116 | /** 117 | * @param array $attributes 118 | * @throws RuntimeException 119 | */ 120 | public static function createShape(string $shape, array $attributes, int $width, int $height): VipsImage 121 | { 122 | $xmlAttributes = implode( 123 | ' ', 124 | array_map( 125 | fn($key, $value) => sprintf('%s="%s"', $key, htmlspecialchars((string) $value)), 126 | array_keys($attributes), 127 | $attributes 128 | ) 129 | ); 130 | 131 | $svg = '' . 132 | '<' . $shape . ' ' . $xmlAttributes . ' />' . 133 | ''; 134 | 135 | try { 136 | return VipsImage::svgload_buffer($svg); 137 | } catch (VipsException $e) { 138 | throw new RuntimeException('Could not create shape: ' . $e->getMessage(), previous: $e); 139 | } 140 | } 141 | 142 | /** 143 | * {@inheritdoc} 144 | * 145 | * @see DriverInterface::colorProcessor() 146 | */ 147 | public function colorProcessor(ColorspaceInterface $colorspace): ColorProcessorInterface 148 | { 149 | return new ColorProcessor($colorspace); 150 | } 151 | 152 | /** 153 | * {@inheritdoc} 154 | * 155 | * @see DriverInterface::fontProcessor() 156 | */ 157 | public function fontProcessor(): FontProcessorInterface 158 | { 159 | return new FontProcessor(); 160 | } 161 | 162 | /** 163 | * {@inheritdoc} 164 | * 165 | * @see DriverInterface::supports() 166 | * 167 | * @throws RuntimeException 168 | */ 169 | public function supports(string|Format|FileExtension|MediaType $identifier): bool 170 | { 171 | try { 172 | $format = Format::create($identifier); 173 | } catch (NotSupportedException) { 174 | return false; 175 | } 176 | 177 | return in_array($format, LoaderDetector::create()->formats()); 178 | } 179 | 180 | /** 181 | * {@inheritdoc} 182 | * 183 | * @see DriverInterface::checkHealth() 184 | */ 185 | public function checkHealth(): void 186 | { 187 | try { 188 | // check health by calling Jcupitt\Vips\FFI::init() 189 | VipsConfig::version(); 190 | } catch (VipsException $e) { 191 | throw new DriverException( 192 | 'libvips does not seem to be installed correctly.', 193 | previous: $e 194 | ); 195 | } 196 | } 197 | 198 | /** 199 | * Return version of libvips library 200 | */ 201 | public static function version(): string 202 | { 203 | return VipsConfig::version(); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/Encoders/AvifEncoder.php: -------------------------------------------------------------------------------- 1 | core()->native()->writeToBuffer('.avif', $this->getOptions()); 24 | 25 | return new EncodedImage($result, 'image/avif'); 26 | } 27 | 28 | /** 29 | * @return array{lossless: bool, Q: int, keep?: int, strip?: bool} 30 | */ 31 | protected function getOptions(): array 32 | { 33 | $options = [ 34 | 'lossless' => $this->quality === 100, 35 | 'Q' => $this->quality, 36 | ]; 37 | 38 | $strip = $this->strip || $this->driver()->config()->strip; 39 | 40 | if (VipsConfig::atLeast(8, 15)) { 41 | $options['keep'] = $strip ? ForeignKeep::ICC : ForeignKeep::ALL; 42 | } else { 43 | $options['strip'] = $strip; 44 | } 45 | 46 | return $options; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Encoders/BmpEncoder.php: -------------------------------------------------------------------------------- 1 | core()->native(); 21 | 22 | if ($image->isAnimated()) { 23 | $vipsImage = $image->core()->frame(0)->native(); 24 | } 25 | 26 | $result = $vipsImage->writeToBuffer('.bmp'); 27 | 28 | return new EncodedImage($result, 'image/bmp'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Encoders/GifEncoder.php: -------------------------------------------------------------------------------- 1 | core()->native()->writeToBuffer('.gif', [ 22 | 'interlace' => $this->interlaced, 23 | ]); 24 | 25 | return new EncodedImage($result, 'image/gif'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Encoders/HeicEncoder.php: -------------------------------------------------------------------------------- 1 | core()->native()->writeToBuffer('.heic', $this->getOptions()); 24 | 25 | return new EncodedImage($result, 'image/heic'); 26 | } 27 | 28 | /** 29 | * @return array{lossless: bool, Q: int, keep?: int, strip?: bool} 30 | */ 31 | protected function getOptions(): array 32 | { 33 | $options = [ 34 | 'lossless' => $this->quality === 100, 35 | 'Q' => $this->quality, 36 | ]; 37 | 38 | $strip = $this->strip || $this->driver()->config()->strip; 39 | 40 | if (VipsConfig::atLeast(8, 15)) { 41 | $options['keep'] = $strip ? ForeignKeep::ICC : ForeignKeep::ALL; 42 | } else { 43 | $options['strip'] = $strip; 44 | } 45 | 46 | return $options; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Encoders/Jpeg2000Encoder.php: -------------------------------------------------------------------------------- 1 | core()->native(); 24 | 25 | if ($image->isAnimated()) { 26 | $vipsImage = $image->core()->frame(0)->native(); 27 | } 28 | 29 | $result = $vipsImage->writeToBuffer('.j2k', $this->getOptions()); 30 | 31 | return new EncodedImage($result, 'image/jp2'); 32 | } 33 | 34 | /** 35 | * @return array{lossless: bool, Q: int, keep?: int, strip?: bool} 36 | */ 37 | protected function getOptions(): array 38 | { 39 | $options = [ 40 | 'lossless' => $this->quality === 100, 41 | 'Q' => $this->quality, 42 | ]; 43 | 44 | $strip = $this->strip || $this->driver()->config()->strip; 45 | 46 | if (VipsConfig::atLeast(8, 15)) { 47 | $options['keep'] = $strip ? ForeignKeep::ICC : ForeignKeep::ALL; 48 | } else { 49 | $options['strip'] = $strip; 50 | } 51 | 52 | return $options; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Encoders/JpegEncoder.php: -------------------------------------------------------------------------------- 1 | core()->native(); 26 | 27 | if ($image->isAnimated()) { 28 | $vipsImage = $image->core()->frame(0)->native(); 29 | } 30 | 31 | $result = $vipsImage->writeToBuffer('.jpg', $this->getOptions()); 32 | 33 | return new EncodedImage($result, 'image/jpeg'); 34 | } 35 | 36 | /** 37 | * @throws RuntimeException 38 | * @return array{Q: int, optimize_coding: bool, background: array, keep?: int, strip?: bool} 39 | */ 40 | protected function getOptions(): array 41 | { 42 | $options = [ 43 | 'Q' => $this->quality, 44 | 'interlace' => $this->progressive, 45 | 'optimize_coding' => true, 46 | 'background' => array_slice($this->driver()->handleInput( 47 | $this->driver()->config()->blendingColor 48 | )->convertTo(Rgb::class)->toArray(), 0, 3), 49 | ]; 50 | 51 | $strip = $this->strip || $this->driver()->config()->strip; 52 | 53 | if (VipsConfig::atLeast(8, 15)) { 54 | $options['keep'] = $strip ? ForeignKeep::ICC : ForeignKeep::ALL; 55 | } else { 56 | $options['strip'] = $strip; 57 | } 58 | 59 | return $options; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Encoders/PngEncoder.php: -------------------------------------------------------------------------------- 1 | core()->native(); 22 | 23 | if ($image->isAnimated()) { 24 | $vipsImage = $image->core()->frame(0)->native(); 25 | } 26 | 27 | $result = $vipsImage->writeToBuffer('.png', [ 28 | 'interlace' => $this->interlaced, 29 | 'palette' => $this->indexed, 30 | ]); 31 | 32 | return new EncodedImage($result, 'image/png'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Encoders/TiffEncoder.php: -------------------------------------------------------------------------------- 1 | core()->native()->writeToBuffer('.tiff', $this->getOptions()); 24 | 25 | return new EncodedImage($result, 'image/tiff'); 26 | } 27 | 28 | /** 29 | * @return array{lossless: bool, Q: int, keep?: int, strip?: bool} 30 | */ 31 | protected function getOptions(): array 32 | { 33 | $options = [ 34 | 'lossless' => $this->quality === 100, 35 | 'Q' => $this->quality, 36 | ]; 37 | 38 | $strip = $this->strip || $this->driver()->config()->strip; 39 | 40 | if (VipsConfig::atLeast(8, 15)) { 41 | $options['keep'] = $strip ? ForeignKeep::ICC : ForeignKeep::ALL; 42 | } else { 43 | $options['strip'] = true; 44 | } 45 | 46 | return $options; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Encoders/WebpEncoder.php: -------------------------------------------------------------------------------- 1 | core()->native()->writeToBuffer('.webp', $this->getOptions()); 24 | 25 | return new EncodedImage($result, 'image/webp'); 26 | } 27 | 28 | /** 29 | * @return array{lossless: bool, Q: int, keep?: int, strip?: bool} 30 | */ 31 | protected function getOptions(): array 32 | { 33 | $options = [ 34 | 'lossless' => $this->quality === 100, 35 | 'Q' => $this->quality, 36 | ]; 37 | 38 | $strip = $this->strip || $this->driver()->config()->strip; 39 | 40 | if (VipsConfig::atLeast(8, 15)) { 41 | $options['keep'] = $strip ? ForeignKeep::ICC : ForeignKeep::ALL; 42 | } else { 43 | $options['strip'] = $strip; 44 | } 45 | 46 | return $options; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/FontProcessor.php: -------------------------------------------------------------------------------- 1 | textToVipsImage($text, $font); 36 | 37 | return new Rectangle( 38 | $text->width, 39 | $text->height, 40 | ); 41 | } 42 | 43 | /** 44 | * Return renderable text/font combination in the specified colour as an vips image 45 | * 46 | * @throws FontException 47 | * @throws RuntimeException 48 | */ 49 | public function textToVipsImage( 50 | string $text, 51 | FontInterface $font, 52 | ColorInterface $color = new Color(0, 0, 0), 53 | ): VipsImage { 54 | return VipsImage::text( 55 | 'pangoAttributes($font, $color) . '>' . htmlspecialchars($text) . '', 56 | [ 57 | 'fontfile' => $font->filename(), 58 | 'font' => TrueTypeFont::fromPath($font->filename())->familyName() . ' ' . $font->size(), 59 | 'dpi' => 72, 60 | 'rgba' => true, 61 | 'width' => $font->wrapWidth(), 62 | 'wrap' => TextWrap::WORD, 63 | 'align' => match ($font->alignment()) { 64 | 'center', 65 | 'middle' => Align::CENTRE, 66 | 'right' => Align::HIGH, 67 | default => Align::LOW, 68 | }, 69 | 'spacing' => 0 70 | ] 71 | ); 72 | } 73 | 74 | /** 75 | * Return a pango markup attribute string based on the given font and color values 76 | */ 77 | private function pangoAttributes(FontInterface $font, ColorInterface $color): string 78 | { 79 | $pango_attributes = [ 80 | 'line_height' => (string) $font->lineHeight() / 1.62, 81 | 'foreground' => $color->toHex('#'), 82 | ]; 83 | 84 | // format pango attributes 85 | return implode(' ', array_map(function ($value, $key): string { 86 | return $key . '="' . $value . '"'; 87 | }, $pango_attributes, array_keys($pango_attributes))); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Frame.php: -------------------------------------------------------------------------------- 1 | vipsImage; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | * 40 | * @see FrameInterface::setNative() 41 | */ 42 | public function setNative(mixed $native): FrameInterface 43 | { 44 | $this->vipsImage = $native; 45 | 46 | return $this; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | * 52 | * @see FrameInterface::toImage() 53 | */ 54 | public function toImage(DriverInterface $driver): ImageInterface 55 | { 56 | return new Image($driver, new Core($this->native())); 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | * 62 | * @see FrameInterface::size() 63 | */ 64 | public function size(): SizeInterface 65 | { 66 | return new Rectangle( 67 | $this->vipsImage->width, 68 | $this->vipsImage->height, 69 | ); 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | * 75 | * @see FrameInterface::delay() 76 | */ 77 | public function delay(): float 78 | { 79 | return $this->delay; 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | * 85 | * @see FrameInterface::delay() 86 | */ 87 | public function setDelay(float $delay): FrameInterface 88 | { 89 | $this->delay = $delay; 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * {@inheritdoc} 96 | * 97 | * Currently not implemented by libvips 98 | * 99 | * @see FrameInterface::dispose() 100 | */ 101 | public function dispose(): int 102 | { 103 | return 0; 104 | } 105 | 106 | /** 107 | * {@inheritdoc} 108 | * 109 | * Currently not implemented by libvips 110 | * 111 | * @see FrameInterface::dispose() 112 | */ 113 | public function setDispose(int $dispose): FrameInterface 114 | { 115 | return $this; 116 | } 117 | 118 | public function setOffset(int $left, int $top): FrameInterface 119 | { 120 | $this->setOffsetLeft($left); 121 | $this->setOffsetTop($top); 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * {@inheritdoc} 128 | * 129 | * @see FrameInterface::offsetLeft() 130 | */ 131 | public function offsetLeft(): int 132 | { 133 | return $this->native()->get('xoffset'); 134 | } 135 | 136 | /** 137 | * {@inheritdoc} 138 | * 139 | * @see FrameInterface::setOffsetLeft() 140 | */ 141 | public function setOffsetLeft(int $offset): FrameInterface 142 | { 143 | $this->native()->set('xoffset', $offset); 144 | 145 | return $this; 146 | } 147 | 148 | /** 149 | * {@inheritdoc} 150 | * 151 | * @see FrameInterface::offsetTop() 152 | */ 153 | public function offsetTop(): int 154 | { 155 | return $this->native()->get('yoffset'); 156 | } 157 | 158 | /** 159 | * {@inheritdoc} 160 | * 161 | * @see FrameInterface::setOffsetTop() 162 | */ 163 | public function setOffsetTop(int $offset): FrameInterface 164 | { 165 | $this->native()->set('yoffset', $offset); 166 | 167 | return $this; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/LoaderDetector.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | protected array $loaders = []; 24 | 25 | /** 26 | * Private constructor, use self::create() 27 | * 28 | * @return void 29 | */ 30 | private function __construct() 31 | { 32 | $base = FFI::gobject()->g_type_from_name("VipsForeignLoad"); // @phpstan-ignore-line 33 | FFI::vips()->vips_type_map($base, $this->collectLoaders(...), null, null); // @phpstan-ignore-line 34 | 35 | // normalize loader names 36 | $this->loaders = array_map(function (string $name): ?string { 37 | preg_match("/^(?P[a-z0-9]+)load_/", $name, $matches); 38 | return $matches['identifier'] ?? null; 39 | }, $this->loaders); 40 | 41 | // filter out null values 42 | $this->loaders = array_filter($this->loaders, fn(?string $identifier): bool => !is_null($identifier)); 43 | 44 | // make unique 45 | $this->loaders = array_unique($this->loaders, SORT_STRING); 46 | } 47 | 48 | /** 49 | * Create instance via singleton variable 50 | */ 51 | public static function create(): self 52 | { 53 | if (self::$instance === null) { 54 | self::$instance = new self(); 55 | } 56 | 57 | return self::$instance; 58 | } 59 | 60 | /** 61 | * Return array of available vips-loaders 62 | * 63 | * @return array 64 | */ 65 | public function loaders(): array 66 | { 67 | return $this->loaders; 68 | } 69 | 70 | /** 71 | * Return array of available formats detected from vips-loaders 72 | * 73 | * @return array 74 | */ 75 | public function formats(): array 76 | { 77 | $formats = []; 78 | 79 | foreach ($this->loaders as $loader) { 80 | switch ($loader) { 81 | case 'jpeg': 82 | $formats[] = Format::JPEG; 83 | break; 84 | 85 | case 'gif': 86 | $formats[] = Format::GIF; 87 | break; 88 | 89 | case 'png': 90 | $formats[] = Format::PNG; 91 | break; 92 | 93 | case 'heif': 94 | $formats[] = Format::AVIF; 95 | $formats[] = Format::HEIC; 96 | break; 97 | 98 | case 'magick': 99 | $formats[] = Format::BMP; 100 | break; 101 | 102 | case 'webp': 103 | $formats[] = Format::WEBP; 104 | break; 105 | 106 | case 'tiff': 107 | $formats[] = Format::TIFF; 108 | break; 109 | 110 | case 'jp2k': 111 | $formats[] = Format::JP2; 112 | break; 113 | } 114 | } 115 | 116 | return $formats; 117 | } 118 | 119 | /** 120 | * Collect loaders 121 | * 122 | * @throws VipsException 123 | */ 124 | private function collectLoaders(string $type): void 125 | { 126 | $name = FFI::vips()->vips_nickname_find($type); // @phpstan-ignore-line 127 | $this->loaders[] = $name; 128 | FFI::vips()->vips_type_map($type, $this->collectLoaders(...), null, null); // @phpstan-ignore-line 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Modifiers/AlignRotationModifier.php: -------------------------------------------------------------------------------- 1 | core()->setNative( 21 | $image->core()->native()->autorot() 22 | ); 23 | 24 | return $image; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Modifiers/BlendTransparencyModifier.php: -------------------------------------------------------------------------------- 1 | blendingColor($this->driver()); 40 | 41 | // create new canvas with blending color as background 42 | $canvas = $this->canvas($image, $color); 43 | 44 | if ($image->isAnimated()) { 45 | $frames = []; 46 | foreach ($image as $frame) { 47 | $frames[] = new Frame( 48 | $canvas->core()->native()->composite2($frame->native(), BlendMode::OVER), 49 | $frame->delay() 50 | ); 51 | } 52 | 53 | $image->core()->setNative( 54 | Core::replaceFrames($image->core()->native(), $frames) 55 | ); 56 | 57 | return $image; 58 | } 59 | 60 | $canvas->core()->setNative( 61 | $canvas->core()->native()->composite2($image->core()->native(), BlendMode::OVER) 62 | ); 63 | 64 | return $canvas; 65 | } 66 | 67 | /** 68 | * Create empty image with given background color in the size of the given image 69 | * 70 | * @throws ColorException 71 | * @throws VipsException 72 | * @throws RuntimeException 73 | */ 74 | private function canvas(ImageInterface $image, ColorInterface $color): ImageInterface 75 | { 76 | /** @var RgbColor $color */ 77 | $vipsImage = VipsImage::black(1, 1) 78 | ->add($color->red()->value()) 79 | ->cast($image->core()->native()->format) 80 | ->embed(0, 0, $image->width(), $image->height(), ['extend' => Extend::COPY]) 81 | ->copy(['interpretation' => $image->core()->native()->interpretation]) 82 | ->bandjoin([ 83 | $color->green()->value(), 84 | $color->blue()->value(), 85 | ]); 86 | 87 | $core = Core::ensureInMemory(new Core($vipsImage)); 88 | 89 | return new Image($this->driver(), $core); 90 | } 91 | 92 | /** 93 | * Decode current blending color of modifier 94 | * 95 | * TODO: Remove this method and use parent class implementation 96 | * (requires unreleased 'intervention/image' version) 97 | * 98 | * @throws RuntimeException 99 | * @throws DecoderException 100 | * @throws VipsException 101 | */ 102 | protected function blendingColor(DriverInterface $driver): ColorInterface 103 | { 104 | // decode blending color 105 | $color = $driver->handleInput( 106 | $this->color ?: $driver->config()->blendingColor 107 | ); 108 | 109 | if (!($color instanceof ColorInterface)) { 110 | throw new DecoderException('Unable to decode blending color.'); 111 | } 112 | 113 | return $color->convertTo(RgbColorspace::class); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Modifiers/BlurModifier.php: -------------------------------------------------------------------------------- 1 | core()->setNative( 21 | $image->core()->native()->gaussblur($this->amount * 0.53) 22 | ); 23 | 24 | return $image; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Modifiers/BrightnessModifier.php: -------------------------------------------------------------------------------- 1 | core()->native()->hasAlpha()) { 21 | $flatten = $image->core()->native()->extract_band(0, ['n' => $image->core()->native()->bands - 1]); 22 | $mask = $image->core()->native()->extract_band($image->core()->native()->bands - 1, ['n' => 1]); 23 | 24 | $brightened = $flatten 25 | ->linear(1, $this->level) 26 | ->bandjoin($mask) 27 | ->cast($image->core()->native()->format) 28 | ; 29 | } else { 30 | $brightened = $image->core()->native() 31 | ->linear(1, $this->level) 32 | ->cast($image->core()->native()->format) 33 | ; 34 | } 35 | 36 | $image->core()->setNative($brightened); 37 | 38 | return $image; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Modifiers/ColorizeModifier.php: -------------------------------------------------------------------------------- 1 | core()->native()->bands; 21 | 22 | $image->core()->setNative( 23 | $image->core()->native()->linear( 24 | 1, 25 | array_pad(array_map(fn(int $value): int => $value * 3, [ 26 | $this->red, 27 | $this->green, 28 | $this->blue, 29 | ]), $bands, 0) 30 | ) 31 | ); 32 | 33 | return $image; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Modifiers/ColorspaceModifier.php: -------------------------------------------------------------------------------- 1 | core()->setNative( 22 | $image->core()->native()->copy([ 23 | 'interpretation' => ColorProcessor::colorspaceToInterpretation( 24 | $this->targetColorspace() 25 | ) 26 | ]) 27 | ); 28 | 29 | return $image; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Modifiers/ContainModifier.php: -------------------------------------------------------------------------------- 1 | getResizeSize($image); 32 | $bgColor = $this->driver()->handleInput($this->background); 33 | 34 | if (!$image->isAnimated()) { 35 | $contained = $this->contain($image->core()->first(), $resize, $bgColor)->native(); 36 | } else { 37 | $frames = []; 38 | foreach ($image as $frame) { 39 | $frames[] = $this->contain($frame, $resize, $bgColor); 40 | } 41 | 42 | $contained = Core::replaceFrames($image->core()->native(), $frames); 43 | } 44 | 45 | $image->core()->setNative($contained); 46 | 47 | return $image; 48 | } 49 | 50 | /** 51 | * @throws ColorException 52 | */ 53 | private function contain(FrameInterface $frame, SizeInterface $resize, ColorInterface $bgColor): FrameInterface 54 | { 55 | $resized = $frame->native()->thumbnail_image($resize->width(), [ 56 | 'height' => $resize->height(), 57 | 'no_rotate' => true, 58 | ]); 59 | 60 | if (!$resized->hasAlpha()) { 61 | $resized = $resized->bandjoin_const(255); 62 | } 63 | 64 | $frame->setNative( 65 | $resized->gravity( 66 | $this->positionToGravity($this->position), 67 | $resize->width(), 68 | $resize->height(), 69 | [ 70 | 'extend' => Extend::BACKGROUND, 71 | 'background' => $bgColor->toArray(), 72 | ] 73 | ) 74 | ); 75 | 76 | return $frame; 77 | } 78 | 79 | /** 80 | * Convert position string to libvips gravity string. 81 | */ 82 | public function positionToGravity(string $position): string 83 | { 84 | return match (strtolower($position)) { 85 | 'top', 'top-center', 86 | 'top-middle', 87 | 'center-top', 88 | 'middle-top' => CompassDirection::NORTH, 89 | 'top-right', 90 | 'right-top' => CompassDirection::NORTH_EAST, 91 | 'left', 92 | 'left-center', 93 | 'left-middle', 94 | 'center-left', 95 | 'middle-left' => CompassDirection::WEST, 96 | 'right', 97 | 'right-center', 98 | 'right-middle', 99 | 'center-right', 100 | 'middle-right' => CompassDirection::EAST, 101 | 'bottom-left', 102 | 'left-bottom' => CompassDirection::SOUTH_WEST, 103 | 'bottom', 104 | 'bottom-center', 105 | 'bottom-middle', 106 | 'center-bottom', 107 | 'middle-bottom' => CompassDirection::SOUTH, 108 | 'bottom-right', 109 | 'right-bottom' => CompassDirection::SOUTH_EAST, 110 | 'top-left', 111 | 'left-top' => CompassDirection::NORTH_WEST, 112 | default => CompassDirection::CENTRE 113 | }; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Modifiers/ContrastModifier.php: -------------------------------------------------------------------------------- 1 | level / 100; 22 | $b = 255 * (1 - $a); 23 | 24 | if ($image->core()->native()->hasAlpha()) { 25 | $flatten = $image->core()->native()->extract_band(0, ['n' => $image->core()->native()->bands - 1]); 26 | $mask = $image->core()->native()->extract_band($image->core()->native()->bands - 1, ['n' => 1]); 27 | 28 | $brightened = $flatten 29 | ->linear($a, $b) 30 | ->bandjoin($mask) 31 | ->cast($image->core()->native()->format) 32 | ; 33 | } else { 34 | $brightened = $image->core()->native() 35 | ->linear($a, $b) 36 | ->cast($image->core()->native()->format) 37 | ; 38 | } 39 | 40 | $image->core()->setNative($brightened); 41 | 42 | return $image; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Modifiers/CoverDownModifier.php: -------------------------------------------------------------------------------- 1 | resizeDown($this->width, $this->height); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Modifiers/CoverModifier.php: -------------------------------------------------------------------------------- 1 | getCropSize($image); 29 | $resize = $this->getResizeSize($crop); 30 | 31 | $frames = []; 32 | foreach ($image as $frame) { 33 | $frames[] = $frame->setNative($this->cropResizeFrame($frame, $crop, $resize)); 34 | } 35 | 36 | $image->core()->setNative( 37 | Core::replaceFrames($image->core()->native(), $frames) 38 | ); 39 | 40 | return $image; 41 | } 42 | 43 | private function cropResizeFrame( 44 | FrameInterface $frame, 45 | SizeInterface $cropSize, 46 | SizeInterface $resizeSize 47 | ): VipsImage { 48 | return $frame->native()->crop( 49 | $cropSize->pivot()->x(), 50 | $cropSize->pivot()->y(), 51 | $cropSize->width(), 52 | $cropSize->height() 53 | )->thumbnail_image($resizeSize->width(), [ 54 | 'height' => $resizeSize->height(), 55 | 'size' => 'force', 56 | 'no_rotate' => true, 57 | ]) ; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Modifiers/CropModifier.php: -------------------------------------------------------------------------------- 1 | size(); 36 | $crop = $this->crop($image); 37 | $background = $this->background($crop, $image); 38 | 39 | if ( 40 | in_array($this->position, $this->getInterestingPositions()) && 41 | ( 42 | $crop->width() < $originalSize->width() || 43 | $crop->height() < $originalSize->height() 44 | ) 45 | ) { 46 | $image->core()->setNative( 47 | $image->core()->native()->smartcrop( 48 | $crop->width(), 49 | $crop->height(), 50 | ['interesting' => str_replace(self::INTERESTING_PREFIX, '', $this->position)] 51 | ) 52 | ); 53 | } else { 54 | $frames = []; 55 | foreach ($image as $frame) { 56 | $frames[] = $frame->setNative($this->cropFrame($frame, $crop, $originalSize, $background)); 57 | } 58 | 59 | $image->core()->setNative( 60 | Core::replaceFrames($image->core()->native(), $frames) 61 | ); 62 | } 63 | 64 | return $image; 65 | } 66 | 67 | /** 68 | * @throws RuntimeException|\Jcupitt\Vips\Exception 69 | */ 70 | private function background(SizeInterface $resizeTo, ImageInterface $image): VipsImage 71 | { 72 | $bgColor = $this->driver()->handleInput($this->background); 73 | 74 | $bands = [ 75 | $bgColor->channel(Green::class)->value(), 76 | $bgColor->channel(Blue::class)->value(), 77 | ]; 78 | 79 | // original image and background must have the same number of bands 80 | if ($image->core()->native()->hasAlpha()) { 81 | $bands[] = $bgColor->channel(Alpha::class)->value(); 82 | } 83 | 84 | return VipsImage::black(1, 1) 85 | ->add($bgColor->channel(Red::class)->value()) 86 | ->cast($image->core()->native()->format) 87 | ->embed(0, 0, $resizeTo->width(), $resizeTo->height(), ['extend' => Extend::COPY]) 88 | ->copy(['interpretation' => $image->core()->native()->interpretation]) 89 | ->bandjoin($bands); 90 | } 91 | 92 | /** 93 | * Smart crop interesting positions, prefixed with `interesting-`. 94 | * 95 | * @return list 96 | */ 97 | private function getInterestingPositions(): array 98 | { 99 | return array_map(fn (string $position): string => self::INTERESTING_PREFIX . $position, [ 100 | Interesting::NONE, 101 | Interesting::CENTRE, 102 | Interesting::ENTROPY, 103 | Interesting::ATTENTION, 104 | Interesting::LOW, 105 | Interesting::HIGH, 106 | Interesting::ALL, 107 | ]); 108 | } 109 | 110 | private function cropFrame( 111 | FrameInterface $frame, 112 | SizeInterface $crop, 113 | SizeInterface $originalSize, 114 | VipsImage $background 115 | ): VipsImage { 116 | $offset_x = $crop->pivot()->x() + $this->offset_x; 117 | $offset_y = $crop->pivot()->y() + $this->offset_y; 118 | 119 | $targetWidth = min($crop->width(), $originalSize->width()); 120 | $targetHeight = min($crop->height(), $originalSize->height()); 121 | 122 | $targetWidth = $targetWidth > $originalSize->width() ? $targetWidth + $offset_x : $targetWidth; 123 | $targetHeight = $targetHeight > $originalSize->height() ? $targetHeight + $offset_y : $targetHeight; 124 | 125 | $cropped = $frame->native()->crop( 126 | max($offset_x, 0), 127 | max($offset_y, 0), 128 | $targetWidth, 129 | $targetHeight 130 | ); 131 | 132 | if ($crop->width() > $originalSize->width() || $cropped->height < $crop->height()) { 133 | $cropped = $background->insert( 134 | $cropped, 135 | max($offset_x * -1, 0), 136 | max($offset_y * -1, 0) 137 | ); 138 | } 139 | 140 | return $cropped; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Modifiers/DrawBezierModifier.php: -------------------------------------------------------------------------------- 1 | drawable->toArray(), 2); 28 | $points = implode(' ', array_map(function (array $coordinates, int $key) use ($chunks): string { 29 | return match ($key) { 30 | 0 => 'M' . implode(' ', $coordinates), 31 | 1 => count($chunks) === 3 ? 'Q' . implode(' ', $coordinates) : 'C' . implode(' ', $coordinates), 32 | default => implode(' ', $coordinates), 33 | }; 34 | }, $chunks, array_keys($chunks))); 35 | 36 | $bezier = Driver::createShape( 37 | 'path', 38 | [ 39 | 'd' => $points, 40 | 'fill' => $this->backgroundColor()->toString(), 41 | 'stroke' => $this->borderColor()->toString(), 42 | 'stroke-width' => $this->drawable->borderSize(), 43 | ], 44 | $image->width(), 45 | $image->height(), 46 | ); 47 | 48 | $frames = []; 49 | foreach ($image as $frame) { 50 | $frames[] = $frame->setNative( 51 | $frame->native()->composite($bezier, [BlendMode::OVER]) 52 | ); 53 | } 54 | 55 | $image->core()->setNative( 56 | Core::replaceFrames($image->core()->native(), $frames) 57 | ); 58 | 59 | return $image; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Modifiers/DrawEllipseModifier.php: -------------------------------------------------------------------------------- 1 | $this->drawable->position()->x(), 29 | 'cy' => $this->drawable->position()->y(), 30 | 'rx' => $this->drawable->width() / 2, 31 | 'ry' => $this->drawable->height() / 2, 32 | 'fill' => $this->backgroundColor()->toString(), 33 | ]; 34 | 35 | if ($this->drawable->hasBorder()) { 36 | $xmlAttributes['stroke'] = $this->borderColor()->toString(); 37 | $xmlAttributes['stroke-width'] = $this->drawable->borderSize(); 38 | } 39 | 40 | $ellipse = Driver::createShape('ellipse', $xmlAttributes, $image->width(), $image->height()); 41 | 42 | $frames = []; 43 | foreach ($image as $frame) { 44 | $frames[] = $frame->setNative( 45 | $frame->native()->composite($ellipse, [BlendMode::OVER]) 46 | ); 47 | } 48 | 49 | $image->core()->setNative( 50 | Core::replaceFrames($image->core()->native(), $frames) 51 | ); 52 | 53 | return $image; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Modifiers/DrawLineModifier.php: -------------------------------------------------------------------------------- 1 | $this->drawable->start()->x(), 31 | 'y1' => $this->drawable->start()->y(), 32 | 'x2' => $this->drawable->end()->x(), 33 | 'y2' => $this->drawable->end()->y(), 34 | 'stroke' => $this->backgroundColor()->toString(), 35 | 'stroke-width' => $this->drawable->width(), 36 | ], 37 | $image->width(), 38 | $image->height(), 39 | ); 40 | 41 | $frames = []; 42 | foreach ($image as $frame) { 43 | $frames[] = $frame->setNative( 44 | $frame->native()->composite($line, [BlendMode::OVER]) 45 | ); 46 | } 47 | 48 | $image->core()->setNative( 49 | Core::replaceFrames($image->core()->native(), $frames) 50 | ); 51 | 52 | return $image; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Modifiers/DrawPixelModifier.php: -------------------------------------------------------------------------------- 1 | driver()->colorProcessor($image->colorspace())->colorToNative( 31 | $this->driver()->handleInput($this->color) 32 | ); 33 | 34 | $pixel = VipsImage::black(1, 1) 35 | ->add($color[0]) // red 36 | ->cast($image->core()->native()->format) 37 | ->copy(['interpretation' => $image->core()->native()->interpretation]) 38 | ->bandjoin([ 39 | $color[1], // green 40 | $color[2], // blue 41 | $color[3], // alpha 42 | ]); 43 | 44 | $frames = []; 45 | foreach ($image as $frame) { 46 | $frames[] = $frame->setNative( 47 | $frame->native()->composite2( 48 | $pixel, 49 | BlendMode::OVER, 50 | [ 51 | 'x' => $this->position->x(), 52 | 'y' => $this->position->y(), 53 | ], 54 | ) 55 | ); 56 | } 57 | 58 | $image->core()->setNative( 59 | Core::replaceFrames($image->core()->native(), $frames) 60 | ); 61 | 62 | return $image; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Modifiers/DrawPolygonModifier.php: -------------------------------------------------------------------------------- 1 | $this->backgroundColor()->toString(), 31 | 'stroke' => $this->borderColor()->toString(), 32 | 'stroke-width' => $this->drawable->borderSize(), 33 | 'points' => implode(' ', array_map( 34 | fn(array $coordinates): string => implode(',', $coordinates), 35 | array_chunk($this->drawable->toArray(), 2), 36 | )), 37 | ], 38 | $image->width(), 39 | $image->height(), 40 | ); 41 | 42 | $frames = []; 43 | foreach ($image as $frame) { 44 | $frames[] = $frame->setNative( 45 | $frame->native()->composite($polygon, [BlendMode::OVER]) 46 | ); 47 | } 48 | 49 | $image->core()->setNative( 50 | Core::replaceFrames($image->core()->native(), $frames) 51 | ); 52 | 53 | return $image; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Modifiers/DrawRectangleModifier.php: -------------------------------------------------------------------------------- 1 | $this->drawable->position()->x(), 29 | 'y' => $this->drawable->position()->y(), 30 | 'width' => $this->drawable->width(), 31 | 'height' => $this->drawable->height(), 32 | 'fill' => $this->backgroundColor()->toString(), 33 | ]; 34 | 35 | if ($this->drawable->hasBorder()) { 36 | $xmlAttributes['stroke'] = $this->borderColor()->toString(); 37 | $xmlAttributes['stroke-width'] = $this->drawable->borderSize(); 38 | } 39 | 40 | $rectangle = Driver::createShape('rect', $xmlAttributes, $image->width(), $image->height()); 41 | 42 | $frames = []; 43 | foreach ($image as $frame) { 44 | $frames[] = $frame->setNative( 45 | $frame->native()->composite($rectangle, [BlendMode::OVER]) 46 | ); 47 | } 48 | 49 | $image->core()->setNative( 50 | Core::replaceFrames($image->core()->native(), $frames) 51 | ); 52 | 53 | return $image; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Modifiers/FillModifier.php: -------------------------------------------------------------------------------- 1 | color(); 32 | 33 | $overlay = VipsImage::black(1, 1) 34 | ->add($color->channel(Red::class)->value()) 35 | ->cast($image->core()->native()->format) 36 | ->embed(0, 0, $image->core()->native()->width, $image->core()->native()->height, ['extend' => Extend::COPY]) 37 | ->copy(['interpretation' => $image->core()->native()->interpretation]) 38 | ->bandjoin([ 39 | $color->channel(Green::class)->value(), 40 | $color->channel(Blue::class)->value(), 41 | $color->channel(Alpha::class)->value(), 42 | ]); 43 | 44 | // original image and overlay must have the same number of bands 45 | if (!$image->core()->native()->hasAlpha()) { 46 | $image->core()->setNative( 47 | $image->core()->native()->bandjoin([255]) 48 | ); 49 | } 50 | 51 | if ($this->hasPosition()) { 52 | $mask = VipsImage::black($image->core()->native()->width, $image->core()->native()->height); 53 | $mask = $mask->draw_flood( 54 | [255], 55 | $this->position->x(), 56 | $this->position->y(), 57 | [ 58 | 'equal' => true, 59 | 'test' => $image->core()->native(), 60 | ] 61 | ); 62 | 63 | if ($overlay->hasAlpha()) { 64 | $mask = $mask->composite2( 65 | $overlay->extract_band($overlay->bands - 1, ['n' => 1]), 66 | BlendMode::DARKEN 67 | ); 68 | $overlay = $overlay->extract_band(0, ['n' => $overlay->bands - 1]); 69 | } 70 | 71 | $overlay = $overlay->bandjoin($mask[0]); 72 | } 73 | 74 | $image->core()->setNative( 75 | $image->core()->native()->composite2($overlay, BlendMode::OVER) 76 | ); 77 | 78 | return $image; 79 | } 80 | 81 | /** 82 | * @throws RuntimeException 83 | */ 84 | private function color(): ColorInterface 85 | { 86 | return $this->driver()->handleInput($this->color); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Modifiers/FlipModifier.php: -------------------------------------------------------------------------------- 1 | core()->setNative( 22 | $image->core()->native()->flip(Direction::VERTICAL) 23 | ); 24 | 25 | return $image; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Modifiers/FlopModifier.php: -------------------------------------------------------------------------------- 1 | core()->setNative( 22 | $image->core()->native()->flip(Direction::HORIZONTAL) 23 | ); 24 | 25 | return $image; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Modifiers/GammaModifier.php: -------------------------------------------------------------------------------- 1 | core()->setNative( 21 | $image->core()->native()->gamma([ 22 | 'exponent' => $this->gamma 23 | ]) 24 | ); 25 | 26 | return $image; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Modifiers/GreyscaleModifier.php: -------------------------------------------------------------------------------- 1 | core()->setNative( 22 | $image->core()->native()->colourspace('b-w') 23 | ); 24 | 25 | // return to srgb colorspace with b/w image 26 | $image->core()->setNative( 27 | $image->core()->native()->colourspace('srgb') 28 | ); 29 | 30 | return $image; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Modifiers/InvertModifier.php: -------------------------------------------------------------------------------- 1 | core()->native(); 21 | $hasAlpha = $vipsImage->hasAlpha(); 22 | 23 | // extract alpha channel to avoid inverting it 24 | if ($hasAlpha) { 25 | $alpha = $vipsImage->extract_band($vipsImage->bands - 1, ['n' => 1]); 26 | $vipsImage = $vipsImage->extract_band(0, ['n' => $vipsImage->bands - 1]); 27 | } 28 | 29 | // invert channels 30 | $vipsImage = $vipsImage->invert(); 31 | 32 | // re-apply alpha channel 33 | if ($hasAlpha) { 34 | $vipsImage = $vipsImage->bandjoin($alpha); 35 | } 36 | 37 | $image->core()->setNative($vipsImage); 38 | 39 | return $image; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Modifiers/PadModifier.php: -------------------------------------------------------------------------------- 1 | getResizeSize($image); 25 | $bgColor = $this->driver()->handleInput($this->background); 26 | 27 | if (!$image->isAnimated()) { 28 | $contained = $this->pad($image->core()->first(), $resize, $bgColor)->native(); 29 | } else { 30 | $frames = []; 31 | foreach ($image as $frame) { 32 | $frames[] = $this->pad($frame, $resize, $bgColor); 33 | } 34 | 35 | $contained = Core::replaceFrames($image->core()->native(), $frames); 36 | } 37 | 38 | $image->core()->setNative($contained); 39 | 40 | return $image; 41 | } 42 | 43 | /** 44 | * Apply padded image resizing 45 | * 46 | * @throws ColorException 47 | */ 48 | private function pad(FrameInterface $frame, SizeInterface $resize, ColorInterface $bgColor): FrameInterface 49 | { 50 | $cropWidth = min($frame->native()->width, $resize->width()); 51 | $cropHeight = min($frame->native()->height, $resize->height()); 52 | 53 | $resized = $frame->native()->thumbnail_image($cropWidth, [ 54 | 'height' => $cropHeight, 55 | 'no_rotate' => true, 56 | ]); 57 | 58 | if (!$resized->hasAlpha()) { 59 | $resized = $resized->bandjoin_const(255); 60 | } 61 | 62 | $frame->setNative( 63 | $resized->gravity( 64 | $this->positionToGravity($this->position), 65 | $resize->width(), 66 | $resize->height(), 67 | [ 68 | 'extend' => Extend::BACKGROUND, 69 | 'background' => $bgColor->toArray(), 70 | ] 71 | ) 72 | ); 73 | 74 | return $frame; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Modifiers/PixelateModifier.php: -------------------------------------------------------------------------------- 1 | isAnimated()) { 28 | $pixelated = $this->pixelate($image->core()->first())->native(); 29 | } else { 30 | $frames = []; 31 | foreach ($image as $frame) { 32 | $frames[] = $this->pixelate($frame); 33 | } 34 | 35 | $pixelated = Core::replaceFrames($image->core()->native(), $frames); 36 | } 37 | 38 | $image->core()->setNative($pixelated); 39 | 40 | return $image; 41 | } 42 | 43 | private function pixelate(FrameInterface $frame): FrameInterface 44 | { 45 | $frame->setNative( 46 | $frame->native() 47 | ->resize(1 / $this->size) 48 | ->resize($this->size, ['kernel' => Kernel::NEAREST]) 49 | ->crop(0, 0, $frame->size()->width(), $frame->size()->height()) 50 | ); 51 | 52 | return $frame; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Modifiers/PlaceModifier.php: -------------------------------------------------------------------------------- 1 | driver()->handleInput($this->element); 32 | $elementNative = $element->core()->native(); 33 | $position = $this->getPosition($image, $element); 34 | 35 | if ($this->opacity < 100) { 36 | if (!$elementNative->hasAlpha()) { 37 | $elementNative = $elementNative->bandjoin_const(255); 38 | } 39 | 40 | $elementNative = $elementNative->multiply([ 41 | 1.0, 42 | 1.0, 43 | 1.0, 44 | $this->opacity / 100, 45 | ]); 46 | } 47 | 48 | if (!$image->isAnimated()) { 49 | $watermarked = $this->placeElement($elementNative, $position, $image->core()->first())->native(); 50 | } else { 51 | $frames = []; 52 | foreach ($image as $frame) { 53 | $frames[] = $this->placeElement($elementNative, $position, $frame); 54 | } 55 | 56 | $watermarked = Core::replaceFrames($image->core()->native(), $frames); 57 | } 58 | 59 | $image->core()->setNative($watermarked); 60 | 61 | return $image; 62 | } 63 | 64 | /** 65 | * @throws RuntimeException 66 | */ 67 | private function placeElement( 68 | VipsImage $elementNative, 69 | PointInterface $position, 70 | FrameInterface $frame 71 | ): FrameInterface { 72 | if ($elementNative->hasAlpha()) { 73 | /** @var Rectangle $size */ 74 | $size = $frame->size(); 75 | $imageSize = $size->align($this->position); 76 | 77 | $elementNative = $elementNative->embed( 78 | $position->x(), 79 | $position->y(), 80 | $imageSize->width(), 81 | $imageSize->height(), 82 | [ 83 | 'extend' => Extend::BACKGROUND, 84 | 'background' => [0, 0, 0, 0], 85 | ] 86 | ); 87 | 88 | $frame->setNative( 89 | $frame->native()->composite2( 90 | $elementNative, 91 | BlendMode::OVER 92 | ) 93 | ); 94 | } else { 95 | $frame->setNative( 96 | $frame->native()->insert( 97 | $elementNative->bandjoin_const(255), 98 | $position->x(), 99 | $position->y() 100 | ) 101 | ); 102 | } 103 | 104 | return $frame; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Modifiers/ProfileModifier.php: -------------------------------------------------------------------------------- 1 | profile); 23 | 24 | // transform to profile 25 | $vipsImage = $image->core()->native()->icc_transform($tempFile); 26 | 27 | // set transformed image 28 | $image->core()->setNative($vipsImage); 29 | 30 | // remove temporary file 31 | unlink($tempFile); 32 | 33 | return $image; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Modifiers/ProfileRemovalModifier.php: -------------------------------------------------------------------------------- 1 | core()->native()->remove('icc-profile-data'); 23 | } catch (VipsException) { 24 | // 25 | } 26 | 27 | return $image; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Modifiers/RemoveAnimationModifier.php: -------------------------------------------------------------------------------- 1 | isAnimated()) { 23 | return $image; 24 | } 25 | 26 | $position = parent::normalizePosition($image); 27 | $page_height = $image->core()->native()->get('page-height'); 28 | 29 | try { 30 | $modified = $image->core()->native()->crop( 31 | 0, 32 | $position * $page_height, 33 | $image->width(), 34 | $page_height, 35 | ); 36 | $modified->set('n-pages', 1); 37 | } catch (VipsException) { 38 | throw new AnimationException('Frame #' . $position . ' could not be found in the image.'); 39 | } 40 | 41 | $image->core()->setNative($modified); 42 | 43 | return $image; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Modifiers/ResizeCanvasModifier.php: -------------------------------------------------------------------------------- 1 | cropSize($image); 21 | 22 | $image->modify(new CropModifier( 23 | $cropSize->width(), 24 | $cropSize->height(), 25 | $cropSize->pivot()->x(), 26 | $cropSize->pivot()->y(), 27 | $this->background, 28 | )); 29 | 30 | return $image; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Modifiers/ResizeCanvasRelativeModifier.php: -------------------------------------------------------------------------------- 1 | cropSize($image, true); 21 | 22 | $image->modify(new CropModifier( 23 | $cropSize->width(), 24 | $cropSize->height(), 25 | $cropSize->pivot()->x(), 26 | $cropSize->pivot()->y(), 27 | $this->background, 28 | )); 29 | 30 | return $image; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Modifiers/ResizeDownModifier.php: -------------------------------------------------------------------------------- 1 | size()->resizeDown($this->width, $this->height); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Modifiers/ResizeModifier.php: -------------------------------------------------------------------------------- 1 | getAdjustedSize($image); 24 | 25 | $image->core()->setNative( 26 | $image->core()->native()->thumbnail_image($resizeTo->width(), [ 27 | 'height' => $resizeTo->height(), 28 | 'size' => 'force', 29 | 'no_rotate' => true, 30 | ]) 31 | ); 32 | 33 | return $image; 34 | } 35 | 36 | /** 37 | * Return the size the modifier will resize to 38 | * 39 | * @throws RuntimeException 40 | * @throws GeometryException 41 | */ 42 | protected function getAdjustedSize(ImageInterface $image): SizeInterface 43 | { 44 | return $image->size()->resize($this->width, $this->height); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Modifiers/ResolutionModifier.php: -------------------------------------------------------------------------------- 1 | core()->setNative( 21 | $image->core()->native()->copy([ 22 | 'xres' => $this->x / 25.4, 23 | 'yres' => $this->y / 25.4, 24 | ]) 25 | ); 26 | 27 | return $image; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Modifiers/RotateModifier.php: -------------------------------------------------------------------------------- 1 | rotationAngle()) { 30 | 0.0 => $frame->native(), 31 | 90.0, -270.0 => $frame->native()->rot90(), 32 | 180.0, -180.0 => $frame->native()->rot180(), 33 | -90.0, 270.0 => $frame->native()->rot270(), 34 | default => $this->rotate($frame->native()), 35 | }; 36 | 37 | $frames[] = $frame->setNative($vipsImage); 38 | } 39 | 40 | $image->core()->setNative( 41 | Core::replaceFrames($image->core()->native(), $frames) 42 | ); 43 | 44 | return $image; 45 | } 46 | 47 | /** 48 | * @throws RuntimeException 49 | */ 50 | public function rotate(VipsImage $vipsImage): VipsImage 51 | { 52 | $background = $this->driver()->handleInput($this->background); 53 | 54 | if (!$vipsImage->hasAlpha()) { 55 | $vipsImage = $vipsImage->bandjoin_const(255); 56 | } 57 | 58 | return $vipsImage->similarity([ 59 | 'background' => $background->toArray(), 60 | 'angle' => $this->rotationAngle(), 61 | ]); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Modifiers/ScaleDownModifier.php: -------------------------------------------------------------------------------- 1 | size()->scaleDown($this->width, $this->height); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Modifiers/ScaleModifier.php: -------------------------------------------------------------------------------- 1 | size()->scale($this->width, $this->height); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Modifiers/SharpenModifier.php: -------------------------------------------------------------------------------- 1 | core()->setNative( 27 | $image->core()->native()->conv($this->getUnsharpMask()) 28 | ); 29 | 30 | return $image; 31 | } 32 | 33 | /** 34 | * Generate unsharp mask 35 | * 36 | * @throws VipsException 37 | */ 38 | private function getUnsharpMask(): VipsImage 39 | { 40 | $min = $this->amount >= 10 ? $this->amount * -0.01 : 0; 41 | $max = $this->amount * -0.025; 42 | $abs = ((4 * $min + 4 * $max) * -1) + 1; 43 | 44 | return VipsImage::newFromArray([ 45 | [$min, $max, $min], 46 | [$max, $abs, $max], 47 | [$min, $max, $min], 48 | ], 1, 0); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Modifiers/SliceAnimationModifier.php: -------------------------------------------------------------------------------- 1 | offset >= $image->count()) { 22 | throw new AnimationException('Offset is not in the range of frames.'); 23 | } 24 | 25 | $image->core()->slice($this->offset, $this->length); 26 | 27 | return $image; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Modifiers/StripMetaModifier.php: -------------------------------------------------------------------------------- 1 | ForeignKeep::ICC, 30 | ] : [ 31 | 'strip' => true, 32 | ]; 33 | 34 | $buf = $image->core()->native()->tiffsave_buffer($options); 35 | 36 | $image->setExif(new Collection()); 37 | $image->core()->setNative( 38 | VipsImage::newFromBuffer($buf) 39 | ); 40 | 41 | return $image; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Modifiers/TextModifier.php: -------------------------------------------------------------------------------- 1 | text); 35 | $fontProcessor = new FontProcessor(); 36 | 37 | // decode text color 38 | $color = $this->driver()->handleInput($this->font->color()); 39 | 40 | // build vips image with text 41 | $textBlockImage = $fontProcessor->textToVipsImage($this->text, $this->font, $color); 42 | 43 | // calculate block position 44 | $blockSize = $this->blockSize($textBlockImage); 45 | 46 | // calculate baseline 47 | $capImage = $fontProcessor->textToVipsImage('T', $this->font); 48 | $baseline = $capImage->height + $capImage->yoffset; 49 | 50 | // adjust block size 51 | switch ($this->font->valignment()) { 52 | case 'top': 53 | $blockSize->movePointsY($baseline * -1); 54 | $blockSize->movePointsY($textBlockImage->yoffset); 55 | $blockSize->movePointsY($capImage->height); 56 | break; 57 | 58 | case 'bottom': 59 | $lastLineImage = $fontProcessor->textToVipsImage((string) $textBlock->last(), $this->font); 60 | $blockSize->movePointsY($lastLineImage->height); 61 | $blockSize->movePointsY($baseline * -1); 62 | $blockSize->movePointsY($lastLineImage->yoffset); 63 | break; 64 | } 65 | 66 | // apply rotation 67 | $blockSize->rotate($this->font->angle()); 68 | 69 | // extract block position 70 | $blockPosition = clone $blockSize->last(); 71 | 72 | // apply text rotation if necessary 73 | $textBlockImage = $this->maybeRotateText($textBlockImage); 74 | 75 | // apply rotation offset to block position 76 | if ($this->font->angle() != 0) { 77 | $blockPosition->move( 78 | $textBlockImage->xoffset * -1, 79 | $textBlockImage->yoffset * -1 80 | ); 81 | } 82 | 83 | if ($this->font->hasStrokeEffect()) { 84 | // decode stroke color 85 | $strokeColor = $this->driver()->handleInput($this->font->strokeColor()); 86 | 87 | // build stroke text image if applicable 88 | $stroke = $fontProcessor->textToVipsImage($this->text, $this->font, $strokeColor); 89 | 90 | // apply rotation for stroke effect 91 | $stroke = $this->maybeRotateText($stroke); 92 | } 93 | 94 | if (!$image->isAnimated()) { 95 | $modified = $image->core()->first(); 96 | 97 | if (isset($stroke)) { 98 | // draw stroke effect with offsets 99 | foreach ($this->strokeOffsets($this->font) as $offset) { 100 | $modified = $this->placeTextOnFrame( 101 | $stroke, 102 | $modified, 103 | $blockPosition->x() - $offset->x(), 104 | $blockPosition->y() - $offset->y() 105 | ); 106 | } 107 | } 108 | 109 | // place text image on original image 110 | $modified = $this->placeTextOnFrame( 111 | $textBlockImage, 112 | $modified, 113 | $blockPosition->x(), 114 | $blockPosition->y() 115 | ); 116 | 117 | $modified = $modified->native(); 118 | } else { 119 | $frames = []; 120 | foreach ($image as $frame) { 121 | $modifiedFrame = $frame; 122 | 123 | if (isset($stroke)) { 124 | // draw stroke effect with offsets 125 | foreach ($this->strokeOffsets($this->font) as $offset) { 126 | $modifiedFrame = $this->placeTextOnFrame( 127 | $stroke, 128 | $modifiedFrame, 129 | $blockPosition->x() - $offset->x(), 130 | $blockPosition->y() - $offset->y() 131 | ); 132 | } 133 | } 134 | 135 | // place text image on original image 136 | $modifiedFrame = $this->placeTextOnFrame( 137 | $textBlockImage, 138 | $modifiedFrame, 139 | $blockPosition->x(), 140 | $blockPosition->y() 141 | ); 142 | 143 | $frames[] = $modifiedFrame; 144 | } 145 | 146 | $modified = Core::replaceFrames($image->core()->native(), $frames); 147 | } 148 | 149 | $image->core()->setNative($modified); 150 | 151 | return $image; 152 | } 153 | 154 | /** 155 | * Place given text image at given position on given frame 156 | */ 157 | private function placeTextOnFrame(VipsImage $text, FrameInterface $frame, int $x, int $y): FrameInterface 158 | { 159 | $frame->setNative( 160 | $frame->native()->composite($text, BlendMode::OVER, ['x' => $x, 'y' => $y]) 161 | ); 162 | 163 | return $frame; 164 | } 165 | 166 | /** 167 | * Build size from given vips image 168 | */ 169 | private function blockSize(VipsImage $blockImage): Rectangle 170 | { 171 | $imageSize = new Rectangle($blockImage->width, $blockImage->height, $this->position); 172 | $imageSize->align($this->font->alignment()); 173 | $imageSize->valign($this->font->valignment()); 174 | 175 | return $imageSize; 176 | } 177 | 178 | /** 179 | * Maybe rotate text image according to current font angle 180 | * 181 | * @throws VipsException 182 | */ 183 | private function maybeRotateText(VipsImage $text): VipsImage 184 | { 185 | return match ($this->font->angle()) { 186 | 0.0 => $text, 187 | 90.0, -270.0 => $text->rot90(), 188 | 180.0, -180.0 => $text->rot180(), 189 | -90.0, 270.0 => $text->rot270(), 190 | default => $text->similarity(['angle' => $this->font->angle()]), 191 | }; 192 | } 193 | 194 | /** @phpstan-ignore method.unused */ 195 | private function debugPos(ImageInterface $image, PointInterface $position, Rectangle $size): void 196 | { 197 | // draw pos 198 | // @phpstan-ignore missingType.checkedException 199 | $image->drawCircle($position->x(), $position->y(), function (CircleFactory $circle): void { 200 | $circle->diameter(8); 201 | $circle->background('red'); 202 | }); 203 | 204 | // draw points of size 205 | foreach (array_chunk($size->toArray(), 2) as $point) { 206 | // @phpstan-ignore missingType.checkedException 207 | $image->drawCircle($point[0], $point[1], function (CircleFactory $circle): void { 208 | $circle->diameter(12); 209 | $circle->border('green'); 210 | }); 211 | } 212 | 213 | // draw size's pivot 214 | // @phpstan-ignore missingType.checkedException 215 | $image->drawCircle($size->pivot()->x(), $size->pivot()->y(), function (CircleFactory $circle): void { 216 | $circle->diameter(20); 217 | $circle->border('blue'); 218 | }); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/Modifiers/TrimModifier.php: -------------------------------------------------------------------------------- 1 | isAnimated()) { 29 | throw new NotSupportedException('Trim modifier cannot be applied to animated images.'); 30 | } 31 | 32 | $core = Core::ensureInMemory($image->core()); 33 | $native = $core->native(); 34 | 35 | // get the color of the 4 corners 36 | $points = [ 37 | $native->getpoint(0, 0), 38 | $native->getpoint($image->width() - 1, 0), 39 | $native->getpoint(0, $image->height() - 1), 40 | $native->getpoint($image->width() - 1, $image->height() - 1), 41 | ]; 42 | 43 | $maxThreshold = match ($image->core()->native()->format) { 44 | BandFormat::USHORT => 65535, 45 | BandFormat::FLOAT => 1, 46 | default => 255, 47 | }; 48 | 49 | foreach ($points as $point) { 50 | $trim = $native->find_trim([ 51 | 'threshold' => min($this->tolerance + 10, $maxThreshold), 52 | 'background' => $point, 53 | ]); 54 | 55 | $native = $native->crop( 56 | min($trim['left'], $image->width() - 1), 57 | min($trim['top'], $image->height() - 1), 58 | max($trim['width'], 1), 59 | max($trim['height'], 1) 60 | ); 61 | 62 | if ($trim['width'] === 0 || $trim['height'] === 0) { 63 | break; 64 | } 65 | } 66 | 67 | $image->core()->setNative($native); 68 | 69 | return $image; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/TrueTypeFont.php: -------------------------------------------------------------------------------- 1 | queryNameTable(1); 28 | } 29 | 30 | /** 31 | * Query name table of current font file 32 | * 33 | * @throws FontException 34 | */ 35 | private function queryNameTable(int $id): string 36 | { 37 | rewind($this->pointer); 38 | 39 | $tableOffset = $this->tableOffset('name'); 40 | fseek($this->pointer, $tableOffset); 41 | 42 | $header = fread($this->pointer, 6); 43 | $recordCount = unpack('n', substr($header, 2, 2))[1]; 44 | $stringStorageOffset = unpack('n', substr($header, 4, 2))[1]; 45 | 46 | for ($i = 0; $i < $recordCount; $i++) { 47 | $record = fread($this->pointer, 12); 48 | 49 | $platformID = unpack('n', substr($record, 0, 2))[1]; 50 | $nameID = unpack('n', substr($record, 6, 2))[1]; 51 | $stringLength = unpack('n', substr($record, 8, 2))[1]; 52 | $stringOffset = unpack('n', substr($record, 10, 2))[1]; 53 | 54 | if ($nameID === $id) { 55 | $currentPos = ftell($this->pointer); 56 | fseek($this->pointer, $tableOffset + $stringStorageOffset + $stringOffset); 57 | $value = fread($this->pointer, $stringLength); 58 | fseek($this->pointer, $currentPos); 59 | 60 | if ($platformID === 0 || $platformID === 3) { 61 | $value = mb_convert_encoding($value, 'UTF-8', 'UTF-16BE'); 62 | } 63 | 64 | return $value; 65 | } 66 | } 67 | 68 | throw new FontException('Unable to find id ' . $id . ' in name table.'); 69 | } 70 | 71 | /** 72 | * Return table offset of given table tag 73 | * 74 | * @throws FontException 75 | */ 76 | private function tableOffset(string $tableTag): int 77 | { 78 | rewind($this->pointer); 79 | 80 | $header = fread($this->pointer, 12); 81 | $tableCount = unpack('n', substr($header, 4, 2))[1]; 82 | fseek($this->pointer, 12); 83 | 84 | $offsets = []; 85 | for ($i = 0; $i < $tableCount; $i++) { 86 | $record = fread($this->pointer, 16); 87 | $offsets[substr($record, 0, 4)] = unpack('N', substr($record, 8, 4))[1]; 88 | } 89 | if (!array_key_exists($tableTag, $offsets)) { 90 | throw new FontException('Unable to find offset for table ' . $tableTag . '.'); 91 | } 92 | 93 | return $offsets[$tableTag]; 94 | } 95 | } 96 | --------------------------------------------------------------------------------