1<?php 2 3/** 4 * This file is part of the Nette Framework (https://nette.org) 5 * Copyright (c) 2004 David Grudl (https://davidgrudl.com) 6 */ 7 8declare(strict_types=1); 9 10namespace Nette\Utils; 11 12use Nette; 13 14 15/** 16 * Basic manipulation with images. Supported types are JPEG, PNG, GIF, WEBP, AVIF and BMP. 17 * 18 * <code> 19 * $image = Image::fromFile('nette.jpg'); 20 * $image->resize(150, 100); 21 * $image->sharpen(); 22 * $image->send(); 23 * </code> 24 * 25 * @method Image affine(array $affine, array $clip = null) 26 * @method array affineMatrixConcat(array $m1, array $m2) 27 * @method array affineMatrixGet(int $type, mixed $options = null) 28 * @method void alphaBlending(bool $on) 29 * @method void antialias(bool $on) 30 * @method void arc($x, $y, $w, $h, $start, $end, $color) 31 * @method void char(int $font, $x, $y, string $char, $color) 32 * @method void charUp(int $font, $x, $y, string $char, $color) 33 * @method int colorAllocate($red, $green, $blue) 34 * @method int colorAllocateAlpha($red, $green, $blue, $alpha) 35 * @method int colorAt($x, $y) 36 * @method int colorClosest($red, $green, $blue) 37 * @method int colorClosestAlpha($red, $green, $blue, $alpha) 38 * @method int colorClosestHWB($red, $green, $blue) 39 * @method void colorDeallocate($color) 40 * @method int colorExact($red, $green, $blue) 41 * @method int colorExactAlpha($red, $green, $blue, $alpha) 42 * @method void colorMatch(Image $image2) 43 * @method int colorResolve($red, $green, $blue) 44 * @method int colorResolveAlpha($red, $green, $blue, $alpha) 45 * @method void colorSet($index, $red, $green, $blue) 46 * @method array colorsForIndex($index) 47 * @method int colorsTotal() 48 * @method int colorTransparent($color = null) 49 * @method void convolution(array $matrix, float $div, float $offset) 50 * @method void copy(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH) 51 * @method void copyMerge(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH, $opacity) 52 * @method void copyMergeGray(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH, $opacity) 53 * @method void copyResampled(Image $src, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH) 54 * @method void copyResized(Image $src, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH) 55 * @method Image cropAuto(int $mode = -1, float $threshold = .5, int $color = -1) 56 * @method void ellipse($cx, $cy, $w, $h, $color) 57 * @method void fill($x, $y, $color) 58 * @method void filledArc($cx, $cy, $w, $h, $s, $e, $color, $style) 59 * @method void filledEllipse($cx, $cy, $w, $h, $color) 60 * @method void filledPolygon(array $points, $numPoints, $color) 61 * @method void filledRectangle($x1, $y1, $x2, $y2, $color) 62 * @method void fillToBorder($x, $y, $border, $color) 63 * @method void filter($filtertype) 64 * @method void flip(int $mode) 65 * @method array ftText($size, $angle, $x, $y, $col, string $fontFile, string $text, array $extrainfo = null) 66 * @method void gammaCorrect(float $inputgamma, float $outputgamma) 67 * @method array getClip() 68 * @method int interlace($interlace = null) 69 * @method bool isTrueColor() 70 * @method void layerEffect($effect) 71 * @method void line($x1, $y1, $x2, $y2, $color) 72 * @method void openPolygon(array $points, int $num_points, int $color) 73 * @method void paletteCopy(Image $source) 74 * @method void paletteToTrueColor() 75 * @method void polygon(array $points, $numPoints, $color) 76 * @method array psText(string $text, $font, $size, $color, $backgroundColor, $x, $y, $space = null, $tightness = null, float $angle = null, $antialiasSteps = null) 77 * @method void rectangle($x1, $y1, $x2, $y2, $col) 78 * @method mixed resolution(int $res_x = null, int $res_y = null) 79 * @method Image rotate(float $angle, $backgroundColor) 80 * @method void saveAlpha(bool $saveflag) 81 * @method Image scale(int $newWidth, int $newHeight = -1, int $mode = IMG_BILINEAR_FIXED) 82 * @method void setBrush(Image $brush) 83 * @method void setClip(int $x1, int $y1, int $x2, int $y2) 84 * @method void setInterpolation(int $method = IMG_BILINEAR_FIXED) 85 * @method void setPixel($x, $y, $color) 86 * @method void setStyle(array $style) 87 * @method void setThickness($thickness) 88 * @method void setTile(Image $tile) 89 * @method void string($font, $x, $y, string $s, $col) 90 * @method void stringUp($font, $x, $y, string $s, $col) 91 * @method void trueColorToPalette(bool $dither, $ncolors) 92 * @method array ttfText($size, $angle, $x, $y, $color, string $fontfile, string $text) 93 * @property-read positive-int $width 94 * @property-read positive-int $height 95 * @property-read \GdImage $imageResource 96 */ 97class Image 98{ 99 use Nette\SmartObject; 100 101 /** Prevent from getting resized to a bigger size than the original */ 102 public const ShrinkOnly = 0b0001; 103 104 /** Resizes to a specified width and height without keeping aspect ratio */ 105 public const Stretch = 0b0010; 106 107 /** Resizes to fit into a specified width and height and preserves aspect ratio */ 108 public const OrSmaller = 0b0000; 109 110 /** Resizes while bounding the smaller dimension to the specified width or height and preserves aspect ratio */ 111 public const OrBigger = 0b0100; 112 113 /** Resizes to the smallest possible size to completely cover specified width and height and reserves aspect ratio */ 114 public const Cover = 0b1000; 115 116 /** @deprecated use Image::ShrinkOnly */ 117 public const SHRINK_ONLY = self::ShrinkOnly; 118 119 /** @deprecated use Image::Stretch */ 120 public const STRETCH = self::Stretch; 121 122 /** @deprecated use Image::OrSmaller */ 123 public const FIT = self::OrSmaller; 124 125 /** @deprecated use Image::OrBigger */ 126 public const FILL = self::OrBigger; 127 128 /** @deprecated use Image::Cover */ 129 public const EXACT = self::Cover; 130 131 /** @deprecated use Image::EmptyGIF */ 132 public const EMPTY_GIF = self::EmptyGIF; 133 134 /** image types */ 135 public const 136 JPEG = ImageType::JPEG, 137 PNG = ImageType::PNG, 138 GIF = ImageType::GIF, 139 WEBP = ImageType::WEBP, 140 AVIF = ImageType::AVIF, 141 BMP = ImageType::BMP; 142 143 public const EmptyGIF = "GIF89a\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00!\xf9\x04\x01\x00\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;"; 144 145 private const Formats = [ImageType::JPEG => 'jpeg', ImageType::PNG => 'png', ImageType::GIF => 'gif', ImageType::WEBP => 'webp', ImageType::AVIF => 'avif', ImageType::BMP => 'bmp']; 146 147 private \GdImage $image; 148 149 150 /** 151 * Returns RGB color (0..255) and transparency (0..127). 152 */ 153 public static function rgb(int $red, int $green, int $blue, int $transparency = 0): array 154 { 155 return [ 156 'red' => max(0, min(255, $red)), 157 'green' => max(0, min(255, $green)), 158 'blue' => max(0, min(255, $blue)), 159 'alpha' => max(0, min(127, $transparency)), 160 ]; 161 } 162 163 164 /** 165 * Reads an image from a file and returns its type in $type. 166 * @throws Nette\NotSupportedException if gd extension is not loaded 167 * @throws UnknownImageFileException if file not found or file type is not known 168 */ 169 public static function fromFile(string $file, ?int &$type = null): static 170 { 171 if (!extension_loaded('gd')) { 172 throw new Nette\NotSupportedException('PHP extension GD is not loaded.'); 173 } 174 175 $type = self::detectTypeFromFile($file); 176 if (!$type) { 177 throw new UnknownImageFileException(is_file($file) ? "Unknown type of file '$file'." : "File '$file' not found."); 178 } 179 180 return self::invokeSafe('imagecreatefrom' . self::Formats[$type], $file, "Unable to open file '$file'.", __METHOD__); 181 } 182 183 184 /** 185 * Reads an image from a string and returns its type in $type. 186 * @throws Nette\NotSupportedException if gd extension is not loaded 187 * @throws ImageException 188 */ 189 public static function fromString(string $s, ?int &$type = null): static 190 { 191 if (!extension_loaded('gd')) { 192 throw new Nette\NotSupportedException('PHP extension GD is not loaded.'); 193 } 194 195 $type = self::detectTypeFromString($s); 196 if (!$type) { 197 throw new UnknownImageFileException('Unknown type of image.'); 198 } 199 200 return self::invokeSafe('imagecreatefromstring', $s, 'Unable to open image from string.', __METHOD__); 201 } 202 203 204 private static function invokeSafe(string $func, string $arg, string $message, string $callee): static 205 { 206 $errors = []; 207 $res = Callback::invokeSafe($func, [$arg], function (string $message) use (&$errors): void { 208 $errors[] = $message; 209 }); 210 211 if (!$res) { 212 throw new ImageException($message . ' Errors: ' . implode(', ', $errors)); 213 } elseif ($errors) { 214 trigger_error($callee . '(): ' . implode(', ', $errors), E_USER_WARNING); 215 } 216 217 return new static($res); 218 } 219 220 221 /** 222 * Creates a new true color image of the given dimensions. The default color is black. 223 * @param positive-int $width 224 * @param positive-int $height 225 * @throws Nette\NotSupportedException if gd extension is not loaded 226 */ 227 public static function fromBlank(int $width, int $height, ?array $color = null): static 228 { 229 if (!extension_loaded('gd')) { 230 throw new Nette\NotSupportedException('PHP extension GD is not loaded.'); 231 } 232 233 if ($width < 1 || $height < 1) { 234 throw new Nette\InvalidArgumentException('Image width and height must be greater than zero.'); 235 } 236 237 $image = imagecreatetruecolor($width, $height); 238 if ($color) { 239 $color += ['alpha' => 0]; 240 $color = imagecolorresolvealpha($image, $color['red'], $color['green'], $color['blue'], $color['alpha']); 241 imagealphablending($image, false); 242 imagefilledrectangle($image, 0, 0, $width - 1, $height - 1, $color); 243 imagealphablending($image, true); 244 } 245 246 return new static($image); 247 } 248 249 250 /** 251 * Returns the type of image from file. 252 * @return ImageType::*|null 253 */ 254 public static function detectTypeFromFile(string $file, &$width = null, &$height = null): ?int 255 { 256 [$width, $height, $type] = @getimagesize($file); // @ - files smaller than 12 bytes causes read error 257 return isset(self::Formats[$type]) ? $type : null; 258 } 259 260 261 /** 262 * Returns the type of image from string. 263 * @return ImageType::*|null 264 */ 265 public static function detectTypeFromString(string $s, &$width = null, &$height = null): ?int 266 { 267 [$width, $height, $type] = @getimagesizefromstring($s); // @ - strings smaller than 12 bytes causes read error 268 return isset(self::Formats[$type]) ? $type : null; 269 } 270 271 272 /** 273 * Returns the file extension for the given image type. 274 * @param ImageType::* $type 275 * @return value-of<self::Formats> 276 */ 277 public static function typeToExtension(int $type): string 278 { 279 if (!isset(self::Formats[$type])) { 280 throw new Nette\InvalidArgumentException("Unsupported image type '$type'."); 281 } 282 283 return self::Formats[$type]; 284 } 285 286 287 /** 288 * Returns the image type for given file extension. 289 * @return ImageType::* 290 */ 291 public static function extensionToType(string $extension): int 292 { 293 $extensions = array_flip(self::Formats) + ['jpg' => ImageType::JPEG]; 294 $extension = strtolower($extension); 295 if (!isset($extensions[$extension])) { 296 throw new Nette\InvalidArgumentException("Unsupported file extension '$extension'."); 297 } 298 299 return $extensions[$extension]; 300 } 301 302 303 /** 304 * Returns the mime type for the given image type. 305 * @param ImageType::* $type 306 */ 307 public static function typeToMimeType(int $type): string 308 { 309 return 'image/' . self::typeToExtension($type); 310 } 311 312 313 /** 314 * @param ImageType::* $type 315 */ 316 public static function isTypeSupported(int $type): bool 317 { 318 return (bool) (imagetypes() & match ($type) { 319 ImageType::JPEG => IMG_JPG, 320 ImageType::PNG => IMG_PNG, 321 ImageType::GIF => IMG_GIF, 322 ImageType::WEBP => IMG_WEBP, 323 ImageType::AVIF => 256, // IMG_AVIF, 324 ImageType::BMP => IMG_BMP, 325 default => 0, 326 }); 327 } 328 329 330 /** 331 * Wraps GD image. 332 */ 333 public function __construct(\GdImage $image) 334 { 335 $this->setImageResource($image); 336 imagesavealpha($image, true); 337 } 338 339 340 /** 341 * Returns image width. 342 * @return positive-int 343 */ 344 public function getWidth(): int 345 { 346 return imagesx($this->image); 347 } 348 349 350 /** 351 * Returns image height. 352 * @return positive-int 353 */ 354 public function getHeight(): int 355 { 356 return imagesy($this->image); 357 } 358 359 360 /** 361 * Sets image resource. 362 */ 363 protected function setImageResource(\GdImage $image): static 364 { 365 $this->image = $image; 366 return $this; 367 } 368 369 370 /** 371 * Returns image GD resource. 372 */ 373 public function getImageResource(): \GdImage 374 { 375 return $this->image; 376 } 377 378 379 /** 380 * Scales an image. Width and height accept pixels or percent. 381 * @param int-mask-of<self::OrSmaller|self::OrBigger|self::Stretch|self::Cover|self::ShrinkOnly> $mode 382 */ 383 public function resize(int|string|null $width, int|string|null $height, int $mode = self::OrSmaller): static 384 { 385 if ($mode & self::Cover) { 386 return $this->resize($width, $height, self::OrBigger)->crop('50%', '50%', $width, $height); 387 } 388 389 [$newWidth, $newHeight] = static::calculateSize($this->getWidth(), $this->getHeight(), $width, $height, $mode); 390 391 if ($newWidth !== $this->getWidth() || $newHeight !== $this->getHeight()) { // resize 392 $newImage = static::fromBlank($newWidth, $newHeight, self::rgb(0, 0, 0, 127))->getImageResource(); 393 imagecopyresampled( 394 $newImage, 395 $this->image, 396 0, 397 0, 398 0, 399 0, 400 $newWidth, 401 $newHeight, 402 $this->getWidth(), 403 $this->getHeight(), 404 ); 405 $this->image = $newImage; 406 } 407 408 if ($width < 0 || $height < 0) { 409 imageflip($this->image, $width < 0 ? ($height < 0 ? IMG_FLIP_BOTH : IMG_FLIP_HORIZONTAL) : IMG_FLIP_VERTICAL); 410 } 411 412 return $this; 413 } 414 415 416 /** 417 * Calculates dimensions of resized image. Width and height accept pixels or percent. 418 * @param int-mask-of<self::OrSmaller|self::OrBigger|self::Stretch|self::Cover|self::ShrinkOnly> $mode 419 */ 420 public static function calculateSize( 421 int $srcWidth, 422 int $srcHeight, 423 $newWidth, 424 $newHeight, 425 int $mode = self::OrSmaller, 426 ): array 427 { 428 if ($newWidth === null) { 429 } elseif (self::isPercent($newWidth)) { 430 $newWidth = (int) round($srcWidth / 100 * abs($newWidth)); 431 $percents = true; 432 } else { 433 $newWidth = abs($newWidth); 434 } 435 436 if ($newHeight === null) { 437 } elseif (self::isPercent($newHeight)) { 438 $newHeight = (int) round($srcHeight / 100 * abs($newHeight)); 439 $mode |= empty($percents) ? 0 : self::Stretch; 440 } else { 441 $newHeight = abs($newHeight); 442 } 443 444 if ($mode & self::Stretch) { // non-proportional 445 if (!$newWidth || !$newHeight) { 446 throw new Nette\InvalidArgumentException('For stretching must be both width and height specified.'); 447 } 448 449 if ($mode & self::ShrinkOnly) { 450 $newWidth = min($srcWidth, $newWidth); 451 $newHeight = min($srcHeight, $newHeight); 452 } 453 } else { // proportional 454 if (!$newWidth && !$newHeight) { 455 throw new Nette\InvalidArgumentException('At least width or height must be specified.'); 456 } 457 458 $scale = []; 459 if ($newWidth > 0) { // fit width 460 $scale[] = $newWidth / $srcWidth; 461 } 462 463 if ($newHeight > 0) { // fit height 464 $scale[] = $newHeight / $srcHeight; 465 } 466 467 if ($mode & self::OrBigger) { 468 $scale = [max($scale)]; 469 } 470 471 if ($mode & self::ShrinkOnly) { 472 $scale[] = 1; 473 } 474 475 $scale = min($scale); 476 $newWidth = (int) round($srcWidth * $scale); 477 $newHeight = (int) round($srcHeight * $scale); 478 } 479 480 return [max($newWidth, 1), max($newHeight, 1)]; 481 } 482 483 484 /** 485 * Crops image. Arguments accepts pixels or percent. 486 */ 487 public function crop(int|string $left, int|string $top, int|string $width, int|string $height): static 488 { 489 [$r['x'], $r['y'], $r['width'], $r['height']] 490 = static::calculateCutout($this->getWidth(), $this->getHeight(), $left, $top, $width, $height); 491 if (gd_info()['GD Version'] === 'bundled (2.1.0 compatible)') { 492 $this->image = imagecrop($this->image, $r); 493 imagesavealpha($this->image, true); 494 } else { 495 $newImage = static::fromBlank($r['width'], $r['height'], self::RGB(0, 0, 0, 127))->getImageResource(); 496 imagecopy($newImage, $this->image, 0, 0, $r['x'], $r['y'], $r['width'], $r['height']); 497 $this->image = $newImage; 498 } 499 500 return $this; 501 } 502 503 504 /** 505 * Calculates dimensions of cutout in image. Arguments accepts pixels or percent. 506 */ 507 public static function calculateCutout( 508 int $srcWidth, 509 int $srcHeight, 510 int|string $left, 511 int|string $top, 512 int|string $newWidth, 513 int|string $newHeight, 514 ): array 515 { 516 if (self::isPercent($newWidth)) { 517 $newWidth = (int) round($srcWidth / 100 * $newWidth); 518 } 519 520 if (self::isPercent($newHeight)) { 521 $newHeight = (int) round($srcHeight / 100 * $newHeight); 522 } 523 524 if (self::isPercent($left)) { 525 $left = (int) round(($srcWidth - $newWidth) / 100 * $left); 526 } 527 528 if (self::isPercent($top)) { 529 $top = (int) round(($srcHeight - $newHeight) / 100 * $top); 530 } 531 532 if ($left < 0) { 533 $newWidth += $left; 534 $left = 0; 535 } 536 537 if ($top < 0) { 538 $newHeight += $top; 539 $top = 0; 540 } 541 542 $newWidth = min($newWidth, $srcWidth - $left); 543 $newHeight = min($newHeight, $srcHeight - $top); 544 return [$left, $top, $newWidth, $newHeight]; 545 } 546 547 548 /** 549 * Sharpens image a little bit. 550 */ 551 public function sharpen(): static 552 { 553 imageconvolution($this->image, [ // my magic numbers ;) 554 [-1, -1, -1], 555 [-1, 24, -1], 556 [-1, -1, -1], 557 ], 16, 0); 558 return $this; 559 } 560 561 562 /** 563 * Puts another image into this image. Left and top accepts pixels or percent. 564 * @param int<0, 100> $opacity 0..100 565 */ 566 public function place(self $image, int|string $left = 0, int|string $top = 0, int $opacity = 100): static 567 { 568 $opacity = max(0, min(100, $opacity)); 569 if ($opacity === 0) { 570 return $this; 571 } 572 573 $width = $image->getWidth(); 574 $height = $image->getHeight(); 575 576 if (self::isPercent($left)) { 577 $left = (int) round(($this->getWidth() - $width) / 100 * $left); 578 } 579 580 if (self::isPercent($top)) { 581 $top = (int) round(($this->getHeight() - $height) / 100 * $top); 582 } 583 584 $output = $input = $image->image; 585 if ($opacity < 100) { 586 $tbl = []; 587 for ($i = 0; $i < 128; $i++) { 588 $tbl[$i] = round(127 - (127 - $i) * $opacity / 100); 589 } 590 591 $output = imagecreatetruecolor($width, $height); 592 imagealphablending($output, false); 593 if (!$image->isTrueColor()) { 594 $input = $output; 595 imagefilledrectangle($output, 0, 0, $width, $height, imagecolorallocatealpha($output, 0, 0, 0, 127)); 596 imagecopy($output, $image->image, 0, 0, 0, 0, $width, $height); 597 } 598 599 for ($x = 0; $x < $width; $x++) { 600 for ($y = 0; $y < $height; $y++) { 601 $c = \imagecolorat($input, $x, $y); 602 $c = ($c & 0xFFFFFF) + ($tbl[$c >> 24] << 24); 603 \imagesetpixel($output, $x, $y, $c); 604 } 605 } 606 607 imagealphablending($output, true); 608 } 609 610 imagecopy( 611 $this->image, 612 $output, 613 $left, 614 $top, 615 0, 616 0, 617 $width, 618 $height, 619 ); 620 return $this; 621 } 622 623 624 /** 625 * Saves image to the file. Quality is in the range 0..100 for JPEG (default 85), WEBP (default 80) and AVIF (default 30) and 0..9 for PNG (default 9). 626 * @param ImageType::*|null $type 627 * @throws ImageException 628 */ 629 public function save(string $file, ?int $quality = null, ?int $type = null): void 630 { 631 $type ??= self::extensionToType(pathinfo($file, PATHINFO_EXTENSION)); 632 $this->output($type, $quality, $file); 633 } 634 635 636 /** 637 * Outputs image to string. Quality is in the range 0..100 for JPEG (default 85), WEBP (default 80) and AVIF (default 30) and 0..9 for PNG (default 9). 638 * @param ImageType::* $type 639 */ 640 public function toString(int $type = ImageType::JPEG, ?int $quality = null): string 641 { 642 return Helpers::capture(function () use ($type, $quality): void { 643 $this->output($type, $quality); 644 }); 645 } 646 647 648 /** 649 * Outputs image to string. 650 */ 651 public function __toString(): string 652 { 653 return $this->toString(); 654 } 655 656 657 /** 658 * Outputs image to browser. Quality is in the range 0..100 for JPEG (default 85), WEBP (default 80) and AVIF (default 30) and 0..9 for PNG (default 9). 659 * @param ImageType::* $type 660 * @throws ImageException 661 */ 662 public function send(int $type = ImageType::JPEG, ?int $quality = null): void 663 { 664 header('Content-Type: ' . self::typeToMimeType($type)); 665 $this->output($type, $quality); 666 } 667 668 669 /** 670 * Outputs image to browser or file. 671 * @param ImageType::* $type 672 * @throws ImageException 673 */ 674 private function output(int $type, ?int $quality, ?string $file = null): void 675 { 676 switch ($type) { 677 case ImageType::JPEG: 678 $quality = $quality === null ? 85 : max(0, min(100, $quality)); 679 $success = @imagejpeg($this->image, $file, $quality); // @ is escalated to exception 680 break; 681 682 case ImageType::PNG: 683 $quality = $quality === null ? 9 : max(0, min(9, $quality)); 684 $success = @imagepng($this->image, $file, $quality); // @ is escalated to exception 685 break; 686 687 case ImageType::GIF: 688 $success = @imagegif($this->image, $file); // @ is escalated to exception 689 break; 690 691 case ImageType::WEBP: 692 $quality = $quality === null ? 80 : max(0, min(100, $quality)); 693 $success = @imagewebp($this->image, $file, $quality); // @ is escalated to exception 694 break; 695 696 case ImageType::AVIF: 697 $quality = $quality === null ? 30 : max(0, min(100, $quality)); 698 $success = @imageavif($this->image, $file, $quality); // @ is escalated to exception 699 break; 700 701 case ImageType::BMP: 702 $success = @imagebmp($this->image, $file); // @ is escalated to exception 703 break; 704 705 default: 706 throw new Nette\InvalidArgumentException("Unsupported image type '$type'."); 707 } 708 709 if (!$success) { 710 throw new ImageException(Helpers::getLastError() ?: 'Unknown error'); 711 } 712 } 713 714 715 /** 716 * Call to undefined method. 717 * @throws Nette\MemberAccessException 718 */ 719 public function __call(string $name, array $args): mixed 720 { 721 $function = 'image' . $name; 722 if (!function_exists($function)) { 723 ObjectHelpers::strictCall(static::class, $name); 724 } 725 726 foreach ($args as $key => $value) { 727 if ($value instanceof self) { 728 $args[$key] = $value->getImageResource(); 729 730 } elseif (is_array($value) && isset($value['red'])) { // rgb 731 $args[$key] = imagecolorallocatealpha( 732 $this->image, 733 $value['red'], 734 $value['green'], 735 $value['blue'], 736 $value['alpha'], 737 ) ?: imagecolorresolvealpha( 738 $this->image, 739 $value['red'], 740 $value['green'], 741 $value['blue'], 742 $value['alpha'], 743 ); 744 } 745 } 746 747 $res = $function($this->image, ...$args); 748 return $res instanceof \GdImage 749 ? $this->setImageResource($res) 750 : $res; 751 } 752 753 754 public function __clone() 755 { 756 ob_start(function () {}); 757 imagepng($this->image, null, 0); 758 $this->setImageResource(imagecreatefromstring(ob_get_clean())); 759 } 760 761 762 private static function isPercent(int|string &$num): bool 763 { 764 if (is_string($num) && str_ends_with($num, '%')) { 765 $num = (float) substr($num, 0, -1); 766 return true; 767 } elseif (is_int($num) || $num === (string) (int) $num) { 768 $num = (int) $num; 769 return false; 770 } 771 772 throw new Nette\InvalidArgumentException("Expected dimension in int|string, '$num' given."); 773 } 774 775 776 /** 777 * Prevents serialization. 778 */ 779 public function __sleep(): array 780 { 781 throw new Nette\NotSupportedException('You cannot serialize or unserialize ' . self::class . ' instances.'); 782 } 783} 784