├── src ├── Exceptions │ ├── MissingParameter.php │ ├── InvalidFont.php │ ├── UnsupportedImageFormat.php │ ├── ImageMethodDoesNotExist.php │ ├── InvalidImageDriver.php │ ├── CannotResize.php │ ├── InvalidColor.php │ ├── CouldNotLoadImage.php │ └── InvalidManipulation.php ├── Enums │ ├── Unit.php │ ├── ImageDriver.php │ ├── Constraint.php │ ├── BorderType.php │ ├── FlipDirection.php │ ├── ColorFormat.php │ ├── Orientation.php │ ├── CropPosition.php │ ├── Fit.php │ └── AlignPosition.php ├── Drivers │ ├── Imagick │ │ ├── Helpers.php │ │ ├── ImagickColor.php │ │ └── ImagickDriver.php │ ├── Concerns │ │ ├── ValidatesArguments.php │ │ ├── PerformsOptimizations.php │ │ ├── GetsOrientationFromExif.php │ │ ├── CalculatesFocalCropCoordinates.php │ │ ├── CalculatesCropOffsets.php │ │ ├── PerformsFitCrops.php │ │ ├── CalculatesFocalCropAndResizeCoordinates.php │ │ └── AddsWatermark.php │ ├── Gd │ │ ├── GdColor.php │ │ └── GdDriver.php │ ├── Color.php │ └── ImageDriver.php ├── Point.php ├── Size.php └── Image.php ├── phpstan.neon.dist ├── LICENSE.md ├── composer.json ├── README.md ├── phpstan-baseline.neon └── CHANGELOG.md /src/Exceptions/MissingParameter.php: -------------------------------------------------------------------------------- 1 | 0 10 | ? $level / 5 11 | : ($level + 100) / 100; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Exceptions/UnsupportedImageFormat.php: -------------------------------------------------------------------------------- 1 | x = $x; 12 | $this->y = $y; 13 | 14 | return $this; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Exceptions/ImageMethodDoesNotExist.php: -------------------------------------------------------------------------------- 1 | value; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidImageDriver.php: -------------------------------------------------------------------------------- 1 | $max) { 12 | throw InvalidManipulation::valueNotInRange($label, $value, $min, $max); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Exceptions/CouldNotLoadImage.php: -------------------------------------------------------------------------------- 1 | optimize = true; 17 | $this->optimizerChain = $optimizerChain ?? OptimizerChainFactory::create(); 18 | 19 | return $this; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Drivers/Concerns/GetsOrientationFromExif.php: -------------------------------------------------------------------------------- 1 | Orientation::Rotate180, 18 | 6 => Orientation::Rotate270, 19 | 8 => Orientation::Rotate90, 20 | default => Orientation::Rotate0, 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Enums/CropPosition.php: -------------------------------------------------------------------------------- 1 | */ 18 | public function offsetPercentages(): array 19 | { 20 | return match ($this) { 21 | self::TopLeft => [0, 0], 22 | self::Top => [50, 0], 23 | self::TopRight => [100, 0], 24 | self::Left => [0, 50], 25 | self::Center => [50, 50], 26 | self::Right => [100, 50], 27 | self::BottomLeft => [0, 100], 28 | self::Bottom => [50, 100], 29 | self::BottomRight => [100, 100], 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 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. 22 | -------------------------------------------------------------------------------- /src/Drivers/Concerns/CalculatesFocalCropCoordinates.php: -------------------------------------------------------------------------------- 1 | */ 9 | protected function calculateFocalCropCoordinates(int $width, int $height, ?int $cropCenterX, ?int $cropCenterY): array 10 | { 11 | $width = min($width, $this->getWidth()); 12 | $height = min($height, $this->getHeight()); 13 | 14 | if ($cropCenterX > 0) { 15 | $maxCropCenterX = $this->getWidth() - $width; 16 | $cropCenterX = (int) ($cropCenterX - ($width / 2)); 17 | $cropCenterX = min($maxCropCenterX, $cropCenterX); 18 | $cropCenterX = max(0, $cropCenterX); 19 | } 20 | 21 | if ($cropCenterY > 0) { 22 | $maxCropCenterY = $this->getHeight() - $height; 23 | $cropCenterY = (int) ($cropCenterY - ($height / 2)); 24 | $cropCenterY = min($maxCropCenterY, $cropCenterY); 25 | $cropCenterY = max(0, $cropCenterY); 26 | } 27 | 28 | return [$width, $height, $cropCenterX, $cropCenterY]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Drivers/Concerns/CalculatesCropOffsets.php: -------------------------------------------------------------------------------- 1 | */ 11 | protected function calculateCropOffsets(int $width, int $height, CropPosition $position): array 12 | { 13 | [$offsetPercentageX, $offsetPercentageY] = $position->offsetPercentages(); 14 | 15 | $offsetX = (int) (($this->getWidth() * $offsetPercentageX / 100) - ($width / 2)); 16 | $offsetY = (int) (($this->getHeight() * $offsetPercentageY / 100) - ($height / 2)); 17 | 18 | $maxOffsetX = $this->getWidth() - $width; 19 | $maxOffsetY = $this->getHeight() - $height; 20 | 21 | if ($offsetX < 0) { 22 | $offsetX = 0; 23 | } 24 | 25 | if ($offsetY < 0) { 26 | $offsetY = 0; 27 | } 28 | 29 | if ($offsetX > $maxOffsetX) { 30 | $offsetX = $maxOffsetX; 31 | } 32 | 33 | if ($offsetY > $maxOffsetY) { 34 | $offsetY = $maxOffsetY; 35 | } 36 | 37 | return [$offsetX, $offsetY]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Enums/Fit.php: -------------------------------------------------------------------------------- 1 | [Constraint::PreserveAspectRatio], 30 | Fit::Fill, Fit::Max, Fit::FillMax => [Constraint::PreserveAspectRatio, Constraint::DoNotUpsize], 31 | Fit::Stretch, Fit::Crop => [], 32 | }; 33 | 34 | return $size->resize($desiredWidth, $desiredHeight, $constraints); 35 | } 36 | 37 | public function shouldResizeCanvas(): bool 38 | { 39 | return in_array($this, [self::Fill, self::FillMax]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Enums/AlignPosition.php: -------------------------------------------------------------------------------- 1 | height($desiredHeight); 29 | } else { 30 | $this->width($desiredWidth); 31 | } 32 | } 33 | 34 | $currentAspectRatio = $originalWidth / $originalHeight; 35 | $desiredAspectRatio = $desiredWidth / $desiredHeight; 36 | 37 | if ($currentAspectRatio > $desiredAspectRatio) { 38 | $this->height($desiredHeight); 39 | } else { 40 | $this->width($desiredWidth); 41 | } 42 | 43 | $this->resizeCanvas($desiredWidth, $desiredHeight, AlignPosition::Center); 44 | 45 | return $this; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Drivers/Concerns/CalculatesFocalCropAndResizeCoordinates.php: -------------------------------------------------------------------------------- 1 | getWidth(); 16 | $originalHeight = $this->getHeight(); 17 | 18 | $targetRatio = $desiredWidth / $desiredHeight; 19 | $originalRatio = $originalWidth / $originalHeight; 20 | 21 | if ($originalRatio > $targetRatio) { 22 | // Image is wider, crop width 23 | $cropHeight = $originalHeight; 24 | $cropWidth = (int) round($cropHeight * $targetRatio); 25 | } else { 26 | // Image is taller, crop height 27 | $cropWidth = $originalWidth; 28 | $cropHeight = (int) round($cropWidth / $targetRatio); 29 | } 30 | 31 | // Focal point in pixels 32 | $focalX = $originalWidth * ($cropCenterX / 100); 33 | $focalY = $originalHeight * ($cropCenterY / 100); 34 | 35 | // Top-left crop coordinates 36 | $cropX = max(0, min((int) ($focalX - $cropWidth / 2), $originalWidth - $cropWidth)); 37 | $cropY = max(0, min((int) ($focalY - $cropHeight / 2), $originalHeight - $cropHeight)); 38 | 39 | return [$cropWidth, $cropHeight, $cropX, $cropY]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/image", 3 | "description": "Manipulate images with an expressive API", 4 | "keywords": [ 5 | "spatie", 6 | "image" 7 | ], 8 | "homepage": "https://github.com/spatie/image", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Freek Van der Herten", 13 | "email": "freek@spatie.be", 14 | "homepage": "https://spatie.be", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.2", 20 | "ext-exif": "*", 21 | "ext-json": "*", 22 | "ext-mbstring": "*", 23 | "spatie/image-optimizer": "^1.7.5", 24 | "spatie/temporary-directory": "^2.2", 25 | "symfony/process": "^6.4|^7.0|^8.0" 26 | }, 27 | "require-dev": { 28 | "ext-gd": "*", 29 | "ext-imagick": "*", 30 | "laravel/sail": "^1.34", 31 | "pestphp/pest": "^3.0|^4.0", 32 | "phpstan/phpstan": "^1.10.50", 33 | "spatie/pest-plugin-snapshots": "^2.1", 34 | "spatie/pixelmatch-php": "^1.0", 35 | "spatie/ray": "^1.40.1", 36 | "symfony/var-dumper": "^6.4|^7.0|^8.0" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Spatie\\Image\\": "src" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "Spatie\\Image\\Test\\": "tests" 46 | } 47 | }, 48 | "scripts": { 49 | "test": "vendor/bin/pest", 50 | "analyse": "phpstan analyse --memory-limit=1G --no-progress", 51 | "baseline": "./vendor/bin/phpstan analyse --memory-limit=2G --generate-baseline" 52 | }, 53 | "config": { 54 | "sort-packages": true, 55 | "allow-plugins": { 56 | "pestphp/pest-plugin": true 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Drivers/Concerns/AddsWatermark.php: -------------------------------------------------------------------------------- 1 | loadFile($watermarkImage); 28 | } 29 | 30 | $this->ensureNumberBetween($alpha, 0, 100, 'alpha'); 31 | 32 | if ($paddingUnit === Unit::Percent) { 33 | $this->ensureNumberBetween($paddingX, 0, 100, 'paddingX'); 34 | $this->ensureNumberBetween($paddingY, 0, 100, 'paddingY'); 35 | } 36 | 37 | if ($widthUnit === Unit::Percent) { 38 | $this->ensureNumberBetween($width, 0, 100, 'width'); 39 | } 40 | 41 | if ($heightUnit === Unit::Percent) { 42 | $this->ensureNumberBetween($height, 0, 100, 'height'); 43 | } 44 | 45 | $paddingX = $this->calculateWatermarkX($paddingX, $paddingUnit); 46 | $paddingY = $this->calculateWatermarkY($paddingY, $paddingUnit); 47 | 48 | $width = $width ? $this->calculateWatermarkX($width, $widthUnit) : null; 49 | $height = $height ? $this->calculateWatermarkY($height, $widthUnit) : null; 50 | 51 | if (is_null($width) && ! is_null($height)) { 52 | $watermarkImage->height($height); 53 | } elseif (! is_null($width) && is_null($height)) { 54 | $watermarkImage->width($width); 55 | } else { 56 | $watermarkImage->fit($fit, $width, $height); 57 | } 58 | 59 | $this->insert($watermarkImage, $position, $paddingX, $paddingY, $alpha); 60 | 61 | return $this; 62 | } 63 | 64 | protected function calculateWatermarkX(int $x, Unit $unit): int 65 | { 66 | if ($unit === Unit::Percent) { 67 | return $this->getWidth() * $x / 100; 68 | } 69 | 70 | return $x; 71 | } 72 | 73 | protected function calculateWatermarkY(int $y, Unit $unit): int 74 | { 75 | if ($unit === Unit::Percent) { 76 | return $this->getHeight() * $y / 100; 77 | } 78 | 79 | return $y; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Drivers/Gd/GdColor.php: -------------------------------------------------------------------------------- 1 | alpha = ($value >> 24) & 0xFF; 22 | $this->red = ($value >> 16) & 0xFF; 23 | $this->green = ($value >> 8) & 0xFF; 24 | $this->blue = $value & 0xFF; 25 | 26 | return $this; 27 | } 28 | 29 | public function initFromArray(array $value): self 30 | { 31 | $value = array_values($value); 32 | 33 | if (count($value) === 4) { 34 | 35 | [$red, $green, $blue, $alpha] = $value; 36 | $this->alpha = $this->alpha2gd($alpha); 37 | 38 | } elseif (count($value) === 3) { 39 | 40 | [$red, $green, $blue] = $value; 41 | $this->alpha = 0; 42 | 43 | } else { 44 | throw InvalidColor::make($value); 45 | } 46 | 47 | $this->red = $red; 48 | $this->green = $green; 49 | $this->blue = $blue; 50 | 51 | return $this; 52 | } 53 | 54 | public function initFromString(string $value): self 55 | { 56 | if ($color = $this->rgbaFromString($value)) { 57 | $this->red = (int) $color[0]; 58 | $this->green = (int) $color[1]; 59 | $this->blue = (int) $color[2]; 60 | $this->alpha = $this->alpha2gd($color[3]); 61 | } 62 | 63 | return $this; 64 | } 65 | 66 | public function initFromRgb(int $red, int $green, int $blue): self 67 | { 68 | $this->red = $red; 69 | $this->green = $green; 70 | $this->blue = $blue; 71 | $this->alpha = 0; 72 | 73 | return $this; 74 | } 75 | 76 | public function initFromRgba(int $red, int $green, int $blue, float $alpha = 1): self 77 | { 78 | $this->red = $red; 79 | $this->green = $green; 80 | $this->blue = $blue; 81 | $this->alpha = $this->alpha2gd($alpha); 82 | 83 | return $this; 84 | } 85 | 86 | public function initFromObject(ImagickPixel $value): never 87 | { 88 | throw InvalidColor::cannotConvertImagickColorToGd(); 89 | } 90 | 91 | public function getInt(): int 92 | { 93 | return ($this->alpha << 24) + ($this->red << 16) + ($this->green << 8) + $this->blue; 94 | } 95 | 96 | public function getHex(string $prefix = ''): string 97 | { 98 | return sprintf('%s%02x%02x%02x', $prefix, $this->red, $this->green, $this->blue); 99 | } 100 | 101 | public function getArray(): array 102 | { 103 | return [ 104 | $this->red, 105 | $this->green, 106 | $this->blue, 107 | round(1 - $this->alpha / 127, 2), 108 | ]; 109 | } 110 | 111 | public function getRgba(): string 112 | { 113 | return sprintf('rgba(%d, %d, %d, %.2F)', 114 | $this->red, 115 | $this->green, 116 | $this->blue, 117 | round(1 - $this->alpha / 127, 2), 118 | ); 119 | } 120 | 121 | public function differs(Color $color, int $tolerance = 0): bool 122 | { 123 | $colorTolerance = round($tolerance * 2.55); 124 | $alphaTolerance = round($tolerance * 1.27); 125 | 126 | $delta = [ 127 | 'r' => abs($color->red - $this->red), 128 | 'g' => abs($color->green - $this->green), 129 | 'b' => abs($color->blue - $this->blue), 130 | 'a' => abs($color->alpha - $this->alpha), 131 | ]; 132 | 133 | return 134 | $delta['r'] > $colorTolerance || 135 | $delta['g'] > $colorTolerance || 136 | $delta['b'] > $colorTolerance || 137 | $delta['a'] > $alphaTolerance; 138 | } 139 | 140 | private function alpha2gd(float $input): int 141 | { 142 | $oldMin = 0; 143 | $oldMax = 1; 144 | 145 | $newMin = 127; 146 | $newMax = 0; 147 | 148 | return (int) ceil(((($input - $oldMin) * ($newMax - $newMin)) / ($oldMax - $oldMin)) + $newMin); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | Logo for image 6 | 7 | 8 | 9 |

Manipulate images with an expressive API

10 | 11 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/image.svg?style=flat-square)](https://packagist.org/packages/spatie/image) 12 | [![MIT Licensed](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 13 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/spatie/image/run-tests.yml?label=tests)](https://github.com/spatie/image/actions/workflows/run-tests.yml) 14 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/image.svg?style=flat-square)](https://packagist.org/packages/spatie/image) 15 | 16 |
17 | 18 | Image manipulation doesn't have to be hard. Here are a few examples on how this package makes it very easy to manipulate images. 19 | 20 | ```php 21 | use Spatie\Image\Image; 22 | 23 | // modifying the image so it fits in a 100x100 rectangle without altering aspect ratio 24 | Image::load($pathToImage) 25 | ->width(100) 26 | ->height(100) 27 | ->save($pathToNewImage); 28 | 29 | // overwriting the original image with a greyscale version 30 | Image::load($pathToImage) 31 | ->greyscale() 32 | ->save(); 33 | 34 | // make image darker and save it in low quality 35 | Image::load($pathToImage) 36 | ->brightness(-30) 37 | ->quality(25) 38 | ->save(); 39 | 40 | // rotate the image and sharpen it 41 | Image::load($pathToImage) 42 | ->orientation(90) 43 | ->sharpen(15) 44 | ->save(); 45 | ``` 46 | 47 | You'll find more examples in [the full documentation](https://docs.spatie.be/image). 48 | 49 | ## Support us 50 | 51 | [](https://spatie.be/github-ad-click/image) 52 | 53 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 54 | 55 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 56 | 57 | ## Installation 58 | 59 | You can install the package via composer: 60 | 61 | ``` bash 62 | composer require spatie/image 63 | ``` 64 | 65 | Please note that since version 1.5.3 this package requires exif extension to be enabled: http://php.net/manual/en/exif.installation.php 66 | 67 | ## Usage 68 | 69 | Head over to [the full documentation](https://spatie.be/docs/image). 70 | 71 | ## Changelog 72 | 73 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 74 | 75 | ## Testing 76 | 77 | ``` bash 78 | npm i pixelmatch 79 | composer test 80 | ``` 81 | 82 | ## Contributing 83 | 84 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 85 | 86 | ## Security 87 | 88 | If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. 89 | 90 | ## Postcardware 91 | 92 | You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. 93 | 94 | Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium. 95 | 96 | We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). 97 | 98 | ## Credits 99 | 100 | - [Freek Van der Herten](https://github.com/freekmurze) 101 | - [All Contributors](../../contributors) 102 | 103 | Large parts of this codebase were copied from [Intervention Image](https://image.intervention.io/v3) by [Oliver Vogel](https://github.com/olivervogel), and modified for readability and to fit our needs. 104 | 105 | ## License 106 | 107 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 108 | -------------------------------------------------------------------------------- /src/Drivers/Color.php: -------------------------------------------------------------------------------- 1 | $value */ 22 | abstract public function initFromArray(array $value): self; 23 | 24 | abstract public function initFromString(string $value): self; 25 | 26 | abstract public function initFromObject(ImagickPixel $value): self; 27 | 28 | abstract public function initFromRgb(int $red, int $green, int $blue): self; 29 | 30 | abstract public function initFromRgba(int $red, int $green, int $blue, float $alpha): self; 31 | 32 | abstract public function getInt(): int; 33 | 34 | abstract public function getHex(string $prefix): string; 35 | 36 | /** @return array */ 37 | abstract public function getArray(): array; 38 | 39 | abstract public function getRgba(): string; 40 | 41 | abstract public function differs(self $color, int $tolerance = 0): bool; 42 | 43 | public function __construct(mixed $value = null) 44 | { 45 | $this->parse($value); 46 | } 47 | 48 | public function parse(mixed $colorValue): self 49 | { 50 | match (true) { 51 | is_string($colorValue) => $this->initFromString($colorValue), 52 | is_int($colorValue) => $this->initFromInteger($colorValue), 53 | is_array($colorValue) => $this->initFromArray($colorValue), 54 | $colorValue instanceof ImagickPixel => $this->initFromObject($colorValue), 55 | is_null($colorValue) => $this->initFromArray([255, 255, 255, 0]), 56 | default => throw InvalidColor::make($colorValue), 57 | }; 58 | 59 | return $this; 60 | } 61 | 62 | public function format(ColorFormat $colorFormat): mixed 63 | { 64 | return match ($colorFormat) { 65 | ColorFormat::Rgba => $this->getRgba(), 66 | ColorFormat::Hex => $this->getHex('#'), 67 | ColorFormat::Int => $this->getInt(), 68 | ColorFormat::Array => $this->getArray(), 69 | ColorFormat::Object => $this, 70 | }; 71 | } 72 | 73 | /** @return array */ 74 | protected function rgbaFromString(string $colorValue): array 75 | { 76 | // parse color string in hexidecimal format like #cccccc or cccccc or ccc 77 | $hexPattern = '/^#?([a-f0-9]{1,2})([a-f0-9]{1,2})([a-f0-9]{1,2})$/i'; 78 | 79 | // parse color string in format rgb(140, 140, 140) 80 | $rgbPattern = '/^rgb ?\(([0-9]{1,3}), ?([0-9]{1,3}), ?([0-9]{1,3})\)$/i'; 81 | 82 | // parse color string in format rgba(255, 0, 0, 0.5) 83 | $rgbaPattern = '/^rgba ?\(([0-9]{1,3}), ?([0-9]{1,3}), ?([0-9]{1,3}), ?([0-9.]{1,4})\)$/i'; 84 | 85 | if (preg_match($hexPattern, $colorValue, $matches)) { 86 | $result = []; 87 | $result[0] = strlen($matches[1]) == '1' ? (int) hexdec($matches[1].$matches[1]) : (int) hexdec($matches[1]); 88 | $result[1] = strlen($matches[2]) == '1' ? (int) hexdec($matches[2].$matches[2]) : (int) hexdec($matches[2]); 89 | $result[2] = strlen($matches[3]) == '1' ? (int) hexdec($matches[3].$matches[3]) : (int) hexdec($matches[3]); 90 | $result[3] = 1; 91 | } elseif (preg_match($rgbPattern, $colorValue, $matches)) { 92 | $result = []; 93 | $result[0] = ($matches[1] >= 0 && $matches[1] <= 255) ? (int) ($matches[1]) : 0; 94 | $result[1] = ($matches[2] >= 0 && $matches[2] <= 255) ? (int) ($matches[2]) : 0; 95 | $result[2] = ($matches[3] >= 0 && $matches[3] <= 255) ? (int) ($matches[3]) : 0; 96 | $result[3] = 1; 97 | } elseif (preg_match($rgbaPattern, $colorValue, $matches)) { 98 | $result = []; 99 | $result[0] = ($matches[1] >= 0 && $matches[1] <= 255) ? (int) ($matches[1]) : 0; 100 | $result[1] = ($matches[2] >= 0 && $matches[2] <= 255) ? (int) ($matches[2]) : 0; 101 | $result[2] = ($matches[3] >= 0 && $matches[3] <= 255) ? (int) ($matches[3]) : 0; 102 | $result[3] = ($matches[4] >= 0 && $matches[4] <= 1) ? (float) ($matches[4]) : 0; 103 | } else { 104 | throw InvalidColor::make($colorValue); 105 | } 106 | 107 | return $result; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Method Spatie\\\\Image\\\\Drivers\\\\Gd\\\\GdDriver\\:\\:pngCompression\\(\\) should return int\\<\\-1, 9\\> but returns int\\.$#" 5 | count: 1 6 | path: src/Drivers/Gd/GdDriver.php 7 | 8 | - 9 | message: "#^Parameter \\#1 \\$data of function imagecreatefromstring expects string, string\\|false given\\.$#" 10 | count: 1 11 | path: src/Drivers/Gd/GdDriver.php 12 | 13 | - 14 | message: "#^Parameter \\#1 \\$exif of method Spatie\\\\Image\\\\Drivers\\\\Gd\\\\GdDriver\\:\\:getOrientationFromExif\\(\\) expects array\\{Orientation\\?\\: int\\}, array\\ given\\.$#" 15 | count: 1 16 | path: src/Drivers/Gd/GdDriver.php 17 | 18 | - 19 | message: "#^Parameter \\#1 \\$string of function base64_encode expects string, string\\|false given\\.$#" 20 | count: 2 21 | path: src/Drivers/Gd/GdDriver.php 22 | 23 | - 24 | message: "#^Parameter \\#1 \\$width of method Spatie\\\\Image\\\\Drivers\\\\Gd\\\\GdDriver\\:\\:manualCrop\\(\\) expects int, int\\|null given\\.$#" 25 | count: 1 26 | path: src/Drivers/Gd/GdDriver.php 27 | 28 | - 29 | message: "#^Parameter \\#2 \\$color of function imagecolorsforindex expects int, int\\<0, max\\>\\|false given\\.$#" 30 | count: 1 31 | path: src/Drivers/Gd/GdDriver.php 32 | 33 | - 34 | message: "#^Parameter \\#2 \\$color of function imagecolortransparent expects int\\|null, int\\<0, max\\>\\|false given\\.$#" 35 | count: 1 36 | path: src/Drivers/Gd/GdDriver.php 37 | 38 | - 39 | message: "#^Parameter \\#2 \\$height of method Spatie\\\\Image\\\\Drivers\\\\Gd\\\\GdDriver\\:\\:manualCrop\\(\\) expects int, int\\|null given\\.$#" 40 | count: 1 41 | path: src/Drivers/Gd/GdDriver.php 42 | 43 | - 44 | message: "#^Parameter \\#2 \\$src_image of function imagecopy expects GdImage, mixed given\\.$#" 45 | count: 1 46 | path: src/Drivers/Gd/GdDriver.php 47 | 48 | - 49 | message: "#^Parameter \\#4 \\$color of function imagefill expects int, int\\<0, max\\>\\|false given\\.$#" 50 | count: 1 51 | path: src/Drivers/Gd/GdDriver.php 52 | 53 | - 54 | message: "#^Parameter \\#6 \\$color of function imagefilledrectangle expects int, int\\<0, max\\>\\|false given\\.$#" 55 | count: 1 56 | path: src/Drivers/Gd/GdDriver.php 57 | 58 | - 59 | message: "#^Property Spatie\\\\Image\\\\Drivers\\\\Gd\\\\GdDriver\\:\\:\\$image \\(GdImage\\) does not accept GdImage\\|false\\.$#" 60 | count: 4 61 | path: src/Drivers/Gd/GdDriver.php 62 | 63 | - 64 | message: "#^Property Spatie\\\\Image\\\\Drivers\\\\Gd\\\\GdDriver\\:\\:\\$image \\(GdImage\\) does not accept mixed\\.$#" 65 | count: 1 66 | path: src/Drivers/Gd/GdDriver.php 67 | 68 | - 69 | message: "#^Unreachable statement \\- code above always terminates\\.$#" 70 | count: 1 71 | path: src/Drivers/Gd/GdDriver.php 72 | 73 | - 74 | message: "#^Cannot call method evaluateImage\\(\\) on mixed\\.$#" 75 | count: 1 76 | path: src/Drivers/Imagick/ImagickDriver.php 77 | 78 | - 79 | message: "#^Cannot call method setImageOrientation\\(\\) on mixed\\.$#" 80 | count: 1 81 | path: src/Drivers/Imagick/ImagickDriver.php 82 | 83 | - 84 | message: "#^Parameter \\#1 \\$composite_object of method Imagick\\:\\:compositeImage\\(\\) expects Imagick, mixed given\\.$#" 85 | count: 1 86 | path: src/Drivers/Imagick/ImagickDriver.php 87 | 88 | - 89 | message: "#^Parameter \\#1 \\$width of method Spatie\\\\Image\\\\Drivers\\\\Imagick\\\\ImagickDriver\\:\\:manualCrop\\(\\) expects int, int\\|null given\\.$#" 90 | count: 1 91 | path: src/Drivers/Imagick/ImagickDriver.php 92 | 93 | - 94 | message: "#^Parameter \\#2 \\$height of method Spatie\\\\Image\\\\Drivers\\\\Imagick\\\\ImagickDriver\\:\\:manualCrop\\(\\) expects int, int\\|null given\\.$#" 95 | count: 1 96 | path: src/Drivers/Imagick/ImagickDriver.php 97 | 98 | - 99 | message: "#^Property Spatie\\\\Image\\\\Drivers\\\\Imagick\\\\ImagickDriver\\:\\:\\$exif type has no value type specified in iterable type array\\.$#" 100 | count: 1 101 | path: src/Drivers/Imagick/ImagickDriver.php 102 | 103 | - 104 | message: "#^Property Spatie\\\\Image\\\\Drivers\\\\Imagick\\\\ImagickDriver\\:\\:\\$image \\(Imagick\\) does not accept mixed\\.$#" 105 | count: 1 106 | path: src/Drivers/Imagick/ImagickDriver.php 107 | 108 | - 109 | message: "#^Part \\$color \\(mixed\\) of encapsed string cannot be cast to string\\.$#" 110 | count: 1 111 | path: src/Exceptions/InvalidColor.php 112 | 113 | - 114 | message: "#^Parameter \\#3 \\$x of method Spatie\\\\Image\\\\Drivers\\\\ImageDriver\\:\\:manualCrop\\(\\) expects int, int\\|null given\\.$#" 115 | count: 1 116 | path: src/Image.php 117 | 118 | - 119 | message: "#^Parameter \\#4 \\$y of method Spatie\\\\Image\\\\Drivers\\\\ImageDriver\\:\\:manualCrop\\(\\) expects int, int\\|null given\\.$#" 120 | count: 1 121 | path: src/Image.php 122 | -------------------------------------------------------------------------------- /src/Drivers/Imagick/ImagickColor.php: -------------------------------------------------------------------------------- 1 | > 24) & 0xFF; 17 | $red = ($value >> 16) & 0xFF; 18 | $green = ($value >> 8) & 0xFF; 19 | $blue = $value & 0xFF; 20 | 21 | $alpha = $this->rgb2alpha($alpha); 22 | 23 | $this->setPixel($red, $green, $blue, $alpha); 24 | 25 | return $this; 26 | } 27 | 28 | public function initFromArray(array $value): self 29 | { 30 | $value = array_values($value); 31 | 32 | [$red, $green, $blue] = $value; 33 | 34 | $alpha = $value[3] ?? 1; 35 | 36 | $this->setPixel($red, $green, $blue, $alpha); 37 | 38 | return $this; 39 | } 40 | 41 | public function initFromString(string $value): self 42 | { 43 | if ($color = $this->rgbaFromString($value)) { 44 | $this->setPixel($color[0], $color[1], $color[2], $color[3]); 45 | } 46 | 47 | return $this; 48 | } 49 | 50 | public function initFromObject(ImagickPixel $value): self 51 | { 52 | $this->pixel = $value; 53 | 54 | return $this; 55 | } 56 | 57 | public function initFromRgb(int $red, int $green, int $blue): self 58 | { 59 | $this->setPixel($red, $green, $blue); 60 | 61 | return $this; 62 | } 63 | 64 | public function initFromRgba(int $red, int $green, int $blue, float $alpha): self 65 | { 66 | $this->setPixel($red, $green, $blue, $alpha); 67 | 68 | return $this; 69 | } 70 | 71 | public function getInt(): int 72 | { 73 | $red = $this->getRedValue(); 74 | $green = $this->getGreenValue(); 75 | $blue = $this->getBlueValue(); 76 | $alpha = (int) (round($this->getAlphaValue() * 255)); 77 | 78 | return ($alpha << 24) + ($red << 16) + ($green << 8) + $blue; 79 | } 80 | 81 | public function getHex(string $prefix = ''): string 82 | { 83 | return sprintf('%s%02x%02x%02x', $prefix, 84 | $this->getRedValue(), 85 | $this->getGreenValue(), 86 | $this->getBlueValue() 87 | ); 88 | } 89 | 90 | public function getArray(): array 91 | { 92 | return [ 93 | $this->getRedValue(), 94 | $this->getGreenValue(), 95 | $this->getBlueValue(), 96 | $this->getAlphaValue(), 97 | ]; 98 | } 99 | 100 | public function getRgba(): string 101 | { 102 | return sprintf('rgba(%d, %d, %d, %.2F)', 103 | $this->getRedValue(), 104 | $this->getGreenValue(), 105 | $this->getBlueValue(), 106 | $this->getAlphaValue() 107 | ); 108 | } 109 | 110 | public function differs(Color $color, int $tolerance = 0): bool 111 | { 112 | if (! $color instanceof self) { 113 | throw new InvalidArgumentException('Color must be an instance of '.self::class); 114 | } 115 | 116 | $colorTolerance = round($tolerance * 2.55); 117 | $alphaTolerance = round($tolerance); 118 | 119 | $delta = [ 120 | 'r' => abs($color->getRedValue() - $this->getRedValue()), 121 | 'g' => abs($color->getGreenValue() - $this->getGreenValue()), 122 | 'b' => abs($color->getBlueValue() - $this->getBlueValue()), 123 | 'a' => abs($color->getAlphaValue() - $this->getAlphaValue()), 124 | ]; 125 | 126 | return 127 | $delta['r'] > $colorTolerance || 128 | $delta['g'] > $colorTolerance || 129 | $delta['b'] > $colorTolerance || 130 | $delta['a'] > $alphaTolerance; 131 | } 132 | 133 | public function getRedValue(): int 134 | { 135 | return (int) round($this->pixel->getColorValue(Imagick::COLOR_RED) * 255); 136 | } 137 | 138 | public function getGreenValue(): int 139 | { 140 | return (int) round($this->pixel->getColorValue(Imagick::COLOR_GREEN) * 255); 141 | } 142 | 143 | public function getBlueValue(): int 144 | { 145 | return (int) round($this->pixel->getColorValue(Imagick::COLOR_BLUE) * 255); 146 | } 147 | 148 | public function getAlphaValue(): float 149 | { 150 | return round($this->pixel->getColorValue(Imagick::COLOR_ALPHA), 2); 151 | } 152 | 153 | private function setPixel( 154 | int|float $red, 155 | int|float $green, 156 | int|float $blue, 157 | int|float|null $alpha = null 158 | ): ImagickPixel { 159 | $alpha = is_null($alpha) ? 1 : $alpha; 160 | 161 | return $this->pixel = new ImagickPixel( 162 | sprintf('rgba(%d, %d, %d, %.2F)', $red, $green, $blue, $alpha) 163 | ); 164 | } 165 | 166 | public function getPixel(): ImagickPixel 167 | { 168 | return $this->pixel; 169 | } 170 | 171 | private function rgb2alpha(int $value): float 172 | { 173 | return round($value / 255, 2); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Drivers/ImageDriver.php: -------------------------------------------------------------------------------- 1 | 91 | */ 92 | public function exif(): array; 93 | 94 | public function flip(FlipDirection $flip): static; 95 | 96 | public function pixelate(int $pixelate): static; 97 | 98 | public function watermark( 99 | ImageDriver|string $watermarkImage, 100 | AlignPosition $position = AlignPosition::BottomRight, 101 | int $paddingX = 0, 102 | int $paddingY = 0, 103 | Unit $paddingUnit = Unit::Pixel, 104 | int $width = 0, 105 | Unit $widthUnit = Unit::Pixel, 106 | int $height = 0, 107 | Unit $heightUnit = Unit::Pixel, 108 | Fit $fit = Fit::Contain, 109 | int $alpha = 100 110 | ): static; 111 | 112 | public function insert( 113 | ImageDriver|string $otherImage, 114 | AlignPosition $position = AlignPosition::Center, 115 | int $x = 0, 116 | int $y = 0, 117 | int $alpha = 100 118 | ): static; 119 | 120 | public function text( 121 | string $text, 122 | int $fontSize, 123 | string $color = '000000', 124 | int $x = 0, 125 | int $y = 0, 126 | int $angle = 0, 127 | string $fontPath = '', 128 | int $width = 0, 129 | ): static; 130 | 131 | public function wrapText( 132 | string $text, 133 | int $fontSize, 134 | string $fontPath = '', 135 | int $angle = 0, 136 | int $width = 0, 137 | ): string; 138 | 139 | public function image(): mixed; 140 | 141 | /** @param array $constraints */ 142 | public function resize(int $width, int $height, array $constraints): static; 143 | 144 | /** @param array $constraints */ 145 | public function width(int $width, array $constraints = []): static; 146 | 147 | /** @param array $constraints */ 148 | public function height(int $height, array $constraints = []): static; 149 | 150 | public function border(int $width, BorderType $type, string $color = '000000'): static; 151 | 152 | public function quality(int $quality): static; 153 | 154 | public function format(string $format): static; 155 | 156 | public function optimize(?OptimizerChain $optimizerChain = null): static; 157 | } 158 | -------------------------------------------------------------------------------- /src/Size.php: -------------------------------------------------------------------------------- 1 | width / $this->height; 21 | } 22 | 23 | /** @param array $constraints */ 24 | public function resize( 25 | ?int $desiredWidth = null, 26 | ?int $desiredHeight = null, 27 | array $constraints = [] 28 | ): self { 29 | if ($desiredWidth === null && $desiredHeight === null) { 30 | throw new InvalidArgumentException("Width and height can't both be null"); 31 | } 32 | 33 | if ($desiredWidth === null) { 34 | throw CannotResize::invalidWidth(); 35 | } 36 | 37 | if ($desiredHeight === null) { 38 | throw CannotResize::invalidHeight(); 39 | } 40 | 41 | $dominantWidthSize = (clone $this) 42 | ->resizeHeight($desiredHeight, $constraints) 43 | ->resizeWidth($desiredWidth, $constraints); 44 | 45 | $dominantHeightSize = (clone $this) 46 | ->resizeWidth($desiredWidth, $constraints) 47 | ->resizeHeight($desiredHeight, $constraints); 48 | 49 | // @todo desiredWidth and desiredHeight can still be null here, which will cause an error 50 | return $dominantHeightSize->fitsInto(new self($desiredWidth, $desiredHeight)) 51 | ? $dominantHeightSize 52 | : $dominantWidthSize; 53 | } 54 | 55 | /** @param array $constraints */ 56 | public function resizeWidth( 57 | ?int $desiredWidth = null, 58 | array $constraints = [] 59 | ): self { 60 | if (! is_numeric($desiredWidth)) { 61 | return $this; 62 | } 63 | 64 | $originalWidth = $this->width; 65 | $originalHeight = $this->height; 66 | 67 | $preserveAspect = in_array(Constraint::PreserveAspectRatio, $constraints); 68 | $doNotUpsize = in_array(Constraint::DoNotUpsize, $constraints); 69 | 70 | $newWidth = $doNotUpsize ? min($desiredWidth, $originalWidth) : $desiredWidth; 71 | $newHeight = $this->height; 72 | 73 | if ($preserveAspect) { 74 | $calculatedHeight = max(1, (int) round($newWidth / (new Size($originalWidth, $originalHeight))->aspectRatio())); 75 | $newHeight = $doNotUpsize ? min($calculatedHeight, $originalHeight) : $calculatedHeight; 76 | } 77 | 78 | $this->width = $newWidth; 79 | $this->height = $newHeight; 80 | 81 | return $this; 82 | } 83 | 84 | /** @param array $constraints */ 85 | public function resizeHeight(?int $desiredHeight = null, array $constraints = []): self 86 | { 87 | if (! is_numeric($desiredHeight)) { 88 | return $this; 89 | } 90 | 91 | $originalWidth = $this->width; 92 | $originalHeight = $this->height; 93 | 94 | $preserveAspect = in_array(Constraint::PreserveAspectRatio, $constraints); 95 | $doNotUpsize = in_array(Constraint::DoNotUpsize, $constraints); 96 | 97 | $newHeight = $doNotUpsize ? min($desiredHeight, $originalHeight) : $desiredHeight; 98 | $newWidth = $this->width; 99 | 100 | if ($preserveAspect) { 101 | $calculatedWidth = max(1, (int) round($newHeight * (new Size($originalWidth, $originalHeight))->aspectRatio())); 102 | $newWidth = $doNotUpsize ? min($calculatedWidth, $originalWidth) : $calculatedWidth; 103 | } 104 | 105 | $this->height = $newHeight; 106 | $this->width = $newWidth; 107 | 108 | return $this; 109 | } 110 | 111 | public function fitsInto(Size $size): bool 112 | { 113 | return ($this->width <= $size->width) && ($this->height <= $size->height); 114 | } 115 | 116 | public function align(AlignPosition $position, int $offsetX = 0, int $offsetY = 0): self 117 | { 118 | 119 | switch ($position) { 120 | 121 | case AlignPosition::Top: 122 | case AlignPosition::TopCenter: 123 | case AlignPosition::TopMiddle: 124 | case AlignPosition::CenterTop: 125 | case AlignPosition::MiddleTop: 126 | $x = (int) ($this->width / 2); 127 | $y = 0 + $offsetY; 128 | break; 129 | 130 | case AlignPosition::TopRight: 131 | case AlignPosition::RightTop: 132 | $x = $this->width - $offsetX; 133 | $y = 0 + $offsetY; 134 | break; 135 | 136 | case AlignPosition::Left: 137 | case AlignPosition::LeftCenter: 138 | case AlignPosition::LeftMiddle: 139 | case AlignPosition::CenterLeft: 140 | case AlignPosition::MiddleLeft: 141 | $x = 0 + $offsetX; 142 | $y = (int) ($this->height / 2); 143 | break; 144 | 145 | case AlignPosition::Right: 146 | case AlignPosition::RightCenter: 147 | case AlignPosition::RightMiddle: 148 | case AlignPosition::CenterRight: 149 | case AlignPosition::MiddleRight: 150 | $x = $this->width - $offsetX; 151 | $y = (int) ($this->height / 2); 152 | break; 153 | 154 | case AlignPosition::BottomLeft: 155 | case AlignPosition::LeftBottom: 156 | $x = 0 + $offsetX; 157 | $y = $this->height - $offsetY; 158 | break; 159 | 160 | case AlignPosition::Bottom: 161 | case AlignPosition::BottomCenter: 162 | case AlignPosition::BottomMiddle: 163 | case AlignPosition::CenterBottom: 164 | case AlignPosition::MiddleBottom: 165 | $x = (int) ($this->width / 2); 166 | $y = $this->height - $offsetY; 167 | break; 168 | 169 | case AlignPosition::BottomRight: 170 | case AlignPosition::RightBottom: 171 | $x = $this->width - $offsetX; 172 | $y = $this->height - $offsetY; 173 | break; 174 | 175 | case AlignPosition::Center: 176 | case AlignPosition::Middle: 177 | case AlignPosition::CenterCenter: 178 | case AlignPosition::MiddleMiddle: 179 | $x = (int) ($this->width / 2) + $offsetX; 180 | $y = (int) ($this->height / 2) + $offsetY; 181 | break; 182 | 183 | default: 184 | case 'top-left': 185 | case 'left-top': 186 | $x = 0 + $offsetX; 187 | $y = 0 + $offsetY; 188 | break; 189 | } 190 | 191 | $this->pivot->setCoordinates($x, $y); 192 | 193 | return $this; 194 | } 195 | 196 | public function relativePosition(Size $size): Point 197 | { 198 | $x = $this->pivot->x - $size->pivot->x; 199 | $y = $this->pivot->y - $size->pivot->y; 200 | 201 | return new Point($x, $y); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/Image.php: -------------------------------------------------------------------------------- 1 | imageDriver = class_exists(Imagick::class) ? new ImagickDriver : new GdDriver; 33 | 34 | if ($pathToImage) { 35 | $this->imageDriver->loadFile($pathToImage); 36 | } 37 | } 38 | 39 | public static function load(string $pathToImage): static 40 | { 41 | if (! file_exists($pathToImage)) { 42 | throw CouldNotLoadImage::fileDoesNotExist($pathToImage); 43 | } 44 | 45 | return new static($pathToImage); 46 | } 47 | 48 | public function loadFile(string $pathToImage): static 49 | { 50 | $this->imageDriver->loadFile($pathToImage); 51 | 52 | return $this; 53 | } 54 | 55 | public static function useImageDriver(ImageDriverEnum|string $imageDriver): static 56 | { 57 | $image = new static; 58 | 59 | if (is_subclass_of($imageDriver, ImageDriver::class)) { 60 | /** @var ImageDriver $imageDriver */ 61 | $image->imageDriver = new $imageDriver; 62 | 63 | return $image; 64 | } 65 | 66 | if (is_string($imageDriver)) { 67 | $imageDriver = ImageDriverEnum::tryFrom($imageDriver) 68 | ?? throw InvalidImageDriver::driver($imageDriver); 69 | } 70 | 71 | $image->imageDriver = match ($imageDriver) { 72 | ImageDriverEnum::Gd => new GdDriver, 73 | ImageDriverEnum::Imagick => new ImagickDriver, 74 | }; 75 | 76 | return $image; 77 | } 78 | 79 | public function new(int $width, int $height, ?string $backgroundColor = null): static 80 | { 81 | $this->imageDriver->new($width, $height, $backgroundColor); 82 | 83 | return $this; 84 | } 85 | 86 | public function driverName(): string 87 | { 88 | return $this->imageDriver->driverName(); 89 | } 90 | 91 | public function save(string $path = ''): static 92 | { 93 | $this->imageDriver->save($path); 94 | 95 | return $this; 96 | } 97 | 98 | public function getWidth(): int 99 | { 100 | return $this->imageDriver->getWidth(); 101 | } 102 | 103 | public function getHeight(): int 104 | { 105 | return $this->imageDriver->getHeight(); 106 | } 107 | 108 | public function brightness(int $brightness): static 109 | { 110 | $this->ensureNumberBetween($brightness, -100, 100, 'brightness'); 111 | 112 | $this->imageDriver->brightness($brightness); 113 | 114 | return $this; 115 | } 116 | 117 | public function gamma(float $gamma): static 118 | { 119 | $this->ensureNumberBetween($gamma, 0.1, 9.99, 'gamma'); 120 | 121 | $this->imageDriver->gamma($gamma); 122 | 123 | return $this; 124 | } 125 | 126 | public function contrast(float $level): static 127 | { 128 | $this->ensureNumberBetween($level, -100, 100, 'contrast'); 129 | 130 | $this->imageDriver->contrast($level); 131 | 132 | return $this; 133 | } 134 | 135 | public function blur(int $blur): static 136 | { 137 | $this->ensureNumberBetween($blur, 0, 100, 'blur'); 138 | 139 | $this->imageDriver->blur($blur); 140 | 141 | return $this; 142 | } 143 | 144 | public function colorize(int $red, int $green, int $blue): static 145 | { 146 | $this->ensureNumberBetween($red, -100, 100, 'red'); 147 | $this->ensureNumberBetween($green, -100, 100, 'green'); 148 | $this->ensureNumberBetween($blue, -100, 100, 'blue'); 149 | 150 | $this->imageDriver->colorize($red, $green, $blue); 151 | 152 | return $this; 153 | } 154 | 155 | public function greyscale(): static 156 | { 157 | $this->imageDriver->greyscale(); 158 | 159 | return $this; 160 | } 161 | 162 | public function sepia(): static 163 | { 164 | $this->imageDriver->sepia(); 165 | 166 | return $this; 167 | } 168 | 169 | public function sharpen(float $amount): static 170 | { 171 | $this->ensureNumberBetween($amount, 0, 100, 'sharpen'); 172 | 173 | $this->imageDriver->sharpen($amount); 174 | 175 | return $this; 176 | } 177 | 178 | public function getSize(): Size 179 | { 180 | return $this->imageDriver->getSize(); 181 | } 182 | 183 | public function fit( 184 | Fit $fit, 185 | ?int $desiredWidth = null, 186 | ?int $desiredHeight = null, 187 | bool $relative = false, 188 | string $backgroundColor = '#ffffff' 189 | ): static { 190 | $this->imageDriver->fit($fit, $desiredWidth, $desiredHeight, $relative, $backgroundColor); 191 | 192 | return $this; 193 | } 194 | 195 | public function pickColor(int $x, int $y, ColorFormat $colorFormat): mixed 196 | { 197 | return $this->imageDriver->pickColor($x, $y, $colorFormat); 198 | } 199 | 200 | public function resizeCanvas(?int $width = null, ?int $height = null, ?AlignPosition $position = null, bool $relative = false, string $backgroundColor = '#000000'): static 201 | { 202 | $this->imageDriver->resizeCanvas($width, $height, $position, $relative, $backgroundColor); 203 | 204 | return $this; 205 | } 206 | 207 | public function manualCrop(int $width, int $height, ?int $x = null, ?int $y = null): static 208 | { 209 | $this->imageDriver->manualCrop($width, $height, $x, $y); 210 | 211 | return $this; 212 | } 213 | 214 | public function crop(int $width, int $height, CropPosition $position = CropPosition::Center): static 215 | { 216 | $this->imageDriver->crop($width, $height, $position); 217 | 218 | return $this; 219 | } 220 | 221 | public function focalCrop(int $width, int $height, ?int $cropCenterX = null, ?int $cropCenterY = null): static 222 | { 223 | $this->imageDriver->focalCrop($width, $height, $cropCenterX, $cropCenterY); 224 | 225 | return $this; 226 | } 227 | 228 | public function focalCropAndResize(int $width, int $height, ?int $cropCenterX = null, ?int $cropCenterY = null): static 229 | { 230 | $this->imageDriver->focalCropAndResize($width, $height, $cropCenterX, $cropCenterY); 231 | 232 | return $this; 233 | } 234 | 235 | public function base64(string $imageFormat = 'jpeg', bool $prefixWithFormat = true): string 236 | { 237 | return $this->imageDriver->base64($imageFormat, $prefixWithFormat); 238 | } 239 | 240 | public function background(string $color): static 241 | { 242 | $this->imageDriver->background($color); 243 | 244 | return $this; 245 | } 246 | 247 | public function overlay(ImageDriver $bottomImage, ImageDriver $topImage, int $x, int $y): static 248 | { 249 | $this->imageDriver->overlay($bottomImage, $topImage, $x, $y); 250 | 251 | return $this; 252 | } 253 | 254 | public function orientation(?Orientation $orientation = null): static 255 | { 256 | $this->imageDriver->orientation($orientation); 257 | 258 | return $this; 259 | } 260 | 261 | /** 262 | * @return array 263 | */ 264 | public function exif(): array 265 | { 266 | return $this->imageDriver->exif(); 267 | } 268 | 269 | public function flip(FlipDirection $flip): static 270 | { 271 | $this->imageDriver->flip($flip); 272 | 273 | return $this; 274 | } 275 | 276 | public function pixelate(int $pixelate = 50): static 277 | { 278 | $this->ensureNumberBetween($pixelate, 0, 100, 'pixelate'); 279 | 280 | $this->imageDriver->pixelate($pixelate); 281 | 282 | return $this; 283 | } 284 | 285 | public function insert(ImageDriver|string $otherImage, AlignPosition $position = AlignPosition::Center, int $x = 0, int $y = 0, int $alpha = 100): static 286 | { 287 | $this->imageDriver->insert($otherImage, $position, $x, $y, $alpha); 288 | 289 | return $this; 290 | } 291 | 292 | public function image(): mixed 293 | { 294 | return $this->imageDriver->image(); 295 | } 296 | 297 | public function resize(int $width, int $height, array $constraints = []): static 298 | { 299 | $this->imageDriver->resize($width, $height, $constraints); 300 | 301 | return $this; 302 | } 303 | 304 | public function width(int $width, array $constraints = [Constraint::PreserveAspectRatio]): static 305 | { 306 | $this->imageDriver->width($width, $constraints); 307 | 308 | return $this; 309 | } 310 | 311 | public function height(int $height, array $constraints = [Constraint::PreserveAspectRatio]): static 312 | { 313 | $this->imageDriver->height($height, $constraints); 314 | 315 | return $this; 316 | } 317 | 318 | public function border(int $width, BorderType $type, string $color = '000000'): static 319 | { 320 | $this->imageDriver->border($width, $type, $color); 321 | 322 | return $this; 323 | } 324 | 325 | public function quality(int $quality): static 326 | { 327 | $this->ensureNumberBetween($quality, 0, 100, 'quality'); 328 | 329 | $this->imageDriver->quality($quality); 330 | 331 | return $this; 332 | } 333 | 334 | public function format(string $format): static 335 | { 336 | $this->imageDriver->format($format); 337 | 338 | return $this; 339 | } 340 | 341 | public function optimize(?OptimizerChain $optimizerChain = null): static 342 | { 343 | $this->imageDriver->optimize($optimizerChain); 344 | 345 | return $this; 346 | } 347 | 348 | public function watermark( 349 | ImageDriver|string $watermarkImage, 350 | AlignPosition $position = AlignPosition::BottomRight, 351 | int $paddingX = 0, 352 | int $paddingY = 0, 353 | Unit $paddingUnit = Unit::Pixel, 354 | int $width = 0, 355 | Unit $widthUnit = Unit::Pixel, 356 | int $height = 0, 357 | Unit $heightUnit = Unit::Pixel, 358 | Fit $fit = Fit::Contain, 359 | int $alpha = 100 360 | ): static { 361 | $this->imageDriver->watermark( 362 | $watermarkImage, 363 | $position, 364 | $paddingX, 365 | $paddingY, 366 | $paddingUnit, 367 | $width, 368 | $widthUnit, 369 | $height, 370 | $heightUnit, 371 | $fit, 372 | $alpha, 373 | ); 374 | 375 | return $this; 376 | } 377 | 378 | public function text( 379 | string $text, 380 | int $fontSize, 381 | string $color = '000000', 382 | int $x = 0, 383 | int $y = 0, 384 | int $angle = 0, 385 | string $fontPath = '', 386 | int $width = 0, 387 | ): static { 388 | $this->imageDriver->text( 389 | $text, 390 | $fontSize, 391 | $color, 392 | $x, 393 | $y, 394 | $angle, 395 | $fontPath, 396 | $width, 397 | ); 398 | 399 | return $this; 400 | } 401 | 402 | public function wrapText(string $text, int $fontSize, string $fontPath = '', int $angle = 0, int $width = 0): string 403 | { 404 | return $this->imageDriver->wrapText( 405 | $text, 406 | $fontSize, 407 | $fontPath, 408 | $angle, 409 | $width, 410 | ); 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `image` will be documented in this file 4 | 5 | ## 3.8.7 - 2025-11-24 6 | 7 | ### What's Changed 8 | 9 | * Bump stefanzweifel/git-auto-commit-action from 6 to 7 by @dependabot[bot] in https://github.com/spatie/image/pull/308 10 | * Bump actions/setup-node from 5 to 6 by @dependabot[bot] in https://github.com/spatie/image/pull/309 11 | * ci: add tests for PHP 8.5 by @Chris53897 in https://github.com/spatie/image/pull/312 12 | * Change default driver to Gd when Imagick is not available by @andreasnij in https://github.com/spatie/image/pull/315 13 | * Bump actions/checkout from 5 to 6 by @dependabot[bot] in https://github.com/spatie/image/pull/314 14 | * Added Symfony 8 support to all symfony/* packages. by @thecaliskan in https://github.com/spatie/image/pull/313 15 | 16 | ### New Contributors 17 | 18 | * @Chris53897 made their first contribution in https://github.com/spatie/image/pull/312 19 | * @andreasnij made their first contribution in https://github.com/spatie/image/pull/315 20 | * @thecaliskan made their first contribution in https://github.com/spatie/image/pull/313 21 | 22 | **Full Changelog**: https://github.com/spatie/image/compare/3.8.6...3.8.7 23 | 24 | ## 3.8.6 - 2025-09-25 25 | 26 | ### What's Changed 27 | 28 | * Bump aglipanci/laravel-pint-action from 2.5 to 2.6 by @dependabot[bot] in https://github.com/spatie/image/pull/302 29 | * Update issue template by @AlexVanderbist in https://github.com/spatie/image/pull/305 30 | * Bump actions/setup-node from 4 to 5 by @dependabot[bot] in https://github.com/spatie/image/pull/304 31 | * Bump actions/checkout from 4 to 5 by @dependabot[bot] in https://github.com/spatie/image/pull/303 32 | * fix(gddriver): fix CouldNotLoadImage::make error by @glesende in https://github.com/spatie/image/pull/307 33 | 34 | ### New Contributors 35 | 36 | * @glesende made their first contribution in https://github.com/spatie/image/pull/307 37 | 38 | **Full Changelog**: https://github.com/spatie/image/compare/3.8.5...3.8.6 39 | 40 | ## 3.8.5 - 2025-06-27 41 | 42 | ### What's Changed 43 | 44 | * Fix tests by @timvandijck in https://github.com/spatie/image/pull/299 45 | * Bump stefanzweifel/git-auto-commit-action from 5 to 6 by @dependabot in https://github.com/spatie/image/pull/300 46 | * Focal Crop and Resize by @GarethSomers in https://github.com/spatie/image/pull/301 47 | 48 | ### New Contributors 49 | 50 | * @GarethSomers made their first contribution in https://github.com/spatie/image/pull/301 51 | 52 | **Full Changelog**: https://github.com/spatie/image/compare/3.8.4...3.8.5 53 | 54 | ## 3.8.4 - 2025-06-04 55 | 56 | ### What's Changed 57 | 58 | * Bump dependabot/fetch-metadata from 2.3.0 to 2.4.0 by @dependabot in https://github.com/spatie/image/pull/297 59 | * Update GdDriver.php by @GhostvOne in https://github.com/spatie/image/pull/298 60 | 61 | ### New Contributors 62 | 63 | * @GhostvOne made their first contribution in https://github.com/spatie/image/pull/298 64 | 65 | **Full Changelog**: https://github.com/spatie/image/compare/3.8.3...3.8.4 66 | 67 | ## 3.8.3 - 2025-04-25 68 | 69 | ### What's Changed 70 | 71 | * refactor: simplify loadFile by replacing fopen/fread with file_get_contents by @Ayoub-Mabrouk in https://github.com/spatie/image/pull/296 72 | * refactor(image): simplify and merge exif and fileinfo extension checks by @Ayoub-Mabrouk in https://github.com/spatie/image/pull/295 73 | 74 | **Full Changelog**: https://github.com/spatie/image/compare/3.8.2...3.8.3 75 | 76 | ## 3.8.2 - 2025-04-24 77 | 78 | ### What's Changed 79 | 80 | * refactor: simplify resizeWidth method by @Ayoub-Mabrouk in https://github.com/spatie/image/pull/292 81 | * refactor: simplify and optimize resizeHeight method by @Ayoub-Mabrouk in https://github.com/spatie/image/pull/293 82 | * refactor: simplify resize method by @Ayoub-Mabrouk in https://github.com/spatie/image/pull/294 83 | 84 | ### New Contributors 85 | 86 | * @Ayoub-Mabrouk made their first contribution in https://github.com/spatie/image/pull/292 87 | 88 | **Full Changelog**: https://github.com/spatie/image/compare/3.8.1...3.8.2 89 | 90 | ## 3.8.1 - 2025-03-27 91 | 92 | ### What's Changed 93 | 94 | * Bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 by @dependabot in https://github.com/spatie/image/pull/287 95 | * Bump aglipanci/laravel-pint-action from 2.4 to 2.5 by @dependabot in https://github.com/spatie/image/pull/288 96 | * Allow custom image drivers by @Peeterush in https://github.com/spatie/image/pull/290 97 | 98 | **Full Changelog**: https://github.com/spatie/image/compare/3.8.0...3.8.1 99 | 100 | ## 3.8.0 - 2025-01-17 101 | 102 | ### What's Changed 103 | 104 | * Add -90 rotation degree by @hbakouane in https://github.com/spatie/image/pull/285 105 | 106 | ### New Contributors 107 | 108 | * @hbakouane made their first contribution in https://github.com/spatie/image/pull/285 109 | 110 | **Full Changelog**: https://github.com/spatie/image/compare/3.7.5...3.8.0 111 | 112 | ## 3.7.5 - 2025-01-13 113 | 114 | ### What's Changed 115 | 116 | * PHP 8.4 tests by @erikn69 in https://github.com/spatie/image/pull/277 117 | * Correct documentation for FillMax in resizing-images.md by @ElGovanni in https://github.com/spatie/image/pull/278 118 | * Correction: Update basic-usage.md Selecting a driver" section by @PrabalPradhan1991 in https://github.com/spatie/image/pull/280 119 | * Update basic-usage.md Reverting back the incorrect changes that I suggested by @PrabalPradhan1991 in https://github.com/spatie/image/pull/281 120 | * Do not remove color profiles when resizing canvas. by @Peeterush in https://github.com/spatie/image/pull/284 121 | 122 | ### New Contributors 123 | 124 | * @ElGovanni made their first contribution in https://github.com/spatie/image/pull/278 125 | * @Peeterush made their first contribution in https://github.com/spatie/image/pull/284 126 | 127 | **Full Changelog**: https://github.com/spatie/image/compare/3.7.4...3.7.5 128 | 129 | ## 3.7.4 - 2024-10-07 130 | 131 | ### What's Changed 132 | 133 | * Fix broken link by @Synchro in https://github.com/spatie/image/pull/272 134 | * Update resizing-images.md by @makakken in https://github.com/spatie/image/pull/273 135 | * Improve DX by specifying namespace for GD functions by @alies-dev in https://github.com/spatie/image/pull/274 136 | * Fix issue with palette images when saving a webp file using GD. 137 | 138 | ### New Contributors 139 | 140 | * @makakken made their first contribution in https://github.com/spatie/image/pull/273 141 | * @alies-dev made their first contribution in https://github.com/spatie/image/pull/274 142 | 143 | **Full Changelog**: https://github.com/spatie/image/compare/3.7.3...3.7.4 144 | 145 | ## 3.7.3 - 2024-08-06 146 | 147 | ### What's Changed 148 | 149 | * Update Image.php to fix $prefixWithFormat by @tanshiqi in https://github.com/spatie/image/pull/269 150 | 151 | ### New Contributors 152 | 153 | * @tanshiqi made their first contribution in https://github.com/spatie/image/pull/269 154 | 155 | **Full Changelog**: https://github.com/spatie/image/compare/3.7.2...3.7.3 156 | 157 | ## 3.7.2 - 2024-07-26 158 | 159 | ### What's Changed 160 | 161 | * fix: Apply image format when saving file in ImagickDriver by @nicoverbruggen in https://github.com/spatie/image/pull/268 162 | 163 | ### New Contributors 164 | 165 | * @nicoverbruggen made their first contribution in https://github.com/spatie/image/pull/268 166 | 167 | **Full Changelog**: https://github.com/spatie/image/compare/3.7.1...3.7.2 168 | 169 | ## 3.7.1 - 2024-07-18 170 | 171 | ### What's Changed 172 | 173 | * fix: GdDriver resizeCanvas save alpha channel; by @olexoliinyk0 in https://github.com/spatie/image/pull/266 174 | 175 | ### New Contributors 176 | 177 | * @olexoliinyk0 made their first contribution in https://github.com/spatie/image/pull/266 178 | 179 | **Full Changelog**: https://github.com/spatie/image/compare/3.7.0...3.7.1 180 | 181 | ## 3.7.0 - 2024-07-18 182 | 183 | ### What's Changed 184 | 185 | * Bump dependabot/fetch-metadata from 2.1.0 to 2.2.0 by @dependabot in https://github.com/spatie/image/pull/263 186 | * Fix bug in ImagickDriver::pixelate, Update tests by @psion-ar in https://github.com/spatie/image/pull/267 187 | 188 | ### New Contributors 189 | 190 | * @psion-ar made their first contribution in https://github.com/spatie/image/pull/267 191 | 192 | **Full Changelog**: https://github.com/spatie/image/compare/3.6.4...3.7.0 193 | 194 | ## 3.6.4 - 2024-06-03 195 | 196 | ### What's Changed 197 | 198 | * Implement Fit::FillMax by @timvandijck in https://github.com/spatie/image/pull/258 199 | 200 | **Full Changelog**: https://github.com/spatie/image/compare/3.6.3...3.6.4 201 | 202 | ## 3.6.3 - 2024-05-24 203 | 204 | **Full Changelog**: https://github.com/spatie/image/compare/3.6.2...3.6.3 205 | 206 | ## 3.6.2 - 2024-05-23 207 | 208 | ### What's Changed 209 | 210 | * Update overview.md by @schmeits in https://github.com/spatie/image/pull/253 211 | * Update saving-images.md by @schmeits in https://github.com/spatie/image/pull/256 212 | * Update introduction.md by @schmeits in https://github.com/spatie/image/pull/255 213 | * Update basic-usage.md by @schmeits in https://github.com/spatie/image/pull/254 214 | * Update composer.json by @thanosalexandris in https://github.com/spatie/image/pull/257 215 | 216 | ### New Contributors 217 | 218 | * @schmeits made their first contribution in https://github.com/spatie/image/pull/253 219 | * @thanosalexandris made their first contribution in https://github.com/spatie/image/pull/257 220 | 221 | **Full Changelog**: https://github.com/spatie/image/compare/3.6.1...3.6.2 222 | 223 | ## 3.6.1 - 2024-05-17 224 | 225 | ### What's Changed 226 | 227 | * Ability to resize canvas when keeping fill background a certain color by @OzanKurt in https://github.com/spatie/image/pull/249 228 | 229 | ### New Contributors 230 | 231 | * @OzanKurt made their first contribution in https://github.com/spatie/image/pull/249 232 | 233 | **Full Changelog**: https://github.com/spatie/image/compare/3.6.0...3.6.1 234 | 235 | ## 3.6.0 - 2024-05-07 236 | 237 | ### What's Changed 238 | 239 | * Bump dependabot/fetch-metadata from 1.6.0 to 2.0.0 by @dependabot in https://github.com/spatie/image/pull/244 240 | * Bump dependabot/fetch-metadata from 2.0.0 to 2.1.0 by @dependabot in https://github.com/spatie/image/pull/251 241 | * Convert Unit and Orientation to BackedEnum by @MrMeshok in https://github.com/spatie/image/pull/252 242 | 243 | ### New Contributors 244 | 245 | * @MrMeshok made their first contribution in https://github.com/spatie/image/pull/252 246 | 247 | **Full Changelog**: https://github.com/spatie/image/compare/3.5.0...3.6.0 248 | 249 | ## 3.5.0 - 2024-04-17 250 | 251 | ### What's Changed 252 | 253 | * Fix saving jfif image by @clementbirkle in https://github.com/spatie/image/pull/248 254 | 255 | ### New Contributors 256 | 257 | * @clementbirkle made their first contribution in https://github.com/spatie/image/pull/248 258 | 259 | **Full Changelog**: https://github.com/spatie/image/compare/3.4.2...3.5.0 260 | 261 | ## 3.4.2 - 2024-04-15 262 | 263 | ### What's Changed 264 | 265 | * Bump aglipanci/laravel-pint-action from 2.3.1 to 2.4 by @dependabot in https://github.com/spatie/image/pull/246 266 | * Suppress warnings from exif_read_data by @glennjacobs in https://github.com/spatie/image/pull/247 267 | * Suppress warnings from imagecreatefromstring by @mattmcdonald-uk in https://github.com/spatie/image/pull/243 268 | 269 | ### New Contributors 270 | 271 | * @glennjacobs made their first contribution in https://github.com/spatie/image/pull/247 272 | * @mattmcdonald-uk made their first contribution in https://github.com/spatie/image/pull/243 273 | 274 | **Full Changelog**: https://github.com/spatie/image/compare/3.4.0...3.4.2 275 | 276 | ## 3.4.1 - 2024-04-15 277 | 278 | ### What's Changed 279 | 280 | * Bump aglipanci/laravel-pint-action from 2.3.1 to 2.4 by @dependabot in https://github.com/spatie/image/pull/246 281 | * Suppress warnings from exif_read_data by @glennjacobs in https://github.com/spatie/image/pull/247 282 | 283 | ### New Contributors 284 | 285 | * @glennjacobs made their first contribution in https://github.com/spatie/image/pull/247 286 | 287 | **Full Changelog**: https://github.com/spatie/image/compare/3.4.0...3.4.1 288 | 289 | ## 3.4.0 - 2024-03-05 290 | 291 | ### What's Changed 292 | 293 | * Bump ramsey/composer-install from 2 to 3 by @dependabot in https://github.com/spatie/image/pull/241 294 | * Add a ->text method to the package by @riasvdv in https://github.com/spatie/image/pull/242 295 | 296 | ### New Contributors 297 | 298 | * @riasvdv made their first contribution in https://github.com/spatie/image/pull/242 299 | 300 | **Full Changelog**: https://github.com/spatie/image/compare/3.3.8...3.4.0 301 | 302 | ## 3.3.8 - 2024-03-04 303 | 304 | ### What's Changed 305 | 306 | * Allow lossless saving of Webp with the GD driver by @fabio-ivona in https://github.com/spatie/image/pull/240 307 | 308 | ### New Contributors 309 | 310 | * @fabio-ivona made their first contribution in https://github.com/spatie/image/pull/240 311 | 312 | **Full Changelog**: https://github.com/spatie/image/compare/3.3.7...3.3.8 313 | 314 | ## 3.3.7 - 2024-03-01 315 | 316 | ### What's Changed 317 | 318 | * Fix for Fit::Max not behaving as described in the documentation by @RaBic in https://github.com/spatie/image/pull/239 319 | 320 | ### New Contributors 321 | 322 | * @RaBic made their first contribution in https://github.com/spatie/image/pull/239 323 | 324 | **Full Changelog**: https://github.com/spatie/image/compare/3.3.6...3.3.7 325 | 326 | ## 3.3.6 - 2024-02-26 327 | 328 | ### What's Changed 329 | 330 | * Bugfix for cases where setting a quality did nothing https://github.com/spatie/image/pull/237 331 | * Bugfix for transparent PNG's not staying transparent when working with GD. 332 | * Autorotate images based on their Exif data https://github.com/spatie/image/pull/238 333 | 334 | **Full Changelog**: https://github.com/spatie/image/compare/3.3.5...3.3.6 335 | 336 | ## 3.3.5 - 2024-02-16 337 | 338 | ### What's Changed 339 | 340 | * Update adding-a-watermark.md by @yoeriboven in https://github.com/spatie/image/pull/235 341 | * Manipulating animated GIF's is now supported using Imagick. by @jhorie in https://github.com/spatie/image/pull/234 342 | 343 | ### New Contributors 344 | 345 | * @yoeriboven made their first contribution in https://github.com/spatie/image/pull/235 346 | * @jhorie made their first contribution in https://github.com/spatie/image/pull/234 347 | 348 | **Full Changelog**: https://github.com/spatie/image/compare/3.3.4...3.3.5 349 | 350 | ## 3.3.4 - 2024-01-15 351 | 352 | ### What's Changed 353 | 354 | * Crop issue fixes by @timvandijck in https://github.com/spatie/image/pull/230 355 | 356 | **Full Changelog**: https://github.com/spatie/image/compare/3.3.3...3.3.4 357 | 358 | ## 3.3.3 - 2024-01-05 359 | 360 | ### What's Changed 361 | 362 | * Fix broken fit/crop operation. 363 | * Bump aglipanci/laravel-pint-action from 2.3.0 to 2.3.1 by @dependabot in https://github.com/spatie/image/pull/224 364 | * Fix enum case by @lukasleitsch in https://github.com/spatie/image/pull/226 365 | * update load image with gd driver example by @lukasleitsch in https://github.com/spatie/image/pull/227 366 | 367 | ### New Contributors 368 | 369 | * @lukasleitsch made their first contribution in https://github.com/spatie/image/pull/226 370 | 371 | **Full Changelog**: https://github.com/spatie/image/compare/3.3.2...3.3.3 372 | 373 | ## 3.3.2 - 2023-12-24 374 | 375 | ### What's Changed 376 | 377 | * move ext requirement to dev by @ariaieboy in https://github.com/spatie/image/pull/222 378 | 379 | **Full Changelog**: https://github.com/spatie/image/compare/3.3.1...3.3.2 380 | 381 | ## 3.3.1 - 2023-12-22 382 | 383 | ### What's Changed 384 | 385 | * update GD driver by @ariaieboy in https://github.com/spatie/image/pull/221 386 | 387 | **Full Changelog**: https://github.com/spatie/image/compare/3.3.0...3.3.1 388 | 389 | ## 3.3.0 - 2023-12-21 390 | 391 | - add watermark method 392 | 393 | ## 3.2.0 - 2023-12-18 394 | 395 | ### What's Changed 396 | 397 | * Bump actions/checkout from 3 to 4 by @dependabot in https://github.com/spatie/image/pull/210 398 | * Add `OptimizerChain::setTimeout()` to the image optimization tests and docs by @golubev in https://github.com/spatie/image/pull/208 399 | * Image load fix by @kbond in https://github.com/spatie/image/pull/214 400 | 401 | ### New Contributors 402 | 403 | * @golubev made their first contribution in https://github.com/spatie/image/pull/208 404 | * @kbond made their first contribution in https://github.com/spatie/image/pull/214 405 | 406 | **Full Changelog**: https://github.com/spatie/image/compare/3.1.0...3.2.0 407 | 408 | ## 3.1.0 - 2023-12-15 409 | 410 | ### What's Changed 411 | 412 | * add Avif Support by @ariaieboy in https://github.com/spatie/image/pull/207 413 | 414 | **Full Changelog**: https://github.com/spatie/image/compare/3.0.0...3.1.0 415 | 416 | ## 3.0.0 - 2023-12-14 417 | 418 | ### What's Changed 419 | 420 | - do not rely on Glide anymore 421 | 422 | ### New Contributors 423 | 424 | * @erikn69 made their first contribution in https://github.com/spatie/image/pull/197 425 | * @timvandijck made their first contribution in https://github.com/spatie/image/pull/199 426 | 427 | **Full Changelog**: https://github.com/spatie/image/compare/2.2.7...3.0.0 428 | 429 | ## 2.2.7 - 2023-07-24 430 | 431 | - bump requirements 432 | 433 | ## 2.2.6 - 2023-05-06 434 | 435 | ### What's Changed 436 | 437 | - Bump dependabot/fetch-metadata from 1.3.5 to 1.3.6 by @dependabot in https://github.com/spatie/image/pull/185 438 | - Bump dependabot/fetch-metadata from 1.3.6 to 1.4.0 by @dependabot in https://github.com/spatie/image/pull/188 439 | - Fit with only width or height by @gdebrauwer in https://github.com/spatie/image/pull/190 440 | 441 | ### New Contributors 442 | 443 | - @dependabot made their first contribution in https://github.com/spatie/image/pull/185 444 | - @gdebrauwer made their first contribution in https://github.com/spatie/image/pull/190 445 | 446 | **Full Changelog**: https://github.com/spatie/image/compare/2.2.5...2.2.6 447 | 448 | ## 2.2.5 - 2023-01-19 449 | 450 | ### What's Changed 451 | 452 | - Refactor tests to pest by @AyoobMH in https://github.com/spatie/image/pull/176 453 | - Add Dependabot Automation by @patinthehat in https://github.com/spatie/image/pull/177 454 | - Add PHP 8.2 Support by @patinthehat in https://github.com/spatie/image/pull/180 455 | - Update Dependabot Automation by @patinthehat in https://github.com/spatie/image/pull/181 456 | - Add fill-max fit mode by @Tofandel in https://github.com/spatie/image/pull/183 457 | 458 | ### New Contributors 459 | 460 | - @AyoobMH made their first contribution in https://github.com/spatie/image/pull/176 461 | - @patinthehat made their first contribution in https://github.com/spatie/image/pull/177 462 | - @Tofandel made their first contribution in https://github.com/spatie/image/pull/183 463 | 464 | **Full Changelog**: https://github.com/spatie/image/compare/2.2.4...2.2.5 465 | 466 | ## 2.2.4 - 2022-08-09 467 | 468 | ### What's Changed 469 | 470 | - Add zero orientation support ignoring EXIF by @danielcastrobalbi in https://github.com/spatie/image/pull/171 471 | 472 | ### New Contributors 473 | 474 | - @danielcastrobalbi made their first contribution in https://github.com/spatie/image/pull/171 475 | 476 | **Full Changelog**: https://github.com/spatie/image/compare/2.2.3...2.2.4 477 | 478 | ## 2.2.3 - 2022-05-21 479 | 480 | ## What's Changed 481 | 482 | - Fix permission issue with temporary directory by @sebastianpopp in https://github.com/spatie/image/pull/163 483 | 484 | ## New Contributors 485 | 486 | - @sebastianpopp made their first contribution in https://github.com/spatie/image/pull/163 487 | 488 | **Full Changelog**: https://github.com/spatie/image/compare/2.2.2...2.2.3 489 | 490 | ## 2.2.2 - 2022-02-22 491 | 492 | - add TIFF support 493 | 494 | ## 1.11.0 - 2022-02-21 495 | 496 | ## What's Changed 497 | 498 | - Fix docs link by @pascalbaljet in https://github.com/spatie/image/pull/154 499 | - Update .gitattributes by @PaolaRuby in https://github.com/spatie/image/pull/158 500 | - Add TIFF support by @Synchro in https://github.com/spatie/image/pull/159 501 | 502 | ## New Contributors 503 | 504 | - @PaolaRuby made their first contribution in https://github.com/spatie/image/pull/158 505 | 506 | **Full Changelog**: https://github.com/spatie/image/compare/2.2.1...1.11.0 507 | 508 | ## 2.2.1 - 2021-12-17 509 | 510 | ## What's Changed 511 | 512 | - Use match expression in convertToGlideParameter method by @mohprilaksono in https://github.com/spatie/image/pull/149 513 | - [REF] updated fit docs description by @JeremyRed in https://github.com/spatie/image/pull/150 514 | - Adding compatibility to Symfony 6 by @spackmat in https://github.com/spatie/image/pull/152 515 | 516 | ## New Contributors 517 | 518 | - @mohprilaksono made their first contribution in https://github.com/spatie/image/pull/149 519 | - @JeremyRed made their first contribution in https://github.com/spatie/image/pull/150 520 | - @spackmat made their first contribution in https://github.com/spatie/image/pull/152 521 | 522 | **Full Changelog**: https://github.com/spatie/image/compare/2.2.0...2.2.1 523 | 524 | ## 2.2.0 - 2021-10-31 525 | 526 | - add avif support (#148) 527 | 528 | ## 2.1.0 - 2021-07-15 529 | 530 | - Drop support for PHP 7 531 | - Make codebase more strict with type hinting 532 | 533 | ## 2.0.0 - 2021-07-15 534 | 535 | - Bump league/glide to v2 [#134](https://github.com/spatie/image/pull/134) 536 | 537 | ## 1.10.4 - 2021-04-07 538 | 539 | - Allow spatie/temporary-directory v2 540 | 541 | ## 1.10.3 - 2021-03-10 542 | 543 | - Bump league/glide to 2.0 [#123](https://github.com/spatie/image/pull/123) 544 | 545 | ## 1.10.2 - 2020-01-26 546 | 547 | - change condition to delete $conversionResultDirectory (#118) 548 | 549 | ## 1.10.1 - 2020-12-27 550 | 551 | - adds zoom option to focalCrop (#112) 552 | 553 | ## 1.9.0 - 2020-11-13 554 | 555 | - allow usage of a custom `OptimizerChain` #110 556 | 557 | ## 1.8.1 - 2020-11-12 558 | 559 | - revert changes from 1.8.0 560 | 561 | ## 1.8.0 - 2020-11-12 562 | 563 | - allow usage of a custom `OptimizerChain` (#108) 564 | 565 | ## 1.7.7 - 2020-11-12 566 | 567 | - add support for PHP 8 568 | 569 | ## 1.7.6 - 2020-01-26 570 | 571 | - change uppercase function to mb_strtoupper instead of strtoupper (#99) 572 | 573 | ## 1.7.5 - 2019-11-23 574 | 575 | - allow symfony 5 components 576 | 577 | ## 1.7.4 - 2019-08-28 578 | 579 | - do not export docs 580 | 581 | ## 1.7.3 - 2019-08-03 582 | 583 | - fix duplicated files (fixes #84) 584 | 585 | ## 1.7.2 - 2019-05-13 586 | 587 | - fixes `optimize()` when used with `apply()` (#78) 588 | 589 | ## 1.7.1 - 2019-04-17 590 | 591 | - change GlideConversion sequence (#76) 592 | 593 | ## 1.7.0 - 2019-02-22 594 | 595 | - add support for `webp` 596 | 597 | ## 1.6.0 - 2019-01-27 598 | 599 | - add `setTemporaryDirectory` 600 | 601 | ## 1.5.3 - 2019-01-10 602 | 603 | - update lower deps 604 | 605 | ## 1.5.2 - 2018-05-05 606 | 607 | - fix exception message 608 | 609 | ## 1.5.1 - 2018-04-18 610 | 611 | - Prevent error when trying to remove `/tmp` 612 | 613 | ## 1.5.0 - 2018-04-13 614 | 615 | - add `flip` 616 | 617 | ## 1.4.2 - 2018-04-11 618 | 619 | - Use the correct driver for getting widths and height of images. 620 | 621 | ## 1.4.1 - 2018-02-08 622 | 623 | - Support symfony ^4.0 624 | - Support phpunit ^7.0 625 | 626 | ## 1.4.0 - 2017-12-05 627 | 628 | - add `getWidth` and `getHeight` 629 | 630 | ## 1.3.5 - 2017-12-04 631 | 632 | - fix for problems when creating directories in the temporary directory 633 | 634 | ## 1.3.4 - 2017-07-25 635 | 636 | - fix `optimize` docblock 637 | 638 | ## 1.3.3 - 2017-07-11 639 | 640 | - make `optimize` method fluent 641 | 642 | ## 1.3.2 - 2017-07-05 643 | 644 | - swap out underlying optimization package 645 | 646 | ## 1.3.1 - 2017-07-02 647 | 648 | - internally treat `optimize` as a manipulation 649 | 650 | ## 1.3.0 - 2017-07-02 651 | 652 | - add `optimize` method 653 | 654 | ## 1.2.1 - 2017-06-29 655 | 656 | - add methods to determine emptyness to `Manipulations` and `ManipulationSequence` 657 | 658 | ## 1.2.0 - 2017-04-17 659 | 660 | - allow `Manipulations` to be constructed with an array of arrays 661 | 662 | ## 1.1.3 - 2017-04-07 663 | 664 | - improve support for multi-volume systems 665 | 666 | ## 1.1.2 - 2017-04-04 667 | 668 | - remove conversion directory after converting image 669 | 670 | ## 1.1.1 - 2017-03-17 671 | 672 | - avoid processing empty manipulations groups 673 | 674 | ## 1.1.0 - 2017-02-06 675 | 676 | - added support for watermarks 677 | 678 | ## 1.0.0 - 2017-02-06 679 | 680 | - initial release 681 | -------------------------------------------------------------------------------- /src/Drivers/Imagick/ImagickDriver.php: -------------------------------------------------------------------------------- 1 | newImage($width, $height, $color->getPixel(), 'png'); 56 | $image->setType(Imagick::IMGTYPE_UNDEFINED); 57 | $image->setImageType(Imagick::IMGTYPE_UNDEFINED); 58 | $image->setColorspace(Imagick::COLORSPACE_UNDEFINED); 59 | 60 | return (new static)->setImage($image); 61 | } 62 | 63 | protected function setImage(Imagick $image): static 64 | { 65 | $this->image = $image; 66 | 67 | return $this; 68 | } 69 | 70 | public function image(): Imagick 71 | { 72 | return $this->image; 73 | } 74 | 75 | public function loadFile(string $path, bool $autoRotate = true): static 76 | { 77 | $this->originalPath = $path; 78 | 79 | $this->optimize = false; 80 | 81 | $this->image = new Imagick($path); 82 | $this->exif = $this->image->getImageProperties('exif:*'); 83 | 84 | if ($autoRotate) { 85 | $this->autoRotate(); 86 | } 87 | 88 | if ($this->isAnimated()) { 89 | $this->image = $this->image->coalesceImages(); 90 | } 91 | 92 | return $this; 93 | } 94 | 95 | protected function isAnimated(): bool 96 | { 97 | return count($this->image) > 1; 98 | } 99 | 100 | public function getWidth(): int 101 | { 102 | return $this->image->getImageWidth(); 103 | } 104 | 105 | public function getHeight(): int 106 | { 107 | return $this->image->getImageHeight(); 108 | } 109 | 110 | public function brightness(int $brightness): static 111 | { 112 | foreach ($this->image as $image) { 113 | $image->modulateImage(100 + $brightness, 100, 100); 114 | } 115 | 116 | return $this; 117 | } 118 | 119 | public function blur(int $blur): static 120 | { 121 | foreach ($this->image as $image) { 122 | $image->blurImage(0.5 * $blur, 0.1 * $blur); 123 | } 124 | 125 | return $this; 126 | } 127 | 128 | public function fit( 129 | Fit $fit, 130 | ?int $desiredWidth = null, 131 | ?int $desiredHeight = null, 132 | bool $relative = false, 133 | ?string $backgroundColor = null, 134 | ): static { 135 | if ($fit === Fit::Crop) { 136 | return $this->fitCrop($fit, $this->getWidth(), $this->getHeight(), $desiredWidth, $desiredHeight); 137 | } 138 | 139 | if ($fit === Fit::FillMax) { 140 | if (is_null($desiredWidth) || is_null($desiredHeight)) { 141 | throw new MissingParameter('Both desiredWidth and desiredHeight must be set when using Fit::FillMax'); 142 | } 143 | 144 | if (is_null($backgroundColor)) { 145 | throw new MissingParameter('backgroundColor must be set when using Fit::FillMax'); 146 | } 147 | 148 | return $this->fitFillMax($desiredWidth, $desiredHeight, $backgroundColor); 149 | } 150 | 151 | $calculatedSize = $fit->calculateSize( 152 | $this->getWidth(), 153 | $this->getHeight(), 154 | $desiredWidth, 155 | $desiredHeight 156 | ); 157 | 158 | foreach ($this->image as $image) { 159 | $image->scaleImage($calculatedSize->width, $calculatedSize->height); 160 | } 161 | 162 | if ($fit->shouldResizeCanvas()) { 163 | $this->resizeCanvas($desiredWidth, $desiredHeight, AlignPosition::Center, $relative, $backgroundColor); 164 | } 165 | 166 | return $this; 167 | } 168 | 169 | public function fitFillMax(int $desiredWidth, int $desiredHeight, string $backgroundColor, bool $relative = false): static 170 | { 171 | $this->resize($desiredWidth, $desiredHeight, [Constraint::PreserveAspectRatio]); 172 | $this->resizeCanvas($desiredWidth, $desiredHeight, AlignPosition::Center, $relative, $backgroundColor); 173 | 174 | return $this; 175 | } 176 | 177 | public function resizeCanvas( 178 | ?int $width = null, 179 | ?int $height = null, 180 | ?AlignPosition $position = null, 181 | bool $relative = false, 182 | ?string $backgroundColor = null 183 | ): static { 184 | $position ??= AlignPosition::Center; 185 | 186 | $originalWidth = $this->getWidth(); 187 | $originalHeight = $this->getHeight(); 188 | 189 | $width ??= $originalWidth; 190 | $height ??= $originalHeight; 191 | 192 | if ($relative) { 193 | $width = $originalWidth + $width; 194 | $height = $originalHeight + $height; 195 | } 196 | 197 | $width = $width <= 0 198 | ? $width + $originalWidth 199 | : $width; 200 | 201 | $height = $height <= 0 202 | ? $height + $originalHeight 203 | : $height; 204 | 205 | $canvas = $this->new($width, $height, $backgroundColor); 206 | 207 | $canvasSize = $canvas->getSize()->align($position); 208 | $imageSize = $this->getSize()->align($position); 209 | $canvasPosition = $imageSize->relativePosition($canvasSize); 210 | $imagePosition = $canvasSize->relativePosition($imageSize); 211 | 212 | if ($width <= $originalWidth) { 213 | $destinationX = 0; 214 | $sourceX = $canvasPosition->x; 215 | $sourceWidth = $canvasSize->width; 216 | } else { 217 | $destinationX = $imagePosition->x; 218 | $sourceX = 0; 219 | $sourceWidth = $originalWidth; 220 | } 221 | 222 | if ($height <= $originalHeight) { 223 | $destinationY = 0; 224 | $sourceY = $canvasPosition->y; 225 | $sourceHeight = $canvasSize->height; 226 | } else { 227 | $destinationY = $imagePosition->y; 228 | $sourceY = 0; 229 | $sourceHeight = $originalHeight; 230 | } 231 | 232 | // make image area transparent to keep transparency 233 | // even if background-color is set 234 | $rect = new ImagickDraw; 235 | $fill = $canvas->pickColor(0, 0, ColorFormat::Hex); 236 | $fill = $fill === '#ff0000' ? '#00ff00' : '#ff0000'; 237 | $rect->setFillColor($fill); 238 | $rect->rectangle($destinationX, $destinationY, $destinationX + $sourceWidth - 1, $destinationY + $sourceHeight - 1); 239 | $canvas->image->drawImage($rect); 240 | $canvas->image->transparentPaintImage($fill, 0, 0, false); 241 | 242 | foreach ($this->image->getImageProfiles() as $key => $value) { 243 | $canvas->image->setImageProfile($key, $value); 244 | } 245 | $canvas->image->setImageColorspace($this->image->getImageColorspace()); 246 | 247 | // copy image into new canvas 248 | $this->image->cropImage($sourceWidth, $sourceHeight, $sourceX, $sourceY); 249 | $canvas->image->compositeImage($this->image, Imagick::COMPOSITE_DEFAULT, $destinationX, $destinationY); 250 | $canvas->image->setImagePage(0, 0, 0, 0); 251 | 252 | // set new core to canvas 253 | $this->image = $canvas->image; 254 | 255 | return $this; 256 | } 257 | 258 | public function pickColor(int $x, int $y, ColorFormat $colorFormat): mixed 259 | { 260 | $color = new ImagickColor($this->image->getImagePixelColor($x, $y)); 261 | 262 | return $color->format($colorFormat); 263 | } 264 | 265 | public function save(?string $path = null): static 266 | { 267 | if (! $path) { 268 | $path = $this->originalPath; 269 | } 270 | if (is_null($this->format)) { 271 | $format = pathinfo($path, PATHINFO_EXTENSION); 272 | } else { 273 | $format = $this->format; 274 | } 275 | $formats = Imagick::queryFormats('*'); 276 | if (in_array('JPEG', $formats)) { 277 | $formats[] = 'JFIF'; 278 | } 279 | if (! in_array(strtoupper($format), $formats)) { 280 | throw UnsupportedImageFormat::make($format); 281 | } 282 | 283 | foreach ($this->image as $image) { 284 | if (strtoupper($format) === 'JFIF') { 285 | $image->setFormat('JPEG'); 286 | } else { 287 | $image->setFormat($format); 288 | } 289 | } 290 | 291 | if ($this->isAnimated()) { 292 | $image = $this->image->deconstructImages(); 293 | $image->writeImages($path, true); 294 | } else { 295 | $this->image->writeImage($path); 296 | } 297 | 298 | if ($this->optimize) { 299 | $this->optimizerChain->optimize($path); 300 | } 301 | $this->format = null; 302 | 303 | return $this; 304 | } 305 | 306 | public function base64(string $imageFormat = 'jpeg', bool $prefixWithFormat = true): string 307 | { 308 | $image = clone $this->image; 309 | $image->setFormat($imageFormat); 310 | 311 | if ($prefixWithFormat) { 312 | return 'data:image/'.$imageFormat.';base64,'.base64_encode($image->getImageBlob()); 313 | } 314 | 315 | return base64_encode($image->getImageBlob()); 316 | } 317 | 318 | public function driverName(): string 319 | { 320 | return 'imagick'; 321 | } 322 | 323 | public function getSize(): Size 324 | { 325 | return new Size($this->getWidth(), $this->getHeight()); 326 | } 327 | 328 | public function gamma(float $gamma): static 329 | { 330 | foreach ($this->image as $image) { 331 | $image->gammaImage($gamma); 332 | } 333 | 334 | return $this; 335 | } 336 | 337 | public function contrast(float $level): static 338 | { 339 | foreach ($this->image as $image) { 340 | $image->brightnessContrastImage(1, $level); 341 | } 342 | 343 | return $this; 344 | } 345 | 346 | public function colorize(int $red, int $green, int $blue): static 347 | { 348 | $quantumRange = $this->image->getQuantumRange(); 349 | 350 | $red = Helpers::normalizeColorizeLevel($red); 351 | $green = Helpers::normalizeColorizeLevel($green); 352 | $blue = Helpers::normalizeColorizeLevel($blue); 353 | 354 | foreach ($this->image as $image) { 355 | $image->levelImage(0, $red, $quantumRange['quantumRangeLong'], Imagick::CHANNEL_RED); 356 | $image->levelImage(0, $green, $quantumRange['quantumRangeLong'], Imagick::CHANNEL_GREEN); 357 | $image->levelImage(0, $blue, $quantumRange['quantumRangeLong'], Imagick::CHANNEL_BLUE); 358 | } 359 | 360 | return $this; 361 | } 362 | 363 | public function greyscale(): static 364 | { 365 | foreach ($this->image as $image) { 366 | $image->modulateImage(100, 0, 100); 367 | } 368 | 369 | return $this; 370 | } 371 | 372 | public function manualCrop(int $width, int $height, ?int $x = null, ?int $y = null): static 373 | { 374 | $cropped = new Size($width, $height); 375 | $position = new Point($x ?? 0, $y ?? 0); 376 | 377 | if (is_null($x) && is_null($y)) { 378 | $position = $this 379 | ->getSize() 380 | ->align(AlignPosition::Center) 381 | ->relativePosition($cropped->align(AlignPosition::Center)); 382 | } 383 | 384 | foreach ($this->image as $image) { 385 | $image->cropImage($cropped->width, $cropped->height, $position->x, $position->y); 386 | $image->setImagePage(0, 0, 0, 0); 387 | } 388 | 389 | return $this; 390 | } 391 | 392 | public function crop(int $width, int $height, CropPosition $position = CropPosition::Center): static 393 | { 394 | [$offsetX, $offsetY] = $this->calculateCropOffsets($width, $height, $position); 395 | 396 | return $this->manualCrop($width, $height, $offsetX, $offsetY); 397 | } 398 | 399 | public function focalCrop(int $width, int $height, ?int $cropCenterX = null, ?int $cropCenterY = null): static 400 | { 401 | [$width, $height, $cropCenterX, $cropCenterY] = $this->calculateFocalCropCoordinates( 402 | $width, 403 | $height, 404 | $cropCenterX, 405 | $cropCenterY 406 | ); 407 | 408 | $this->manualCrop($width, $height, $cropCenterX, $cropCenterY); 409 | 410 | return $this; 411 | } 412 | 413 | public function focalCropAndResize(int $width, int $height, ?int $cropCenterX = null, ?int $cropCenterY = null): static 414 | { 415 | [$cropWidth, $cropHeight, $cropX, $cropY] = $this->calculateFocalCropAndResizeCoordinates( 416 | $width, 417 | $height, 418 | $cropCenterX, 419 | $cropCenterY 420 | ); 421 | 422 | $this->manualCrop($cropWidth, $cropHeight, $cropX, $cropY) 423 | ->width($width) 424 | ->height($height); 425 | 426 | return $this; 427 | } 428 | 429 | public function sepia(): static 430 | { 431 | return $this 432 | ->greyscale() 433 | ->brightness(-40) 434 | ->contrast(20) 435 | ->colorize(50, 35, 20) 436 | ->brightness(-10) 437 | ->contrast(10); 438 | } 439 | 440 | public function sharpen(float $amount): static 441 | { 442 | foreach ($this->image as $image) { 443 | $image->unsharpMaskImage(1, 1, $amount / 6.25, 0); 444 | } 445 | 446 | return $this; 447 | } 448 | 449 | public function background(string $color): static 450 | { 451 | $background = $this->new($this->getWidth(), $this->getHeight(), $color); 452 | 453 | $this->overlay($background, $this, 0, 0); 454 | 455 | return $this; 456 | } 457 | 458 | public function overlay(ImageDriver $bottomImage, ImageDriver $topImage, int $x = 0, int $y = 0): static 459 | { 460 | $bottomImage->insert($topImage, AlignPosition::Center, $x, $y); 461 | $this->image = $bottomImage->image(); 462 | 463 | return $this; 464 | } 465 | 466 | public function orientation(?Orientation $orientation = null): static 467 | { 468 | if (is_null($orientation)) { 469 | $orientation = $this->getOrientationFromExif($this->exif); 470 | } 471 | 472 | foreach ($this->image as $image) { 473 | $image->rotateImage(new ImagickPixel('none'), $orientation->degrees()); 474 | } 475 | 476 | return $this; 477 | } 478 | 479 | /** 480 | * @return array 481 | */ 482 | public function exif(): array 483 | { 484 | return $this->exif; 485 | } 486 | 487 | public function flip(FlipDirection $flip): static 488 | { 489 | foreach ($this->image as $image) { 490 | switch ($flip) { 491 | case FlipDirection::Vertical: 492 | $image->flipImage(); 493 | break; 494 | case FlipDirection::Horizontal: 495 | $image->flopImage(); 496 | break; 497 | case FlipDirection::Both: 498 | $image->flipImage(); 499 | $image->flopImage(); 500 | break; 501 | } 502 | } 503 | 504 | return $this; 505 | } 506 | 507 | public function pixelate(int $pixelate = 50): static 508 | { 509 | if ($pixelate === 0) { 510 | return $this; 511 | } 512 | 513 | $width = $this->getWidth(); 514 | $height = $this->getHeight(); 515 | 516 | foreach ($this->image as $image) { 517 | $image->scaleImage(max(1, (int) ($width / $pixelate)), max(1, (int) ($height / $pixelate))); 518 | $image->scaleImage($width, $height); 519 | } 520 | 521 | return $this; 522 | } 523 | 524 | public function insert( 525 | ImageDriver|string $otherImage, 526 | AlignPosition $position = AlignPosition::Center, 527 | int $x = 0, 528 | int $y = 0, 529 | int $alpha = 100 530 | ): static { 531 | $this->ensureNumberBetween($alpha, 0, 100, 'alpha'); 532 | if (is_string($otherImage)) { 533 | $otherImage = (new static)->loadFile($otherImage); 534 | } 535 | 536 | $otherImage->image()->setImageOrientation(Imagick::ORIENTATION_UNDEFINED); 537 | $otherImage->image()->evaluateImage(Imagick::EVALUATE_DIVIDE, (1 / ($alpha / 100)), Imagick::CHANNEL_ALPHA); 538 | 539 | $imageSize = $this->getSize()->align($position, $x, $y); 540 | $watermarkSize = $otherImage->getSize()->align($position); 541 | $target = $imageSize->relativePosition($watermarkSize); 542 | 543 | foreach ($this->image as $image) { 544 | $image->compositeImage( 545 | $otherImage->image(), 546 | Imagick::COMPOSITE_OVER, 547 | $target->x, 548 | $target->y 549 | ); 550 | } 551 | 552 | return $this; 553 | } 554 | 555 | public function resize(int $width, int $height, array $constraints = []): static 556 | { 557 | $resized = $this->getSize()->resize($width, $height, $constraints); 558 | 559 | foreach ($this->image as $image) { 560 | $image->scaleImage($resized->width, $resized->height); 561 | } 562 | 563 | return $this; 564 | } 565 | 566 | public function width(int $width, array $constraints = [Constraint::PreserveAspectRatio]): static 567 | { 568 | $newHeight = (int) round($width / $this->getSize()->aspectRatio()); 569 | 570 | $this->resize($width, $newHeight, $constraints); 571 | 572 | return $this; 573 | } 574 | 575 | public function height(int $height, array $constraints = [Constraint::PreserveAspectRatio]): static 576 | { 577 | $newWidth = (int) round($height * $this->getSize()->aspectRatio()); 578 | 579 | $this->resize($newWidth, $height, $constraints); 580 | 581 | return $this; 582 | } 583 | 584 | public function border(int $width, BorderType $type, string $color = '000000'): static 585 | { 586 | if ($type === BorderType::Shrink) { 587 | $originalWidth = $this->getWidth(); 588 | $originalHeight = $this->getHeight(); 589 | 590 | $this 591 | ->resize( 592 | (int) round($this->getWidth() - ($width * 2)), 593 | (int) round($this->getHeight() - ($width * 2)), 594 | [Constraint::PreserveAspectRatio], 595 | ) 596 | ->resizeCanvas( 597 | $originalWidth, 598 | $originalHeight, 599 | AlignPosition::Center, 600 | false, 601 | $color, 602 | ); 603 | 604 | return $this; 605 | } 606 | 607 | if ($type === BorderType::Expand) { 608 | $this->resizeCanvas( 609 | (int) round($width * 2), 610 | (int) round($width * 2), 611 | AlignPosition::Center, 612 | true, 613 | $color, 614 | ); 615 | 616 | return $this; 617 | } 618 | 619 | if ($type === BorderType::Overlay) { 620 | $shape = new ImagickDraw; 621 | 622 | $backgroundColor = new ImagickColor; 623 | $shape->setFillColor($backgroundColor->getPixel()); 624 | 625 | $borderColor = new ImagickColor($color); 626 | $shape->setStrokeColor($borderColor->getPixel()); 627 | 628 | $shape->setStrokeWidth($width); 629 | 630 | $shape->rectangle( 631 | (int) round($width / 2), 632 | (int) round($width / 2), 633 | (int) round($this->getWidth() - ($width / 2)), 634 | (int) round($this->getHeight() - ($width / 2)), 635 | ); 636 | 637 | foreach ($this->image as $image) { 638 | $image->drawImage($shape); 639 | } 640 | 641 | return $this; 642 | } 643 | } 644 | 645 | public function quality(int $quality): static 646 | { 647 | foreach ($this->image as $image) { 648 | $this->image->setImageCompressionQuality($quality); 649 | $this->image->setCompressionQuality(100 - $quality); // For PNGs 650 | } 651 | 652 | return $this; 653 | } 654 | 655 | public function format(string $format): static 656 | { 657 | $this->format = $format; 658 | 659 | return $this; 660 | } 661 | 662 | public function autoRotate(): void 663 | { 664 | switch ($this->image->getImageOrientation()) { 665 | case Imagick::ORIENTATION_TOPLEFT: 666 | break; 667 | case Imagick::ORIENTATION_TOPRIGHT: 668 | $this->image->flopImage(); 669 | break; 670 | case Imagick::ORIENTATION_BOTTOMRIGHT: 671 | $this->image->rotateImage('#000', 180); 672 | break; 673 | case Imagick::ORIENTATION_BOTTOMLEFT: 674 | $this->image->flopImage(); 675 | $this->image->rotateImage('#000', 180); 676 | break; 677 | case Imagick::ORIENTATION_LEFTTOP: 678 | $this->image->flopImage(); 679 | $this->image->rotateImage('#000', -90); 680 | break; 681 | case Imagick::ORIENTATION_RIGHTTOP: 682 | $this->image->rotateImage('#000', 90); 683 | break; 684 | case Imagick::ORIENTATION_RIGHTBOTTOM: 685 | $this->image->flopImage(); 686 | $this->image->rotateImage('#000', 90); 687 | break; 688 | case Imagick::ORIENTATION_LEFTBOTTOM: 689 | $this->image->rotateImage('#000', -90); 690 | break; 691 | default: // Invalid orientation 692 | break; 693 | } 694 | 695 | $this->image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT); 696 | } 697 | 698 | public function text( 699 | string $text, 700 | int $fontSize, 701 | string $color = '000000', 702 | int $x = 0, 703 | int $y = 0, 704 | int $angle = 0, 705 | string $fontPath = '', 706 | int $width = 0, 707 | ): static { 708 | if ($fontPath && ! file_exists($fontPath)) { 709 | throw InvalidFont::make($fontPath); 710 | } 711 | 712 | $textColor = new ImagickColor($color); 713 | 714 | $draw = new ImagickDraw; 715 | $draw->setFillColor($textColor->getPixel()); 716 | $draw->setFontSize($fontSize); 717 | if ($fontPath) { 718 | $draw->setFont($fontPath); 719 | } 720 | 721 | $this->image->annotateImage( 722 | $draw, 723 | $x, 724 | $y, 725 | $angle, 726 | $width > 0 727 | ? $this->wrapText($text, $fontSize, $fontPath, $angle, $width) 728 | : $text, 729 | ); 730 | 731 | return $this; 732 | } 733 | 734 | public function wrapText(string $text, int $fontSize, string $fontPath = '', int $angle = 0, int $width = 0): string 735 | { 736 | if ($fontPath && ! file_exists($fontPath)) { 737 | throw InvalidFont::make($fontPath); 738 | } 739 | 740 | $wrapped = ''; 741 | $words = explode(' ', $text); 742 | 743 | foreach ($words as $word) { 744 | $teststring = "{$wrapped} {$word}"; 745 | 746 | $draw = new ImagickDraw; 747 | if ($fontPath) { 748 | $draw->setFont($fontPath); 749 | } 750 | $draw->setFontSize($fontSize); 751 | 752 | $metrics = (new Imagick)->queryFontMetrics($draw, $teststring); 753 | 754 | if ($metrics['textWidth'] > $width) { 755 | $wrapped .= "\n".$word; 756 | } else { 757 | $wrapped .= ' '.$word; 758 | } 759 | } 760 | 761 | return $wrapped; 762 | } 763 | } 764 | -------------------------------------------------------------------------------- /src/Drivers/Gd/GdDriver.php: -------------------------------------------------------------------------------- 1 | */ 48 | protected array $exif = []; 49 | 50 | protected int $quality = -1; 51 | 52 | protected string $originalPath; 53 | 54 | public function new(int $width, int $height, ?string $backgroundColor = null): static 55 | { 56 | $image = imagecreatetruecolor($width, $height); 57 | 58 | if (! $image) { 59 | throw new Exception('Could not create image'); 60 | } 61 | 62 | $backgroundColor = new GdColor($backgroundColor); 63 | 64 | imagefill($image, 0, 0, $backgroundColor->getInt()); 65 | 66 | return (new static)->setImage($image); 67 | } 68 | 69 | protected function setImage(GdImage $image): static 70 | { 71 | $this->image = $image; 72 | 73 | return $this; 74 | } 75 | 76 | public function loadFile(string $path, bool $autoRotate = true): static 77 | { 78 | $this->optimize = false; 79 | $this->quality = -1; 80 | $this->originalPath = $path; 81 | 82 | $contents = is_file($path) && filesize($path) > 0 83 | ? file_get_contents($path) 84 | : ''; 85 | 86 | $this->setExif($path); 87 | 88 | try { 89 | $image = imagecreatefromstring($contents); 90 | } catch (Throwable $throwable) { 91 | throw CouldNotLoadImage::make("{$path} : {$throwable->getMessage()}"); 92 | } 93 | 94 | if (! $image) { 95 | throw CouldNotLoadImage::make($path); 96 | } 97 | 98 | imagealphablending($image, false); 99 | imagesavealpha($image, true); 100 | 101 | $this->image = $image; 102 | 103 | if ($autoRotate) { 104 | $this->autoRotate(); 105 | } 106 | 107 | return $this; 108 | } 109 | 110 | public function image(): GdImage 111 | { 112 | return $this->image; 113 | } 114 | 115 | public function getWidth(): int 116 | { 117 | return imagesx($this->image); 118 | } 119 | 120 | public function getHeight(): int 121 | { 122 | return imagesy($this->image); 123 | } 124 | 125 | public function brightness(int $brightness): static 126 | { 127 | // TODO: Convert value between -100 and 100 to -255 and 255 128 | $brightness = round($brightness * 2.55); 129 | 130 | imagefilter($this->image, IMG_FILTER_BRIGHTNESS, $brightness); 131 | 132 | return $this; 133 | } 134 | 135 | public function blur(int $blur): static 136 | { 137 | for ($i = 0; $i < $blur; $i++) { 138 | imagefilter($this->image, IMG_FILTER_GAUSSIAN_BLUR); 139 | } 140 | 141 | return $this; 142 | } 143 | 144 | public function save(?string $path = null): static 145 | { 146 | if (! $path) { 147 | $path = $this->originalPath; 148 | } 149 | if (is_null($this->format)) { 150 | $extension = pathinfo($path, PATHINFO_EXTENSION); 151 | } else { 152 | $extension = $this->format; 153 | } 154 | switch (strtolower($extension)) { 155 | case 'jpg': 156 | case 'jpeg': 157 | case 'jfif': 158 | \imagejpeg($this->image, $path, $this->quality); 159 | break; 160 | case 'png': 161 | \imagepng($this->image, $path, $this->pngCompression()); 162 | break; 163 | case 'gif': 164 | \imagegif($this->image, $path); 165 | break; 166 | case 'webp': 167 | $quality = $this->quality === 100 ? IMG_WEBP_LOSSLESS : $this->quality; 168 | \imagepalettetotruecolor($this->image); 169 | \imagewebp($this->image, $path, $quality); 170 | break; 171 | case 'avif': 172 | \imageavif($this->image, $path, $this->quality); 173 | break; 174 | default: 175 | throw UnsupportedImageFormat::make($extension); 176 | } 177 | 178 | if ($this->optimize) { 179 | $this->optimizerChain->optimize($path); 180 | } 181 | $this->format = null; 182 | 183 | return $this; 184 | } 185 | 186 | public function base64(string $imageFormat = 'jpeg', bool $prefixWithFormat = true): string 187 | { 188 | ob_start(); 189 | 190 | switch (strtolower($imageFormat)) { 191 | case 'jpg': 192 | case 'jpeg': 193 | case 'jfif': 194 | \imagejpeg($this->image, null, $this->quality); 195 | break; 196 | case 'png': 197 | \imagepng($this->image, null, $this->pngCompression()); 198 | break; 199 | case 'gif': 200 | \imagegif($this->image, null); 201 | break; 202 | case 'webp': 203 | \imagewebp($this->image, null); 204 | break; 205 | case 'avif': 206 | \imageavif($this->image, null); 207 | break; 208 | default: 209 | throw UnsupportedImageFormat::make($imageFormat); 210 | } 211 | 212 | $imageData = ob_get_contents(); 213 | ob_end_clean(); 214 | 215 | if ($prefixWithFormat) { 216 | return 'data:image/'.$imageFormat.';base64,'.base64_encode($imageData); 217 | } 218 | 219 | return base64_encode($imageData); 220 | } 221 | 222 | public function driverName(): string 223 | { 224 | return 'gd'; 225 | } 226 | 227 | public function getSize(): Size 228 | { 229 | return new Size($this->getWidth(), $this->getHeight()); 230 | } 231 | 232 | public function fit( 233 | Fit $fit, 234 | ?int $desiredWidth = null, 235 | ?int $desiredHeight = null, 236 | bool $relative = false, 237 | string $backgroundColor = '#ffffff' 238 | ): static { 239 | if ($fit === Fit::Crop) { 240 | return $this->fitCrop($fit, $this->getWidth(), $this->getHeight(), $desiredWidth, $desiredHeight); 241 | } 242 | 243 | if ($fit === Fit::FillMax) { 244 | if (is_null($desiredWidth) || is_null($desiredHeight)) { 245 | throw new MissingParameter('Both desiredWidth and desiredHeight must be set when using Fit::FillMax'); 246 | } 247 | 248 | return $this->fitFillMax($desiredWidth, $desiredHeight, $backgroundColor); 249 | } 250 | 251 | $calculatedSize = $fit->calculateSize( 252 | $this->getWidth(), 253 | $this->getHeight(), 254 | $desiredWidth, 255 | $desiredHeight 256 | ); 257 | 258 | $this->modify( 259 | $calculatedSize->width, 260 | $calculatedSize->height, 261 | 0, 262 | 0, 263 | $this->getWidth(), 264 | $this->getHeight(), 265 | ); 266 | 267 | if ($fit->shouldResizeCanvas()) { 268 | $this->resizeCanvas($desiredWidth, $desiredHeight, AlignPosition::Center, $relative, $backgroundColor); 269 | } 270 | 271 | return $this; 272 | } 273 | 274 | public function fitFillMax(int $desiredWidth, int $desiredHeight, string $backgroundColor, bool $relative = false): static 275 | { 276 | $this->resize($desiredWidth, $desiredHeight, [Constraint::PreserveAspectRatio]); 277 | $this->resizeCanvas($desiredWidth, $desiredHeight, AlignPosition::Center, $relative, $backgroundColor); 278 | 279 | return $this; 280 | } 281 | 282 | protected function modify( 283 | int $desiredWidth, 284 | int $desiredHeight, 285 | int $sourceX = 0, 286 | int $sourceY = 0, 287 | int $sourceWidth = 0, 288 | int $sourceHeight = 0, 289 | ): static { 290 | $newImage = imagecreatetruecolor($desiredWidth, $desiredHeight); 291 | 292 | $transparentColorValue = imagecolortransparent($this->image); 293 | 294 | if ($transparentColorValue !== -1) { 295 | $rgba = imagecolorsforindex($newImage, $transparentColorValue); 296 | 297 | $transparentColor = imagecolorallocatealpha( 298 | $newImage, 299 | $rgba['red'], 300 | $rgba['green'], 301 | $rgba['blue'], 302 | 127 303 | ); 304 | imagefill($newImage, 0, 0, $transparentColor); 305 | imagecolortransparent($newImage, $transparentColor); 306 | } else { 307 | imagealphablending($newImage, false); 308 | imagesavealpha($newImage, true); 309 | } 310 | 311 | imagecopyresampled( 312 | $newImage, 313 | $this->image, 314 | 0, 315 | 0, 316 | $sourceX, 317 | $sourceY, 318 | $desiredWidth, 319 | $desiredHeight, 320 | $sourceWidth, 321 | $sourceHeight, 322 | ); 323 | 324 | $this->image = $newImage; 325 | 326 | return $this; 327 | } 328 | 329 | public function pickColor(int $x, int $y, ColorFormat $colorFormat): mixed 330 | { 331 | $color = imagecolorat($this->image, $x, $y); 332 | 333 | if (! imageistruecolor($this->image)) { 334 | $color = imagecolorsforindex($this->image, $color); 335 | $color['alpha'] = round(1 - $color['alpha'] / 127, 2); 336 | } 337 | 338 | $color = new GdColor($color); 339 | 340 | return $color->format($colorFormat); 341 | } 342 | 343 | public function resizeCanvas( 344 | ?int $width = null, 345 | ?int $height = null, 346 | ?AlignPosition $position = null, 347 | bool $relative = false, 348 | string $backgroundColor = '#ffffff' 349 | ): static { 350 | $position ??= AlignPosition::Center; 351 | 352 | $originalWidth = $this->getWidth(); 353 | $originalHeight = $this->getHeight(); 354 | 355 | $width ??= $originalWidth; 356 | $height ??= $originalHeight; 357 | 358 | if ($relative) { 359 | $width = $originalWidth + $width; 360 | $height = $originalHeight + $height; 361 | } 362 | 363 | // check for negative width/height 364 | $width = ($width <= 0) ? $width + $originalWidth : $width; 365 | $height = ($height <= 0) ? $height + $originalHeight : $height; 366 | 367 | // create new canvas 368 | $canvas = $this->new($width, $height, $backgroundColor); 369 | 370 | // set copy position 371 | $canvasSize = $canvas->getSize()->align($position); 372 | $imageSize = $this->getSize()->align($position); 373 | $canvasPosition = $imageSize->relativePosition($canvasSize); 374 | $imagePosition = $canvasSize->relativePosition($imageSize); 375 | 376 | if ($width <= $originalWidth) { 377 | $destinationX = 0; 378 | $sourceX = $canvasPosition->x; 379 | $sourceWidth = $canvasSize->width; 380 | } else { 381 | $destinationX = $imagePosition->x; 382 | $sourceX = 0; 383 | $sourceWidth = $originalWidth; 384 | } 385 | 386 | if ($height <= $originalHeight) { 387 | $destinationY = 0; 388 | $sourceY = $canvasPosition->y; 389 | $sourceHeight = $canvasSize->height; 390 | } else { 391 | $destinationY = $imagePosition->y; 392 | $sourceY = 0; 393 | $sourceHeight = $originalHeight; 394 | } 395 | 396 | // make image area transparent to keep transparency 397 | // even if background-color is set 398 | $transparent = imagecolorallocatealpha($canvas->image, 255, 255, 255, 127); 399 | imagealphablending($canvas->image, false); // do not blend / just overwrite 400 | imagesavealpha($canvas->image, true); // save alpha channel 401 | imagefilledrectangle($canvas->image, $destinationX, $destinationY, $destinationX + $sourceWidth - 1, $destinationY + $sourceHeight - 1, $transparent); 402 | 403 | // copy image into new canvas 404 | imagecopy($canvas->image, $this->image, $destinationX, $destinationY, $sourceX, $sourceY, $sourceWidth, $sourceHeight); 405 | 406 | // set new core to canvas 407 | $this->image = $canvas->image; 408 | 409 | return $this; 410 | } 411 | 412 | public function gamma(float $gamma): static 413 | { 414 | imagegammacorrect($this->image, 1, $gamma); 415 | 416 | return $this; 417 | } 418 | 419 | public function contrast(float $level): static 420 | { 421 | imagefilter($this->image, IMG_FILTER_CONTRAST, ($level * -1)); 422 | 423 | return $this; 424 | } 425 | 426 | public function colorize(int $red, int $green, int $blue): static 427 | { 428 | $red = round($red * 2.55); 429 | $green = round($green * 2.55); 430 | $blue = round($blue * 2.55); 431 | 432 | imagefilter($this->image, IMG_FILTER_COLORIZE, $red, $green, $blue); 433 | 434 | return $this; 435 | } 436 | 437 | public function greyscale(): static 438 | { 439 | imagefilter($this->image, IMG_FILTER_GRAYSCALE); 440 | 441 | return $this; 442 | } 443 | 444 | public function manualCrop(int $width, int $height, ?int $x = null, ?int $y = null): static 445 | { 446 | $cropped = new Size($width, $height); 447 | $position = new Point($x ?? 0, $y ?? 0); 448 | 449 | if (is_null($x) && is_null($y)) { 450 | $position = $this 451 | ->getSize() 452 | ->align(AlignPosition::Center) 453 | ->relativePosition($cropped->align(AlignPosition::Center)); 454 | } 455 | 456 | $maxCroppedWidth = $this->getWidth() - $x; 457 | $maxCroppedHeight = $this->getHeight() - $y; 458 | 459 | $width = min($cropped->width, $maxCroppedWidth); 460 | $height = min($cropped->height, $maxCroppedHeight); 461 | 462 | $this->modify( 463 | $width, 464 | $height, 465 | $position->x, 466 | $position->y, 467 | $width, 468 | $height, 469 | ); 470 | 471 | return $this; 472 | } 473 | 474 | public function crop(int $width, int $height, CropPosition $position = CropPosition::Center): static 475 | { 476 | $width = min($width, $this->getWidth()); 477 | $height = min($height, $this->getHeight()); 478 | 479 | [$offsetX, $offsetY] = $this->calculateCropOffsets($width, $height, $position); 480 | 481 | $maxWidth = $this->getWidth() - $offsetX; 482 | $maxHeight = $this->getHeight() - $offsetY; 483 | $width = min($width, $maxWidth); 484 | $height = min($height, $maxHeight); 485 | 486 | return $this->manualCrop($width, $height, $offsetX, $offsetY); 487 | } 488 | 489 | public function focalCrop(int $width, int $height, ?int $cropCenterX = null, ?int $cropCenterY = null): static 490 | { 491 | [$width, $height, $cropCenterX, $cropCenterY] = $this->calculateFocalCropCoordinates( 492 | $width, 493 | $height, 494 | $cropCenterX, 495 | $cropCenterY 496 | ); 497 | 498 | $this->manualCrop($width, $height, $cropCenterX, $cropCenterY); 499 | 500 | return $this; 501 | } 502 | 503 | public function focalCropAndResize(int $width, int $height, ?int $cropCenterX = null, ?int $cropCenterY = null): static 504 | { 505 | [$cropWidth, $cropHeight, $cropX, $cropY] = $this->calculateFocalCropAndResizeCoordinates( 506 | $width, 507 | $height, 508 | $cropCenterX, 509 | $cropCenterY 510 | ); 511 | 512 | $this->manualCrop($cropWidth, $cropHeight, $cropX, $cropY) 513 | ->width($width) 514 | ->height($height); 515 | 516 | return $this; 517 | } 518 | 519 | public function sepia(): static 520 | { 521 | return $this 522 | ->greyscale() 523 | ->brightness(0) 524 | ->contrast(5) 525 | ->colorize(38, 25, 10) 526 | ->contrast(5); 527 | } 528 | 529 | public function sharpen(float $amount): static 530 | { 531 | $min = $amount >= 10 ? $amount * -0.01 : 0; 532 | $max = $amount * -0.025; 533 | $abs = ((4 * $min + 4 * $max) * -1) + 1; 534 | 535 | $matrix = [ 536 | [$min, $max, $min], 537 | [$max, $abs, $max], 538 | [$min, $max, $min], 539 | ]; 540 | 541 | imageconvolution($this->image, $matrix, 1, 0); 542 | 543 | return $this; 544 | } 545 | 546 | public function background(string $color): static 547 | { 548 | $width = $this->getWidth(); 549 | $height = $this->getHeight(); 550 | 551 | $newImage = $this->new($width, $height, $color); 552 | 553 | $backgroundSize = $newImage->getSize()->align(AlignPosition::TopLeft); 554 | $overlaySize = $this->getSize()->align(AlignPosition::TopLeft); 555 | $target = $backgroundSize->relativePosition($overlaySize); 556 | 557 | $this->overlay($newImage, $this, $target->x, $target->y); 558 | 559 | return $this; 560 | } 561 | 562 | public function overlay(ImageDriver $bottomImage, ImageDriver $topImage, int $x = 0, int $y = 0): static 563 | { 564 | $bottomImage->insert($topImage, AlignPosition::TopLeft, $x, $y); 565 | $this->image = $bottomImage->image(); 566 | 567 | return $this; 568 | } 569 | 570 | public function orientation(?Orientation $orientation = null): static 571 | { 572 | if (is_null($orientation)) { 573 | $orientation = $this->getOrientationFromExif($this->exif); 574 | } 575 | 576 | $this->image = imagerotate($this->image, $orientation->degrees() * -1, 0); 577 | 578 | return $this; 579 | } 580 | 581 | public function setExif(string $path): void 582 | { 583 | if (! extension_loaded('exif') || ! extension_loaded('fileinfo')) { 584 | return; 585 | } 586 | 587 | $fInfo = finfo_open(FILEINFO_RAW); 588 | if (! $fInfo) { 589 | return; 590 | } 591 | 592 | $info = finfo_file($fInfo, $path); 593 | finfo_close($fInfo); 594 | 595 | if (! is_string($info) || ! str_contains($info, 'Exif')) { 596 | return; 597 | } 598 | 599 | $result = @exif_read_data($path); 600 | $this->exif = is_array($result) ? $result : []; 601 | } 602 | 603 | /** 604 | * @return array 605 | */ 606 | public function exif(): array 607 | { 608 | return $this->exif; 609 | } 610 | 611 | public function flip(FlipDirection $flip): static 612 | { 613 | $direction = match ($flip) { 614 | FlipDirection::Horizontal => IMG_FLIP_HORIZONTAL, 615 | FlipDirection::Vertical => IMG_FLIP_VERTICAL, 616 | FlipDirection::Both => IMG_FLIP_BOTH, 617 | }; 618 | 619 | imageflip($this->image, $direction); 620 | 621 | return $this; 622 | } 623 | 624 | public function pixelate(int $pixelate = 50): static 625 | { 626 | imagefilter($this->image, IMG_FILTER_PIXELATE, $pixelate, true); 627 | 628 | return $this; 629 | } 630 | 631 | public function insert( 632 | ImageDriver|string $otherImage, 633 | AlignPosition $position = AlignPosition::Center, 634 | int $x = 0, 635 | int $y = 0, 636 | int $alpha = 100 637 | ): static { 638 | $this->ensureNumberBetween($alpha, 0, 100, 'alpha'); 639 | if (is_string($otherImage)) { 640 | $otherImage = (new static)->loadFile($otherImage); 641 | } 642 | 643 | $imageSize = $this->getSize()->align($position, $x, $y); 644 | $otherImageSize = $otherImage->getSize()->align($position); 645 | $target = $imageSize->relativePosition($otherImageSize); 646 | 647 | imagealphablending($this->image, true); 648 | // check here for the next 3 line https://www.php.net/manual/en/function.imagecopymerge.php#92787 649 | $cut = imagecreatetruecolor($otherImageSize->width, $otherImageSize->height); 650 | if (! $cut) { 651 | throw new Exception('Could not create image'); 652 | } 653 | imagecopy($cut, $this->image, 0, 0, $target->x, $target->y, $otherImageSize->width, $otherImageSize->height); 654 | imagecopy($cut, $otherImage->image(), 0, 0, 0, 0, $otherImageSize->width, $otherImageSize->height); 655 | 656 | imagecopymerge( 657 | $this->image, 658 | $cut, 659 | $target->x, 660 | $target->y, 661 | 0, 662 | 0, 663 | $otherImageSize->width, 664 | $otherImageSize->height, 665 | $alpha 666 | ); 667 | 668 | return $this; 669 | } 670 | 671 | public function resize(int $width, int $height, array $constraints = []): static 672 | { 673 | $resized = $this->getSize()->resize($width, $height, $constraints); 674 | 675 | $this->modify($resized->width, $resized->height, 0, 0, $this->getWidth(), $this->getHeight()); 676 | 677 | return $this; 678 | } 679 | 680 | public function width(int $width, array $constraints = [Constraint::PreserveAspectRatio]): static 681 | { 682 | $newHeight = (int) round($width / $this->getSize()->aspectRatio()); 683 | 684 | $this->resize($width, $newHeight, $constraints); 685 | 686 | return $this; 687 | } 688 | 689 | public function height(int $height, array $constraints = [Constraint::PreserveAspectRatio]): static 690 | { 691 | $newWidth = (int) round($height * $this->getSize()->aspectRatio()); 692 | 693 | $this->resize($newWidth, $height, $constraints); 694 | 695 | return $this; 696 | } 697 | 698 | public function border(int $width, BorderType $type, string $color = '000000'): static 699 | { 700 | imagealphablending($this->image, true); 701 | imagesavealpha($this->image, true); 702 | 703 | if ($type === BorderType::Shrink) { 704 | $originalWidth = $this->getWidth(); 705 | $originalHeight = $this->getHeight(); 706 | 707 | $this 708 | ->resize( 709 | (int) round($this->getWidth() - ($width * 2)), 710 | (int) round($this->getHeight() - ($width * 2)), 711 | [Constraint::PreserveAspectRatio], 712 | ) 713 | ->resizeCanvas( 714 | $originalWidth, 715 | $originalHeight, 716 | AlignPosition::Center, 717 | false, 718 | $color, 719 | ); 720 | 721 | return $this; 722 | } 723 | 724 | if ($type === BorderType::Expand) { 725 | $this->resizeCanvas( 726 | (int) round($width * 2), 727 | (int) round($width * 2), 728 | AlignPosition::Center, 729 | true, 730 | $color, 731 | ); 732 | 733 | return $this; 734 | } 735 | 736 | if ($type === BorderType::Overlay) { 737 | $backgroundColor = new GdColor(null); 738 | 739 | imagefilledrectangle( 740 | $this->image, 741 | (int) round($width / 2), 742 | (int) round($width / 2), 743 | (int) round($this->getWidth() - ($width / 2)), 744 | (int) round($this->getHeight() - ($width / 2)), 745 | $backgroundColor->getInt() 746 | ); 747 | 748 | $borderColor = new GdColor($color); 749 | 750 | imagesetthickness($this->image, $width); 751 | 752 | imagerectangle( 753 | $this->image, 754 | (int) round($width / 2), 755 | (int) round($width / 2), 756 | (int) round($this->getWidth() - ($width / 2)), 757 | (int) round($this->getHeight() - ($width / 2)), 758 | $borderColor->getInt() 759 | ); 760 | 761 | return $this; 762 | } 763 | 764 | return $this; 765 | } 766 | 767 | /** @param int<-1, 100> $quality */ 768 | public function quality(int $quality): static 769 | { 770 | $this->quality = $quality; 771 | 772 | return $this; 773 | } 774 | 775 | /** @return int<-1, 9> */ 776 | protected function pngCompression(): int 777 | { 778 | if ($this->quality === -1) { 779 | return -1; 780 | } 781 | 782 | return (int) round((100 - $this->quality) / 10); 783 | } 784 | 785 | public function format(string $format): static 786 | { 787 | if (! in_array($format, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif'])) { 788 | throw UnsupportedImageFormat::make($format); 789 | } 790 | $this->format = $format; 791 | 792 | return $this; 793 | } 794 | 795 | public function autoRotate(): void 796 | { 797 | if (! $this->exif || empty($this->exif['Orientation'])) { 798 | return; 799 | } 800 | 801 | switch ($this->exif['Orientation']) { 802 | case 8: 803 | $this->image = imagerotate($this->image, 90, 0); 804 | break; 805 | case 3: 806 | $this->image = imagerotate($this->image, 180, 0); 807 | break; 808 | case 5: 809 | case 7: 810 | case 6: 811 | $this->image = imagerotate($this->image, -90, 0); 812 | break; 813 | } 814 | } 815 | 816 | public function text( 817 | string $text, 818 | int $fontSize, 819 | string $color = '000000', 820 | int $x = 0, 821 | int $y = 0, 822 | int $angle = 0, 823 | string $fontPath = '', 824 | int $width = 0, 825 | ): static { 826 | $textColor = new GdColor($color); 827 | 828 | if (! $fontPath || ! file_exists($fontPath)) { 829 | throw InvalidFont::make($fontPath); 830 | } 831 | 832 | imagettftext( 833 | $this->image, 834 | $fontSize, 835 | $angle, 836 | $x, 837 | $y, 838 | $textColor->getInt(), 839 | $fontPath, 840 | $width > 0 841 | ? $this->wrapText($text, $fontSize, $fontPath, $angle, $width) 842 | : $text, 843 | ); 844 | 845 | return $this; 846 | } 847 | 848 | public function wrapText(string $text, int $fontSize, string $fontPath = '', int $angle = 0, int $width = 0): string 849 | { 850 | if (! $fontPath || ! file_exists($fontPath)) { 851 | throw InvalidFont::make($fontPath); 852 | } 853 | 854 | $wrapped = ''; 855 | $words = explode(' ', $text); 856 | 857 | foreach ($words as $word) { 858 | $teststring = "{$wrapped} {$word}"; 859 | 860 | $testbox = imagettfbbox($fontSize, $angle, $fontPath, $teststring); 861 | 862 | if (! $testbox) { 863 | $wrapped .= ' '.$word; 864 | 865 | continue; 866 | } 867 | 868 | if ($testbox[2] > $width) { 869 | $wrapped .= "\n".$word; 870 | } else { 871 | $wrapped .= ' '.$word; 872 | } 873 | } 874 | 875 | return $wrapped; 876 | } 877 | } 878 | --------------------------------------------------------------------------------