├── README.md ├── LICENSE ├── composer.json ├── .phpstorm.meta.php └── src └── Image.php /README.md: -------------------------------------------------------------------------------- 1 | Aplus Framework Image Library 2 | 3 | # Aplus Framework Image Library 4 | 5 | - [Home](https://aplus-framework.com/packages/image) 6 | - [User Guide](https://docs.aplus-framework.com/guides/libraries/image/index.html) 7 | - [API Documentation](https://docs.aplus-framework.com/packages/image.html) 8 | 9 | [![tests](https://github.com/aplus-framework/image/actions/workflows/tests.yml/badge.svg)](https://github.com/aplus-framework/image/actions/workflows/tests.yml) 10 | [![coverage](https://coveralls.io/repos/github/aplus-framework/image/badge.svg?branch=master)](https://coveralls.io/github/aplus-framework/image?branch=master) 11 | [![packagist](https://img.shields.io/packagist/v/aplus/image)](https://packagist.org/packages/aplus/image) 12 | [![open-source](https://img.shields.io/badge/open--source-sponsor-magenta)](https://aplus-framework.com/sponsor) 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Natan Felles 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aplus/image", 3 | "description": "Aplus Framework Image Library", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "image", 8 | "opacity", 9 | "watermark", 10 | "filter", 11 | "flatten", 12 | "gd" 13 | ], 14 | "authors": [ 15 | { 16 | "name": "Natan Felles", 17 | "email": "natanfelles@gmail.com", 18 | "homepage": "https://natanfelles.github.io" 19 | } 20 | ], 21 | "homepage": "https://aplus-framework.com/packages/image", 22 | "support": { 23 | "email": "support@aplus-framework.com", 24 | "issues": "https://github.com/aplus-framework/image/issues", 25 | "forum": "https://aplus-framework.com/forum", 26 | "source": "https://github.com/aplus-framework/image", 27 | "docs": "https://docs.aplus-framework.com/guides/libraries/image/" 28 | }, 29 | "funding": [ 30 | { 31 | "type": "Aplus Sponsor", 32 | "url": "https://aplus-framework.com/sponsor" 33 | } 34 | ], 35 | "require": { 36 | "php": ">=8.3", 37 | "ext-gd": "*", 38 | "ext-json": "*" 39 | }, 40 | "require-dev": { 41 | "ext-xdebug": "*", 42 | "aplus/coding-standard": "^2.8", 43 | "ergebnis/composer-normalize": "^2.15", 44 | "jetbrains/phpstorm-attributes": "^1.0", 45 | "phpmd/phpmd": "^2.13", 46 | "phpstan/phpstan": "^1.11", 47 | "phpunit/phpunit": "^10.5" 48 | }, 49 | "minimum-stability": "dev", 50 | "prefer-stable": true, 51 | "autoload": { 52 | "psr-4": { 53 | "Framework\\Image\\": "src/" 54 | } 55 | }, 56 | "autoload-dev": { 57 | "psr-4": { 58 | "Tests\\Image\\": "tests/" 59 | } 60 | }, 61 | "config": { 62 | "allow-plugins": { 63 | "ergebnis/composer-normalize": true 64 | }, 65 | "optimize-autoloader": true, 66 | "preferred-install": "dist", 67 | "sort-packages": true 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace PHPSTORM_META; 11 | 12 | registerArgumentsSet( 13 | 'flip_directions', 14 | 'b', 15 | 'both', 16 | 'h', 17 | 'horizontal', 18 | 'v', 19 | 'vertical', 20 | ); 21 | registerArgumentsSet( 22 | 'filter_types', 23 | \IMG_FILTER_NEGATE, 24 | \IMG_FILTER_GRAYSCALE, 25 | \IMG_FILTER_BRIGHTNESS, 26 | \IMG_FILTER_CONTRAST, 27 | \IMG_FILTER_COLORIZE, 28 | \IMG_FILTER_EDGEDETECT, 29 | \IMG_FILTER_EMBOSS, 30 | \IMG_FILTER_GAUSSIAN_BLUR, 31 | \IMG_FILTER_SELECTIVE_BLUR, 32 | \IMG_FILTER_MEAN_REMOVAL, 33 | \IMG_FILTER_SMOOTH, 34 | \IMG_FILTER_PIXELATE, 35 | \IMG_FILTER_SCATTER, 36 | ); 37 | registerArgumentsSet( 38 | 'filter_arg1', 39 | \IMG_FILTER_BRIGHTNESS, 40 | \IMG_FILTER_CONTRAST, 41 | \IMG_FILTER_COLORIZE, 42 | \IMG_FILTER_SMOOTH, 43 | \IMG_FILTER_PIXELATE, 44 | \IMG_FILTER_SCATTER, 45 | ); 46 | registerArgumentsSet( 47 | 'filter_arg2', 48 | \IMG_FILTER_COLORIZE, 49 | \IMG_FILTER_PIXELATE, 50 | \IMG_FILTER_SCATTER, 51 | ); 52 | registerArgumentsSet( 53 | 'filter_arg3', 54 | \IMG_FILTER_COLORIZE, 55 | \IMG_FILTER_SCATTER, 56 | ); 57 | registerArgumentsSet( 58 | 'filter_arg4', 59 | \IMG_FILTER_COLORIZE, 60 | ); 61 | expectedArguments( 62 | \Framework\Image\Image::flip(), 63 | 0, 64 | argumentsSet('flip_directions') 65 | ); 66 | expectedArguments( 67 | \Framework\Image\Image::filter(), 68 | 0, 69 | argumentsSet('filter_types') 70 | ); 71 | expectedArguments( 72 | \Framework\Image\Image::filter(), 73 | 1, 74 | argumentsSet('filter_arg1') 75 | ); 76 | expectedArguments( 77 | \Framework\Image\Image::filter(), 78 | 2, 79 | argumentsSet('filter_arg2') 80 | ); 81 | expectedArguments( 82 | \Framework\Image\Image::filter(), 83 | 3, 84 | argumentsSet('filter_arg3') 85 | ); 86 | expectedArguments( 87 | \Framework\Image\Image::filter(), 88 | 4, 89 | argumentsSet('filter_arg4') 90 | ); 91 | -------------------------------------------------------------------------------- /src/Image.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Framework\Image; 11 | 12 | use GdImage; 13 | use InvalidArgumentException; 14 | use JetBrains\PhpStorm\ArrayShape; 15 | use JetBrains\PhpStorm\Pure; 16 | use LogicException; 17 | use RuntimeException; 18 | 19 | /** 20 | * Class Image. 21 | * 22 | * @package image 23 | */ 24 | class Image implements \JsonSerializable, \Stringable 25 | { 26 | /** 27 | * Path to the image file. 28 | */ 29 | protected string $filename; 30 | /** 31 | * Image type. One of IMAGETYPE_* constants. 32 | */ 33 | protected int $type; 34 | /** 35 | * MIME type. 36 | */ 37 | protected string $mime; 38 | /** 39 | * GdImage instance. 40 | */ 41 | protected GdImage $instance; 42 | /** 43 | * The image quality/compression level. 44 | * 45 | * 0 to 9 on PNG, default is 6. 0 to 100 on JPEG, default is 75. 46 | * Null to update to the default when getQuality is called. 47 | * 48 | * @see Image::getQuality() 49 | */ 50 | protected ?int $quality = null; 51 | 52 | /** 53 | * Image constructor. 54 | * 55 | * @param string $filename Path to the image file. 56 | * Acceptable types are: GIF, JPEG and PNG 57 | * 58 | * @throws InvalidArgumentException for invalid file 59 | * @throws RuntimeException for unsupported image type of could not get image info 60 | */ 61 | public function __construct(string $filename) 62 | { 63 | $realpath = \realpath($filename); 64 | if ($realpath === false || !\is_file($realpath) || !\is_readable($realpath)) { 65 | throw new InvalidArgumentException('File does not exists or is not readable: ' . $filename); 66 | } 67 | $this->filename = $realpath; 68 | $info = \getimagesize($this->filename); 69 | if ($info === false) { 70 | throw new RuntimeException( 71 | 'Could not get image info from the given filename: ' . $this->filename 72 | ); 73 | } 74 | if (!(\imagetypes() & $info[2])) { 75 | throw new RuntimeException('Unsupported image type: ' . $info[2]); 76 | } 77 | $this->type = $info[2]; 78 | $this->mime = $info['mime']; 79 | $instance = match ($this->type) { 80 | \IMAGETYPE_PNG => \imagecreatefrompng($this->filename), 81 | \IMAGETYPE_JPEG => \imagecreatefromjpeg($this->filename), 82 | \IMAGETYPE_GIF => \imagecreatefromgif($this->filename), 83 | default => throw new RuntimeException('Image type is not acceptable: ' . $this->type), 84 | }; 85 | if (!$instance instanceof GdImage) { 86 | throw new RuntimeException( 87 | "Image of type '{$this->type}' does not returned a GdImage instance" 88 | ); 89 | } 90 | $this->instance = $instance; 91 | } 92 | 93 | public function __destruct() 94 | { 95 | $this->destroy(); 96 | } 97 | 98 | public function __toString() : string 99 | { 100 | return $this->getDataUrl(); 101 | } 102 | 103 | /** 104 | * Destroys the GdImage instance. 105 | * 106 | * @return bool 107 | */ 108 | public function destroy() : bool 109 | { 110 | return \imagedestroy($this->instance); 111 | } 112 | 113 | /** 114 | * Gets the GdImage instance. 115 | * 116 | * @return GdImage GdImage instance 117 | */ 118 | #[Pure] 119 | public function getInstance() : GdImage 120 | { 121 | return $this->instance; 122 | } 123 | 124 | /** 125 | * Sets the GdImage instance. 126 | * 127 | * @param GdImage $instance GdImage instance 128 | * 129 | * @return static 130 | */ 131 | public function setInstance(GdImage $instance) : static 132 | { 133 | $this->instance = $instance; 134 | return $this; 135 | } 136 | 137 | /** 138 | * Gets the image quality/compression level. 139 | * 140 | * @return int|null An integer for PNG and JPEG types or null for GIF 141 | */ 142 | public function getQuality() : ?int 143 | { 144 | if ($this->quality === null) { 145 | if ($this->type === \IMAGETYPE_PNG) { 146 | $this->quality = 6; 147 | } elseif ($this->type === \IMAGETYPE_JPEG) { 148 | $this->quality = 75; 149 | } 150 | } 151 | return $this->quality; 152 | } 153 | 154 | /** 155 | * Sets the image quality/compression level. 156 | * 157 | * @param int $quality The quality/compression level 158 | * 159 | * @throws LogicException when trying to set a quality value for a GIF image 160 | * @throws InvalidArgumentException if the image type is PNG and the value 161 | * is not between 0 and 9 or if the image type is JPEG and the value is not 162 | * between 0 and 100 163 | * 164 | * @return static 165 | */ 166 | public function setQuality(int $quality) : static 167 | { 168 | if ($this->type === \IMAGETYPE_GIF) { 169 | throw new LogicException( 170 | 'GIF images does not receive a quality value' 171 | ); 172 | } 173 | if ($this->type === \IMAGETYPE_PNG && ($quality < 0 || $quality > 9)) { 174 | throw new InvalidArgumentException( 175 | 'PNG images must receive a quality value between 0 and 9, ' . $quality . ' given' 176 | ); 177 | } 178 | if ($this->type === \IMAGETYPE_JPEG && ($quality < 0 || $quality > 100)) { 179 | throw new InvalidArgumentException( 180 | 'JPEG images must receive a quality value between 0 and 100, ' . $quality . ' given' 181 | ); 182 | } 183 | $this->quality = $quality; 184 | return $this; 185 | } 186 | 187 | /** 188 | * Gets the image resolution. 189 | * 190 | * @throws RuntimeException for image could not get resolution 191 | * 192 | * @return array Returns an array containing two keys, horizontal and 193 | * vertical, with integers as values 194 | */ 195 | #[ArrayShape(['horizontal' => 'int', 'vertical' => 'int'])] 196 | public function getResolution() : array 197 | { 198 | $resolution = \imageresolution($this->instance); 199 | if ($resolution === false) { 200 | throw new RuntimeException('Image could not to get resolution'); 201 | } 202 | return [ 203 | 'horizontal' => $resolution[0], // @phpstan-ignore-line 204 | // @phpstan-ignore-next-line 205 | 'vertical' => $resolution[1], 206 | ]; 207 | } 208 | 209 | /** 210 | * Sets the image resolution. 211 | * 212 | * @param int $horizontal The horizontal resolution in DPI 213 | * @param int $vertical The vertical resolution in DPI 214 | * 215 | * @throws RuntimeException for image could not to set resolution 216 | * 217 | * @return static 218 | */ 219 | public function setResolution(int $horizontal = 96, int $vertical = 96) : static 220 | { 221 | $set = \imageresolution($this->instance, $horizontal, $vertical); 222 | if ($set === false) { 223 | throw new RuntimeException('Image could not to set resolution'); 224 | } 225 | return $this; 226 | } 227 | 228 | /** 229 | * Gets the image height. 230 | * 231 | * @return int 232 | */ 233 | #[Pure] 234 | public function getHeight() : int 235 | { 236 | return \imagesy($this->instance); 237 | } 238 | 239 | /** 240 | * Gets the image width. 241 | * 242 | * @return int 243 | */ 244 | #[Pure] 245 | public function getWidth() : int 246 | { 247 | return \imagesx($this->instance); 248 | } 249 | 250 | /** 251 | * Gets the file extension for image type. 252 | * 253 | * @return false|string a string with the extension corresponding to the 254 | * given image type or false on fail 255 | */ 256 | #[Pure] 257 | public function getExtension() : false | string 258 | { 259 | return \image_type_to_extension($this->type); 260 | } 261 | 262 | /** 263 | * Gets the image MIME type. 264 | * 265 | * @return string 266 | */ 267 | #[Pure] 268 | public function getMime() : string 269 | { 270 | return $this->mime; 271 | } 272 | 273 | /** 274 | * Saves the image contents to a given filename. 275 | * 276 | * @param string|null $filename Optional filename or null to use the original 277 | * 278 | * @return bool 279 | */ 280 | public function save(?string $filename = null) : bool 281 | { 282 | $filename ??= $this->filename; 283 | return match ($this->type) { 284 | \IMAGETYPE_PNG => \imagepng($this->instance, $filename, $this->getQuality()), 285 | \IMAGETYPE_JPEG => \imagejpeg($this->instance, $filename, $this->getQuality()), 286 | \IMAGETYPE_GIF => \imagegif($this->instance, $filename), 287 | default => false, 288 | }; 289 | } 290 | 291 | /** 292 | * Sends the image contents to the output buffer. 293 | * 294 | * @return bool 295 | */ 296 | public function send() : bool 297 | { 298 | if (\in_array($this->type, [\IMAGETYPE_PNG, \IMAGETYPE_GIF], true)) { 299 | \imagesavealpha($this->instance, true); 300 | } 301 | return match ($this->type) { 302 | \IMAGETYPE_PNG => \imagepng($this->instance, null, $this->getQuality()), 303 | \IMAGETYPE_JPEG => \imagejpeg($this->instance, null, $this->getQuality()), 304 | \IMAGETYPE_GIF => \imagegif($this->instance), 305 | default => false, 306 | }; 307 | } 308 | 309 | /** 310 | * Renders the image contents. 311 | * 312 | * @throws RuntimeException for image could not be rendered 313 | * 314 | * @return string The image contents 315 | */ 316 | public function render() : string 317 | { 318 | \ob_start(); 319 | $status = $this->send(); 320 | $contents = \ob_get_clean(); 321 | if ($status === false || $contents === false) { 322 | throw new RuntimeException('Image could not be rendered'); 323 | } 324 | return $contents; 325 | } 326 | 327 | /** 328 | * Crops the image. 329 | * 330 | * @param int $width Width in pixels 331 | * @param int $height Height in pixels 332 | * @param int $marginLeft Margin left in pixels 333 | * @param int $marginTop Margin top in pixels 334 | * 335 | * @throws RuntimeException for image could not to crop 336 | * 337 | * @return static 338 | */ 339 | public function crop(int $width, int $height, int $marginLeft = 0, int $marginTop = 0) : static 340 | { 341 | $crop = \imagecrop($this->instance, [ 342 | 'x' => $marginLeft, 343 | 'y' => $marginTop, 344 | 'width' => $width, 345 | 'height' => $height, 346 | ]); 347 | if ($crop === false) { 348 | throw new RuntimeException('Image could not to crop'); 349 | } 350 | $this->instance = $crop; 351 | return $this; 352 | } 353 | 354 | /** 355 | * Flips the image. 356 | * 357 | * @param string $direction Allowed values are: h or horizontal. v or vertical. b or both. 358 | * 359 | * @throws InvalidArgumentException for invalid image flip direction 360 | * @throws RuntimeException for image could not to flip 361 | * 362 | * @return static 363 | */ 364 | public function flip(string $direction = 'horizontal') : static 365 | { 366 | $direction = match ($direction) { 367 | 'h', 'horizontal' => \IMG_FLIP_HORIZONTAL, 368 | 'v', 'vertical' => \IMG_FLIP_VERTICAL, 369 | 'b', 'both' => \IMG_FLIP_BOTH, 370 | default => throw new InvalidArgumentException('Invalid image flip direction: ' . $direction), 371 | }; 372 | $flip = \imageflip($this->instance, $direction); 373 | if ($flip === false) { 374 | throw new RuntimeException('Image could not to flip'); 375 | } 376 | return $this; 377 | } 378 | 379 | /** 380 | * Applies a filter to the image. 381 | * 382 | * @param int $type IMG_FILTER_* constants 383 | * @param int ...$arguments Arguments for the filter type 384 | * 385 | * @see https://www.php.net/manual/en/function.imagefilter.php 386 | * 387 | * @throws RuntimeException for image could not apply the filter 388 | * 389 | * @return static 390 | */ 391 | public function filter(int $type, int ...$arguments) : static 392 | { 393 | $filtered = \imagefilter($this->instance, $type, ...$arguments); 394 | if ($filtered === false) { 395 | throw new RuntimeException('Image could not apply the filter'); 396 | } 397 | return $this; 398 | } 399 | 400 | /** 401 | * Flattens the image. 402 | * 403 | * Replaces transparency with an RGB color. 404 | * 405 | * @param int $red 406 | * @param int $green 407 | * @param int $blue 408 | * 409 | * @throws RuntimeException for could not create a true color image, could 410 | * not allocate a color or image could not to flatten 411 | * 412 | * @return static 413 | */ 414 | public function flatten(int $red = 255, int $green = 255, int $blue = 255) : static 415 | { 416 | \imagesavealpha($this->instance, false); 417 | $image = \imagecreatetruecolor($this->getWidth(), $this->getHeight()); 418 | if ($image === false) { 419 | throw new RuntimeException('Could not create a true color image'); 420 | } 421 | $color = \imagecolorallocate($image, $red, $green, $blue); 422 | if ($color === false) { 423 | throw new RuntimeException('Image could not allocate a color'); 424 | } 425 | \imagefilledrectangle( 426 | $image, 427 | 0, 428 | 0, 429 | $this->getWidth(), 430 | $this->getHeight(), 431 | $color 432 | ); 433 | $copied = \imagecopy( 434 | $image, 435 | $this->instance, 436 | 0, 437 | 0, 438 | 0, 439 | 0, 440 | $this->getWidth(), 441 | $this->getHeight() 442 | ); 443 | if ($copied === false) { 444 | throw new RuntimeException('Image could not to flatten'); 445 | } 446 | $this->instance = $image; 447 | return $this; 448 | } 449 | 450 | /** 451 | * Sets the image opacity level. 452 | * 453 | * @param int $opacity Opacity percentage: from 0 to 100 454 | * 455 | * @return static 456 | */ 457 | public function opacity(int $opacity = 100) : static 458 | { 459 | if ($opacity < 0 || $opacity > 100) { 460 | throw new InvalidArgumentException( 461 | 'Opacity percentage must be between 0 and 100, ' . $opacity . ' given' 462 | ); 463 | } 464 | if ($opacity === 100) { 465 | \imagealphablending($this->instance, true); 466 | return $this; 467 | } 468 | $opacity = (int) \round(\abs(($opacity * 127 / 100) - 127)); 469 | \imagelayereffect($this->instance, \IMG_EFFECT_OVERLAY); 470 | $color = \imagecolorallocatealpha($this->instance, 127, 127, 127, $opacity); 471 | if ($color === false) { 472 | throw new RuntimeException('Image could not allocate a color'); 473 | } 474 | \imagefilledrectangle( 475 | $this->instance, 476 | 0, 477 | 0, 478 | $this->getWidth(), 479 | $this->getHeight(), 480 | $color 481 | ); 482 | \imagesavealpha($this->instance, true); 483 | \imagealphablending($this->instance, false); 484 | return $this; 485 | } 486 | 487 | /** 488 | * Rotates the image with a given angle. 489 | * 490 | * @param float $angle Rotation angle, in degrees. Clockwise direction. 491 | * 492 | * @throws RuntimeException for image could not allocate a color or could not rotate 493 | * 494 | * @return static 495 | */ 496 | public function rotate(float $angle) : static 497 | { 498 | if (\in_array($this->type, [\IMAGETYPE_PNG, \IMAGETYPE_GIF], true)) { 499 | \imagealphablending($this->instance, false); 500 | \imagesavealpha($this->instance, true); 501 | $background = \imagecolorallocatealpha($this->instance, 0, 0, 0, 127); 502 | } else { 503 | $background = \imagecolorallocate($this->instance, 255, 255, 255); 504 | } 505 | if ($background === false) { 506 | throw new RuntimeException('Image could not allocate a color'); 507 | } 508 | $rotate = \imagerotate($this->instance, -1 * $angle, $background); 509 | if ($rotate === false) { 510 | throw new RuntimeException('Image could not to rotate'); 511 | } 512 | $this->instance = $rotate; 513 | return $this; 514 | } 515 | 516 | /** 517 | * Scales the image. 518 | * 519 | * @param int $width Width in pixels 520 | * @param int $height Height in pixels. Use -1 to use a proportional height 521 | * based on the width. 522 | * 523 | * @throws RuntimeException for image could not to scale 524 | * 525 | * @return static 526 | */ 527 | public function scale(int $width, int $height = -1) : static 528 | { 529 | $scale = \imagescale($this->instance, $width, $height); 530 | if ($scale === false) { 531 | throw new RuntimeException('Image could not to scale'); 532 | } 533 | $this->instance = $scale; 534 | return $this; 535 | } 536 | 537 | /** 538 | * Adds a watermark to the image. 539 | * 540 | * @param Image $watermark The image to use as watermark 541 | * @param int $horizontalPosition Horizontal position 542 | * @param int $verticalPosition Vertical position 543 | * 544 | * @throws RuntimeException for image could not to create watermark 545 | * 546 | * @return static 547 | */ 548 | public function watermark( 549 | Image $watermark, 550 | int $horizontalPosition = 0, 551 | int $verticalPosition = 0 552 | ) : static { 553 | if ($horizontalPosition < 0) { 554 | $horizontalPosition = $this->getWidth() 555 | - (-1 * $horizontalPosition + $watermark->getWidth()); 556 | } 557 | if ($verticalPosition < 0) { 558 | $verticalPosition = $this->getHeight() 559 | - (-1 * $verticalPosition + $watermark->getHeight()); 560 | } 561 | $copied = \imagecopy( 562 | $this->instance, 563 | $watermark->getInstance(), 564 | $horizontalPosition, 565 | $verticalPosition, 566 | 0, 567 | 0, 568 | $watermark->getWidth(), 569 | $watermark->getHeight() 570 | ); 571 | if ($copied === false) { 572 | throw new RuntimeException('Image could not to create watermark'); 573 | } 574 | return $this; 575 | } 576 | 577 | /** 578 | * Allow embed the image contents in a document. 579 | * 580 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs 581 | * @see https://datatracker.ietf.org/doc/html/rfc2397 582 | * 583 | * @return string The image "data" URL 584 | */ 585 | public function getDataUrl() : string 586 | { 587 | return 'data:' . $this->getMime() . ';base64,' . \base64_encode($this->render()); 588 | } 589 | 590 | /** 591 | * @return string 592 | */ 593 | public function jsonSerialize() : string 594 | { 595 | return $this->getDataUrl(); 596 | } 597 | 598 | /** 599 | * Indicates if a given filename has an acceptable image type. 600 | * 601 | * @param string $filename 602 | * 603 | * @return bool 604 | */ 605 | public static function isAcceptable(string $filename) : bool 606 | { 607 | $filename = \realpath($filename); 608 | if ($filename === false || !\is_file($filename) || !\is_readable($filename)) { 609 | return false; 610 | } 611 | $info = \getimagesize($filename); 612 | if ($info === false) { 613 | return false; 614 | } 615 | return match ($info[2]) { 616 | \IMAGETYPE_PNG, \IMAGETYPE_JPEG, \IMAGETYPE_GIF => true, 617 | default => false, 618 | }; 619 | } 620 | } 621 | --------------------------------------------------------------------------------