├── .editorconfig ├── composer.json ├── LICENSE.md ├── composer.lock ├── README.md └── src └── claviska └── SimpleImage.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "claviska/simpleimage", 3 | "description": "A PHP class that makes working with images as simple as possible.", 4 | "license": "MIT", 5 | "require": { 6 | "php": ">=8.0", 7 | "ext-gd": "*", 8 | "league/color-extractor": "0.4.*" 9 | }, 10 | "authors": [ 11 | { 12 | "name": "Cory LaViska", 13 | "homepage": "http://www.abeautifulsite.net/", 14 | "role": "Developer" 15 | } 16 | ], 17 | "autoload": { 18 | "psr-0": { 19 | "claviska": "src/" 20 | } 21 | }, 22 | "require-dev": { 23 | "laravel/pint": "^1.5", 24 | "phpstan/phpstan": "^1.10" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2017 A Beautiful Site, LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "eb94dc95686ec297093755af85d5e7dd", 8 | "packages": [ 9 | { 10 | "name": "league/color-extractor", 11 | "version": "0.4.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/thephpleague/color-extractor.git", 15 | "reference": "21fcac6249c5ef7d00eb83e128743ee6678fe505" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/thephpleague/color-extractor/zipball/21fcac6249c5ef7d00eb83e128743ee6678fe505", 20 | "reference": "21fcac6249c5ef7d00eb83e128743ee6678fe505", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "ext-gd": "*", 25 | "php": "^7.3 || ^8.0" 26 | }, 27 | "replace": { 28 | "matthecat/colorextractor": "*" 29 | }, 30 | "require-dev": { 31 | "friendsofphp/php-cs-fixer": "~2", 32 | "phpunit/phpunit": "^9.5" 33 | }, 34 | "suggest": { 35 | "ext-curl": "To download images from remote URLs if allow_url_fopen is disabled for security reasons" 36 | }, 37 | "type": "library", 38 | "autoload": { 39 | "psr-4": { 40 | "League\\ColorExtractor\\": "src" 41 | } 42 | }, 43 | "notification-url": "https://packagist.org/downloads/", 44 | "license": [ 45 | "MIT" 46 | ], 47 | "authors": [ 48 | { 49 | "name": "Mathieu Lechat", 50 | "email": "math.lechat@gmail.com", 51 | "homepage": "http://matthecat.com", 52 | "role": "Developer" 53 | } 54 | ], 55 | "description": "Extract colors from an image as a human would do.", 56 | "homepage": "https://github.com/thephpleague/color-extractor", 57 | "keywords": [ 58 | "color", 59 | "extract", 60 | "human", 61 | "image", 62 | "palette" 63 | ], 64 | "support": { 65 | "issues": "https://github.com/thephpleague/color-extractor/issues", 66 | "source": "https://github.com/thephpleague/color-extractor/tree/0.4.0" 67 | }, 68 | "time": "2022-09-24T15:57:16+00:00" 69 | } 70 | ], 71 | "packages-dev": [ 72 | { 73 | "name": "laravel/pint", 74 | "version": "v1.5.0", 75 | "source": { 76 | "type": "git", 77 | "url": "https://github.com/laravel/pint.git", 78 | "reference": "e0a8cef58b74662f27355be9cdea0e726bbac362" 79 | }, 80 | "dist": { 81 | "type": "zip", 82 | "url": "https://api.github.com/repos/laravel/pint/zipball/e0a8cef58b74662f27355be9cdea0e726bbac362", 83 | "reference": "e0a8cef58b74662f27355be9cdea0e726bbac362", 84 | "shasum": "" 85 | }, 86 | "require": { 87 | "ext-json": "*", 88 | "ext-mbstring": "*", 89 | "ext-tokenizer": "*", 90 | "ext-xml": "*", 91 | "php": "^8.0" 92 | }, 93 | "require-dev": { 94 | "friendsofphp/php-cs-fixer": "^3.14.4", 95 | "illuminate/view": "^9.51.0", 96 | "laravel-zero/framework": "^9.2.0", 97 | "mockery/mockery": "^1.5.1", 98 | "nunomaduro/larastan": "^2.4.0", 99 | "nunomaduro/termwind": "^1.15.1", 100 | "pestphp/pest": "^1.22.4" 101 | }, 102 | "bin": [ 103 | "builds/pint" 104 | ], 105 | "type": "project", 106 | "autoload": { 107 | "psr-4": { 108 | "App\\": "app/", 109 | "Database\\Seeders\\": "database/seeders/", 110 | "Database\\Factories\\": "database/factories/" 111 | } 112 | }, 113 | "notification-url": "https://packagist.org/downloads/", 114 | "license": [ 115 | "MIT" 116 | ], 117 | "authors": [ 118 | { 119 | "name": "Nuno Maduro", 120 | "email": "enunomaduro@gmail.com" 121 | } 122 | ], 123 | "description": "An opinionated code formatter for PHP.", 124 | "homepage": "https://laravel.com", 125 | "keywords": [ 126 | "format", 127 | "formatter", 128 | "lint", 129 | "linter", 130 | "php" 131 | ], 132 | "support": { 133 | "issues": "https://github.com/laravel/pint/issues", 134 | "source": "https://github.com/laravel/pint" 135 | }, 136 | "time": "2023-02-14T16:31:02+00:00" 137 | }, 138 | { 139 | "name": "phpstan/phpstan", 140 | "version": "1.10.2", 141 | "source": { 142 | "type": "git", 143 | "url": "https://github.com/phpstan/phpstan.git", 144 | "reference": "a2ffec7db373d8da4973d1d62add872db5cd22dd" 145 | }, 146 | "dist": { 147 | "type": "zip", 148 | "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a2ffec7db373d8da4973d1d62add872db5cd22dd", 149 | "reference": "a2ffec7db373d8da4973d1d62add872db5cd22dd", 150 | "shasum": "" 151 | }, 152 | "require": { 153 | "php": "^7.2|^8.0" 154 | }, 155 | "conflict": { 156 | "phpstan/phpstan-shim": "*" 157 | }, 158 | "bin": [ 159 | "phpstan", 160 | "phpstan.phar" 161 | ], 162 | "type": "library", 163 | "autoload": { 164 | "files": [ 165 | "bootstrap.php" 166 | ] 167 | }, 168 | "notification-url": "https://packagist.org/downloads/", 169 | "license": [ 170 | "MIT" 171 | ], 172 | "description": "PHPStan - PHP Static Analysis Tool", 173 | "keywords": [ 174 | "dev", 175 | "static analysis" 176 | ], 177 | "support": { 178 | "issues": "https://github.com/phpstan/phpstan/issues", 179 | "source": "https://github.com/phpstan/phpstan/tree/1.10.2" 180 | }, 181 | "funding": [ 182 | { 183 | "url": "https://github.com/ondrejmirtes", 184 | "type": "github" 185 | }, 186 | { 187 | "url": "https://github.com/phpstan", 188 | "type": "github" 189 | }, 190 | { 191 | "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", 192 | "type": "tidelift" 193 | } 194 | ], 195 | "time": "2023-02-23T14:36:46+00:00" 196 | } 197 | ], 198 | "aliases": [], 199 | "minimum-stability": "stable", 200 | "stability-flags": [], 201 | "prefer-stable": false, 202 | "prefer-lowest": false, 203 | "platform": { 204 | "php": ">=8.0", 205 | "ext-gd": "*" 206 | }, 207 | "platform-dev": [], 208 | "plugin-api-version": "2.3.0" 209 | } 210 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleImage 2 | 3 | A PHP class that makes working with images as simple as possible. 4 | 5 | Developed and maintained by [Cory LaViska](https://github.com/claviska). 6 | 7 | _If this project has you loving PHP image manipulation again, please consider [sponsoring me](https://github.com/sponsors/claviska) to support its development._ 8 | 9 | --- 10 | 11 | ## Overview 12 | 13 | ```php 14 | fromFile('image.jpg') // load image.jpg 22 | ->autoOrient() // adjust orientation based on exif data 23 | ->resize(320, 200) // resize to 320x200 pixels 24 | ->flip('x') // flip horizontally 25 | ->colorize('DarkBlue') // tint dark blue 26 | ->border('black', 10) // add a 10 pixel black border 27 | ->overlay('watermark.png', 'bottom right') // add a watermark image 28 | ->toFile('new-image.png', 'image/png') // convert to PNG and save a copy to new-image.png 29 | ->toScreen(); // output to the screen 30 | 31 | // And much more! 💪 32 | } catch(Exception $err) { 33 | // Handle errors 34 | echo $err->getMessage(); 35 | } 36 | ``` 37 | 38 | ## Requirements 39 | 40 | - PHP 8.0+ 41 | - [GD extension](http://php.net/manual/en/book.image.php) 42 | 43 | For PHP versions below 7.0, please use [SimpleImage v3.7.2](https://github.com/claviska/SimpleImage/releases/tag/3.7.2) 44 | 45 | ## Features 46 | 47 | - Supports reading, writing, and converting GIF, JPEG, PNG, WEBP, BMP, AVIF formats. 48 | - Reads and writes files, data URIs, and image strings. 49 | - Manipulation: crop, resize, overlay/watermark, adding TTF text 50 | - Drawing: arc, border, dot, ellipse, line, polygon, rectangle, rounded rectangle 51 | - Filters: blur, brighten, colorize, contrast, darken, desaturate, edge detect, emboss, invert, opacity, pixelate, sepia, sharpen, sketch 52 | - Utilities: color adjustment, darken/lighten color, extract colors 53 | - Properties: exif data, height/width, mime type, orientation 54 | - Color arguments can be passed in as any CSS color (e.g. `LightBlue`), a hex color, or an RGB(A) array. 55 | - Support for alpha-transparency (GIF, PNG, WEBP, AVIF) 56 | - Chainable methods 57 | - Uses exceptions 58 | - Load with Composer or manually (just one file) 59 | - [Semantic Versioning](http://semver.org/) 60 | 61 | ## Installation 62 | 63 | Install with Composer: 64 | 65 | ``` 66 | composer require claviska/simpleimage 67 | ``` 68 | 69 | Or include the library manually: 70 | 71 | ```php 72 | toFile($file, 'image/avif', [ 207 | // JPG, WEBP, AVIF (default 100) 208 | 'quality' => 100, 209 | 210 | // AVIF (default -1 which is 6) 211 | // range of slow and small file 0 to 10 fast but big file 212 | 'speed' => -1, 213 | ]); 214 | ``` 215 | 216 | ```php 217 | $image->toFile($file, 'image/bmp', [ 218 | // BMP: boolean (default true) 219 | 'compression' => true, 220 | 221 | // BMP, JPG (default null, keep the same) 222 | 'interlace' => null, 223 | ]); 224 | ``` 225 | 226 | ```php 227 | $image->toFile($file, 'image/gif', [ 228 | // GIF, PNG (default true) 229 | 'alpha' => true, 230 | ]); 231 | ``` 232 | 233 | ```php 234 | $image->toFile($file, 'image/jpeg', [ 235 | // BMP, JPG (default null, keep the same) 236 | 'interlace' => null, 237 | 238 | // JPG, WEBP, AVIF (default 100) 239 | 'quality' => 100, 240 | ]); 241 | ``` 242 | 243 | ```php 244 | $image->toFile($file, 'image/png', [ 245 | // GIF, PNG (default true) 246 | 'alpha' => true, 247 | 248 | // PNG: 0-10, defaults to zlib (default 6) 249 | 'compression' => -1, 250 | 251 | // PNG (default -1) 252 | 'filters' => -1, 253 | 254 | // has no effect on PNG images, since the format is lossless 255 | // 'quality' => 100, 256 | ]); 257 | ``` 258 | 259 | ```php 260 | $image->toFile($file, 'image/webp', [ 261 | // JPG, WEBP, AVIF (default 100) 262 | 'quality' => 100, 263 | ]); 264 | ``` 265 | 266 | ### Utilities 267 | 268 | #### `getAspectRatio()` 269 | 270 | Gets the image's current aspect ratio. 271 | 272 | Returns the aspect ratio as a float. 273 | 274 | #### `getExif()` 275 | 276 | Gets the image's exif data. 277 | 278 | Returns an array of exif data or null if no data is available. 279 | 280 | #### `getHeight()` 281 | 282 | Gets the image's current height. 283 | 284 | Returns the height as an integer. 285 | 286 | #### `getMimeType()` 287 | 288 | Gets the mime type of the loaded image. 289 | 290 | Returns a mime type string. 291 | 292 | #### `getOrientation()` 293 | 294 | Gets the image's current orientation. 295 | 296 | Returns a string: 'landscape', 'portrait', or 'square' 297 | 298 | #### `getResolution()` 299 | 300 | Gets the image's current resolution in DPI. 301 | 302 | Returns an array of integers: [0 => 96, 1 => 96] 303 | 304 | #### `getWidth()` 305 | 306 | Gets the image's current width. 307 | 308 | Returns the width as an integer. 309 | 310 | #### `hasImage()` 311 | 312 | Checks if the SimpleImage object has loaded an image. 313 | 314 | Returns a boolean. 315 | 316 | #### `reset()` 317 | 318 | > [!NOTE] 319 | > The `reset()` method has been deprecated in SimpleImage 4.4.0. Calling the method has no effect. 320 | 321 | Destroys the image resource. 322 | 323 | Returns a SimpleImage object. 324 | 325 | ### Manipulation 326 | 327 | #### `autoOrient()` 328 | 329 | Rotates an image so the orientation will be correct based on its exif data. It is safe to call this method on images that don't have exif data (no changes will be made). 330 | Returns a SimpleImage object. 331 | 332 | #### `bestFit($maxWidth, $maxHeight)` 333 | 334 | Proportionally resize the image to fit inside a specific width and height. 335 | 336 | - `$maxWidth`* (int) - The maximum width the image can be. 337 | - `$maxHeight`* (int) - The maximum height the image can be. 338 | 339 | Returns a SimpleImage object. 340 | 341 | #### `crop($x1, $y1, $x2, $y2)` 342 | 343 | Crop the image. 344 | 345 | - $x1 - Top left x coordinate. 346 | - $y1 - Top left y coordinate. 347 | - $x2 - Bottom right x coordinate. 348 | - $y2 - Bottom right x coordinate. 349 | 350 | Returns a SimpleImage object. 351 | 352 | #### `fitToHeight($height)` (DEPRECATED) 353 | 354 | Proportionally resize the image to a specific height. 355 | 356 | _This method was deprecated in version 3.2.2 and will be removed in version 4.0. Please use `resize(null, $height)` instead._ 357 | 358 | - `$height`* (int) - The height to resize the image to. 359 | 360 | Returns a SimpleImage object. 361 | 362 | #### `fitToWidth($width)` (DEPRECATED) 363 | 364 | Proportionally resize the image to a specific width. 365 | 366 | _This method was deprecated in version 3.2.2 and will be removed in version 4.0. Please use `resize($width, null)` instead._ 367 | 368 | - `$width`* (int) - The width to resize the image to. 369 | 370 | Returns a SimpleImage object. 371 | 372 | #### `flip($direction)` 373 | 374 | Flip the image horizontally or vertically. 375 | 376 | - `$direction`* (string) - The direction to flip: x|y|both 377 | 378 | Returns a SimpleImage object. 379 | 380 | #### `maxColors($max, $dither)` 381 | 382 | Reduces the image to a maximum number of colors. 383 | 384 | - `$max`* (int) - The maximum number of colors to use. 385 | - `$dither` (bool) - Whether or not to use a dithering effect (default true). 386 | 387 | Returns a SimpleImage object. 388 | 389 | #### `overlay($overlay, $anchor, $opacity, $xOffset, $yOffset)` 390 | 391 | Place an image on top of the current image. 392 | 393 | - `$overlay`* (string|SimpleImage) - The image to overlay. This can be a filename, a data URI, or a SimpleImage object. 394 | - `$anchor` (string) - The anchor point: 'center', 'top', 'bottom', 'left', 'right', 'top left', 'top right', 'bottom left', 'bottom right' (default 'center') 395 | - `$opacity` (float) - The opacity level of the overlay 0-1 (default 1). 396 | - `$xOffset` (int) - Horizontal offset in pixels (default 0). 397 | - `$yOffset` (int) - Vertical offset in pixels (default 0). 398 | - `$calculateOffsetFromEdge` (bool) - Calculate Offset referring to the edges of the image. $xOffset and $yOffset have no effect in center anchor. (default false). 399 | 400 | Returns a SimpleImage object. 401 | 402 | #### `resize($width, $height)` 403 | 404 | Resize an image to the specified dimensions. If only one dimension is specified, the image will be resized proportionally. 405 | 406 | - `$width`* (int) - The new image width. 407 | - `$height`* (int) - The new image height. 408 | 409 | Returns a SimpleImage object. 410 | 411 | #### `resolution($res_x, $res_y)` 412 | 413 | Changes the resolution (DPI) of an image. 414 | 415 | - `$res_x`* (int) - The horizontal resolution, in DPI. 416 | - `$res_y` (int) - The vertical resolution, in DPI. 417 | 418 | Returns a SimpleImage object. 419 | 420 | #### `rotate($angle, $backgroundColor)` 421 | 422 | Rotates the image. 423 | 424 | - `$angle`* (int) - The angle of rotation (-360 - 360). 425 | - `$backgroundColor` (string|array) - The background color to use for the uncovered zone area after rotation (default 'transparent'). 426 | 427 | Returns a SimpleImage object. 428 | 429 | #### `text($text, $options, &$boundary)` 430 | 431 | Adds text to the image. 432 | 433 | - `$text*` (string) - The desired text. 434 | - `$options` (array) - An array of options. 435 | - `fontFile`* (string) - The TrueType (or compatible) font file to use. 436 | - `size` (int) - The size of the font in pixels (default 12). 437 | - `color` (string|array) - The text color (default black). 438 | - `anchor` (string) - The anchor point: 'center', 'top', 'bottom', 'left', 'right', 439 | 'top left', 'top right', 'bottom left', 'bottom right' (default 'center'). 440 | - `xOffset` (int) - The horizontal offset in pixels (default 0). 441 | - `yOffset` (int) - The vertical offset in pixels (default 0). 442 | - `shadow` (array) - Text shadow params. 443 | - `x`* (int) - Horizontal offset in pixels. 444 | - `y`* (int) - Vertical offset in pixels. 445 | - `color`* (string|array) - The text shadow color. 446 | - `calculateOffsetFromEdge` (bool) - Calculate Offset referring to the edges of the image (default false). 447 | - `baselineAlign` (bool) - Align the text font with the baseline. (default true). 448 | - `$boundary` (array) - If passed, this variable will contain an array with coordinates that 449 | surround the text: [x1, y1, x2, y2, width, height]. This can be used for calculating the 450 | text's position after it gets added to the image. 451 | 452 | Returns a SimpleImage object. 453 | 454 | #### `thumbnail($width, $height, $anchor)` 455 | 456 | Creates a thumbnail image. This function attempts to get the image as close to the provided dimensions as possible, then crops the remaining overflow to force the desired size. Useful for generating thumbnail images. 457 | 458 | - `$width`* (int) - The thumbnail width. 459 | - `$height`* (int) - The thumbnail height. 460 | - `$anchor` (string) - The anchor point: 'center', 'top', 'bottom', 'left', 'right', 'top left', 'top right', 'bottom left', 'bottom right' (default 'center'). 461 | 462 | Returns a SimpleImage object. 463 | 464 | ### Drawing 465 | 466 | #### `arc($x, $y, $width, $height, $start, $end, $color, $thickness)` 467 | 468 | Draws an arc. 469 | 470 | - `$x`* (int) - The x coordinate of the arc's center. 471 | - `$y`* (int) - The y coordinate of the arc's center. 472 | - `$width`* (int) - The width of the arc. 473 | - `$height`* (int) - The height of the arc. 474 | - `$start`* (int) - The start of the arc in degrees. 475 | - `$end`* (int) - The end of the arc in degrees. 476 | - `$color`* (string|array) - The arc color. 477 | - `$thickness` (int|string) - Line thickness in pixels or 'filled' (default 1). 478 | 479 | Returns a SimpleImage object. 480 | 481 | #### `border($color, $thickness)` 482 | 483 | Draws a border around the image. 484 | 485 | - `$color`* (string|array) - The border color. 486 | - `$thickness` (int) - The thickness of the border (default 1). 487 | 488 | Returns a SimpleImage object. 489 | 490 | #### `dot($x, $y, $color)` 491 | 492 | Draws a single pixel dot. 493 | 494 | - `$x`* (int) - The x coordinate of the dot. 495 | - `$y`* (int) - The y coordinate of the dot. 496 | - `$color`* (string|array) - The dot color. 497 | 498 | Returns a SimpleImage object. 499 | 500 | #### `ellipse($x, $y, $width, $height, $color, $thickness)` 501 | 502 | Draws an ellipse. 503 | 504 | - `$x`* (int) - The x coordinate of the center. 505 | - `$y`* (int) - The y coordinate of the center. 506 | - `$width`* (int) - The ellipse width. 507 | - `$height`* (int) - The ellipse height. 508 | - `$color`* (string|array) - The ellipse color. 509 | - `$thickness` (int|string) - Line thickness in pixels or 'filled' (default 1). 510 | 511 | Returns a SimpleImage object. 512 | 513 | #### `fill($color)` 514 | 515 | Fills the image with a solid color. 516 | 517 | - `$color` (string|array) - The fill color. 518 | 519 | Returns a SimpleImage object. 520 | 521 | #### `line($x1, $y1, $x2, $y2, $color, $thickness)` 522 | 523 | Draws a line. 524 | 525 | - `$x1`* (int) - The x coordinate for the first point. 526 | - `$y1`* (int) - The y coordinate for the first point. 527 | - `$x2`* (int) - The x coordinate for the second point. 528 | - `$y2`* (int) - The y coordinate for the second point. 529 | - `$color` (string|array) - The line color. 530 | - `$thickness` (int) - The line thickness (default 1). 531 | 532 | Returns a SimpleImage object. 533 | 534 | #### `polygon($vertices, $color, $thickness)` 535 | 536 | Draws a polygon. 537 | 538 | - `$vertices`* (array) - The polygon's vertices in an array of x/y arrays. Example: 539 | ``` 540 | [ 541 | ['x' => x1, 'y' => y1], 542 | ['x' => x2, 'y' => y2], 543 | ['x' => xN, 'y' => yN] 544 | ] 545 | ``` 546 | - `$color`* (string|array) - The polygon color. 547 | - `$thickness` (int|string) - Line thickness in pixels or 'filled' (default 1). 548 | 549 | Returns a SimpleImage object. 550 | 551 | #### `rectangle($x1, $y1, $x2, $y2, $color, $thickness)` 552 | 553 | Draws a rectangle. 554 | 555 | - `$x1`* (int) - The upper left x coordinate. 556 | - `$y1`* (int) - The upper left y coordinate. 557 | - `$x2`* (int) - The bottom right x coordinate. 558 | - `$y2`* (int) - The bottom right y coordinate. 559 | - `$color`* (string|array) - The rectangle color. 560 | - `$thickness` (int|string) - Line thickness in pixels or 'filled' (default 1). 561 | 562 | Returns a SimpleImage object. 563 | 564 | #### `roundedRectangle($x1, $y1, $x2, $y2, $radius, $color, $thickness)` 565 | 566 | Draws a rounded rectangle. 567 | 568 | - `$x1`* (int) - The upper left x coordinate. 569 | - `$y1`* (int) - The upper left y coordinate. 570 | - `$x2`* (int) - The bottom right x coordinate. 571 | - `$y2`* (int) - The bottom right y coordinate. 572 | - `$radius`* (int) - The border radius in pixels. 573 | - `$color`* (string|array) - The rectangle color. 574 | - `$thickness` (int|string) - Line thickness in pixels or 'filled' (default 1). 575 | 576 | Returns a SimpleImage object. 577 | 578 | ### Filters 579 | 580 | #### `blur($type, $passes)` 581 | 582 | Applies the blur filter. 583 | 584 | - `$type` (string) - The blur algorithm to use: 'selective', 'gaussian' (default 'gaussian'). 585 | - `$passes` (int) - The number of time to apply the filter, enhancing the effect (default 1). 586 | 587 | Returns a SimpleImage object. 588 | 589 | #### `brighten($percentage)` 590 | 591 | Applies the brightness filter to brighten the image. 592 | 593 | - `$percentage`* (int) - Percentage to brighten the image (0 - 100). 594 | 595 | Returns a SimpleImage object. 596 | 597 | #### `colorize($color)` 598 | 599 | Applies the colorize filter. 600 | 601 | - `$color`* (string|array) - The filter color. 602 | 603 | Returns a SimpleImage object. 604 | 605 | #### `contrast($percentage)` 606 | 607 | Applies the contrast filter. 608 | 609 | - `$percentage`* (int) - Percentage to adjust (-100 - 100). 610 | 611 | Returns a SimpleImage object. 612 | 613 | #### `darken($percentage)` 614 | 615 | Applies the brightness filter to darken the image. 616 | 617 | - `$percentage`* (int) - Percentage to darken the image (0 - 100). 618 | 619 | Returns a SimpleImage object. 620 | 621 | #### `desaturate()` 622 | 623 | Applies the desaturate (grayscale) filter. 624 | 625 | Returns a SimpleImage object. 626 | 627 | #### `duotone($lightColor, $darkColor)` 628 | 629 | Applies the duotone filter to the image. 630 | 631 | - `$lightColor`* (string|array) - The lightest color in the duotone. 632 | - `$darkColor`* (string|array) - The darkest color in the duotone. 633 | 634 | Returns a SimpleImage object. 635 | 636 | #### `edgeDetect()` 637 | 638 | Applies the edge detect filter. 639 | 640 | Returns a SimpleImage object. 641 | 642 | #### `emboss()` 643 | 644 | Applies the emboss filter. 645 | 646 | Returns a SimpleImage object. 647 | 648 | #### `invert()` 649 | 650 | Inverts the image's colors. 651 | 652 | Returns a SimpleImage object. 653 | 654 | #### `opacity()` 655 | 656 | Changes the image's opacity level. 657 | 658 | - `$opacity`* (float) - The desired opacity level (0 - 1). 659 | 660 | Returns a SimpleImage object. 661 | 662 | #### `pixelate($size)` 663 | 664 | Applies the pixelate filter. 665 | 666 | - `$size` (int) - The size of the blocks in pixels (default 10). 667 | 668 | Returns a SimpleImage object. 669 | 670 | #### `sepia()` 671 | 672 | Simulates a sepia effect by desaturating the image and applying a sepia tone. 673 | 674 | Returns a SimpleImage object. 675 | 676 | #### `sharpen($amount)` 677 | 678 | Sharpens the image. 679 | 680 | - `$amount` (int) - Sharpening amount (1 - 100, default 50) 681 | 682 | Returns a SimpleImage object. 683 | 684 | #### `sketch()` 685 | 686 | Applies the mean remove filter to produce a sketch effect. 687 | 688 | Returns a SimpleImage object. 689 | 690 | ### Color utilities 691 | 692 | #### `(static) adjustColor($color, $red, $green, $blue, $alpha)` 693 | 694 | Adjusts a color by increasing/decreasing red/green/blue/alpha values independently. 695 | 696 | - `$color`* (string|array) - The color to adjust. 697 | - `$red`* (int) - Red adjustment (-255 - 255). 698 | - `$green`* (int) - Green adjustment (-255 - 255). 699 | - `$blue`* (int) - Blue adjustment (-255 - 255). 700 | - `$alpha`* (float) - Alpha adjustment (-1 - 1). 701 | 702 | Returns an RGBA color array. 703 | 704 | #### `(static) darkenColor($color, $amount)` 705 | 706 | Darkens a color. 707 | 708 | - `$color`* (string|array) - The color to darken. 709 | - `$amount`* (int) - Amount to darken (0 - 255). 710 | 711 | Returns an RGBA color array. 712 | 713 | #### `extractColors($count = 10, $backgroundColor = null)` 714 | 715 | Extracts colors from an image like a human would do.™ This method requires the third-party library \League\ColorExtractor. If you're using Composer, it will be installed for you automatically. 716 | 717 | - `$count` (int) - The max number of colors to extract (default 5). 718 | - `$backgroundColor` (string|array) - By default any pixel with alpha value greater than zero will be discarded. This is because transparent colors are not perceived as is. For example, fully transparent black would be seen white on a white background. So if you want to take transparency into account, you have to specify a default background color. 719 | 720 | Returns an array of RGBA colors arrays. 721 | 722 | #### `getColorAt($x, $y)` 723 | 724 | Gets the RGBA value of a single pixel. 725 | 726 | - `$x`* (int) - The horizontal position of the pixel. 727 | - `$y`* (int) - The vertical position of the pixel. 728 | 729 | Returns an RGBA color array or false if the x/y position is off the canvas. 730 | 731 | #### `(static) lightenColor($color, $amount)` 732 | 733 | Lightens a color. 734 | 735 | - `$color`* (string|array) - The color to lighten. 736 | - `$amount`* (int) - Amount to darken (0 - 255). 737 | 738 | Returns an RGBA color array. 739 | 740 | #### `(static) normalizeColor($color)` 741 | 742 | Normalizes a hex or array color value to a well-formatted RGBA array. 743 | 744 | - `$color`* (string|array) - A CSS color name, hex string, or an array [red, green, blue, alpha]. 745 | 746 | You can pipe alpha transparency through hex strings and color names. For example: 747 | 748 | #fff|0.50 <-- 50% white 749 | red|0.25 <-- 25% red 750 | 751 | Returns an array: [red, green, blue, alpha] 752 | 753 | ### Exceptions 754 | 755 | SimpleImage throws standard exceptions when things go wrong. You should always use a try/catch block around your code to properly handle them. 756 | 757 | ```php 758 | getMessage(); 764 | } 765 | ``` 766 | 767 | To check for specific errors, compare `$err->getCode()` to the defined error constants. 768 | 769 | ```php 770 | getCode() === $image::ERR_FILE_NOT_FOUND) { 776 | echo 'File not found!'; 777 | } else { 778 | echo $err->getMessage(); 779 | } 780 | } 781 | ``` 782 | 783 | As a best practice, always use the defined constants instead of their integers values. The values will likely change in future versions, and WILL NOT be considered a breaking change. 784 | 785 | - `ERR_FILE_NOT_FOUND` - The specified file could not be found or loaded for some reason. 786 | - `ERR_FONT_FILE` - The specified font file could not be loaded. 787 | - `ERR_FREETYPE_NOT_ENABLED` - Freetype support is not enabled in your version of PHP. 788 | - `ERR_GD_NOT_ENABLED` - The GD extension is not enabled in your version of PHP. 789 | - `ERR_LIB_NOT_LOADED` - A required library has not been loaded. 790 | - `ERR_INVALID_COLOR` - An invalid color value was passed as an argument. 791 | - `ERR_INVALID_DATA_URI` - The specified data URI is not valid. 792 | - `ERR_INVALID_IMAGE` - The specified image is not valid. 793 | - `ERR_UNSUPPORTED_FORMAT` - The image format specified is not valid. 794 | - `ERR_WEBP_NOT_ENABLED` - WEBP support is not enabled in your version of PHP. 795 | - `ERR_WRITE` - Unable to write to the file system. 796 | - `ERR_INVALID_FLAG` - The specified flag key does not exist. 797 | 798 | ### Useful Things To Know 799 | 800 | - Color arguments can be a CSS color name (e.g. `LightBlue`), a hex color string (e.g. `#0099dd`), or an RGB(A) array (e.g. `['red' => 255, 'green' => 0, 'blue' => 0, 'alpha' => 1]`). 801 | 802 | - When `$thickness` > 1, GD draws lines of the desired thickness from the center origin. For example, a rectangle drawn at [10, 10, 20, 20] with a thickness of 3 will actually be draw at [9, 9, 21, 21]. This is true for all shapes and is not a bug in the SimpleImage library. 803 | 804 | ### Instance flags 805 | 806 | Tweak the behavior of a SimpleImage instance by setting instance flag values with the `setFlag($key, $value)` method. 807 | 808 | ```php 809 | $image = new \claviska\SimpleImage('image.jpeg')->setFlag("foo", "bar"); 810 | ``` 811 | 812 | You can also pass an associative array to the SimpleImage constructor to set instance flags. 813 | 814 | ```php 815 | $image = new \claviska\SimpleImage('image.jpeg', ['foo' => 'bar']); 816 | // .. or without an $image 817 | $image = new \claviska\SimpleImage(flags: ['foo' => 'bar']); 818 | ``` 819 | 820 | *Note: `setFlag()` throws an `ERR_INVALID_FLAG` exception if the key does not exist (no default value).* 821 | 822 | #### `sslVerify` 823 | 824 | Setting `sslVerify` to `false` (defaults to `true`) will make all images loaded over HTTPS forgo certificate peer validation. This is especially usefull for self-signed certificates. 825 | 826 | ```php 827 | $image = new \claviska\SimpleImage('https://localhost/image.jpeg', ['sslVerify' => false]); 828 | // Would normally throw an OpenSSL exception, but is ignored with the sslVerify flag set to false. 829 | ``` 830 | 831 | ## Differences from SimpleImage 2.x 832 | 833 | - Normalized color arguments (colors can be a CSS color name, hex color, or RGB(A) array). 834 | - Normalized alpha (opacity) arguments: 0 (transparent) - 1 (opaque) 835 | - Added text shadow to `text` method. 836 | - Added `fromString()` method to load images from strings. 837 | - Added `toString()` method to generate image strings. 838 | - Added `arc` method for drawing arcs. 839 | - Added `border` method for drawing borders. 840 | - Added `dot` method for drawing individual pixels. 841 | - Added `ellipse` method for drawing ellipses and circles. 842 | - Added `line` method for drawing lines. 843 | - Added `polygon` method for drawing polygons. 844 | - Added `rectangle` method for drawing rectangles. 845 | - Added `roundedRectangle` method for drawing rounded rectangles. 846 | - Added `adjustColor` method for modifying RGBA color channels to create relative color variations. 847 | - Added `darkenColor` method to darken a color. 848 | - Added `extractColors` method to get the most common colors from the image. 849 | - Added `getColorAt` method to get the RGBA values of a specific pixel. 850 | - Added `lightenColor` method to lighten a color. 851 | - Added `toDownload` method to force the image to download on the client's machine. 852 | - Added `duotone` filter to create duotone images. 853 | - Added `sharpen` method to sharpen the image. 854 | - Changed namespace from `abeautifulsite` to `claviska`. 855 | - Changed `create` method to `fromNew`. 856 | - Changed `load` method to `fromFile`. 857 | - Changed `load_base64` method to `fromDataUri`. 858 | - Changed `output` method to `toScreen`.x 859 | - Changed `output_base64` method to `toDataUri`. 860 | - Changed `save` method to `toFile`. 861 | - Changed `text` method to accept an array of options instead of tons of arguments. 862 | - Removed text stroke from `text` method because it produced dirty results and didn't support transparency. 863 | - Removed `smooth` method because its arguments in the PHP manual aren't documented well. 864 | - Removed deprecated method `adaptive_resize` (use `thumbnail` instead). 865 | - Removed `get_meta_data` (use `getExif`, `getHeight`, `getMime`, `getOrientation`, and `getWidth` instead). 866 | - Added [.editorconfig](http://editorconfig.org/) file. Please make sure your editor supports these settings before submitting contributions. 867 | - Switched from four spaces to two for indentations (sorry PHP-FIG!). 868 | - Switched from underscore_methods to camelCaseMethods. 869 | - Organized methods into groups based on function 870 | - Removed PHPDoc comments. At this time, I don't wish to incorporate them into the library. 871 | -------------------------------------------------------------------------------- /src/claviska/SimpleImage.php: -------------------------------------------------------------------------------- 1 | . 9 | // 10 | // Copyright A Beautiful Site, LLC. 11 | // 12 | // Source: https://github.com/claviska/SimpleImage 13 | // 14 | // Licensed under the MIT license 15 | // 16 | 17 | namespace claviska; 18 | 19 | use Exception; 20 | use GdImage; 21 | use League\ColorExtractor\Color; 22 | use League\ColorExtractor\ColorExtractor; 23 | use League\ColorExtractor\Palette; 24 | 25 | /** 26 | * A PHP class that makes working with images as simple as possible. 27 | */ 28 | class SimpleImage 29 | { 30 | public const 31 | ERR_FILE_NOT_FOUND = 1; 32 | 33 | public const 34 | ERR_FONT_FILE = 2; 35 | 36 | public const 37 | ERR_FREETYPE_NOT_ENABLED = 3; 38 | 39 | public const 40 | ERR_GD_NOT_ENABLED = 4; 41 | 42 | public const 43 | ERR_INVALID_COLOR = 5; 44 | 45 | public const 46 | ERR_INVALID_DATA_URI = 6; 47 | 48 | public const 49 | ERR_INVALID_IMAGE = 7; 50 | 51 | public const 52 | ERR_LIB_NOT_LOADED = 8; 53 | 54 | public const 55 | ERR_UNSUPPORTED_FORMAT = 9; 56 | 57 | public const 58 | ERR_WEBP_NOT_ENABLED = 10; 59 | 60 | public const 61 | ERR_WRITE = 11; 62 | 63 | public const 64 | ERR_INVALID_FLAG = 12; 65 | 66 | protected array $flags; 67 | 68 | protected $image = null; 69 | 70 | protected string $mimeType; 71 | 72 | protected null|array|false $exif = null; 73 | 74 | ////////////////////////////////////////////////////////////////////////////////////////////////// 75 | // Magic methods 76 | ////////////////////////////////////////////////////////////////////////////////////////////////// 77 | 78 | /** 79 | * Creates a new SimpleImage object. 80 | * 81 | * @param string $image An image file or a data URI to load. 82 | * @param array $flags Optional override of default flags. 83 | * 84 | * @throws Exception Thrown if the GD library is not found; file|URI or image data is invalid. 85 | */ 86 | public function __construct(string $image = '', array $flags = []) 87 | { 88 | // Check for the required GD extension 89 | if (extension_loaded('gd')) { 90 | // Ignore JPEG warnings that cause imagecreatefromjpeg() to fail 91 | ini_set('gd.jpeg_ignore_warning', '1'); 92 | } else { 93 | throw new Exception('Required extension GD is not loaded.', self::ERR_GD_NOT_ENABLED); 94 | } 95 | 96 | // Associative array of flags. 97 | $this->flags = [ 98 | 'sslVerify' => true, // Skip SSL peer validation 99 | ]; 100 | 101 | // Override default flag values. 102 | foreach ($flags as $flag => $value) { 103 | $this->setFlag($flag, $value); 104 | } 105 | 106 | // Load an image through the constructor 107 | if (preg_match('/^data:(.*?);/', $image)) { 108 | $this->fromDataUri($image); 109 | } elseif ($image) { 110 | $this->fromFile($image); 111 | } 112 | } 113 | 114 | /** 115 | * Destroys the image resource. 116 | */ 117 | public function __destruct() 118 | { 119 | $this->reset(); 120 | } 121 | 122 | ////////////////////////////////////////////////////////////////////////////////////////////////// 123 | // Helper functions 124 | ////////////////////////////////////////////////////////////////////////////////////////////////// 125 | 126 | /** 127 | * Checks if the SimpleImage object has loaded an image. 128 | */ 129 | public function hasImage(): bool 130 | { 131 | return $this->image instanceof GdImage; 132 | } 133 | 134 | /** 135 | * @deprecated 4.4.0 Has no effect anymore 136 | */ 137 | public function reset(): static 138 | { 139 | return $this; 140 | } 141 | 142 | /** 143 | * Set flag value. 144 | * 145 | * @param string $flag Name of the flag to set. 146 | * @param bool $value State of the flag. 147 | * 148 | * @throws Exception Thrown if flag does not exist (no default value). 149 | */ 150 | public function setFlag(string $flag, bool $value): void 151 | { 152 | // Throw if flag does not exist 153 | if (! in_array($flag, array_keys($this->flags))) { 154 | throw new Exception('Invalid flag.', self::ERR_INVALID_FLAG); 155 | } 156 | 157 | // Set flag value by name 158 | $this->flags[$flag] = $value; 159 | } 160 | 161 | /** 162 | * Get flag value. 163 | * 164 | * @param string $flag Name of the flag to get. 165 | */ 166 | public function getFlag(string $flag): ?bool 167 | { 168 | return in_array($flag, array_keys($this->flags)) ? $this->flags[$flag] : null; 169 | } 170 | 171 | ////////////////////////////////////////////////////////////////////////////////////////////////// 172 | // Loaders 173 | ////////////////////////////////////////////////////////////////////////////////////////////////// 174 | 175 | /** 176 | * Loads an image from a data URI. 177 | * 178 | * @param string $uri A data URI. 179 | * @return SimpleImage 180 | * 181 | * @throws Exception Thrown if URI or image data is invalid. 182 | */ 183 | public function fromDataUri(string $uri): static 184 | { 185 | // Basic formatting check 186 | preg_match('/^data:(.*?);/', $uri, $matches); 187 | if (! count($matches)) { 188 | throw new Exception('Invalid data URI.', self::ERR_INVALID_DATA_URI); 189 | } 190 | 191 | // Determine mime type 192 | $mimeType = $matches[1]; 193 | if (! preg_match('/^image\/(gif|jpeg|png)$/', $mimeType)) { 194 | throw new Exception( 195 | 'Unsupported format: ' . $mimeType, 196 | self::ERR_UNSUPPORTED_FORMAT 197 | ); 198 | } 199 | 200 | // Get image data 201 | $data = base64_decode(strval(preg_replace('/^data:(.*?);base64,/', '', $uri))); 202 | 203 | $this->fromString($data, $mimeType); 204 | 205 | return $this; 206 | } 207 | 208 | /** 209 | * Loads an image from a file. 210 | * 211 | * @param string $file The image file to load. 212 | * @return SimpleImage 213 | * 214 | * @throws Exception Thrown if file or image data is invalid. 215 | */ 216 | public function fromFile(string $file): static 217 | { 218 | // Set fopen options. 219 | $sslVerify = $this->getFlag('sslVerify'); // Don't perform peer validation when true 220 | $opts = [ 221 | 'ssl' => [ 222 | 'verify_peer' => $sslVerify, 223 | 'verify_peer_name' => $sslVerify, 224 | ], 225 | ]; 226 | 227 | // Check if the file exists and is readable. 228 | $data = @file_get_contents($file, false, stream_context_create($opts)); 229 | if ($data === false) { 230 | throw new Exception("File not found: $file", self::ERR_FILE_NOT_FOUND); 231 | } 232 | 233 | $this->fromString($data); 234 | 235 | return $this; 236 | } 237 | 238 | /** 239 | * Creates a new image from an image data string. 240 | * 241 | * @param string $data 242 | * @param string|null $mimeType 243 | * @return SimpleImage 244 | * @throws Exception 245 | */ 246 | public function fromString(string $data, ?string $mimeType = null): static 247 | { 248 | 249 | if($mimeType === null) { 250 | // Get image info 251 | $info = @getimagesizefromstring($data); 252 | if ($info === false) { 253 | throw new Exception("Invalid image data", self::ERR_INVALID_IMAGE); 254 | } 255 | $this->mimeType = $info['mime']; 256 | } else { 257 | $this->mimeType = $mimeType; 258 | } 259 | 260 | // Create image object from string 261 | $this->image = imagecreatefromstring($data); 262 | 263 | if (! $this->image) { 264 | throw new Exception('Unsupported format: '.$this->mimeType, self::ERR_UNSUPPORTED_FORMAT); 265 | } 266 | 267 | switch($this->mimeType) { 268 | case 'image/gif': 269 | // Copy the gif over to a true color image to preserve its transparency. This is a 270 | // workaround to prevent imagepalettetotruecolor() from borking transparency. 271 | $width = imagesx($this->image); 272 | $height = imagesx($this->image); 273 | 274 | $gif = imagecreatetruecolor((int) $width, (int) $height); 275 | $alpha = imagecolorallocatealpha($gif, 0, 0, 0, 127); 276 | imagecolortransparent($gif, $alpha ?: null); 277 | imagefill($gif, 0, 0, $alpha); 278 | 279 | imagecopy($this->image, $gif, 0, 0, 0, 0, $width, $height); 280 | imagedestroy($gif); 281 | break; 282 | case 'image/jpeg': 283 | // Load exif data from JPEG images 284 | if (function_exists('exif_read_data')) { 285 | $this->exif = @exif_read_data('data://image/jpeg;base64,'.base64_encode($data)); 286 | } 287 | break; 288 | } 289 | 290 | // Convert palette images to true color images 291 | imagepalettetotruecolor($this->image); 292 | 293 | return $this; 294 | } 295 | 296 | /** 297 | * Creates a new image. 298 | * 299 | * @param int $width The width of the image. 300 | * @param int $height The height of the image. 301 | * @param string|array $color Optional fill color for the new image (default 'transparent'). 302 | * @return SimpleImage 303 | * 304 | * @throws Exception 305 | */ 306 | public function fromNew(int $width, int $height, string|array $color = 'transparent'): static 307 | { 308 | $this->image = imagecreatetruecolor($width, $height); 309 | 310 | // Use PNG for dynamically created images because it's lossless and supports transparency 311 | $this->mimeType = 'image/png'; 312 | 313 | // Fill the image with color 314 | $this->fill($color); 315 | 316 | return $this; 317 | } 318 | 319 | 320 | ////////////////////////////////////////////////////////////////////////////////////////////////// 321 | // Savers 322 | ////////////////////////////////////////////////////////////////////////////////////////////////// 323 | 324 | /** 325 | * Generates an image. 326 | * 327 | * @param string|null $mimeType The image format to output as a mime type (defaults to the original mime type). 328 | * @param array|int $options Array or Image quality as a percentage (default 100). 329 | * @return array Returns an array containing the image data and mime type ['data' => '', 'mimeType' => '']. 330 | * 331 | * @throws Exception Thrown when WEBP support is not enabled or unsupported format. 332 | */ 333 | public function generate(string|null $mimeType = null, array|int $options = 100): array 334 | { 335 | // Format defaults to the original mime type 336 | $mimeType = $mimeType ?: $this->mimeType; 337 | 338 | $quality = null; 339 | // allow $options to be an int for backwards compatibility to v3 340 | if (is_int($options)) { 341 | $quality = $options; 342 | $options = []; 343 | } 344 | 345 | // get quality if passed as an option 346 | if (is_array($options) && array_key_exists('quality', $options)) { 347 | $quality = intval($options['quality']); 348 | } 349 | 350 | // Ensure quality is a valid integer 351 | if ($quality === null) { 352 | $quality = 100; 353 | } 354 | $quality = (int) round(self::keepWithin((int) $quality, 0, 100)); 355 | 356 | $alpha = true; 357 | // get alpha if passed as an option 358 | if (is_array($options) && array_key_exists('alpha', $options)) { 359 | $alpha = boolval($options['alpha']); 360 | } 361 | 362 | $interlace = null; // keep the same 363 | // get interlace if passed as an option 364 | if (is_array($options) && array_key_exists('interlace', $options)) { 365 | $interlace = boolval($options['interlace']); 366 | } 367 | 368 | // get raw stream from image* functions in providing no path 369 | $file = null; 370 | 371 | // Capture output 372 | ob_start(); 373 | 374 | // Generate the image 375 | switch($mimeType) { 376 | case 'image/gif': 377 | imagesavealpha($this->image, $alpha); 378 | imagegif($this->image, $file); 379 | break; 380 | case 'image/jpeg': 381 | imageinterlace($this->image, $interlace); 382 | imagejpeg($this->image, $file, $quality); 383 | break; 384 | case 'image/png': 385 | $filters = -1; // imagepng default 386 | // get filters if passed as an option 387 | if (is_array($options) && array_key_exists('filters', $options)) { 388 | $filters = intval($options['filters']); 389 | } 390 | // compression param is called quality in imagepng but that would be 391 | // misleading in context of SimpleImage 392 | $compression = -1; // defaults to zlib default which is 6 393 | // get compression if passed as an option 394 | if (is_array($options) && array_key_exists('compression', $options)) { 395 | $compression = intval($options['compression']); 396 | } 397 | if ($compression !== -1) { 398 | $compression = (int) round(self::keepWithin($compression, 0, 10)); 399 | } 400 | imagesavealpha($this->image, $alpha); 401 | imagepng($this->image, $file, $compression, $filters); 402 | break; 403 | case 'image/webp': 404 | // Not all versions of PHP will have webp support enabled 405 | if (! function_exists('imagewebp')) { 406 | throw new Exception( 407 | 'WEBP support is not enabled in your version of PHP.', 408 | self::ERR_WEBP_NOT_ENABLED 409 | ); 410 | } 411 | // useless but recommended, see https://www.php.net/manual/en/function.imagesavealpha.php 412 | imagesavealpha($this->image, $alpha); 413 | imagewebp($this->image, $file, $quality); 414 | break; 415 | case 'image/bmp': 416 | case 'image/x-ms-bmp': 417 | case 'image/x-windows-bmp': 418 | // Not all versions of PHP support bmp 419 | if (! function_exists('imagebmp')) { 420 | throw new Exception( 421 | 'BMP support is not available in your version of PHP.', 422 | self::ERR_UNSUPPORTED_FORMAT 423 | ); 424 | } 425 | $compression = true; // imagebmp default 426 | // get compression if passed as an option 427 | if (is_array($options) && array_key_exists('compression', $options)) { 428 | $compression = is_int($options['compression']) ? 429 | $options['compression'] > 0 : boolval($options['compression']); 430 | } 431 | imageinterlace($this->image, $interlace); 432 | imagebmp($this->image, $file, $compression); 433 | break; 434 | case 'image/avif': 435 | // Not all versions of PHP support avif 436 | if (! function_exists('imageavif')) { 437 | throw new Exception( 438 | 'AVIF support is not available in your version of PHP.', 439 | self::ERR_UNSUPPORTED_FORMAT 440 | ); 441 | } 442 | $speed = -1; // imageavif default 443 | // get speed if passed as an option 444 | if (is_array($options) && array_key_exists('speed', $options)) { 445 | $speed = intval($options['speed']); 446 | $speed = self::keepWithin($speed, 0, 10); 447 | } 448 | // useless but recommended, see https://www.php.net/manual/en/function.imagesavealpha.php 449 | imagesavealpha($this->image, $alpha); 450 | imageavif($this->image, $file, $quality, $speed); 451 | break; 452 | default: 453 | throw new Exception('Unsupported format: '.$mimeType, self::ERR_UNSUPPORTED_FORMAT); 454 | } 455 | 456 | // Stop capturing 457 | $data = ob_get_contents(); 458 | ob_end_clean(); 459 | 460 | return [ 461 | 'data' => $data, 462 | 'mimeType' => $mimeType, 463 | ]; 464 | } 465 | 466 | /** 467 | * Generates a data URI. 468 | * 469 | * @param string|null $mimeType The image format to output as a mime type (defaults to the original mime type). 470 | * @param array|int $options Array or Image quality as a percentage (default 100). 471 | * @return string Returns a string containing a data URI. 472 | * 473 | * @throws Exception 474 | */ 475 | public function toDataUri(string|null $mimeType = null, array|int $options = 100): string 476 | { 477 | $image = $this->generate($mimeType, $options); 478 | 479 | return 'data:'.$image['mimeType'].';base64,'.base64_encode($image['data']); 480 | } 481 | 482 | /** 483 | * Forces the image to be downloaded to the clients machine. Must be called before any output is sent to the screen. 484 | * 485 | * @param string $filename The filename (without path) to send to the client (e.g. 'image.jpeg'). 486 | * @param string|null $mimeType The image format to output as a mime type (defaults to the original mime type). 487 | * @param array|int $options Array or Image quality as a percentage (default 100). 488 | * @return SimpleImage 489 | * 490 | * @throws Exception 491 | */ 492 | public function toDownload(string $filename, string|null $mimeType = null, array|int $options = 100): static 493 | { 494 | $image = $this->generate($mimeType, $options); 495 | 496 | // Set download headers 497 | header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); 498 | header('Content-Description: File Transfer'); 499 | header('Content-Length: '.strlen($image['data'])); 500 | header('Content-Transfer-Encoding: Binary'); 501 | header('Content-Type: application/octet-stream'); 502 | header("Content-Disposition: attachment; filename=\"$filename\""); 503 | 504 | echo $image['data']; 505 | 506 | return $this; 507 | } 508 | 509 | /** 510 | * Writes the image to a file. 511 | * 512 | * @param string $file The image format to output as a mime type (defaults to the original mime type). 513 | * @param string|null $mimeType Image quality as a percentage (default 100). 514 | * @param array|int $options Array or Image quality as a percentage (default 100). 515 | * @return SimpleImage 516 | * 517 | * @throws Exception Thrown if failed write to file. 518 | */ 519 | public function toFile(string $file, string|null $mimeType = null, array|int $options = 100): static 520 | { 521 | $image = $this->generate($mimeType, $options); 522 | 523 | // Save the image to file 524 | if (! file_put_contents($file, $image['data'])) { 525 | throw new Exception("Failed to write image to file: $file", self::ERR_WRITE); 526 | } 527 | 528 | return $this; 529 | } 530 | 531 | /** 532 | * Outputs the image to the screen. Must be called before any output is sent to the screen. 533 | * 534 | * @param string|null $mimeType The image format to output as a mime type (defaults to the original mime type). 535 | * @param array|int $options Array or Image quality as a percentage (default 100). 536 | * @return SimpleImage 537 | * 538 | * @throws Exception 539 | */ 540 | public function toScreen(string|null $mimeType = null, array|int $options = 100): static 541 | { 542 | $image = $this->generate($mimeType, $options); 543 | 544 | // Output the image to stdout 545 | header('Content-Type: '.$image['mimeType']); 546 | echo $image['data']; 547 | 548 | return $this; 549 | } 550 | 551 | /** 552 | * Generates an image string. 553 | * 554 | * @param string|null $mimeType The image format to output as a mime type (defaults to the original mime type). 555 | * @param array|int $options Array or Image quality as a percentage (default 100). 556 | * 557 | * @throws Exception 558 | */ 559 | public function toString(string|null $mimeType = null, array|int $options = 100): string 560 | { 561 | return $this->generate($mimeType, $options)['data']; 562 | } 563 | 564 | ////////////////////////////////////////////////////////////////////////////////////////////////// 565 | // Utilities 566 | ////////////////////////////////////////////////////////////////////////////////////////////////// 567 | /** 568 | * Ensures a numeric value is always within the min and max range. 569 | * 570 | * @param int|float $value A numeric value to test. 571 | * @param int|float $min The minimum allowed value. 572 | * @param int|float $max The maximum allowed value. 573 | */ 574 | protected static function keepWithin(int|float $value, int|float $min, int|float $max): int|float 575 | { 576 | if ($value < $min) { 577 | return $min; 578 | } 579 | if ($value > $max) { 580 | return $max; 581 | } 582 | 583 | return $value; 584 | } 585 | 586 | /** 587 | * Gets the image's current aspect ratio. 588 | * 589 | * @return float|int Returns the aspect ratio as a float. 590 | */ 591 | public function getAspectRatio(): float|int 592 | { 593 | return $this->getWidth() / $this->getHeight(); 594 | } 595 | 596 | /** 597 | * Gets the image's exif data. 598 | * 599 | * @return array|null Returns an array of exif data or null if no data is available. 600 | */ 601 | public function getExif(): ?array 602 | { 603 | // returns null if exif value is falsy: null, false or empty array. 604 | return $this->exif ?: null; 605 | } 606 | 607 | /** 608 | * Gets the image's current height. 609 | */ 610 | public function getHeight(): int 611 | { 612 | return (int) imagesy($this->image); 613 | } 614 | 615 | /** 616 | * Gets the mime type of the loaded image. 617 | */ 618 | public function getMimeType(): string 619 | { 620 | return $this->mimeType; 621 | } 622 | 623 | /** 624 | * Gets the corresponding extension for the current MIME type 625 | * 626 | * @throws Exception Thrown if invalid MIME type found (edge case) 627 | */ 628 | public function getExtension(): string 629 | { 630 | return match (strtolower($this->mimeType)) { 631 | 'image/jpeg' => 'jpg', 632 | 'image/png' => 'png', 633 | 'image/gif' => 'gif', 634 | 'image/webp' => 'webp', 635 | 'image/avif' => 'avif', 636 | 'image/bmp', 'image/x-ms-bmp', 'image/x-windows-bmp' => 'bmp', 637 | //other image types supported are WAP BMP and GD's internal pics 638 | default => throw new \Exception( 639 | 'Unsupported format: '.$this->mimeType, 640 | self::ERR_UNSUPPORTED_FORMAT 641 | ) 642 | }; 643 | } 644 | 645 | /** 646 | * Gets the image's current orientation. 647 | * 648 | * @return string One of the values: 'landscape', 'portrait', or 'square' 649 | */ 650 | public function getOrientation(): string 651 | { 652 | $width = $this->getWidth(); 653 | $height = $this->getHeight(); 654 | 655 | if ($width > $height) { 656 | return 'landscape'; 657 | } 658 | if ($width < $height) { 659 | return 'portrait'; 660 | } 661 | 662 | return 'square'; 663 | } 664 | 665 | /** 666 | * Gets the resolution of the image 667 | * 668 | * @return array|bool The resolution as an array of integers: [96, 96] 669 | */ 670 | public function getResolution(): bool|array 671 | { 672 | return imageresolution($this->image); 673 | } 674 | 675 | /** 676 | * Gets the image's current width. 677 | */ 678 | public function getWidth(): int 679 | { 680 | return (int) imagesx($this->image); 681 | } 682 | 683 | ////////////////////////////////////////////////////////////////////////////////////////////////// 684 | // Manipulation 685 | ////////////////////////////////////////////////////////////////////////////////////////////////// 686 | 687 | /** 688 | * Same as PHP's imagecopymerge, but works with transparent images. Used internally for overlay. 689 | * 690 | * @param GdImage $dstIm Destination image. 691 | * @param GdImage $srcIm Source image. 692 | * @param int $dstX x-coordinate of destination point. 693 | * @param int $dstY y-coordinate of destination point. 694 | * @param int $srcX x-coordinate of source point. 695 | * @param int $srcY y-coordinate of source point. 696 | * @param int $srcW Source width. 697 | * @param int $srcH Source height. 698 | * @return bool true if success. 699 | */ 700 | protected static function imageCopyMergeAlpha(GdImage $dstIm, GdImage $srcIm, int $dstX, int $dstY, int $srcX, int $srcY, int $srcW, int $srcH, int $pct): bool 701 | { 702 | // Are we merging with transparency? 703 | if ($pct < 100) { 704 | // Disable alpha blending and "colorize" the image using a transparent color 705 | imagealphablending($srcIm, false); 706 | imagefilter($srcIm, IMG_FILTER_COLORIZE, 0, 0, 0, round(127 * ((100 - $pct) / 100))); 707 | } 708 | 709 | imagecopy($dstIm, $srcIm, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH); 710 | 711 | return true; 712 | } 713 | 714 | /** 715 | * Rotates an image so the orientation will be correct based on its exif data. It is safe to call 716 | * this method on images that don't have exif data (no changes will be made). 717 | * 718 | * @return SimpleImage 719 | * 720 | * @throws Exception 721 | */ 722 | public function autoOrient(): static 723 | { 724 | $exif = $this->getExif(); 725 | 726 | if (! $exif || ! isset($exif['Orientation'])) { 727 | return $this; 728 | } 729 | 730 | switch($exif['Orientation']) { 731 | case 1: // Do nothing! 732 | break; 733 | case 2: // Flip horizontally 734 | $this->flip('x'); 735 | break; 736 | case 3: // Rotate 180 degrees 737 | $this->rotate(180); 738 | break; 739 | case 4: // Flip vertically 740 | $this->flip('y'); 741 | break; 742 | case 5: // Rotate 90 degrees clockwise and flip vertically 743 | $this->flip('y')->rotate(90); 744 | break; 745 | case 6: // Rotate 90 clockwise 746 | $this->rotate(90); 747 | break; 748 | case 7: // Rotate 90 clockwise and flip horizontally 749 | $this->flip('x')->rotate(90); 750 | break; 751 | case 8: // Rotate 90 counterclockwise 752 | $this->rotate(-90); 753 | break; 754 | } 755 | 756 | return $this; 757 | } 758 | 759 | /** 760 | * Proportionally resize the image to fit inside a specific width and height. 761 | * 762 | * @param int $maxWidth The maximum width the image can be. 763 | * @param int $maxHeight The maximum height the image can be. 764 | * @return SimpleImage 765 | */ 766 | public function bestFit(int $maxWidth, int $maxHeight): static 767 | { 768 | // If the image already fits, there's nothing to do 769 | if ($this->getWidth() <= $maxWidth && $this->getHeight() <= $maxHeight) { 770 | return $this; 771 | } 772 | 773 | // Calculate max width or height based on orientation 774 | if ($this->getOrientation() === 'portrait') { 775 | $height = $maxHeight; 776 | $width = (int) round($maxHeight * $this->getAspectRatio()); 777 | } else { 778 | $width = $maxWidth; 779 | $height = (int) round($maxWidth / $this->getAspectRatio()); 780 | } 781 | 782 | // Reduce to max width 783 | if ($width > $maxWidth) { 784 | $width = $maxWidth; 785 | $height = (int) round($width / $this->getAspectRatio()); 786 | } 787 | 788 | // Reduce to max height 789 | if ($height > $maxHeight) { 790 | $height = $maxHeight; 791 | $width = (int) round($height * $this->getAspectRatio()); 792 | } 793 | 794 | return $this->resize($width, $height); 795 | } 796 | 797 | /** 798 | * Crop the image. 799 | * 800 | * @param int|float $x1 Top left x coordinate. 801 | * @param int|float $y1 Top left y coordinate. 802 | * @param int|float $x2 Bottom right x coordinate. 803 | * @param int|float $y2 Bottom right x coordinate. 804 | * @return SimpleImage 805 | */ 806 | public function crop(int|float $x1, int|float $y1, int|float $x2, int|float $y2): static 807 | { 808 | // Keep crop within image dimensions 809 | $x1 = self::keepWithin($x1, 0, $this->getWidth()); 810 | $x2 = self::keepWithin($x2, 0, $this->getWidth()); 811 | $y1 = self::keepWithin($y1, 0, $this->getHeight()); 812 | $y2 = self::keepWithin($y2, 0, $this->getHeight()); 813 | 814 | // Avoid using native imagecrop() because of a bug with PNG transparency 815 | $dstW = abs($x2 - $x1); 816 | $dstH = abs($y2 - $y1); 817 | $newImage = imagecreatetruecolor((int) $dstW, (int) $dstH); 818 | $transparentColor = imagecolorallocatealpha($newImage, 0, 0, 0, 127); 819 | imagecolortransparent($newImage, $transparentColor ?: null); 820 | imagefill($newImage, 0, 0, $transparentColor); 821 | 822 | // Crop it 823 | imagecopyresampled( 824 | $newImage, 825 | $this->image, 826 | 0, 827 | 0, 828 | (int) round(min($x1, $x2)), 829 | (int) round(min($y1, $y2)), 830 | (int) $dstW, 831 | (int) $dstH, 832 | (int) $dstW, 833 | (int) $dstH 834 | ); 835 | 836 | // Swap out the new image 837 | $this->image = $newImage; 838 | 839 | return $this; 840 | } 841 | 842 | /** 843 | * Applies a duotone filter to the image. 844 | * 845 | * @param string|array $lightColor The lightest color in the duotone. 846 | * @param string|array $darkColor The darkest color in the duotone. 847 | * @return SimpleImage 848 | * 849 | * @throws Exception 850 | */ 851 | public function duotone(string|array $lightColor, string|array $darkColor): static 852 | { 853 | $lightColor = self::normalizeColor($lightColor); 854 | $darkColor = self::normalizeColor($darkColor); 855 | 856 | // Calculate averages between light and dark colors 857 | $redAvg = $lightColor['red'] - $darkColor['red']; 858 | $greenAvg = $lightColor['green'] - $darkColor['green']; 859 | $blueAvg = $lightColor['blue'] - $darkColor['blue']; 860 | 861 | // Create a matrix of all possible duotone colors based on gray values 862 | $pixels = []; 863 | for ($i = 0; $i <= 255; $i++) { 864 | $grayAvg = $i / 255; 865 | $pixels['red'][$i] = $darkColor['red'] + $grayAvg * $redAvg; 866 | $pixels['green'][$i] = $darkColor['green'] + $grayAvg * $greenAvg; 867 | $pixels['blue'][$i] = $darkColor['blue'] + $grayAvg * $blueAvg; 868 | } 869 | 870 | // Apply the filter pixel by pixel 871 | for ($x = 0; $x < $this->getWidth(); $x++) { 872 | for ($y = 0; $y < $this->getHeight(); $y++) { 873 | $rgb = $this->getColorAt($x, $y); 874 | $gray = min(255, round(0.299 * $rgb['red'] + 0.114 * $rgb['blue'] + 0.587 * $rgb['green'])); 875 | $this->dot($x, $y, [ 876 | 'red' => $pixels['red'][$gray], 877 | 'green' => $pixels['green'][$gray], 878 | 'blue' => $pixels['blue'][$gray], 879 | ]); 880 | } 881 | } 882 | 883 | return $this; 884 | } 885 | 886 | /** 887 | * Proportionally resize the image to a specific width. 888 | * 889 | * @param int $width The width to resize the image to. 890 | * @return SimpleImage 891 | * 892 | *@deprecated 893 | * This method was deprecated in version 3.2.2 and will be removed in version 4.0. 894 | * Please use `resize(null, $height)` instead. 895 | */ 896 | public function fitToWidth(int $width): static 897 | { 898 | return $this->resize($width); 899 | } 900 | 901 | /** 902 | * Flip the image horizontally or vertically. 903 | * 904 | * @param string $direction The direction to flip: x|y|both. 905 | * @return SimpleImage 906 | */ 907 | public function flip(string $direction): static 908 | { 909 | match ($direction) { 910 | 'x' => imageflip($this->image, IMG_FLIP_HORIZONTAL), 911 | 'y' => imageflip($this->image, IMG_FLIP_VERTICAL), 912 | 'both' => imageflip($this->image, IMG_FLIP_BOTH), 913 | default => $this, 914 | }; 915 | 916 | return $this; 917 | } 918 | 919 | /** 920 | * Reduces the image to a maximum number of colors. 921 | * 922 | * @param int $max The maximum number of colors to use. 923 | * @param bool $dither Whether or not to use a dithering effect (default true). 924 | * @return SimpleImage 925 | */ 926 | public function maxColors(int $max, bool $dither = true): static 927 | { 928 | imagetruecolortopalette($this->image, $dither, max(1, $max)); 929 | 930 | return $this; 931 | } 932 | 933 | /** 934 | * Place an image on top of the current image. 935 | * 936 | * @param string|SimpleImage $overlay The image to overlay. This can be a filename, a data URI, or a SimpleImage object. 937 | * @param string $anchor The anchor point: 'center', 'top', 'bottom', 'left', 'right', 'top left', 'top right', 'bottom left', 'bottom right' (default 'center'). 938 | * @param float|int $opacity The opacity level of the overlay 0-1 (default 1). 939 | * @param int $xOffset Horizontal offset in pixels (default 0). 940 | * @param int $yOffset Vertical offset in pixels (default 0). 941 | * @param bool $calculateOffsetFromEdge Calculate Offset referring to the edges of the image (default false). 942 | * @return SimpleImage 943 | * 944 | * @throws Exception 945 | */ 946 | public function overlay(string|SimpleImage $overlay, string $anchor = 'center', float|int $opacity = 1, int $xOffset = 0, int $yOffset = 0, bool $calculateOffsetFromEdge = false): static 947 | { 948 | // Load overlay image 949 | if (! ($overlay instanceof SimpleImage)) { 950 | $overlay = new SimpleImage($overlay); 951 | } 952 | 953 | // Convert opacity 954 | $opacity = (int) round(self::keepWithin($opacity, 0, 1) * 100); 955 | 956 | // Get available space 957 | $spaceX = $this->getWidth() - $overlay->getWidth(); 958 | $spaceY = $this->getHeight() - $overlay->getHeight(); 959 | 960 | // Set default center 961 | $x = (int) round(($spaceX / 2) + ($calculateOffsetFromEdge ? 0 : $xOffset)); 962 | $y = (int) round(($spaceY / 2) + ($calculateOffsetFromEdge ? 0 : $yOffset)); 963 | 964 | // Determine if top|bottom 965 | if (str_contains($anchor, 'top')) { 966 | $y = $yOffset; 967 | } elseif (str_contains($anchor, 'bottom')) { 968 | $y = $spaceY + ($calculateOffsetFromEdge ? -$yOffset : $yOffset); 969 | } 970 | 971 | // Determine if left|right 972 | if (str_contains($anchor, 'left')) { 973 | $x = $xOffset; 974 | } elseif (str_contains($anchor, 'right')) { 975 | $x = $spaceX + ($calculateOffsetFromEdge ? -$xOffset : $xOffset); 976 | } 977 | 978 | // Perform the overlay 979 | self::imageCopyMergeAlpha( 980 | $this->image, 981 | $overlay->image, 982 | $x, $y, 983 | 0, 0, 984 | $overlay->getWidth(), 985 | $overlay->getHeight(), 986 | $opacity 987 | ); 988 | 989 | return $this; 990 | } 991 | 992 | /** 993 | * Resize an image to the specified dimensions. If only one dimension is specified, the image will be resized proportionally. 994 | * 995 | * @param int|null $width The new image width. 996 | * @param int|null $height The new image height. 997 | * @return SimpleImage 998 | */ 999 | public function resize(int|null $width = null, int|null $height = null): static 1000 | { 1001 | // No dimensions specified 1002 | if (! $width && ! $height) { 1003 | return $this; 1004 | } 1005 | 1006 | // Resize to width 1007 | if ($width && ! $height) { 1008 | $height = (int) round($width / $this->getAspectRatio()); 1009 | } 1010 | 1011 | // Resize to height 1012 | if (! $width && $height) { 1013 | $width = (int) round($height * $this->getAspectRatio()); 1014 | } 1015 | 1016 | // If the dimensions are the same, there's no need to resize 1017 | if ($this->getWidth() === $width && $this->getHeight() === $height) { 1018 | return $this; 1019 | } 1020 | 1021 | // We can't use imagescale because it doesn't seem to preserve transparency properly. The 1022 | // workaround is to create a new truecolor image, allocate a transparent color, and copy the 1023 | // image over to it using imagecopyresampled. 1024 | $newImage = imagecreatetruecolor($width, $height); 1025 | $transparentColor = imagecolorallocatealpha($newImage, 0, 0, 0, 127); 1026 | imagecolortransparent($newImage, $transparentColor); 1027 | imagefill($newImage, 0, 0, $transparentColor); 1028 | imagecopyresampled( 1029 | $newImage, 1030 | $this->image, 1031 | 0, 0, 0, 0, 1032 | $width, 1033 | $height, 1034 | $this->getWidth(), 1035 | $this->getHeight() 1036 | ); 1037 | 1038 | // Swap out the new image 1039 | $this->image = $newImage; 1040 | 1041 | return $this; 1042 | } 1043 | 1044 | /** 1045 | * Sets an image's resolution, as per https://www.php.net/manual/en/function.imageresolution.php 1046 | * 1047 | * @param int $res_x The horizontal resolution in DPI. 1048 | * @param int|null $res_y The vertical resolution in DPI 1049 | * @return SimpleImage 1050 | */ 1051 | public function resolution(int $res_x, int|null $res_y = null): static 1052 | { 1053 | if (is_null($res_y)) { 1054 | imageresolution($this->image, $res_x); 1055 | } else { 1056 | imageresolution($this->image, $res_x, $res_y); 1057 | } 1058 | 1059 | return $this; 1060 | } 1061 | 1062 | /** 1063 | * Rotates the image. 1064 | * 1065 | * @param int $angle The angle of rotation (-360 - 360). 1066 | * @param string|array $backgroundColor The background color to use for the uncovered zone area after rotation (default 'transparent'). 1067 | * @return SimpleImage 1068 | * 1069 | * @throws Exception 1070 | */ 1071 | public function rotate(int $angle, string|array $backgroundColor = 'transparent'): static 1072 | { 1073 | // Rotate the image on a canvas with the desired background color 1074 | $backgroundColor = $this->allocateColor($backgroundColor); 1075 | 1076 | $this->image = imagerotate( 1077 | $this->image, 1078 | -(self::keepWithin($angle, -360, 360)), 1079 | $backgroundColor 1080 | ); 1081 | imagecolortransparent($this->image, imagecolorallocatealpha($this->image, 0, 0, 0, 127)); 1082 | 1083 | return $this; 1084 | } 1085 | 1086 | /** 1087 | * Adds text to the image. 1088 | * 1089 | * @param string $text The desired text. 1090 | * @param array $options 1091 | * An array of options. 1092 | * - fontFile* (string) - The TrueType (or compatible) font file to use. 1093 | * - size (integer) - The size of the font in pixels (default 12). 1094 | * - color (string|array) - The text color (default black). 1095 | * - anchor (string) - The anchor point: 'center', 'top', 'bottom', 'left', 'right', 'top left', 'top right', 'bottom left', 'bottom right' (default 'center'). 1096 | * - xOffset (integer) - The horizontal offset in pixels (default 0). 1097 | * - yOffset (integer) - The vertical offset in pixels (default 0). 1098 | * - shadow (array) - Text shadow params. 1099 | * - x* (integer) - Horizontal offset in pixels. 1100 | * - y* (integer) - Vertical offset in pixels. 1101 | * - color* (string|array) - The text shadow color. 1102 | * - $calculateOffsetFromEdge (bool) - Calculate offsets from the edge of the image (default false). 1103 | * - $baselineAlign (bool) - Align the text font with the baseline. (default true). 1104 | * @param array|null $boundary 1105 | * If passed, this variable will contain an array with coordinates that surround the text: [x1, y1, x2, y2, width, height]. 1106 | * This can be used for calculating the text's position after it gets added to the image. 1107 | * @return SimpleImage 1108 | * 1109 | * @throws Exception 1110 | */ 1111 | public function text(string $text, array $options, array|null &$boundary = null): static 1112 | { 1113 | // Check for freetype support 1114 | if (! function_exists('imagettftext')) { 1115 | throw new Exception( 1116 | 'Freetype support is not enabled in your version of PHP.', 1117 | self::ERR_FREETYPE_NOT_ENABLED 1118 | ); 1119 | } 1120 | 1121 | // Default options 1122 | $options = array_merge([ 1123 | 'fontFile' => null, 1124 | 'size' => 12, 1125 | 'color' => 'black', 1126 | 'anchor' => 'center', 1127 | 'xOffset' => 0, 1128 | 'yOffset' => 0, 1129 | 'shadow' => null, 1130 | 'calculateOffsetFromEdge' => false, 1131 | 'baselineAlign' => true, 1132 | ], $options); 1133 | 1134 | // Extract and normalize options 1135 | $fontFile = $options['fontFile']; 1136 | $size = ($options['size'] / 96) * 72; // Convert px to pt (72pt per inch, 96px per inch) 1137 | $color = $this->allocateColor($options['color']); 1138 | $anchor = $options['anchor']; 1139 | $xOffset = $options['xOffset']; 1140 | $yOffset = $options['yOffset']; 1141 | $calculateOffsetFromEdge = $options['calculateOffsetFromEdge']; 1142 | $baselineAlign = $options['baselineAlign']; 1143 | $angle = 0; 1144 | 1145 | // Calculate the bounding box dimensions 1146 | // 1147 | // Since imagettfbox() returns a bounding box from the text's baseline, we can end up with 1148 | // different heights for different strings of the same font size. For example, 'type' will often 1149 | // be taller than 'text' because the former has a descending letter. 1150 | // 1151 | // To compensate for this, we created a temporary bounding box to measure the maximum height 1152 | // that the font used can occupy. Based on this, we can adjust the text vertically so that it 1153 | // appears inside the box with a good consistency. 1154 | // 1155 | // See: https://github.com/claviska/SimpleImage/issues/165 1156 | // 1157 | 1158 | $boxText = imagettfbbox($size, $angle, $fontFile, $text); 1159 | if (! $boxText) { 1160 | throw new Exception("Unable to load font file: $fontFile", self::ERR_FONT_FILE); 1161 | } 1162 | 1163 | $boxWidth = abs($boxText[4] - $boxText[0]); 1164 | $boxHeight = abs($boxText[5] - $boxText[1]); 1165 | 1166 | // Calculate Offset referring to the edges of the image. 1167 | // Just invert the value for bottom|right; 1168 | if ($calculateOffsetFromEdge) { 1169 | if (str_contains($anchor, 'bottom')) { 1170 | $yOffset *= -1; 1171 | } 1172 | if (str_contains($anchor, 'right')) { 1173 | $xOffset *= -1; 1174 | } 1175 | } 1176 | 1177 | // Align the text font with the baseline. 1178 | // I use $yOffset to inject the vertical alignment correction value. 1179 | if ($baselineAlign) { 1180 | // Create a temporary box to obtain the maximum height that this font can use. 1181 | $boxFull = imagettfbbox($size, $angle, $fontFile, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'); 1182 | // Based on the maximum height, the text is aligned. 1183 | if (str_contains($anchor, 'bottom')) { 1184 | $yOffset -= $boxFull[1]; 1185 | } elseif (str_contains($anchor, 'top')) { 1186 | $yOffset += abs($boxFull[5]) - $boxHeight; 1187 | } else { // center 1188 | $boxFullHeight = abs($boxFull[1]) + abs($boxFull[5]); 1189 | $yOffset += ($boxFullHeight / 2) - ($boxHeight / 2) - abs($boxFull[1]); 1190 | } 1191 | } else { 1192 | // Prevents fonts rendered outside the box boundary from being cut. 1193 | // Example: 'Scriptina' font, some letters invade the space of the previous or subsequent letter. 1194 | $yOffset -= $boxText[1]; 1195 | } 1196 | 1197 | // Prevents fonts rendered outside the box boundary from being cut. 1198 | // Example: 'Scriptina' font, some letters invade the space of the previous or subsequent letter. 1199 | $xOffset -= $boxText[0]; 1200 | 1201 | // Determine position 1202 | switch($anchor) { 1203 | case 'top left': 1204 | $x = $xOffset; 1205 | $y = $yOffset + $boxHeight; 1206 | break; 1207 | case 'top right': 1208 | $x = $this->getWidth() - $boxWidth + $xOffset; 1209 | $y = $yOffset + $boxHeight; 1210 | break; 1211 | case 'top': 1212 | $x = ($this->getWidth() / 2) - ($boxWidth / 2) + $xOffset; 1213 | $y = $yOffset + $boxHeight; 1214 | break; 1215 | case 'bottom left': 1216 | $x = $xOffset; 1217 | $y = $this->getHeight() + $yOffset; 1218 | break; 1219 | case 'bottom right': 1220 | $x = $this->getWidth() - $boxWidth + $xOffset; 1221 | $y = $this->getHeight() + $yOffset; 1222 | break; 1223 | case 'bottom': 1224 | $x = ($this->getWidth() / 2) - ($boxWidth / 2) + $xOffset; 1225 | $y = $this->getHeight() + $yOffset; 1226 | break; 1227 | case 'left': 1228 | $x = $xOffset; 1229 | $y = ($this->getHeight() / 2) - (($boxHeight / 2) - $boxHeight) + $yOffset; 1230 | break; 1231 | case 'right': 1232 | $x = $this->getWidth() - $boxWidth + $xOffset; 1233 | $y = ($this->getHeight() / 2) - (($boxHeight / 2) - $boxHeight) + $yOffset; 1234 | break; 1235 | default: // center 1236 | $x = ($this->getWidth() / 2) - ($boxWidth / 2) + $xOffset; 1237 | $y = ($this->getHeight() / 2) - (($boxHeight / 2) - $boxHeight) + $yOffset; 1238 | break; 1239 | } 1240 | $x = (int) round($x); 1241 | $y = (int) round($y); 1242 | 1243 | // Pass the boundary back by reference 1244 | $boundary = [ 1245 | 'x1' => $x + $boxText[0], 1246 | 'y1' => $y + $boxText[1] - $boxHeight, // $y is the baseline, not the top! 1247 | 'x2' => $x + $boxWidth + $boxText[0], 1248 | 'y2' => $y + $boxText[1], 1249 | 'width' => $boxWidth, 1250 | 'height' => $boxHeight, 1251 | ]; 1252 | 1253 | // Text shadow 1254 | if (is_array($options['shadow'])) { 1255 | imagettftext( 1256 | $this->image, 1257 | $size, 1258 | $angle, 1259 | $x + $options['shadow']['x'], 1260 | $y + $options['shadow']['y'], 1261 | $this->allocateColor($options['shadow']['color']), 1262 | $fontFile, 1263 | $text 1264 | ); 1265 | } 1266 | 1267 | // Draw the text 1268 | imagettftext($this->image, $size, $angle, $x, $y, $color, $fontFile, $text); 1269 | 1270 | return $this; 1271 | } 1272 | 1273 | /** 1274 | * Adds text with a line break to the image. 1275 | * 1276 | * @param string $text The desired text. 1277 | * @param array $options 1278 | * An array of options. 1279 | * - fontFile* (string) - The TrueType (or compatible) font file to use. 1280 | * - size (integer) - The size of the font in pixels (default 12). 1281 | * - color (string|array) - The text color (default black). 1282 | * - anchor (string) - The anchor point: 'center', 'top', 'bottom', 'left', 'right', 'top left', 'top right', 'bottom left', 'bottom right' (default 'center'). 1283 | * - xOffset (integer) - The horizontal offset in pixels (default 0). Has no effect when anchor is 'center'. 1284 | * - yOffset (integer) - The vertical offset in pixels (default 0). Has no effect when anchor is 'center'. 1285 | * - shadow (array) - Text shadow params. 1286 | * - x* (integer) - Horizontal offset in pixels. 1287 | * - y* (integer) - Vertical offset in pixels. 1288 | * - color* (string|array) - The text shadow color. 1289 | * - $calculateOffsetFromEdge (bool) - Calculate offsets from the edge of the image (default false). 1290 | * - width (int) - Width of text box (default image width). 1291 | * - align (string) - How to align text: 'left', 'right', 'center', 'justify' (default 'left'). 1292 | * - leading (float) - Increase/decrease spacing between lines of text (default 0). 1293 | * - opacity (float) - The opacity level of the text 0-1 (default 1). 1294 | * @return SimpleImage 1295 | * 1296 | * @throws Exception 1297 | */ 1298 | public function textBox(string $text, array $options): static 1299 | { 1300 | // default width of image 1301 | $maxWidth = $this->getWidth(); 1302 | // Default options 1303 | $options = array_merge([ 1304 | 'fontFile' => null, 1305 | 'size' => 12, 1306 | 'color' => 'black', 1307 | 'anchor' => 'center', 1308 | 'xOffset' => 0, 1309 | 'yOffset' => 0, 1310 | 'shadow' => null, 1311 | 'calculateOffsetFromEdge' => false, 1312 | 'width' => $maxWidth, 1313 | 'align' => 'left', 1314 | 'leading' => 0, 1315 | 'opacity' => 1, 1316 | ], $options); 1317 | 1318 | // Extract and normalize options 1319 | $fontFile = $options['fontFile']; 1320 | $fontSize = $fontSizePx = $options['size']; 1321 | $fontSize = ($fontSize / 96) * 72; // Convert px to pt (72pt per inch, 96px per inch) 1322 | $color = $options['color']; 1323 | $anchor = $options['anchor']; 1324 | $xOffset = $options['xOffset']; 1325 | $yOffset = $options['yOffset']; 1326 | $shadow = $options['shadow']; 1327 | $calculateOffsetFromEdge = $options['calculateOffsetFromEdge']; 1328 | $maxWidth = intval($options['width']); 1329 | $leading = $options['leading']; 1330 | $leading = self::keepWithin($leading, ($fontSizePx * -1), $leading); 1331 | $opacity = $options['opacity']; 1332 | 1333 | $align = $options['align']; 1334 | if ($align == 'right') { 1335 | $align = 'top right'; 1336 | } elseif ($align == 'center') { 1337 | $align = 'top'; 1338 | } elseif ($align == 'justify') { 1339 | $align = 'justify'; 1340 | } else { 1341 | $align = 'top left'; 1342 | } 1343 | 1344 | [$lines, $isLastLine, $lastLineHeight] = self::textSeparateLines($text, $fontFile, $fontSize, $maxWidth); 1345 | 1346 | $maxHeight = (int) round(((is_countable($lines) ? count($lines) : 0) - 1) * ($fontSizePx * 1.2 + $leading) + $lastLineHeight); 1347 | 1348 | $imageText = new SimpleImage(); 1349 | $imageText->fromNew($maxWidth, $maxHeight); 1350 | 1351 | // Align left/center/right 1352 | if ($align != 'justify') { 1353 | foreach ($lines as $key => $line) { 1354 | if ($align == 'top') { 1355 | $line = trim($line); 1356 | } // If is justify = 'center' 1357 | $imageText->text($line, ['fontFile' => $fontFile, 'size' => $fontSizePx, 'color' => $color, 'anchor' => $align, 'xOffset' => 0, 'yOffset' => $key * ($fontSizePx * 1.2 + $leading), 'shadow' => $shadow, 'calculateOffsetFromEdge' => true]); 1358 | } 1359 | 1360 | // Justify 1361 | } else { 1362 | foreach ($lines as $keyLine => $line) { 1363 | // Check if there are spaces at the beginning of the sentence 1364 | $spaces = 0; 1365 | if (preg_match("/^\s+/", $line, $match)) { 1366 | // Count spaces 1367 | $spaces = strlen($match[0]); 1368 | $line = ltrim($line); 1369 | } 1370 | 1371 | // Separate words 1372 | $words = preg_split("/\s+/", $line); 1373 | // Include spaces with the first word 1374 | $words[0] = str_repeat(' ', $spaces).$words[0]; 1375 | 1376 | // Calculates the space occupied by all words 1377 | $wordsSize = []; 1378 | foreach ($words as $key => $word) { 1379 | $wordBox = imagettfbbox($fontSize, 0, $fontFile, $word); 1380 | $wordWidth = abs($wordBox[4] - $wordBox[0]); 1381 | $wordsSize[$key] = $wordWidth; 1382 | } 1383 | $wordsSizeTotal = array_sum($wordsSize); 1384 | 1385 | // Calculates the required space between words 1386 | $countWords = count($words); 1387 | $wordSpacing = 0; 1388 | if ($countWords > 1) { 1389 | $wordSpacing = ($maxWidth - $wordsSizeTotal) / ($countWords - 1); 1390 | $wordSpacing = round($wordSpacing, 3); 1391 | } 1392 | 1393 | $xOffsetJustify = 0; 1394 | foreach ($words as $key => $word) { 1395 | if ($isLastLine[$keyLine]) { 1396 | if ($key < (count($words) - 1)) { 1397 | continue; 1398 | } 1399 | $word = $line; 1400 | } 1401 | $imageText->text($word, ['fontFile' => $fontFile, 'size' => $fontSizePx, 'color' => $color, 'anchor' => 'top left', 'xOffset' => $xOffsetJustify, 'yOffset' => $keyLine * ($fontSizePx * 1.2 + $leading), 'shadow' => $shadow, 'calculateOffsetFromEdge' => true] 1402 | ); 1403 | // Calculate offset for next word 1404 | $xOffsetJustify += $wordsSize[$key] + $wordSpacing; 1405 | } 1406 | } 1407 | } 1408 | 1409 | $this->overlay($imageText, $anchor, $opacity, $xOffset, $yOffset, $calculateOffsetFromEdge); 1410 | 1411 | return $this; 1412 | } 1413 | 1414 | /** 1415 | * Receives a text and breaks into LINES. 1416 | */ 1417 | private function textSeparateLines(string $text, string $fontFile, float $fontSize, int $maxWidth): array 1418 | { 1419 | $lines = []; 1420 | $words = self::textSeparateWords($text); 1421 | $countWords = count($words) - 1; 1422 | $lines[0] = ''; 1423 | $lineKey = 0; 1424 | $isLastLine = []; 1425 | for ($i = 0; $i < $countWords; $i++) { 1426 | $word = $words[$i]; 1427 | $isLastLine[$lineKey] = false; 1428 | if ($word === PHP_EOL) { 1429 | $isLastLine[$lineKey] = true; 1430 | $lineKey++; 1431 | $lines[$lineKey] = ''; 1432 | 1433 | continue; 1434 | } 1435 | $lineBox = imagettfbbox($fontSize, 0, $fontFile, $lines[$lineKey].$word); 1436 | if (abs($lineBox[4] - $lineBox[0]) < $maxWidth) { 1437 | $lines[$lineKey] .= $word.' '; 1438 | } else { 1439 | $lineKey++; 1440 | $lines[$lineKey] = $word.' '; 1441 | } 1442 | } 1443 | $isLastLine[$lineKey] = true; 1444 | // Exclude space of right 1445 | $lines = array_map('rtrim', $lines); 1446 | // Calculate height of last line 1447 | $boxFull = imagettfbbox($fontSize, 0, $fontFile, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'); 1448 | $lineBox = imagettfbbox($fontSize, 0, $fontFile, $lines[$lineKey]); 1449 | // Height of last line = ascender of $boxFull + descender of $lineBox 1450 | $lastLineHeight = abs($lineBox[1]) + abs($boxFull[5]); 1451 | 1452 | return [$lines, $isLastLine, $lastLineHeight]; 1453 | } 1454 | 1455 | /** 1456 | * Receives a text and breaks into WORD / SPACE / NEW LINE. 1457 | */ 1458 | private function textSeparateWords(string $text): array 1459 | { 1460 | // Normalizes line break 1461 | $text = strval(preg_replace('/(\r\n|\n|\r)/', PHP_EOL, $text)); 1462 | $text = explode(PHP_EOL, $text); 1463 | $newText = []; 1464 | foreach ($text as $line) { 1465 | $newText = array_merge($newText, explode(' ', $line), [PHP_EOL]); 1466 | } 1467 | 1468 | return $newText; 1469 | } 1470 | 1471 | /** 1472 | * Creates a thumbnail image. This function attempts to get the image as close to the provided 1473 | * dimensions as possible, then crops the remaining overflow to force the desired size. Useful 1474 | * for generating thumbnail images. 1475 | * 1476 | * @param int $width The thumbnail width. 1477 | * @param int $height The thumbnail height. 1478 | * @param string $anchor The anchor point: 'center', 'top', 'bottom', 'left', 'right', 'top left', 'top right', 'bottom left', 'bottom right' (default 'center'). 1479 | * @return SimpleImage 1480 | */ 1481 | public function thumbnail(int $width, int $height, string $anchor = 'center'): SimpleImage|static 1482 | { 1483 | // Determine aspect ratios 1484 | $currentRatio = $this->getHeight() / $this->getWidth(); 1485 | $targetRatio = $height / $width; 1486 | 1487 | // Fit to height/width 1488 | if ($targetRatio > $currentRatio) { 1489 | $this->resize(null, $height); 1490 | } else { 1491 | $this->resize($width); 1492 | } 1493 | 1494 | switch($anchor) { 1495 | case 'top': 1496 | $x1 = floor(($this->getWidth() / 2) - ($width / 2)); 1497 | $x2 = $width + $x1; 1498 | $y1 = 0; 1499 | $y2 = $height; 1500 | break; 1501 | case 'bottom': 1502 | $x1 = floor(($this->getWidth() / 2) - ($width / 2)); 1503 | $x2 = $width + $x1; 1504 | $y1 = $this->getHeight() - $height; 1505 | $y2 = $this->getHeight(); 1506 | break; 1507 | case 'left': 1508 | $x1 = 0; 1509 | $x2 = $width; 1510 | $y1 = floor(($this->getHeight() / 2) - ($height / 2)); 1511 | $y2 = $height + $y1; 1512 | break; 1513 | case 'right': 1514 | $x1 = $this->getWidth() - $width; 1515 | $x2 = $this->getWidth(); 1516 | $y1 = floor(($this->getHeight() / 2) - ($height / 2)); 1517 | $y2 = $height + $y1; 1518 | break; 1519 | case 'top left': 1520 | $x1 = 0; 1521 | $x2 = $width; 1522 | $y1 = 0; 1523 | $y2 = $height; 1524 | break; 1525 | case 'top right': 1526 | $x1 = $this->getWidth() - $width; 1527 | $x2 = $this->getWidth(); 1528 | $y1 = 0; 1529 | $y2 = $height; 1530 | break; 1531 | case 'bottom left': 1532 | $x1 = 0; 1533 | $x2 = $width; 1534 | $y1 = $this->getHeight() - $height; 1535 | $y2 = $this->getHeight(); 1536 | break; 1537 | case 'bottom right': 1538 | $x1 = $this->getWidth() - $width; 1539 | $x2 = $this->getWidth(); 1540 | $y1 = $this->getHeight() - $height; 1541 | $y2 = $this->getHeight(); 1542 | break; 1543 | default: 1544 | $x1 = floor(($this->getWidth() / 2) - ($width / 2)); 1545 | $x2 = $width + $x1; 1546 | $y1 = floor(($this->getHeight() / 2) - ($height / 2)); 1547 | $y2 = $height + $y1; 1548 | break; 1549 | } 1550 | 1551 | // Return the cropped thumbnail image 1552 | return $this->crop($x1, $y1, $x2, $y2); 1553 | } 1554 | 1555 | ////////////////////////////////////////////////////////////////////////////////////////////////// 1556 | // Drawing 1557 | ////////////////////////////////////////////////////////////////////////////////////////////////// 1558 | 1559 | /** 1560 | * Draws an arc. 1561 | * 1562 | * @param int $x The x coordinate of the arc's center. 1563 | * @param int $y The y coordinate of the arc's center. 1564 | * @param int $width The width of the arc. 1565 | * @param int $height The height of the arc. 1566 | * @param int $start The start of the arc in degrees. 1567 | * @param int $end The end of the arc in degrees. 1568 | * @param string|array $color The arc color. 1569 | * @param int|string $thickness Line thickness in pixels or 'filled' (default 1). 1570 | * @return SimpleImage 1571 | * 1572 | * @throws Exception 1573 | */ 1574 | public function arc(int $x, int $y, int $width, int $height, int $start, int $end, string|array $color, int|string $thickness = 1): static 1575 | { 1576 | // Allocate the color 1577 | $tempColor = $this->allocateColor($color); 1578 | imagesetthickness($this->image, 1); 1579 | 1580 | // Draw an arc 1581 | if ($thickness === 'filled') { 1582 | imagefilledarc($this->image, $x, $y, $width, $height, $start, $end, $tempColor, IMG_ARC_PIE); 1583 | } elseif ($thickness === 1) { 1584 | imagearc($this->image, $x, $y, $width, $height, $start, $end, $tempColor); 1585 | } else { 1586 | // New temp image 1587 | $tempImage = new SimpleImage(); 1588 | $tempImage->fromNew($this->getWidth(), $this->getHeight()); 1589 | 1590 | // Draw a large ellipse filled with $color (+$thickness pixels) 1591 | $tempColor = $tempImage->allocateColor($color); 1592 | imagefilledarc($tempImage->image, $x, $y, $width + $thickness, $height + $thickness, $start, $end, $tempColor, IMG_ARC_PIE); 1593 | 1594 | // Draw a smaller ellipse filled with red|blue (-$thickness pixels) 1595 | $tempColor = (self::normalizeColor($color)['red'] == 255) ? 'blue' : 'red'; 1596 | $tempColor = $tempImage->allocateColor($tempColor); 1597 | imagefilledarc($tempImage->image, $x, $y, $width - $thickness, $height - $thickness, $start, $end, $tempColor, IMG_ARC_PIE); 1598 | 1599 | // Replace the color of the smaller ellipse with 'transparent' 1600 | $tempImage->excludeInsideColor($x, $y, $color); 1601 | 1602 | // Apply the temp image 1603 | $this->overlay($tempImage); 1604 | } 1605 | 1606 | return $this; 1607 | } 1608 | 1609 | /** 1610 | * Draws a border around the image. 1611 | * 1612 | * @param string|array $color The border color. 1613 | * @param int $thickness The thickness of the border (default 1). 1614 | * @return SimpleImage 1615 | * 1616 | * @throws Exception 1617 | */ 1618 | public function border(string|array $color, int $thickness = 1): static 1619 | { 1620 | $x1 = -1; 1621 | $y1 = 0; 1622 | $x2 = $this->getWidth(); 1623 | $y2 = $this->getHeight() - 1; 1624 | 1625 | $color = $this->allocateColor($color); 1626 | imagesetthickness($this->image, $thickness * 2); 1627 | imagerectangle($this->image, $x1, $y1, $x2, $y2, $color); 1628 | 1629 | return $this; 1630 | } 1631 | 1632 | /** 1633 | * Draws a single pixel dot. 1634 | * 1635 | * @param int $x The x coordinate of the dot. 1636 | * @param int $y The y coordinate of the dot. 1637 | * @param string|array $color The dot color. 1638 | * @return SimpleImage 1639 | * 1640 | * @throws Exception 1641 | */ 1642 | public function dot(int $x, int $y, string|array $color): static 1643 | { 1644 | $color = $this->allocateColor($color); 1645 | imagesetpixel($this->image, $x, $y, $color); 1646 | 1647 | return $this; 1648 | } 1649 | 1650 | /** 1651 | * Draws an ellipse. 1652 | * 1653 | * @param int $x The x coordinate of the center. 1654 | * @param int $y The y coordinate of the center. 1655 | * @param int $width The ellipse width. 1656 | * @param int $height The ellipse height. 1657 | * @param string|array $color The ellipse color. 1658 | * @param string|int|array $thickness Line thickness in pixels or 'filled' (default 1). 1659 | * @return SimpleImage 1660 | * 1661 | * @throws Exception 1662 | */ 1663 | public function ellipse(int $x, int $y, int $width, int $height, string|array $color, string|int|array $thickness = 1): static 1664 | { 1665 | // Allocate the color 1666 | $tempColor = $this->allocateColor($color); 1667 | imagesetthickness($this->image, 1); 1668 | 1669 | // Draw an ellipse 1670 | if ($thickness == 'filled') { 1671 | imagefilledellipse($this->image, $x, $y, $width, $height, $tempColor); 1672 | } elseif ($thickness === 1) { 1673 | imageellipse($this->image, $x, $y, $width, $height, $tempColor); 1674 | } else { 1675 | // New temp image 1676 | $tempImage = new SimpleImage(); 1677 | $tempImage->fromNew($this->getWidth(), $this->getHeight()); 1678 | 1679 | // Draw a large ellipse filled with $color (+$thickness pixels) 1680 | $tempColor = $tempImage->allocateColor($color); 1681 | imagefilledellipse($tempImage->image, $x, $y, $width + $thickness, $height + $thickness, $tempColor); 1682 | 1683 | // Draw a smaller ellipse filled with red|blue (-$thickness pixels) 1684 | $tempColor = (self::normalizeColor($color)['red'] == 255) ? 'blue' : 'red'; 1685 | $tempColor = $tempImage->allocateColor($tempColor); 1686 | imagefilledellipse($tempImage->image, $x, $y, $width - $thickness, $height - $thickness, $tempColor); 1687 | 1688 | // Replace the color of the smaller ellipse with 'transparent' 1689 | $tempImage->excludeInsideColor($x, $y, $color); 1690 | 1691 | // Apply the temp image 1692 | $this->overlay($tempImage); 1693 | } 1694 | 1695 | return $this; 1696 | } 1697 | 1698 | /** 1699 | * Fills the image with a solid color. 1700 | * 1701 | * @param string|array $color The fill color. 1702 | * @return SimpleImage 1703 | * 1704 | * @throws Exception 1705 | */ 1706 | public function fill(string|array $color): static 1707 | { 1708 | // Draw a filled rectangle over the entire image 1709 | $this->rectangle(0, 0, $this->getWidth(), $this->getHeight(), 'white', 'filled'); 1710 | 1711 | // Now flood it with the appropriate color 1712 | $color = $this->allocateColor($color); 1713 | imagefill($this->image, 0, 0, $color); 1714 | 1715 | return $this; 1716 | } 1717 | 1718 | /** 1719 | * Draws a line. 1720 | * 1721 | * @param int $x1 The x coordinate for the first point. 1722 | * @param int $y1 The y coordinate for the first point. 1723 | * @param int $x2 The x coordinate for the second point. 1724 | * @param int $y2 The y coordinate for the second point. 1725 | * @param string|array $color The line color. 1726 | * @param int $thickness The line thickness (default 1). 1727 | * @return SimpleImage 1728 | * 1729 | * @throws Exception 1730 | */ 1731 | public function line(int $x1, int $y1, int $x2, int $y2, string|array $color, int $thickness = 1): static 1732 | { 1733 | // Allocate the color 1734 | $color = $this->allocateColor($color); 1735 | 1736 | // Draw a line 1737 | imagesetthickness($this->image, $thickness); 1738 | imageline($this->image, $x1, $y1, $x2, $y2, $color); 1739 | 1740 | return $this; 1741 | } 1742 | 1743 | /** 1744 | * Draws a polygon. 1745 | * 1746 | * @param array $vertices 1747 | * The polygon's vertices in an array of x/y arrays. 1748 | * Example: 1749 | * [ 1750 | * ['x' => x1, 'y' => y1], 1751 | * ['x' => x2, 'y' => y2], 1752 | * ['x' => xN, 'y' => yN] 1753 | * ] 1754 | * @param string|array $color The polygon color. 1755 | * @param string|int|array $thickness Line thickness in pixels or 'filled' (default 1). 1756 | * @return SimpleImage 1757 | * 1758 | * @throws Exception 1759 | */ 1760 | public function polygon(array $vertices, string|array $color, string|int|array $thickness = 1): static 1761 | { 1762 | // Allocate the color 1763 | $color = $this->allocateColor($color); 1764 | 1765 | // Convert [['x' => x1, 'y' => x1], ['x' => x1, 'y' => y2], ...] to [x1, y1, x2, y2, ...] 1766 | $points = []; 1767 | foreach ($vertices as $vals) { 1768 | $points[] = $vals['x']; 1769 | $points[] = $vals['y']; 1770 | } 1771 | 1772 | // Draw a polygon 1773 | if ($thickness == 'filled') { 1774 | imagesetthickness($this->image, 1); 1775 | imagefilledpolygon($this->image, $points, count($vertices), $color); 1776 | } else { 1777 | imagesetthickness($this->image, $thickness); 1778 | imagepolygon($this->image, $points, count($vertices), $color); 1779 | } 1780 | 1781 | return $this; 1782 | } 1783 | 1784 | /** 1785 | * Draws a rectangle. 1786 | * 1787 | * @param int $x1 The upper left x coordinate. 1788 | * @param int $y1 The upper left y coordinate. 1789 | * @param int $x2 The bottom right x coordinate. 1790 | * @param int $y2 The bottom right y coordinate. 1791 | * @param string|array $color The rectangle color. 1792 | * @param string|int|array $thickness Line thickness in pixels or 'filled' (default 1). 1793 | * @return SimpleImage 1794 | * 1795 | * @throws Exception 1796 | */ 1797 | public function rectangle(int $x1, int $y1, int $x2, int $y2, string|array $color, string|int|array $thickness = 1): static 1798 | { 1799 | // Allocate the color 1800 | $color = $this->allocateColor($color); 1801 | 1802 | // Draw a rectangle 1803 | if ($thickness == 'filled') { 1804 | imagesetthickness($this->image, 1); 1805 | imagefilledrectangle($this->image, $x1, $y1, $x2, $y2, $color); 1806 | } else { 1807 | imagesetthickness($this->image, $thickness); 1808 | imagerectangle($this->image, $x1, $y1, $x2, $y2, $color); 1809 | } 1810 | 1811 | return $this; 1812 | } 1813 | 1814 | /** 1815 | * Draws a rounded rectangle. 1816 | * 1817 | * @param int $x1 The upper left x coordinate. 1818 | * @param int $y1 The upper left y coordinate. 1819 | * @param int $x2 The bottom right x coordinate. 1820 | * @param int $y2 The bottom right y coordinate. 1821 | * @param int $radius The border radius in pixels. 1822 | * @param string|array $color The rectangle color. 1823 | * @param string|int|array $thickness Line thickness in pixels or 'filled' (default 1). 1824 | * @return SimpleImage 1825 | * 1826 | * @throws Exception 1827 | */ 1828 | public function roundedRectangle(int $x1, int $y1, int $x2, int $y2, int $radius, string|array $color, string|int|array $thickness = 1): static 1829 | { 1830 | if ($thickness == 'filled') { 1831 | // Draw the filled rectangle without edges 1832 | $this->rectangle($x1 + $radius + 1, $y1, $x2 - $radius - 1, $y2, $color, 'filled'); 1833 | $this->rectangle($x1, $y1 + $radius + 1, $x1 + $radius, $y2 - $radius - 1, $color, 'filled'); 1834 | $this->rectangle($x2 - $radius, $y1 + $radius + 1, $x2, $y2 - $radius - 1, $color, 'filled'); 1835 | 1836 | // Fill in the edges with arcs 1837 | $this->arc($x1 + $radius, $y1 + $radius, $radius * 2, $radius * 2, 180, 270, $color, 'filled'); 1838 | $this->arc($x2 - $radius, $y1 + $radius, $radius * 2, $radius * 2, 270, 360, $color, 'filled'); 1839 | $this->arc($x1 + $radius, $y2 - $radius, $radius * 2, $radius * 2, 90, 180, $color, 'filled'); 1840 | $this->arc($x2 - $radius, $y2 - $radius, $radius * 2, $radius * 2, 360, 90, $color, 'filled'); 1841 | } else { 1842 | $offset = $thickness / 2; 1843 | $x1 -= $offset; 1844 | $x2 += $offset; 1845 | $y1 -= $offset; 1846 | $y2 += $offset; 1847 | $radius = self::keepWithin($radius, 0, min(($x2 - $x1) / 2, ($y2 - $y1) / 2) - 1); 1848 | $radius = (int) floor($radius); 1849 | $thickness = self::keepWithin($thickness, 1, min(($x2 - $x1) / 2, ($y2 - $y1) / 2)); 1850 | 1851 | // New temp image 1852 | $tempImage = new SimpleImage(); 1853 | $tempImage->fromNew($this->getWidth(), $this->getHeight()); 1854 | 1855 | // Draw a large rectangle filled with $color 1856 | $tempImage->roundedRectangle($x1, $y1, $x2, $y2, $radius, $color, 'filled'); 1857 | 1858 | // Draw a smaller rectangle filled with red|blue (-$thickness pixels on each side) 1859 | $tempColor = (self::normalizeColor($color)['red'] == 255) ? 'blue' : 'red'; 1860 | $radius = $radius - $thickness; 1861 | $radius = self::keepWithin($radius, 0, $radius); 1862 | $tempImage->roundedRectangle( 1863 | $x1 + $thickness, 1864 | $y1 + $thickness, 1865 | $x2 - $thickness, 1866 | $y2 - $thickness, 1867 | $radius, 1868 | $tempColor, 1869 | 'filled' 1870 | ); 1871 | 1872 | // Replace the color of the smaller rectangle with 'transparent' 1873 | $tempImage->excludeInsideColor(($x2 + $x1) / 2, ($y2 + $y1) / 2, $color); 1874 | 1875 | // Apply the temp image 1876 | $this->overlay($tempImage); 1877 | } 1878 | 1879 | return $this; 1880 | } 1881 | 1882 | /** 1883 | * Exclude inside color. 1884 | * Used for roundedRectangle(), ellipse() and arc() 1885 | * 1886 | * @param int $x certer x of rectangle. 1887 | * @param int $y certer y of rectangle. 1888 | * @param string|array $borderColor The color of border. 1889 | * 1890 | * @throws Exception 1891 | */ 1892 | private function excludeInsideColor(int $x, int $y, string|array $borderColor): static 1893 | { 1894 | $borderColor = $this->allocateColor($borderColor); 1895 | $transparent = $this->allocateColor('transparent'); 1896 | imagefilltoborder($this->image, $x, $y, $borderColor, $transparent); 1897 | 1898 | return $this; 1899 | } 1900 | 1901 | ////////////////////////////////////////////////////////////////////////////////////////////////// 1902 | // Filters 1903 | ////////////////////////////////////////////////////////////////////////////////////////////////// 1904 | 1905 | /** 1906 | * Applies the blur filter. 1907 | * 1908 | * @param string $type The blur algorithm to use: 'selective', 'gaussian' (default 'gaussian'). 1909 | * @param int $passes The number of time to apply the filter, enhancing the effect (default 1). 1910 | * @return SimpleImage 1911 | */ 1912 | public function blur(string $type = 'selective', int $passes = 1): static 1913 | { 1914 | $filter = $type === 'gaussian' ? IMG_FILTER_GAUSSIAN_BLUR : IMG_FILTER_SELECTIVE_BLUR; 1915 | 1916 | for ($i = 0; $i < $passes; $i++) { 1917 | imagefilter($this->image, $filter); 1918 | } 1919 | 1920 | return $this; 1921 | } 1922 | 1923 | /** 1924 | * Applies the brightness filter to brighten the image. 1925 | * 1926 | * @param int $percentage Percentage to brighten the image (0 - 100). 1927 | * @return SimpleImage 1928 | */ 1929 | public function brighten(int $percentage): static 1930 | { 1931 | $percentage = self::keepWithin(255 * $percentage / 100, 0, 255); 1932 | 1933 | imagefilter($this->image, IMG_FILTER_BRIGHTNESS, $percentage); 1934 | 1935 | return $this; 1936 | } 1937 | 1938 | /** 1939 | * Applies the colorize filter. 1940 | * 1941 | * @param string|array $color The filter color. 1942 | * @return SimpleImage 1943 | * 1944 | * @throws Exception 1945 | */ 1946 | public function colorize(string|array $color): static 1947 | { 1948 | $color = self::normalizeColor($color); 1949 | 1950 | imagefilter( 1951 | $this->image, 1952 | IMG_FILTER_COLORIZE, 1953 | $color['red'], 1954 | $color['green'], 1955 | $color['blue'], 1956 | 127 - ($color['alpha'] * 127) 1957 | ); 1958 | 1959 | return $this; 1960 | } 1961 | 1962 | /** 1963 | * Applies the contrast filter. 1964 | * 1965 | * @param int $percentage Percentage to adjust (-100 - 100). 1966 | * @return SimpleImage 1967 | */ 1968 | public function contrast(int $percentage): static 1969 | { 1970 | imagefilter($this->image, IMG_FILTER_CONTRAST, self::keepWithin($percentage, -100, 100)); 1971 | 1972 | return $this; 1973 | } 1974 | 1975 | /** 1976 | * Applies the brightness filter to darken the image. 1977 | * 1978 | * @param int $percentage Percentage to darken the image (0 - 100). 1979 | * @return SimpleImage 1980 | */ 1981 | public function darken(int $percentage): static 1982 | { 1983 | $percentage = self::keepWithin(255 * $percentage / 100, 0, 255); 1984 | 1985 | imagefilter($this->image, IMG_FILTER_BRIGHTNESS, -$percentage); 1986 | 1987 | return $this; 1988 | } 1989 | 1990 | /** 1991 | * Applies the desaturate (grayscale) filter. 1992 | * 1993 | * @return SimpleImage 1994 | */ 1995 | public function desaturate(): static 1996 | { 1997 | imagefilter($this->image, IMG_FILTER_GRAYSCALE); 1998 | 1999 | return $this; 2000 | } 2001 | 2002 | /** 2003 | * Applies the edge detect filter. 2004 | * 2005 | * @return SimpleImage 2006 | */ 2007 | public function edgeDetect(): static 2008 | { 2009 | imagefilter($this->image, IMG_FILTER_EDGEDETECT); 2010 | 2011 | return $this; 2012 | } 2013 | 2014 | /** 2015 | * Applies the emboss filter. 2016 | * 2017 | * @return SimpleImage 2018 | */ 2019 | public function emboss(): static 2020 | { 2021 | imagefilter($this->image, IMG_FILTER_EMBOSS); 2022 | 2023 | return $this; 2024 | } 2025 | 2026 | /** 2027 | * Inverts the image's colors. 2028 | * 2029 | * @return SimpleImage 2030 | */ 2031 | public function invert(): static 2032 | { 2033 | imagefilter($this->image, IMG_FILTER_NEGATE); 2034 | 2035 | return $this; 2036 | } 2037 | 2038 | /** 2039 | * Changes the image's opacity level. 2040 | * 2041 | * @param float $opacity The desired opacity level (0 - 1). 2042 | * @return SimpleImage 2043 | * 2044 | * @throws Exception 2045 | */ 2046 | public function opacity(float $opacity): static 2047 | { 2048 | // Create a transparent image 2049 | $newImage = new SimpleImage(); 2050 | $newImage->fromNew($this->getWidth(), $this->getHeight()); 2051 | 2052 | // Copy the current image (with opacity) onto the transparent image 2053 | self::imageCopyMergeAlpha( 2054 | $newImage->image, 2055 | $this->image, 2056 | 0, 0, 2057 | 0, 0, 2058 | $this->getWidth(), 2059 | $this->getHeight(), 2060 | (int) round(self::keepWithin($opacity, 0, 1) * 100) 2061 | ); 2062 | 2063 | return $this; 2064 | } 2065 | 2066 | /** 2067 | * Applies the pixelate filter. 2068 | * 2069 | * @param int $size The size of the blocks in pixels (default 10). 2070 | * @return SimpleImage 2071 | */ 2072 | public function pixelate(int $size = 10): static 2073 | { 2074 | imagefilter($this->image, IMG_FILTER_PIXELATE, $size, true); 2075 | 2076 | return $this; 2077 | } 2078 | 2079 | /** 2080 | * Simulates a sepia effect by desaturating the image and applying a sepia tone. 2081 | * 2082 | * @return SimpleImage 2083 | */ 2084 | public function sepia(): static 2085 | { 2086 | imagefilter($this->image, IMG_FILTER_GRAYSCALE); 2087 | imagefilter($this->image, IMG_FILTER_COLORIZE, 70, 35, 0); 2088 | 2089 | return $this; 2090 | } 2091 | 2092 | /** 2093 | * Sharpens the image. 2094 | * 2095 | * @param int $amount Sharpening amount (default 50). 2096 | * @return SimpleImage 2097 | */ 2098 | public function sharpen(int $amount = 50): static 2099 | { 2100 | // Normalize amount 2101 | $amount = max(1, min(100, $amount)) / 100; 2102 | 2103 | $sharpen = [ 2104 | [-1, -1, -1], 2105 | [-1, 8 / $amount, -1], 2106 | [-1, -1, -1], 2107 | ]; 2108 | $divisor = array_sum(array_map('array_sum', $sharpen)); 2109 | 2110 | imageconvolution($this->image, $sharpen, $divisor, 0); 2111 | 2112 | return $this; 2113 | } 2114 | 2115 | /** 2116 | * Applies the mean remove filter to produce a sketch effect. 2117 | * 2118 | * @return SimpleImage 2119 | */ 2120 | public function sketch(): static 2121 | { 2122 | imagefilter($this->image, IMG_FILTER_MEAN_REMOVAL); 2123 | 2124 | return $this; 2125 | } 2126 | 2127 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2128 | // Color utilities 2129 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2130 | 2131 | /** 2132 | * Converts a "friendly color" into a color identifier for use with GD's image functions. 2133 | * 2134 | * @param string|array $color The color to allocate. 2135 | * 2136 | * @throws Exception 2137 | */ 2138 | protected function allocateColor(string|array $color): int 2139 | { 2140 | $color = self::normalizeColor($color); 2141 | 2142 | // Was this color already allocated? 2143 | $index = imagecolorexactalpha( 2144 | $this->image, 2145 | $color['red'], 2146 | $color['green'], 2147 | $color['blue'], 2148 | (int) (127 - ($color['alpha'] * 127)) 2149 | ); 2150 | if ($index > -1) { 2151 | // Yes, return this color index 2152 | return $index; 2153 | } 2154 | 2155 | // Allocate a new color index 2156 | return imagecolorallocatealpha( 2157 | $this->image, 2158 | $color['red'], 2159 | $color['green'], 2160 | $color['blue'], 2161 | 127 - ($color['alpha'] * 127) 2162 | ); 2163 | } 2164 | 2165 | /** 2166 | * Adjusts a color by increasing/decreasing red/green/blue/alpha values independently. 2167 | * 2168 | * @param string|array $color The color to adjust. 2169 | * @param int $red Red adjustment (-255 - 255). 2170 | * @param int $green Green adjustment (-255 - 255). 2171 | * @param int $blue Blue adjustment (-255 - 255). 2172 | * @param int $alpha Alpha adjustment (-1 - 1). 2173 | * @return int[] An RGBA color array. 2174 | * 2175 | * @throws Exception 2176 | */ 2177 | public static function adjustColor(string|array $color, int $red, int $green, int $blue, int $alpha): array 2178 | { 2179 | // Normalize to RGBA 2180 | $color = self::normalizeColor($color); 2181 | 2182 | // Adjust each channel 2183 | return self::normalizeColor([ 2184 | 'red' => $color['red'] + $red, 2185 | 'green' => $color['green'] + $green, 2186 | 'blue' => $color['blue'] + $blue, 2187 | 'alpha' => $color['alpha'] + $alpha, 2188 | ]); 2189 | } 2190 | 2191 | /** 2192 | * Darkens a color. 2193 | * 2194 | * @param string|array $color The color to darken. 2195 | * @param int $amount Amount to darken (0 - 255). 2196 | * @return int[] An RGBA color array. 2197 | * 2198 | * @throws Exception 2199 | */ 2200 | public static function darkenColor(string|array $color, int $amount): array 2201 | { 2202 | return self::adjustColor($color, -$amount, -$amount, -$amount, 0); 2203 | } 2204 | 2205 | /** 2206 | * Extracts colors from an image like a human would do.™ This method requires the third-party 2207 | * library \League\ColorExtractor. If you're using Composer, it will be installed for you 2208 | * automatically. 2209 | * 2210 | * @param int $count The max number of colors to extract (default 5). 2211 | * @param string|array|null $backgroundColor 2212 | * By default any pixel with alpha value greater than zero will 2213 | * be discarded. This is because transparent colors are not perceived as is. For example, fully 2214 | * transparent black would be seen white on a white background. So if you want to take 2215 | * transparency into account, you have to specify a default background color. 2216 | * @return int[] An array of RGBA colors arrays. 2217 | * 2218 | * @throws Exception Thrown if library \League\ColorExtractor is missing. 2219 | */ 2220 | public function extractColors(int $count = 5, string|array|null $backgroundColor = null): array 2221 | { 2222 | // Check for required library 2223 | if (! class_exists('\\'.ColorExtractor::class)) { 2224 | throw new Exception( 2225 | 'Required library \League\ColorExtractor is missing.', 2226 | self::ERR_LIB_NOT_LOADED 2227 | ); 2228 | } 2229 | 2230 | // Convert background color to an integer value 2231 | if ($backgroundColor) { 2232 | $backgroundColor = self::normalizeColor($backgroundColor); 2233 | $backgroundColor = Color::fromRgbToInt([ 2234 | 'r' => $backgroundColor['red'], 2235 | 'g' => $backgroundColor['green'], 2236 | 'b' => $backgroundColor['blue'], 2237 | ]); 2238 | } 2239 | 2240 | // Extract colors from the image 2241 | $palette = Palette::fromGD($this->image, $backgroundColor); 2242 | $extractor = new ColorExtractor($palette); 2243 | $colors = $extractor->extract($count); 2244 | 2245 | // Convert colors to an RGBA color array 2246 | foreach ($colors as $key => $value) { 2247 | $colors[$key] = self::normalizeColor(Color::fromIntToHex($value)); 2248 | } 2249 | 2250 | return $colors; 2251 | } 2252 | 2253 | /** 2254 | * Gets the RGBA value of a single pixel. 2255 | * 2256 | * @param int $x The horizontal position of the pixel. 2257 | * @param int $y The vertical position of the pixel. 2258 | * @return bool|int[] An RGBA color array or false if the x/y position is off the canvas. 2259 | */ 2260 | public function getColorAt(int $x, int $y): array|bool 2261 | { 2262 | // Coordinates must be on the canvas 2263 | if ($x < 0 || $x > $this->getWidth() || $y < 0 || $y > $this->getHeight()) { 2264 | return false; 2265 | } 2266 | 2267 | // Get the color of this pixel and convert it to RGBA 2268 | $color = imagecolorat($this->image, $x, $y); 2269 | $rgba = imagecolorsforindex($this->image, $color); 2270 | $rgba['alpha'] = 127 - ($color >> 24) & 0xFF; 2271 | 2272 | return $rgba; 2273 | } 2274 | 2275 | /** 2276 | * Lightens a color. 2277 | * 2278 | * @param string|array $color The color to lighten. 2279 | * @param int $amount Amount to lighten (0 - 255). 2280 | * @return int[] An RGBA color array. 2281 | * 2282 | * @throws Exception 2283 | */ 2284 | public static function lightenColor(string|array $color, int $amount): array 2285 | { 2286 | return self::adjustColor($color, $amount, $amount, $amount, 0); 2287 | } 2288 | 2289 | /** 2290 | * Normalizes a hex or array color value to a well-formatted RGBA array. 2291 | * 2292 | * @param string|array $color 2293 | * A CSS color name, hex string, or an array [red, green, blue, alpha]. 2294 | * You can pipe alpha transparency through hex strings and color names. For example: 2295 | * #fff|0.50 <-- 50% white 2296 | * red|0.25 <-- 25% red 2297 | * @return array [red, green, blue, alpha]. 2298 | * 2299 | * @throws Exception Thrown if color value is invalid. 2300 | */ 2301 | public static function normalizeColor(string|array $color): array 2302 | { 2303 | // 140 CSS color names and hex values 2304 | $cssColors = [ 2305 | 'aliceblue' => '#f0f8ff', 'antiquewhite' => '#faebd7', 'aqua' => '#00ffff', 2306 | 'aquamarine' => '#7fffd4', 'azure' => '#f0ffff', 'beige' => '#f5f5dc', 'bisque' => '#ffe4c4', 2307 | 'black' => '#000000', 'blanchedalmond' => '#ffebcd', 'blue' => '#0000ff', 2308 | 'blueviolet' => '#8a2be2', 'brown' => '#a52a2a', 'burlywood' => '#deb887', 2309 | 'cadetblue' => '#5f9ea0', 'chartreuse' => '#7fff00', 'chocolate' => '#d2691e', 2310 | 'coral' => '#ff7f50', 'cornflowerblue' => '#6495ed', 'cornsilk' => '#fff8dc', 2311 | 'crimson' => '#dc143c', 'cyan' => '#00ffff', 'darkblue' => '#00008b', 'darkcyan' => '#008b8b', 2312 | 'darkgoldenrod' => '#b8860b', 'darkgray' => '#a9a9a9', 'darkgrey' => '#a9a9a9', 2313 | 'darkgreen' => '#006400', 'darkkhaki' => '#bdb76b', 'darkmagenta' => '#8b008b', 2314 | 'darkolivegreen' => '#556b2f', 'darkorange' => '#ff8c00', 'darkorchid' => '#9932cc', 2315 | 'darkred' => '#8b0000', 'darksalmon' => '#e9967a', 'darkseagreen' => '#8fbc8f', 2316 | 'darkslateblue' => '#483d8b', 'darkslategray' => '#2f4f4f', 'darkslategrey' => '#2f4f4f', 2317 | 'darkturquoise' => '#00ced1', 'darkviolet' => '#9400d3', 'deeppink' => '#ff1493', 2318 | 'deepskyblue' => '#00bfff', 'dimgray' => '#696969', 'dimgrey' => '#696969', 2319 | 'dodgerblue' => '#1e90ff', 'firebrick' => '#b22222', 'floralwhite' => '#fffaf0', 2320 | 'forestgreen' => '#228b22', 'fuchsia' => '#ff00ff', 'gainsboro' => '#dcdcdc', 2321 | 'ghostwhite' => '#f8f8ff', 'gold' => '#ffd700', 'goldenrod' => '#daa520', 'gray' => '#808080', 2322 | 'grey' => '#808080', 'green' => '#008000', 'greenyellow' => '#adff2f', 2323 | 'honeydew' => '#f0fff0', 'hotpink' => '#ff69b4', 'indianred ' => '#cd5c5c', 2324 | 'indigo ' => '#4b0082', 'ivory' => '#fffff0', 'khaki' => '#f0e68c', 'lavender' => '#e6e6fa', 2325 | 'lavenderblush' => '#fff0f5', 'lawngreen' => '#7cfc00', 'lemonchiffon' => '#fffacd', 2326 | 'lightblue' => '#add8e6', 'lightcoral' => '#f08080', 'lightcyan' => '#e0ffff', 2327 | 'lightgoldenrodyellow' => '#fafad2', 'lightgray' => '#d3d3d3', 'lightgrey' => '#d3d3d3', 2328 | 'lightgreen' => '#90ee90', 'lightpink' => '#ffb6c1', 'lightsalmon' => '#ffa07a', 2329 | 'lightseagreen' => '#20b2aa', 'lightskyblue' => '#87cefa', 'lightslategray' => '#778899', 2330 | 'lightslategrey' => '#778899', 'lightsteelblue' => '#b0c4de', 'lightyellow' => '#ffffe0', 2331 | 'lime' => '#00ff00', 'limegreen' => '#32cd32', 'linen' => '#faf0e6', 'magenta' => '#ff00ff', 2332 | 'maroon' => '#800000', 'mediumaquamarine' => '#66cdaa', 'mediumblue' => '#0000cd', 2333 | 'mediumorchid' => '#ba55d3', 'mediumpurple' => '#9370db', 'mediumseagreen' => '#3cb371', 2334 | 'mediumslateblue' => '#7b68ee', 'mediumspringgreen' => '#00fa9a', 2335 | 'mediumturquoise' => '#48d1cc', 'mediumvioletred' => '#c71585', 'midnightblue' => '#191970', 2336 | 'mintcream' => '#f5fffa', 'mistyrose' => '#ffe4e1', 'moccasin' => '#ffe4b5', 2337 | 'navajowhite' => '#ffdead', 'navy' => '#000080', 'oldlace' => '#fdf5e6', 'olive' => '#808000', 2338 | 'olivedrab' => '#6b8e23', 'orange' => '#ffa500', 'orangered' => '#ff4500', 2339 | 'orchid' => '#da70d6', 'palegoldenrod' => '#eee8aa', 'palegreen' => '#98fb98', 2340 | 'paleturquoise' => '#afeeee', 'palevioletred' => '#db7093', 'papayawhip' => '#ffefd5', 2341 | 'peachpuff' => '#ffdab9', 'peru' => '#cd853f', 'pink' => '#ffc0cb', 'plum' => '#dda0dd', 2342 | 'powderblue' => '#b0e0e6', 'purple' => '#800080', 'rebeccapurple' => '#663399', 2343 | 'red' => '#ff0000', 'rosybrown' => '#bc8f8f', 'royalblue' => '#4169e1', 2344 | 'saddlebrown' => '#8b4513', 'salmon' => '#fa8072', 'sandybrown' => '#f4a460', 2345 | 'seagreen' => '#2e8b57', 'seashell' => '#fff5ee', 'sienna' => '#a0522d', 2346 | 'silver' => '#c0c0c0', 'skyblue' => '#87ceeb', 'slateblue' => '#6a5acd', 2347 | 'slategray' => '#708090', 'slategrey' => '#708090', 'snow' => '#fffafa', 2348 | 'springgreen' => '#00ff7f', 'steelblue' => '#4682b4', 'tan' => '#d2b48c', 'teal' => '#008080', 2349 | 'thistle' => '#d8bfd8', 'tomato' => '#ff6347', 'turquoise' => '#40e0d0', 2350 | 'violet' => '#ee82ee', 'wheat' => '#f5deb3', 'white' => '#ffffff', 'whitesmoke' => '#f5f5f5', 2351 | 'yellow' => '#ffff00', 'yellowgreen' => '#9acd32', 2352 | ]; 2353 | 2354 | // Parse alpha from '#fff|.5' and 'white|.5' 2355 | if (is_string($color) && strstr($color, '|')) { 2356 | $color = explode('|', $color); 2357 | $alpha = (float) $color[1]; 2358 | $color = trim($color[0]); 2359 | } else { 2360 | $alpha = 1; 2361 | } 2362 | 2363 | // Translate CSS color names to hex values 2364 | if (is_string($color) && array_key_exists(strtolower($color), $cssColors)) { 2365 | $color = $cssColors[strtolower($color)]; 2366 | } 2367 | 2368 | // Translate transparent keyword to a transparent color 2369 | if ($color === 'transparent') { 2370 | $color = ['red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 0]; 2371 | } 2372 | 2373 | // Convert hex values to RGBA 2374 | if (is_string($color)) { 2375 | // Remove # 2376 | $hex = strval(preg_replace('/^#/', '', $color)); 2377 | 2378 | // Support short and standard hex codes 2379 | if (strlen($hex) === 3 || strlen($hex) === 4) { 2380 | [$red, $green, $blue] = [ 2381 | $hex[0].$hex[0], 2382 | $hex[1].$hex[1], 2383 | $hex[2].$hex[2], 2384 | ]; 2385 | if (strlen($hex) === 4) { 2386 | $alpha = hexdec($hex[3]) / 255; 2387 | } 2388 | } elseif (strlen($hex) === 6 || strlen($hex) === 8) { 2389 | [$red, $green, $blue] = [ 2390 | $hex[0].$hex[1], 2391 | $hex[2].$hex[3], 2392 | $hex[4].$hex[5], 2393 | ]; 2394 | if (strlen($hex) === 8) { 2395 | $alpha = hexdec($hex[6].$hex[7]) / 255; 2396 | } 2397 | } else { 2398 | throw new Exception("Invalid color value: $color", self::ERR_INVALID_COLOR); 2399 | } 2400 | 2401 | // Turn color into an array 2402 | $color = [ 2403 | 'red' => hexdec($red), 2404 | 'green' => hexdec($green), 2405 | 'blue' => hexdec($blue), 2406 | 'alpha' => $alpha, 2407 | ]; 2408 | } 2409 | 2410 | // Enforce color value ranges 2411 | if (is_array($color)) { 2412 | // RGB default to 0 2413 | $color['red'] ??= 0; 2414 | $color['green'] ??= 0; 2415 | $color['blue'] ??= 0; 2416 | 2417 | // Alpha defaults to 1 2418 | $color['alpha'] ??= 1; 2419 | 2420 | return [ 2421 | 'red' => (int) self::keepWithin((int) $color['red'], 0, 255), 2422 | 'green' => (int) self::keepWithin((int) $color['green'], 0, 255), 2423 | 'blue' => (int) self::keepWithin((int) $color['blue'], 0, 255), 2424 | 'alpha' => self::keepWithin($color['alpha'], 0, 1), 2425 | ]; 2426 | } 2427 | 2428 | throw new Exception("Invalid color value: $color", self::ERR_INVALID_COLOR); 2429 | } 2430 | } 2431 | --------------------------------------------------------------------------------