├── 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 __toString() : string 94 | { 95 | return $this->getDataUrl(); 96 | } 97 | 98 | /** 99 | * Destroys the GdImage instance. 100 | * 101 | * @return bool 102 | * 103 | * @deprecated 104 | * 105 | * @codeCoverageIgnore 106 | */ 107 | public function destroy() : bool 108 | { 109 | return \imagedestroy($this->instance); 110 | } 111 | 112 | /** 113 | * Gets the GdImage instance. 114 | * 115 | * @return GdImage GdImage instance 116 | */ 117 | #[Pure] 118 | public function getInstance() : GdImage 119 | { 120 | return $this->instance; 121 | } 122 | 123 | /** 124 | * Sets the GdImage instance. 125 | * 126 | * @param GdImage $instance GdImage instance 127 | * 128 | * @return static 129 | */ 130 | public function setInstance(GdImage $instance) : static 131 | { 132 | $this->instance = $instance; 133 | return $this; 134 | } 135 | 136 | /** 137 | * Gets the image quality/compression level. 138 | * 139 | * @return int|null An integer for PNG and JPEG types or null for GIF 140 | */ 141 | public function getQuality() : ?int 142 | { 143 | if ($this->quality === null) { 144 | if ($this->type === \IMAGETYPE_PNG) { 145 | $this->quality = 6; 146 | } elseif ($this->type === \IMAGETYPE_JPEG) { 147 | $this->quality = 75; 148 | } 149 | } 150 | return $this->quality; 151 | } 152 | 153 | /** 154 | * Sets the image quality/compression level. 155 | * 156 | * @param int $quality The quality/compression level 157 | * 158 | * @throws LogicException when trying to set a quality value for a GIF image 159 | * @throws InvalidArgumentException if the image type is PNG and the value 160 | * is not between 0 and 9 or if the image type is JPEG and the value is not 161 | * between 0 and 100 162 | * 163 | * @return static 164 | */ 165 | public function setQuality(int $quality) : static 166 | { 167 | if ($this->type === \IMAGETYPE_GIF) { 168 | throw new LogicException( 169 | 'GIF images does not receive a quality value' 170 | ); 171 | } 172 | if ($this->type === \IMAGETYPE_PNG && ($quality < 0 || $quality > 9)) { 173 | throw new InvalidArgumentException( 174 | 'PNG images must receive a quality value between 0 and 9, ' . $quality . ' given' 175 | ); 176 | } 177 | if ($this->type === \IMAGETYPE_JPEG && ($quality < 0 || $quality > 100)) { 178 | throw new InvalidArgumentException( 179 | 'JPEG images must receive a quality value between 0 and 100, ' . $quality . ' given' 180 | ); 181 | } 182 | $this->quality = $quality; 183 | return $this; 184 | } 185 | 186 | /** 187 | * Gets the image resolution. 188 | * 189 | * @throws RuntimeException for image could not get resolution 190 | * 191 | * @return array Returns an array containing two keys, horizontal and 192 | * vertical, with integers as values 193 | */ 194 | #[ArrayShape(['horizontal' => 'int', 'vertical' => 'int'])] 195 | public function getResolution() : array 196 | { 197 | $resolution = \imageresolution($this->instance); 198 | if ($resolution === false) { 199 | throw new RuntimeException('Image could not to get resolution'); 200 | } 201 | return [ 202 | 'horizontal' => $resolution[0], // @phpstan-ignore-line 203 | // @phpstan-ignore-next-line 204 | 'vertical' => $resolution[1], 205 | ]; 206 | } 207 | 208 | /** 209 | * Sets the image resolution. 210 | * 211 | * @param int $horizontal The horizontal resolution in DPI 212 | * @param int $vertical The vertical resolution in DPI 213 | * 214 | * @throws RuntimeException for image could not to set resolution 215 | * 216 | * @return static 217 | */ 218 | public function setResolution(int $horizontal = 96, int $vertical = 96) : static 219 | { 220 | $set = \imageresolution($this->instance, $horizontal, $vertical); 221 | if ($set === false) { 222 | throw new RuntimeException('Image could not to set resolution'); 223 | } 224 | return $this; 225 | } 226 | 227 | /** 228 | * Gets the image height. 229 | * 230 | * @return int 231 | */ 232 | #[Pure] 233 | public function getHeight() : int 234 | { 235 | return \imagesy($this->instance); 236 | } 237 | 238 | /** 239 | * Gets the image width. 240 | * 241 | * @return int 242 | */ 243 | #[Pure] 244 | public function getWidth() : int 245 | { 246 | return \imagesx($this->instance); 247 | } 248 | 249 | /** 250 | * Gets the file extension for image type. 251 | * 252 | * @return false|string a string with the extension corresponding to the 253 | * given image type or false on fail 254 | */ 255 | #[Pure] 256 | public function getExtension() : false | string 257 | { 258 | return \image_type_to_extension($this->type); 259 | } 260 | 261 | /** 262 | * Gets the image MIME type. 263 | * 264 | * @return string 265 | */ 266 | #[Pure] 267 | public function getMime() : string 268 | { 269 | return $this->mime; 270 | } 271 | 272 | /** 273 | * Saves the image contents to a given filename. 274 | * 275 | * @param string|null $filename Optional filename or null to use the original 276 | * 277 | * @return bool 278 | */ 279 | public function save(?string $filename = null) : bool 280 | { 281 | $filename ??= $this->filename; 282 | return match ($this->type) { 283 | \IMAGETYPE_PNG => \imagepng($this->instance, $filename, $this->getQuality()), 284 | \IMAGETYPE_JPEG => \imagejpeg($this->instance, $filename, $this->getQuality()), 285 | \IMAGETYPE_GIF => \imagegif($this->instance, $filename), 286 | default => false, 287 | }; 288 | } 289 | 290 | /** 291 | * Sends the image contents to the output buffer. 292 | * 293 | * @return bool 294 | */ 295 | public function send() : bool 296 | { 297 | if (\in_array($this->type, [\IMAGETYPE_PNG, \IMAGETYPE_GIF], true)) { 298 | \imagesavealpha($this->instance, true); 299 | } 300 | return match ($this->type) { 301 | \IMAGETYPE_PNG => \imagepng($this->instance, null, $this->getQuality()), 302 | \IMAGETYPE_JPEG => \imagejpeg($this->instance, null, $this->getQuality()), 303 | \IMAGETYPE_GIF => \imagegif($this->instance), 304 | default => false, 305 | }; 306 | } 307 | 308 | /** 309 | * Renders the image contents. 310 | * 311 | * @throws RuntimeException for image could not be rendered 312 | * 313 | * @return string The image contents 314 | */ 315 | public function render() : string 316 | { 317 | \ob_start(); 318 | $status = $this->send(); 319 | $contents = \ob_get_clean(); 320 | if ($status === false || $contents === false) { 321 | throw new RuntimeException('Image could not be rendered'); 322 | } 323 | return $contents; 324 | } 325 | 326 | /** 327 | * Crops the image. 328 | * 329 | * @param int $width Width in pixels 330 | * @param int $height Height in pixels 331 | * @param int $marginLeft Margin left in pixels 332 | * @param int $marginTop Margin top in pixels 333 | * 334 | * @throws RuntimeException for image could not to crop 335 | * 336 | * @return static 337 | */ 338 | public function crop(int $width, int $height, int $marginLeft = 0, int $marginTop = 0) : static 339 | { 340 | $crop = \imagecrop($this->instance, [ 341 | 'x' => $marginLeft, 342 | 'y' => $marginTop, 343 | 'width' => $width, 344 | 'height' => $height, 345 | ]); 346 | if ($crop === false) { 347 | throw new RuntimeException('Image could not to crop'); 348 | } 349 | $this->instance = $crop; 350 | return $this; 351 | } 352 | 353 | /** 354 | * Flips the image. 355 | * 356 | * @param string $direction Allowed values are: h or horizontal. v or vertical. b or both. 357 | * 358 | * @throws InvalidArgumentException for invalid image flip direction 359 | * @throws RuntimeException for image could not to flip 360 | * 361 | * @return static 362 | */ 363 | public function flip(string $direction = 'horizontal') : static 364 | { 365 | $direction = match ($direction) { 366 | 'h', 'horizontal' => \IMG_FLIP_HORIZONTAL, 367 | 'v', 'vertical' => \IMG_FLIP_VERTICAL, 368 | 'b', 'both' => \IMG_FLIP_BOTH, 369 | default => throw new InvalidArgumentException('Invalid image flip direction: ' . $direction), 370 | }; 371 | $flip = \imageflip($this->instance, $direction); 372 | if ($flip === false) { 373 | throw new RuntimeException('Image could not to flip'); 374 | } 375 | return $this; 376 | } 377 | 378 | /** 379 | * Applies a filter to the image. 380 | * 381 | * @param int $type IMG_FILTER_* constants 382 | * @param int ...$arguments Arguments for the filter type 383 | * 384 | * @see https://www.php.net/manual/en/function.imagefilter.php 385 | * 386 | * @throws RuntimeException for image could not apply the filter 387 | * 388 | * @return static 389 | */ 390 | public function filter(int $type, int ...$arguments) : static 391 | { 392 | $filtered = \imagefilter($this->instance, $type, ...$arguments); 393 | if ($filtered === false) { 394 | throw new RuntimeException('Image could not apply the filter'); 395 | } 396 | return $this; 397 | } 398 | 399 | /** 400 | * Flattens the image. 401 | * 402 | * Replaces transparency with an RGB color. 403 | * 404 | * @param int $red 405 | * @param int $green 406 | * @param int $blue 407 | * 408 | * @throws RuntimeException for could not create a true color image, could 409 | * not allocate a color or image could not to flatten 410 | * 411 | * @return static 412 | */ 413 | public function flatten(int $red = 255, int $green = 255, int $blue = 255) : static 414 | { 415 | \imagesavealpha($this->instance, false); 416 | $image = \imagecreatetruecolor($this->getWidth(), $this->getHeight()); 417 | if ($image === false) { 418 | throw new RuntimeException('Could not create a true color image'); 419 | } 420 | $color = \imagecolorallocate($image, $red, $green, $blue); 421 | if ($color === false) { 422 | throw new RuntimeException('Image could not allocate a color'); 423 | } 424 | \imagefilledrectangle( 425 | $image, 426 | 0, 427 | 0, 428 | $this->getWidth(), 429 | $this->getHeight(), 430 | $color 431 | ); 432 | $copied = \imagecopy( 433 | $image, 434 | $this->instance, 435 | 0, 436 | 0, 437 | 0, 438 | 0, 439 | $this->getWidth(), 440 | $this->getHeight() 441 | ); 442 | if ($copied === false) { 443 | throw new RuntimeException('Image could not to flatten'); 444 | } 445 | $this->instance = $image; 446 | return $this; 447 | } 448 | 449 | /** 450 | * Sets the image opacity level. 451 | * 452 | * @param int $opacity Opacity percentage: from 0 to 100 453 | * 454 | * @return static 455 | */ 456 | public function opacity(int $opacity = 100) : static 457 | { 458 | if ($opacity < 0 || $opacity > 100) { 459 | throw new InvalidArgumentException( 460 | 'Opacity percentage must be between 0 and 100, ' . $opacity . ' given' 461 | ); 462 | } 463 | if ($opacity === 100) { 464 | \imagealphablending($this->instance, true); 465 | return $this; 466 | } 467 | $opacity = (int) \round(\abs(($opacity * 127 / 100) - 127)); 468 | \imagelayereffect($this->instance, \IMG_EFFECT_OVERLAY); 469 | $color = \imagecolorallocatealpha($this->instance, 127, 127, 127, $opacity); 470 | if ($color === false) { 471 | throw new RuntimeException('Image could not allocate a color'); 472 | } 473 | \imagefilledrectangle( 474 | $this->instance, 475 | 0, 476 | 0, 477 | $this->getWidth(), 478 | $this->getHeight(), 479 | $color 480 | ); 481 | \imagesavealpha($this->instance, true); 482 | \imagealphablending($this->instance, false); 483 | return $this; 484 | } 485 | 486 | /** 487 | * Rotates the image with a given angle. 488 | * 489 | * @param float $angle Rotation angle, in degrees. Clockwise direction. 490 | * 491 | * @throws RuntimeException for image could not allocate a color or could not rotate 492 | * 493 | * @return static 494 | */ 495 | public function rotate(float $angle) : static 496 | { 497 | if (\in_array($this->type, [\IMAGETYPE_PNG, \IMAGETYPE_GIF], true)) { 498 | \imagealphablending($this->instance, false); 499 | \imagesavealpha($this->instance, true); 500 | $background = \imagecolorallocatealpha($this->instance, 0, 0, 0, 127); 501 | } else { 502 | $background = \imagecolorallocate($this->instance, 255, 255, 255); 503 | } 504 | if ($background === false) { 505 | throw new RuntimeException('Image could not allocate a color'); 506 | } 507 | $rotate = \imagerotate($this->instance, -1 * $angle, $background); 508 | if ($rotate === false) { 509 | throw new RuntimeException('Image could not to rotate'); 510 | } 511 | $this->instance = $rotate; 512 | return $this; 513 | } 514 | 515 | /** 516 | * Scales the image. 517 | * 518 | * @param int $width Width in pixels 519 | * @param int $height Height in pixels. Use -1 to use a proportional height 520 | * based on the width. 521 | * 522 | * @throws RuntimeException for image could not to scale 523 | * 524 | * @return static 525 | */ 526 | public function scale(int $width, int $height = -1) : static 527 | { 528 | $scale = \imagescale($this->instance, $width, $height); 529 | if ($scale === false) { 530 | throw new RuntimeException('Image could not to scale'); 531 | } 532 | $this->instance = $scale; 533 | return $this; 534 | } 535 | 536 | /** 537 | * Adds a watermark to the image. 538 | * 539 | * @param Image $watermark The image to use as watermark 540 | * @param int $horizontalPosition Horizontal position 541 | * @param int $verticalPosition Vertical position 542 | * 543 | * @throws RuntimeException for image could not to create watermark 544 | * 545 | * @return static 546 | */ 547 | public function watermark( 548 | Image $watermark, 549 | int $horizontalPosition = 0, 550 | int $verticalPosition = 0 551 | ) : static { 552 | if ($horizontalPosition < 0) { 553 | $horizontalPosition = $this->getWidth() 554 | - (-1 * $horizontalPosition + $watermark->getWidth()); 555 | } 556 | if ($verticalPosition < 0) { 557 | $verticalPosition = $this->getHeight() 558 | - (-1 * $verticalPosition + $watermark->getHeight()); 559 | } 560 | $copied = \imagecopy( 561 | $this->instance, 562 | $watermark->getInstance(), 563 | $horizontalPosition, 564 | $verticalPosition, 565 | 0, 566 | 0, 567 | $watermark->getWidth(), 568 | $watermark->getHeight() 569 | ); 570 | if ($copied === false) { 571 | throw new RuntimeException('Image could not to create watermark'); 572 | } 573 | return $this; 574 | } 575 | 576 | /** 577 | * Allow embed the image contents in a document. 578 | * 579 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs 580 | * @see https://datatracker.ietf.org/doc/html/rfc2397 581 | * 582 | * @return string The image "data" URL 583 | */ 584 | public function getDataUrl() : string 585 | { 586 | return 'data:' . $this->getMime() . ';base64,' . \base64_encode($this->render()); 587 | } 588 | 589 | /** 590 | * @return string 591 | */ 592 | public function jsonSerialize() : string 593 | { 594 | return $this->getDataUrl(); 595 | } 596 | 597 | /** 598 | * Indicates if a given filename has an acceptable image type. 599 | * 600 | * @param string $filename 601 | * 602 | * @return bool 603 | */ 604 | public static function isAcceptable(string $filename) : bool 605 | { 606 | $filename = \realpath($filename); 607 | if ($filename === false || !\is_file($filename) || !\is_readable($filename)) { 608 | return false; 609 | } 610 | $info = \getimagesize($filename); 611 | if ($info === false) { 612 | return false; 613 | } 614 | return match ($info[2]) { 615 | \IMAGETYPE_PNG, \IMAGETYPE_JPEG, \IMAGETYPE_GIF => true, 616 | default => false, 617 | }; 618 | } 619 | } 620 | --------------------------------------------------------------------------------