ArrayHash.php
2 years ago
ArrayList.php
2 years ago
Arrays.php
2 years ago
Callback.php
2 years ago
DateTime.php
2 years ago
FileSystem.php
2 years ago
Floats.php
2 years ago
Helpers.php
2 years ago
Html.php
2 years ago
Image.php
2 years ago
Json.php
2 years ago
ObjectHelpers.php
2 years ago
ObjectMixin.php
2 years ago
Paginator.php
2 years ago
Random.php
2 years ago
Reflection.php
2 years ago
Strings.php
2 years ago
Type.php
2 years ago
Validators.php
2 years ago
exceptions.php
2 years ago
Image.php
612 lines
| 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 | declare (strict_types=1); |
| 8 | namespace FapiMember\Library\Nette\Utils; |
| 9 | |
| 10 | use FapiMember\Library\Nette; |
| 11 | /** |
| 12 | * Basic manipulation with images. Supported types are JPEG, PNG, GIF, WEBP, AVIF and BMP. |
| 13 | * |
| 14 | * <code> |
| 15 | * $image = Image::fromFile('nette.jpg'); |
| 16 | * $image->resize(150, 100); |
| 17 | * $image->sharpen(); |
| 18 | * $image->send(); |
| 19 | * </code> |
| 20 | * |
| 21 | * @method Image affine(array $affine, array $clip = null) |
| 22 | * @method array affineMatrixConcat(array $m1, array $m2) |
| 23 | * @method array affineMatrixGet(int $type, mixed $options = null) |
| 24 | * @method void alphaBlending(bool $on) |
| 25 | * @method void antialias(bool $on) |
| 26 | * @method void arc($x, $y, $w, $h, $start, $end, $color) |
| 27 | * @method void char(int $font, $x, $y, string $char, $color) |
| 28 | * @method void charUp(int $font, $x, $y, string $char, $color) |
| 29 | * @method int colorAllocate($red, $green, $blue) |
| 30 | * @method int colorAllocateAlpha($red, $green, $blue, $alpha) |
| 31 | * @method int colorAt($x, $y) |
| 32 | * @method int colorClosest($red, $green, $blue) |
| 33 | * @method int colorClosestAlpha($red, $green, $blue, $alpha) |
| 34 | * @method int colorClosestHWB($red, $green, $blue) |
| 35 | * @method void colorDeallocate($color) |
| 36 | * @method int colorExact($red, $green, $blue) |
| 37 | * @method int colorExactAlpha($red, $green, $blue, $alpha) |
| 38 | * @method void colorMatch(Image $image2) |
| 39 | * @method int colorResolve($red, $green, $blue) |
| 40 | * @method int colorResolveAlpha($red, $green, $blue, $alpha) |
| 41 | * @method void colorSet($index, $red, $green, $blue) |
| 42 | * @method array colorsForIndex($index) |
| 43 | * @method int colorsTotal() |
| 44 | * @method int colorTransparent($color = null) |
| 45 | * @method void convolution(array $matrix, float $div, float $offset) |
| 46 | * @method void copy(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH) |
| 47 | * @method void copyMerge(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH, $opacity) |
| 48 | * @method void copyMergeGray(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH, $opacity) |
| 49 | * @method void copyResampled(Image $src, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH) |
| 50 | * @method void copyResized(Image $src, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH) |
| 51 | * @method Image cropAuto(int $mode = -1, float $threshold = .5, int $color = -1) |
| 52 | * @method void ellipse($cx, $cy, $w, $h, $color) |
| 53 | * @method void fill($x, $y, $color) |
| 54 | * @method void filledArc($cx, $cy, $w, $h, $s, $e, $color, $style) |
| 55 | * @method void filledEllipse($cx, $cy, $w, $h, $color) |
| 56 | * @method void filledPolygon(array $points, $numPoints, $color) |
| 57 | * @method void filledRectangle($x1, $y1, $x2, $y2, $color) |
| 58 | * @method void fillToBorder($x, $y, $border, $color) |
| 59 | * @method void filter($filtertype) |
| 60 | * @method void flip(int $mode) |
| 61 | * @method array ftText($size, $angle, $x, $y, $col, string $fontFile, string $text, array $extrainfo = null) |
| 62 | * @method void gammaCorrect(float $inputgamma, float $outputgamma) |
| 63 | * @method array getClip() |
| 64 | * @method int interlace($interlace = null) |
| 65 | * @method bool isTrueColor() |
| 66 | * @method void layerEffect($effect) |
| 67 | * @method void line($x1, $y1, $x2, $y2, $color) |
| 68 | * @method void openPolygon(array $points, int $num_points, int $color) |
| 69 | * @method void paletteCopy(Image $source) |
| 70 | * @method void paletteToTrueColor() |
| 71 | * @method void polygon(array $points, $numPoints, $color) |
| 72 | * @method array psText(string $text, $font, $size, $color, $backgroundColor, $x, $y, $space = null, $tightness = null, float $angle = null, $antialiasSteps = null) |
| 73 | * @method void rectangle($x1, $y1, $x2, $y2, $col) |
| 74 | * @method mixed resolution(int $res_x = null, int $res_y = null) |
| 75 | * @method Image rotate(float $angle, $backgroundColor) |
| 76 | * @method void saveAlpha(bool $saveflag) |
| 77 | * @method Image scale(int $newWidth, int $newHeight = -1, int $mode = IMG_BILINEAR_FIXED) |
| 78 | * @method void setBrush(Image $brush) |
| 79 | * @method void setClip(int $x1, int $y1, int $x2, int $y2) |
| 80 | * @method void setInterpolation(int $method = IMG_BILINEAR_FIXED) |
| 81 | * @method void setPixel($x, $y, $color) |
| 82 | * @method void setStyle(array $style) |
| 83 | * @method void setThickness($thickness) |
| 84 | * @method void setTile(Image $tile) |
| 85 | * @method void string($font, $x, $y, string $s, $col) |
| 86 | * @method void stringUp($font, $x, $y, string $s, $col) |
| 87 | * @method void trueColorToPalette(bool $dither, $ncolors) |
| 88 | * @method array ttfText($size, $angle, $x, $y, $color, string $fontfile, string $text) |
| 89 | * @property-read positive-int $width |
| 90 | * @property-read positive-int $height |
| 91 | * @property-read resource|\GdImage $imageResource |
| 92 | */ |
| 93 | class Image |
| 94 | { |
| 95 | use Nette\SmartObject; |
| 96 | /** Prevent from getting resized to a bigger size than the original */ |
| 97 | public const SHRINK_ONLY = 0b1; |
| 98 | /** Resizes to a specified width and height without keeping aspect ratio */ |
| 99 | public const STRETCH = 0b10; |
| 100 | /** Resizes to fit into a specified width and height and preserves aspect ratio */ |
| 101 | public const FIT = 0b0; |
| 102 | /** Resizes while bounding the smaller dimension to the specified width or height and preserves aspect ratio */ |
| 103 | public const FILL = 0b100; |
| 104 | /** Resizes to the smallest possible size to completely cover specified width and height and reserves aspect ratio */ |
| 105 | public const EXACT = 0b1000; |
| 106 | /** image types */ |
| 107 | public const JPEG = \IMAGETYPE_JPEG, PNG = \IMAGETYPE_PNG, GIF = \IMAGETYPE_GIF, WEBP = \IMAGETYPE_WEBP, AVIF = 19, BMP = \IMAGETYPE_BMP; |
| 108 | public const EMPTY_GIF = "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;"; |
| 109 | private const Formats = [self::JPEG => 'jpeg', self::PNG => 'png', self::GIF => 'gif', self::WEBP => 'webp', self::AVIF => 'avif', self::BMP => 'bmp']; |
| 110 | /** @var resource|\GdImage */ |
| 111 | private $image; |
| 112 | /** |
| 113 | * Returns RGB color (0..255) and transparency (0..127). |
| 114 | */ |
| 115 | public static function rgb(int $red, int $green, int $blue, int $transparency = 0): array |
| 116 | { |
| 117 | return ['red' => max(0, min(255, $red)), 'green' => max(0, min(255, $green)), 'blue' => max(0, min(255, $blue)), 'alpha' => max(0, min(127, $transparency))]; |
| 118 | } |
| 119 | /** |
| 120 | * Reads an image from a file and returns its type in $type. |
| 121 | * @throws Nette\NotSupportedException if gd extension is not loaded |
| 122 | * @throws UnknownImageFileException if file not found or file type is not known |
| 123 | * @return static |
| 124 | */ |
| 125 | public static function fromFile(string $file, ?int &$type = null) |
| 126 | { |
| 127 | if (!extension_loaded('gd')) { |
| 128 | throw new Nette\NotSupportedException('PHP extension GD is not loaded.'); |
| 129 | } |
| 130 | $type = self::detectTypeFromFile($file); |
| 131 | if (!$type) { |
| 132 | throw new UnknownImageFileException(is_file($file) ? "Unknown type of file '{$file}'." : "File '{$file}' not found."); |
| 133 | } |
| 134 | return self::invokeSafe('imagecreatefrom' . self::Formats[$type], $file, "Unable to open file '{$file}'.", __METHOD__); |
| 135 | } |
| 136 | /** |
| 137 | * Reads an image from a string and returns its type in $type. |
| 138 | * @return static |
| 139 | * @throws Nette\NotSupportedException if gd extension is not loaded |
| 140 | * @throws ImageException |
| 141 | */ |
| 142 | public static function fromString(string $s, ?int &$type = null) |
| 143 | { |
| 144 | if (!extension_loaded('gd')) { |
| 145 | throw new Nette\NotSupportedException('PHP extension GD is not loaded.'); |
| 146 | } |
| 147 | $type = self::detectTypeFromString($s); |
| 148 | if (!$type) { |
| 149 | throw new UnknownImageFileException('Unknown type of image.'); |
| 150 | } |
| 151 | return self::invokeSafe('imagecreatefromstring', $s, 'Unable to open image from string.', __METHOD__); |
| 152 | } |
| 153 | private static function invokeSafe(string $func, string $arg, string $message, string $callee): self |
| 154 | { |
| 155 | $errors = []; |
| 156 | $res = Callback::invokeSafe($func, [$arg], function (string $message) use (&$errors): void { |
| 157 | $errors[] = $message; |
| 158 | }); |
| 159 | if (!$res) { |
| 160 | throw new ImageException($message . ' Errors: ' . implode(', ', $errors)); |
| 161 | } elseif ($errors) { |
| 162 | trigger_error($callee . '(): ' . implode(', ', $errors), \E_USER_WARNING); |
| 163 | } |
| 164 | return new static($res); |
| 165 | } |
| 166 | /** |
| 167 | * Creates a new true color image of the given dimensions. The default color is black. |
| 168 | * @param positive-int $width |
| 169 | * @param positive-int $height |
| 170 | * @return static |
| 171 | * @throws Nette\NotSupportedException if gd extension is not loaded |
| 172 | */ |
| 173 | public static function fromBlank(int $width, int $height, ?array $color = null) |
| 174 | { |
| 175 | if (!extension_loaded('gd')) { |
| 176 | throw new Nette\NotSupportedException('PHP extension GD is not loaded.'); |
| 177 | } |
| 178 | if ($width < 1 || $height < 1) { |
| 179 | throw new Nette\InvalidArgumentException('Image width and height must be greater than zero.'); |
| 180 | } |
| 181 | $image = imagecreatetruecolor($width, $height); |
| 182 | if ($color) { |
| 183 | $color += ['alpha' => 0]; |
| 184 | $color = imagecolorresolvealpha($image, $color['red'], $color['green'], $color['blue'], $color['alpha']); |
| 185 | imagealphablending($image, \false); |
| 186 | imagefilledrectangle($image, 0, 0, $width - 1, $height - 1, $color); |
| 187 | imagealphablending($image, \true); |
| 188 | } |
| 189 | return new static($image); |
| 190 | } |
| 191 | /** |
| 192 | * Returns the type of image from file. |
| 193 | */ |
| 194 | public static function detectTypeFromFile(string $file, &$width = null, &$height = null): ?int |
| 195 | { |
| 196 | [$width, $height, $type] = @getimagesize($file); |
| 197 | // @ - files smaller than 12 bytes causes read error |
| 198 | return isset(self::Formats[$type]) ? $type : null; |
| 199 | } |
| 200 | /** |
| 201 | * Returns the type of image from string. |
| 202 | */ |
| 203 | public static function detectTypeFromString(string $s, &$width = null, &$height = null): ?int |
| 204 | { |
| 205 | [$width, $height, $type] = @getimagesizefromstring($s); |
| 206 | // @ - strings smaller than 12 bytes causes read error |
| 207 | return isset(self::Formats[$type]) ? $type : null; |
| 208 | } |
| 209 | /** |
| 210 | * Returns the file extension for the given `Image::XXX` constant. |
| 211 | */ |
| 212 | public static function typeToExtension(int $type): string |
| 213 | { |
| 214 | if (!isset(self::Formats[$type])) { |
| 215 | throw new Nette\InvalidArgumentException("Unsupported image type '{$type}'."); |
| 216 | } |
| 217 | return self::Formats[$type]; |
| 218 | } |
| 219 | /** |
| 220 | * Returns the `Image::XXX` constant for given file extension. |
| 221 | */ |
| 222 | public static function extensionToType(string $extension): int |
| 223 | { |
| 224 | $extensions = array_flip(self::Formats) + ['jpg' => self::JPEG]; |
| 225 | $extension = strtolower($extension); |
| 226 | if (!isset($extensions[$extension])) { |
| 227 | throw new Nette\InvalidArgumentException("Unsupported file extension '{$extension}'."); |
| 228 | } |
| 229 | return $extensions[$extension]; |
| 230 | } |
| 231 | /** |
| 232 | * Returns the mime type for the given `Image::XXX` constant. |
| 233 | */ |
| 234 | public static function typeToMimeType(int $type): string |
| 235 | { |
| 236 | return 'image/' . self::typeToExtension($type); |
| 237 | } |
| 238 | /** |
| 239 | * Wraps GD image. |
| 240 | * @param resource|\GdImage $image |
| 241 | */ |
| 242 | public function __construct($image) |
| 243 | { |
| 244 | $this->setImageResource($image); |
| 245 | imagesavealpha($image, \true); |
| 246 | } |
| 247 | /** |
| 248 | * Returns image width. |
| 249 | * @return positive-int |
| 250 | */ |
| 251 | public function getWidth(): int |
| 252 | { |
| 253 | return imagesx($this->image); |
| 254 | } |
| 255 | /** |
| 256 | * Returns image height. |
| 257 | * @return positive-int |
| 258 | */ |
| 259 | public function getHeight(): int |
| 260 | { |
| 261 | return imagesy($this->image); |
| 262 | } |
| 263 | /** |
| 264 | * Sets image resource. |
| 265 | * @param resource|\GdImage $image |
| 266 | * @return static |
| 267 | */ |
| 268 | protected function setImageResource($image) |
| 269 | { |
| 270 | if (!$image instanceof \GdImage && !(is_resource($image) && get_resource_type($image) === 'gd')) { |
| 271 | throw new Nette\InvalidArgumentException('Image is not valid.'); |
| 272 | } |
| 273 | $this->image = $image; |
| 274 | return $this; |
| 275 | } |
| 276 | /** |
| 277 | * Returns image GD resource. |
| 278 | * @return resource|\GdImage |
| 279 | */ |
| 280 | public function getImageResource() |
| 281 | { |
| 282 | return $this->image; |
| 283 | } |
| 284 | /** |
| 285 | * Scales an image. |
| 286 | * @param int|string|null $width in pixels or percent |
| 287 | * @param int|string|null $height in pixels or percent |
| 288 | * @return static |
| 289 | */ |
| 290 | public function resize($width, $height, int $flags = self::FIT) |
| 291 | { |
| 292 | if ($flags & self::EXACT) { |
| 293 | return $this->resize($width, $height, self::FILL)->crop('50%', '50%', $width, $height); |
| 294 | } |
| 295 | [$newWidth, $newHeight] = static::calculateSize($this->getWidth(), $this->getHeight(), $width, $height, $flags); |
| 296 | if ($newWidth !== $this->getWidth() || $newHeight !== $this->getHeight()) { |
| 297 | // resize |
| 298 | $newImage = static::fromBlank($newWidth, $newHeight, self::rgb(0, 0, 0, 127))->getImageResource(); |
| 299 | imagecopyresampled($newImage, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->getWidth(), $this->getHeight()); |
| 300 | $this->image = $newImage; |
| 301 | } |
| 302 | if ($width < 0 || $height < 0) { |
| 303 | imageflip($this->image, ($width < 0) ? ($height < 0) ? \IMG_FLIP_BOTH : \IMG_FLIP_HORIZONTAL : \IMG_FLIP_VERTICAL); |
| 304 | } |
| 305 | return $this; |
| 306 | } |
| 307 | /** |
| 308 | * Calculates dimensions of resized image. |
| 309 | * @param int|string|null $newWidth in pixels or percent |
| 310 | * @param int|string|null $newHeight in pixels or percent |
| 311 | */ |
| 312 | public static function calculateSize(int $srcWidth, int $srcHeight, $newWidth, $newHeight, int $flags = self::FIT): array |
| 313 | { |
| 314 | if ($newWidth === null) { |
| 315 | } elseif (self::isPercent($newWidth)) { |
| 316 | $newWidth = (int) round($srcWidth / 100 * abs($newWidth)); |
| 317 | $percents = \true; |
| 318 | } else { |
| 319 | $newWidth = abs($newWidth); |
| 320 | } |
| 321 | if ($newHeight === null) { |
| 322 | } elseif (self::isPercent($newHeight)) { |
| 323 | $newHeight = (int) round($srcHeight / 100 * abs($newHeight)); |
| 324 | $flags |= empty($percents) ? 0 : self::STRETCH; |
| 325 | } else { |
| 326 | $newHeight = abs($newHeight); |
| 327 | } |
| 328 | if ($flags & self::STRETCH) { |
| 329 | // non-proportional |
| 330 | if (!$newWidth || !$newHeight) { |
| 331 | throw new Nette\InvalidArgumentException('For stretching must be both width and height specified.'); |
| 332 | } |
| 333 | if ($flags & self::SHRINK_ONLY) { |
| 334 | $newWidth = (int) round($srcWidth * min(1, $newWidth / $srcWidth)); |
| 335 | $newHeight = (int) round($srcHeight * min(1, $newHeight / $srcHeight)); |
| 336 | } |
| 337 | } else { |
| 338 | // proportional |
| 339 | if (!$newWidth && !$newHeight) { |
| 340 | throw new Nette\InvalidArgumentException('At least width or height must be specified.'); |
| 341 | } |
| 342 | $scale = []; |
| 343 | if ($newWidth > 0) { |
| 344 | // fit width |
| 345 | $scale[] = $newWidth / $srcWidth; |
| 346 | } |
| 347 | if ($newHeight > 0) { |
| 348 | // fit height |
| 349 | $scale[] = $newHeight / $srcHeight; |
| 350 | } |
| 351 | if ($flags & self::FILL) { |
| 352 | $scale = [max($scale)]; |
| 353 | } |
| 354 | if ($flags & self::SHRINK_ONLY) { |
| 355 | $scale[] = 1; |
| 356 | } |
| 357 | $scale = min($scale); |
| 358 | $newWidth = (int) round($srcWidth * $scale); |
| 359 | $newHeight = (int) round($srcHeight * $scale); |
| 360 | } |
| 361 | return [max($newWidth, 1), max($newHeight, 1)]; |
| 362 | } |
| 363 | /** |
| 364 | * Crops image. |
| 365 | * @param int|string $left in pixels or percent |
| 366 | * @param int|string $top in pixels or percent |
| 367 | * @param int|string $width in pixels or percent |
| 368 | * @param int|string $height in pixels or percent |
| 369 | * @return static |
| 370 | */ |
| 371 | public function crop($left, $top, $width, $height) |
| 372 | { |
| 373 | [$r['x'], $r['y'], $r['width'], $r['height']] = static::calculateCutout($this->getWidth(), $this->getHeight(), $left, $top, $width, $height); |
| 374 | if (gd_info()['GD Version'] === 'bundled (2.1.0 compatible)') { |
| 375 | $this->image = imagecrop($this->image, $r); |
| 376 | imagesavealpha($this->image, \true); |
| 377 | } else { |
| 378 | $newImage = static::fromBlank($r['width'], $r['height'], self::RGB(0, 0, 0, 127))->getImageResource(); |
| 379 | imagecopy($newImage, $this->image, 0, 0, $r['x'], $r['y'], $r['width'], $r['height']); |
| 380 | $this->image = $newImage; |
| 381 | } |
| 382 | return $this; |
| 383 | } |
| 384 | /** |
| 385 | * Calculates dimensions of cutout in image. |
| 386 | * @param int|string $left in pixels or percent |
| 387 | * @param int|string $top in pixels or percent |
| 388 | * @param int|string $newWidth in pixels or percent |
| 389 | * @param int|string $newHeight in pixels or percent |
| 390 | */ |
| 391 | public static function calculateCutout(int $srcWidth, int $srcHeight, $left, $top, $newWidth, $newHeight): array |
| 392 | { |
| 393 | if (self::isPercent($newWidth)) { |
| 394 | $newWidth = (int) round($srcWidth / 100 * $newWidth); |
| 395 | } |
| 396 | if (self::isPercent($newHeight)) { |
| 397 | $newHeight = (int) round($srcHeight / 100 * $newHeight); |
| 398 | } |
| 399 | if (self::isPercent($left)) { |
| 400 | $left = (int) round(($srcWidth - $newWidth) / 100 * $left); |
| 401 | } |
| 402 | if (self::isPercent($top)) { |
| 403 | $top = (int) round(($srcHeight - $newHeight) / 100 * $top); |
| 404 | } |
| 405 | if ($left < 0) { |
| 406 | $newWidth += $left; |
| 407 | $left = 0; |
| 408 | } |
| 409 | if ($top < 0) { |
| 410 | $newHeight += $top; |
| 411 | $top = 0; |
| 412 | } |
| 413 | $newWidth = min($newWidth, $srcWidth - $left); |
| 414 | $newHeight = min($newHeight, $srcHeight - $top); |
| 415 | return [$left, $top, $newWidth, $newHeight]; |
| 416 | } |
| 417 | /** |
| 418 | * Sharpens image a little bit. |
| 419 | * @return static |
| 420 | */ |
| 421 | public function sharpen() |
| 422 | { |
| 423 | imageconvolution($this->image, [ |
| 424 | // my magic numbers ;) |
| 425 | [-1, -1, -1], |
| 426 | [-1, 24, -1], |
| 427 | [-1, -1, -1], |
| 428 | ], 16, 0); |
| 429 | return $this; |
| 430 | } |
| 431 | /** |
| 432 | * Puts another image into this image. |
| 433 | * @param int|string $left in pixels or percent |
| 434 | * @param int|string $top in pixels or percent |
| 435 | * @param int<0, 100> $opacity 0..100 |
| 436 | * @return static |
| 437 | */ |
| 438 | public function place(self $image, $left = 0, $top = 0, int $opacity = 100) |
| 439 | { |
| 440 | $opacity = max(0, min(100, $opacity)); |
| 441 | if ($opacity === 0) { |
| 442 | return $this; |
| 443 | } |
| 444 | $width = $image->getWidth(); |
| 445 | $height = $image->getHeight(); |
| 446 | if (self::isPercent($left)) { |
| 447 | $left = (int) round(($this->getWidth() - $width) / 100 * $left); |
| 448 | } |
| 449 | if (self::isPercent($top)) { |
| 450 | $top = (int) round(($this->getHeight() - $height) / 100 * $top); |
| 451 | } |
| 452 | $output = $input = $image->image; |
| 453 | if ($opacity < 100) { |
| 454 | $tbl = []; |
| 455 | for ($i = 0; $i < 128; $i++) { |
| 456 | $tbl[$i] = round(127 - (127 - $i) * $opacity / 100); |
| 457 | } |
| 458 | $output = imagecreatetruecolor($width, $height); |
| 459 | imagealphablending($output, \false); |
| 460 | if (!$image->isTrueColor()) { |
| 461 | $input = $output; |
| 462 | imagefilledrectangle($output, 0, 0, $width, $height, imagecolorallocatealpha($output, 0, 0, 0, 127)); |
| 463 | imagecopy($output, $image->image, 0, 0, 0, 0, $width, $height); |
| 464 | } |
| 465 | for ($x = 0; $x < $width; $x++) { |
| 466 | for ($y = 0; $y < $height; $y++) { |
| 467 | $c = \imagecolorat($input, $x, $y); |
| 468 | $c = ($c & 0xffffff) + ($tbl[$c >> 24] << 24); |
| 469 | \imagesetpixel($output, $x, $y, $c); |
| 470 | } |
| 471 | } |
| 472 | imagealphablending($output, \true); |
| 473 | } |
| 474 | imagecopy($this->image, $output, $left, $top, 0, 0, $width, $height); |
| 475 | return $this; |
| 476 | } |
| 477 | /** |
| 478 | * 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). |
| 479 | * @throws ImageException |
| 480 | */ |
| 481 | public function save(string $file, ?int $quality = null, ?int $type = null): void |
| 482 | { |
| 483 | $type = $type ?? self::extensionToType(pathinfo($file, \PATHINFO_EXTENSION)); |
| 484 | $this->output($type, $quality, $file); |
| 485 | } |
| 486 | /** |
| 487 | * 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). |
| 488 | */ |
| 489 | public function toString(int $type = self::JPEG, ?int $quality = null): string |
| 490 | { |
| 491 | return Helpers::capture(function () use ($type, $quality) { |
| 492 | $this->output($type, $quality); |
| 493 | }); |
| 494 | } |
| 495 | /** |
| 496 | * Outputs image to string. |
| 497 | */ |
| 498 | public function __toString(): string |
| 499 | { |
| 500 | try { |
| 501 | return $this->toString(); |
| 502 | } catch (\Throwable $e) { |
| 503 | if (func_num_args() || \PHP_VERSION_ID >= 70400) { |
| 504 | throw $e; |
| 505 | } |
| 506 | trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", \E_USER_ERROR); |
| 507 | return ''; |
| 508 | } |
| 509 | } |
| 510 | /** |
| 511 | * 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). |
| 512 | * @throws ImageException |
| 513 | */ |
| 514 | public function send(int $type = self::JPEG, ?int $quality = null): void |
| 515 | { |
| 516 | header('Content-Type: ' . self::typeToMimeType($type)); |
| 517 | $this->output($type, $quality); |
| 518 | } |
| 519 | /** |
| 520 | * Outputs image to browser or file. |
| 521 | * @throws ImageException |
| 522 | */ |
| 523 | private function output(int $type, ?int $quality, ?string $file = null): void |
| 524 | { |
| 525 | switch ($type) { |
| 526 | case self::JPEG: |
| 527 | $quality = ($quality === null) ? 85 : max(0, min(100, $quality)); |
| 528 | $success = @imagejpeg($this->image, $file, $quality); |
| 529 | // @ is escalated to exception |
| 530 | break; |
| 531 | case self::PNG: |
| 532 | $quality = ($quality === null) ? 9 : max(0, min(9, $quality)); |
| 533 | $success = @imagepng($this->image, $file, $quality); |
| 534 | // @ is escalated to exception |
| 535 | break; |
| 536 | case self::GIF: |
| 537 | $success = @imagegif($this->image, $file); |
| 538 | // @ is escalated to exception |
| 539 | break; |
| 540 | case self::WEBP: |
| 541 | $quality = ($quality === null) ? 80 : max(0, min(100, $quality)); |
| 542 | $success = @imagewebp($this->image, $file, $quality); |
| 543 | // @ is escalated to exception |
| 544 | break; |
| 545 | case self::AVIF: |
| 546 | $quality = ($quality === null) ? 30 : max(0, min(100, $quality)); |
| 547 | $success = @imageavif($this->image, $file, $quality); |
| 548 | // @ is escalated to exception |
| 549 | break; |
| 550 | case self::BMP: |
| 551 | $success = @imagebmp($this->image, $file); |
| 552 | // @ is escalated to exception |
| 553 | break; |
| 554 | default: |
| 555 | throw new Nette\InvalidArgumentException("Unsupported image type '{$type}'."); |
| 556 | } |
| 557 | if (!$success) { |
| 558 | throw new ImageException(Helpers::getLastError() ?: 'Unknown error'); |
| 559 | } |
| 560 | } |
| 561 | /** |
| 562 | * Call to undefined method. |
| 563 | * @return mixed |
| 564 | * @throws Nette\MemberAccessException |
| 565 | */ |
| 566 | public function __call(string $name, array $args) |
| 567 | { |
| 568 | $function = 'image' . $name; |
| 569 | if (!function_exists($function)) { |
| 570 | ObjectHelpers::strictCall(static::class, $name); |
| 571 | } |
| 572 | foreach ($args as $key => $value) { |
| 573 | if ($value instanceof self) { |
| 574 | $args[$key] = $value->getImageResource(); |
| 575 | } elseif (is_array($value) && isset($value['red'])) { |
| 576 | // rgb |
| 577 | $args[$key] = imagecolorallocatealpha($this->image, $value['red'], $value['green'], $value['blue'], $value['alpha']) ?: imagecolorresolvealpha($this->image, $value['red'], $value['green'], $value['blue'], $value['alpha']); |
| 578 | } |
| 579 | } |
| 580 | $res = $function($this->image, ...$args); |
| 581 | return ($res instanceof \GdImage || is_resource($res) && get_resource_type($res) === 'gd') ? $this->setImageResource($res) : $res; |
| 582 | } |
| 583 | public function __clone() |
| 584 | { |
| 585 | ob_start(function () { |
| 586 | }); |
| 587 | imagepng($this->image, null, 0); |
| 588 | $this->setImageResource(imagecreatefromstring(ob_get_clean())); |
| 589 | } |
| 590 | /** |
| 591 | * @param int|string $num in pixels or percent |
| 592 | */ |
| 593 | private static function isPercent(&$num): bool |
| 594 | { |
| 595 | if (is_string($num) && substr($num, -1) === '%') { |
| 596 | $num = (float) substr($num, 0, -1); |
| 597 | return \true; |
| 598 | } elseif (is_int($num) || $num === (string) (int) $num) { |
| 599 | $num = (int) $num; |
| 600 | return \false; |
| 601 | } |
| 602 | throw new Nette\InvalidArgumentException("Expected dimension in int|string, '{$num}' given."); |
| 603 | } |
| 604 | /** |
| 605 | * Prevents serialization. |
| 606 | */ |
| 607 | public function __sleep(): array |
| 608 | { |
| 609 | throw new Nette\NotSupportedException('You cannot serialize or unserialize ' . self::class . ' instances.'); |
| 610 | } |
| 611 | } |
| 612 |