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