• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / utils / 21971729263

13 Feb 2026 01:58AM UTC coverage: 93.199% (+0.006%) from 93.193%
21971729263

push

github

dg
added CLAUDE.md

2083 of 2235 relevant lines covered (93.2%)

0.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.14
/src/Utils/Image.php
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

8
declare(strict_types=1);
9

10
namespace Nette\Utils;
11

12
use Nette;
13
use function is_array, is_int, is_string;
14
use 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<int, float|int> $affine, ?array{x: int, y: int, width: int, height: int} $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{red: int, green: int, blue: int, alpha: int} colorsForIndex(int $color)
45
 * @method int colorsTotal()
46
 * @method int colorTransparent(?int $color = null)
47
 * @method void convolution(array<int, array<int, float>> $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 = 0, 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<int, int> $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<int, int> ftText(float $size, float $angle, int $x, int $y, ImageColor $color, string $fontFile, string $text, array<string, mixed> $options = [])
64
 * @method void gammaCorrect(float $inputgamma, float $outputgamma)
65
 * @method array{int, int, int, int} 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<int, int> $points, ImageColor $color)
72
 * @method void paletteCopy(Image $source)
73
 * @method void paletteToTrueColor()
74
 * @method void polygon(array<int, int> $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 = 3)
80
 * @method void setBrush(Image $brush)
81
 * @method void setClip(int $x1, int $y1, int $x2, int $y2)
82
 * @method void setInterpolation(int $method = 3)
83
 * @method void setPixel(int $x, int $y, ImageColor $color)
84
 * @method void setStyle(array<int, int> $style)
85
 * @method void setThickness(int $thickness)
86
 * @method void setTile(Image $tile)
87
 * @method void trueColorToPalette(bool $dither, int $ncolors)
88
 * @method array<int, int> ttfText(float $size, float $angle, int $x, int $y, ImageColor $color, string $fontfile, string $text, array<string, mixed> $options = [])
89
 * @property-read positive-int $width
90
 * @property-read positive-int $height
91
 * @property-read \GdImage $imageResource
92
 */
93
class 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
         * @return array{red: int, green: int, blue: int, alpha: int}
150
         */
151
        public static function rgb(int $red, int $green, int $blue, int $transparency = 0): array
1✔
152
        {
153
                return [
154
                        'red' => max(0, min(255, $red)),
1✔
155
                        'green' => max(0, min(255, $green)),
1✔
156
                        'blue' => max(0, min(255, $blue)),
1✔
157
                        'alpha' => max(0, min(127, $transparency)),
1✔
158
                ];
159
        }
160

161

162
        /**
163
         * Reads an image from a file and returns its type in $type.
164
         * @throws Nette\NotSupportedException if gd extension is not loaded
165
         * @throws UnknownImageFileException if file not found or file type is not known
166
         */
167
        public static function fromFile(string $file, ?int &$type = null): static
1✔
168
        {
169
                self::ensureExtension();
1✔
170
                $type = self::detectTypeFromFile($file);
1✔
171
                if (!$type) {
1✔
172
                        throw new UnknownImageFileException(is_file($file) ? "Unknown type of file '$file'." : "File '$file' not found.");
1✔
173
                }
174

175
                return self::invokeSafe('imagecreatefrom' . self::Formats[$type], $file, "Unable to open file '$file'.", __METHOD__);
1✔
176
        }
177

178

179
        /**
180
         * Reads an image from a string and returns its type in $type.
181
         * @throws Nette\NotSupportedException if gd extension is not loaded
182
         * @throws ImageException
183
         */
184
        public static function fromString(string $s, ?int &$type = null): static
1✔
185
        {
186
                self::ensureExtension();
1✔
187
                $type = self::detectTypeFromString($s);
1✔
188
                if (!$type) {
1✔
189
                        throw new UnknownImageFileException('Unknown type of image.');
1✔
190
                }
191

192
                return self::invokeSafe('imagecreatefromstring', $s, 'Unable to open image from string.', __METHOD__);
1✔
193
        }
194

195

196
        /** @param  callable-string  $func */
197
        private static function invokeSafe(string $func, string $arg, string $message, string $callee): static
1✔
198
        {
199
                $errors = [];
1✔
200
                $res = Callback::invokeSafe($func, [$arg], function (string $message) use (&$errors): void {
1✔
201
                        $errors[] = $message;
1✔
202
                });
1✔
203

204
                if (!$res) {
1✔
205
                        throw new ImageException($message . ' Errors: ' . implode(', ', $errors));
1✔
206
                } elseif ($errors) {
1✔
207
                        trigger_error($callee . '(): ' . implode(', ', $errors), E_USER_WARNING);
×
208
                }
209

210
                return new static($res);
1✔
211
        }
212

213

214
        /**
215
         * Creates a new true color image of the given dimensions. The default color is black.
216
         * @param  positive-int  $width
217
         * @param  positive-int  $height
218
         * @param  ImageColor|array{red: int, green: int, blue: int, alpha?: int}|null  $color
219
         * @throws Nette\NotSupportedException if gd extension is not loaded
220
         */
221
        public static function fromBlank(int $width, int $height, ImageColor|array|null $color = null): static
1✔
222
        {
223
                self::ensureExtension();
1✔
224
                if ($width < 1 || $height < 1) {
1✔
225
                        throw new Nette\InvalidArgumentException('Image width and height must be greater than zero.');
×
226
                }
227

228
                $image = new static(imagecreatetruecolor($width, $height));
1✔
229
                if ($color) {
1✔
230
                        $image->alphaBlending(false);
1✔
231
                        $image->filledRectangle(0, 0, $width - 1, $height - 1, ImageColor::from($color));
1✔
232
                        $image->alphaBlending(true);
1✔
233
                }
234

235
                return $image;
1✔
236
        }
237

238

239
        /**
240
         * Returns the type of image from file.
241
         * @param-out ?int  $width
242
         * @param-out ?int  $height
243
         * @return ?ImageType::*
244
         */
245
        public static function detectTypeFromFile(string $file, mixed &$width = null, mixed &$height = null): ?int
1✔
246
        {
247
                [$width, $height, $type] = Helpers::falseToNull(@getimagesize($file)); // @ - files smaller than 12 bytes causes read error
1✔
248
                return $type && isset(self::Formats[$type]) ? $type : null;
1✔
249
        }
250

251

252
        /**
253
         * Returns the type of image from string.
254
         * @param-out ?int  $width
255
         * @param-out ?int  $height
256
         * @return ?ImageType::*
257
         */
258
        public static function detectTypeFromString(string $s, mixed &$width = null, mixed &$height = null): ?int
1✔
259
        {
260
                [$width, $height, $type] = Helpers::falseToNull(@getimagesizefromstring($s)); // @ - strings smaller than 12 bytes causes read error
1✔
261
                return $type && isset(self::Formats[$type]) ? $type : null;
1✔
262
        }
263

264

265
        /**
266
         * Returns the file extension for the given image type.
267
         * @param  ImageType::*  $type
268
         * @return value-of<self::Formats>
269
         */
270
        public static function typeToExtension(int $type): string
1✔
271
        {
272
                if (!isset(self::Formats[$type])) {
1✔
273
                        throw new Nette\InvalidArgumentException("Unsupported image type '$type'.");
1✔
274
                }
275

276
                return self::Formats[$type];
1✔
277
        }
278

279

280
        /**
281
         * Returns the image type for given file extension.
282
         * @return ImageType::*
283
         */
284
        public static function extensionToType(string $extension): int
1✔
285
        {
286
                $extensions = array_flip(self::Formats) + ['jpg' => ImageType::JPEG];
1✔
287
                $extension = strtolower($extension);
1✔
288
                if (!isset($extensions[$extension])) {
1✔
289
                        throw new Nette\InvalidArgumentException("Unsupported file extension '$extension'.");
1✔
290
                }
291

292
                return $extensions[$extension];
1✔
293
        }
294

295

296
        /**
297
         * Returns the mime type for the given image type.
298
         * @param  ImageType::*  $type
299
         */
300
        public static function typeToMimeType(int $type): string
1✔
301
        {
302
                return 'image/' . self::typeToExtension($type);
1✔
303
        }
304

305

306
        /**
307
         * @param  ImageType::*  $type
308
         */
309
        public static function isTypeSupported(int $type): bool
1✔
310
        {
311
                self::ensureExtension();
1✔
312
                return (bool) (imagetypes() & match ($type) {
1✔
313
                        ImageType::JPEG => IMG_JPG,
1✔
314
                        ImageType::PNG => IMG_PNG,
1✔
315
                        ImageType::GIF => IMG_GIF,
1✔
316
                        ImageType::WEBP => IMG_WEBP,
1✔
317
                        ImageType::AVIF => 256, // IMG_AVIF,
1✔
318
                        ImageType::BMP => IMG_BMP,
×
319
                        default => 0,
1✔
320
                });
321
        }
322

323

324
        /** @return  ImageType::*[] */
325
        public static function getSupportedTypes(): array
326
        {
327
                self::ensureExtension();
1✔
328
                $flag = imagetypes();
1✔
329
                return array_filter([
1✔
330
                        $flag & IMG_GIF ? ImageType::GIF : null,
1✔
331
                        $flag & IMG_JPG ? ImageType::JPEG : null,
1✔
332
                        $flag & IMG_PNG ? ImageType::PNG : null,
1✔
333
                        $flag & IMG_WEBP ? ImageType::WEBP : null,
1✔
334
                        $flag & 256 ? ImageType::AVIF : null, // IMG_AVIF
1✔
335
                        $flag & IMG_BMP ? ImageType::BMP : null,
1✔
336
                ]);
337
        }
338

339

340
        /**
341
         * Wraps GD image.
342
         */
343
        public function __construct(\GdImage $image)
1✔
344
        {
345
                $this->setImageResource($image);
1✔
346
                imagesavealpha($image, true);
1✔
347
        }
1✔
348

349

350
        /**
351
         * Returns image width.
352
         * @return positive-int
353
         */
354
        public function getWidth(): int
355
        {
356
                return imagesx($this->image);
1✔
357
        }
358

359

360
        /**
361
         * Returns image height.
362
         * @return positive-int
363
         */
364
        public function getHeight(): int
365
        {
366
                return imagesy($this->image);
1✔
367
        }
368

369

370
        /**
371
         * Sets image resource.
372
         */
373
        protected function setImageResource(\GdImage $image): static
1✔
374
        {
375
                $this->image = $image;
1✔
376
                return $this;
1✔
377
        }
378

379

380
        /**
381
         * Returns image GD resource.
382
         */
383
        public function getImageResource(): \GdImage
384
        {
385
                return $this->image;
1✔
386
        }
387

388

389
        /**
390
         * Scales an image. Width and height accept pixels or percent.
391
         * @param  int-mask-of<self::OrSmaller|self::OrBigger|self::Stretch|self::Cover|self::ShrinkOnly>  $mode
392
         */
393
        public function resize(int|string|null $width, int|string|null $height, int $mode = self::OrSmaller): static
1✔
394
        {
395
                if ($mode & self::Cover) {
1✔
396
                        return $this->resize($width, $height, self::OrBigger)->crop('50%', '50%', $width, $height);
1✔
397
                }
398

399
                [$newWidth, $newHeight] = static::calculateSize($this->getWidth(), $this->getHeight(), $width, $height, $mode);
1✔
400

401
                if ($newWidth !== $this->getWidth() || $newHeight !== $this->getHeight()) { // resize
1✔
402
                        $newImage = static::fromBlank($newWidth, $newHeight, ImageColor::rgb(0, 0, 0, 0))->getImageResource();
1✔
403
                        imagecopyresampled(
1✔
404
                                $newImage,
1✔
405
                                $this->image,
1✔
406
                                0,
1✔
407
                                0,
1✔
408
                                0,
1✔
409
                                0,
1✔
410
                                $newWidth,
411
                                $newHeight,
412
                                $this->getWidth(),
1✔
413
                                $this->getHeight(),
1✔
414
                        );
415
                        $this->image = $newImage;
1✔
416
                }
417

418
                if ($width < 0 || $height < 0) {
1✔
419
                        imageflip($this->image, $width < 0 ? ($height < 0 ? IMG_FLIP_BOTH : IMG_FLIP_HORIZONTAL) : IMG_FLIP_VERTICAL);
1✔
420
                }
421

422
                return $this;
1✔
423
        }
424

425

426
        /**
427
         * Calculates dimensions of resized image. Width and height accept pixels or percent.
428
         * @param  int|string|null  $newWidth
429
         * @param  int|string|null  $newHeight
430
         * @param  int-mask-of<self::OrSmaller|self::OrBigger|self::Stretch|self::Cover|self::ShrinkOnly>  $mode
431
         * @return array{int<1, max>, int<1, max>}
432
         */
433
        public static function calculateSize(
1✔
434
                int $srcWidth,
435
                int $srcHeight,
436
                $newWidth,
437
                $newHeight,
438
                int $mode = self::OrSmaller,
439
        ): array
440
        {
441
                if ($newWidth === null) {
1✔
442
                } elseif (self::isPercent($newWidth)) {
1✔
443
                        $newWidth = (int) round($srcWidth / 100 * abs($newWidth));
1✔
444
                        $percents = true;
1✔
445
                } else {
446
                        $newWidth = abs($newWidth);
1✔
447
                }
448

449
                if ($newHeight === null) {
1✔
450
                } elseif (self::isPercent($newHeight)) {
1✔
451
                        $newHeight = (int) round($srcHeight / 100 * abs($newHeight));
1✔
452
                        $mode |= empty($percents) ? 0 : self::Stretch;
1✔
453
                } else {
454
                        $newHeight = abs($newHeight);
1✔
455
                }
456

457
                if ($mode & self::Stretch) { // non-proportional
1✔
458
                        if (!$newWidth || !$newHeight) {
1✔
459
                                throw new Nette\InvalidArgumentException('For stretching must be both width and height specified.');
×
460
                        }
461

462
                        if ($mode & self::ShrinkOnly) {
1✔
463
                                $newWidth = min($srcWidth, $newWidth);
1✔
464
                                $newHeight = min($srcHeight, $newHeight);
1✔
465
                        }
466
                } else {  // proportional
467
                        if (!$newWidth && !$newHeight) {
1✔
468
                                throw new Nette\InvalidArgumentException('At least width or height must be specified.');
×
469
                        }
470

471
                        $scale = [];
1✔
472
                        if ($newWidth > 0) { // fit width
1✔
473
                                $scale[] = $newWidth / $srcWidth;
1✔
474
                        }
475

476
                        if ($newHeight > 0) { // fit height
1✔
477
                                $scale[] = $newHeight / $srcHeight;
1✔
478
                        }
479

480
                        if ($mode & self::OrBigger) {
1✔
481
                                $scale = [max($scale ?: [1])];
1✔
482
                        }
483

484
                        if ($mode & self::ShrinkOnly) {
1✔
485
                                $scale[] = 1;
1✔
486
                        }
487

488
                        $scale = min($scale ?: [1]);
1✔
489
                        $newWidth = (int) round($srcWidth * $scale);
1✔
490
                        $newHeight = (int) round($srcHeight * $scale);
1✔
491
                }
492

493
                return [max((int) $newWidth, 1), max((int) $newHeight, 1)];
1✔
494
        }
495

496

497
        /**
498
         * Crops image. Arguments accepts pixels or percent.
499
         */
500
        public function crop(int|string $left, int|string $top, int|string $width, int|string $height): static
1✔
501
        {
502
                [$r['x'], $r['y'], $r['width'], $r['height']]
1✔
503
                        = static::calculateCutout($this->getWidth(), $this->getHeight(), $left, $top, $width, $height);
1✔
504
                if (gd_info()['GD Version'] === 'bundled (2.1.0 compatible)') {
1✔
505
                        $this->image = imagecrop($this->image, $r);
×
506
                        imagesavealpha($this->image, true);
×
507
                } else {
508
                        $newImage = static::fromBlank(max(1, $r['width']), max(1, $r['height']), ImageColor::rgb(0, 0, 0, 0))->getImageResource();
1✔
509
                        imagecopy($newImage, $this->image, 0, 0, $r['x'], $r['y'], $r['width'], $r['height']);
1✔
510
                        $this->image = $newImage;
1✔
511
                }
512

513
                return $this;
1✔
514
        }
515

516

517
        /**
518
         * Calculates dimensions of cutout in image. Arguments accepts pixels or percent.
519
         * @return array{int, int, int, int}
520
         */
521
        public static function calculateCutout(
1✔
522
                int $srcWidth,
523
                int $srcHeight,
524
                int|string $left,
525
                int|string $top,
526
                int|string $newWidth,
527
                int|string $newHeight,
528
        ): array
529
        {
530
                $newWidth = (int) (self::isPercent($newWidth) ? round($srcWidth / 100 * $newWidth) : $newWidth);
1✔
531
                $newHeight = (int) (self::isPercent($newHeight) ? round($srcHeight / 100 * $newHeight) : $newHeight);
1✔
532
                $left = (int) (self::isPercent($left) ? round(($srcWidth - $newWidth) / 100 * $left) : $left);
1✔
533
                $top = (int) (self::isPercent($top) ? round(($srcHeight - $newHeight) / 100 * $top) : $top);
1✔
534

535
                if ($left < 0) {
1✔
536
                        $newWidth += $left;
×
537
                        $left = 0;
×
538
                }
539

540
                if ($top < 0) {
1✔
541
                        $newHeight += $top;
×
542
                        $top = 0;
×
543
                }
544

545
                $newWidth = min($newWidth, $srcWidth - $left);
1✔
546
                $newHeight = min($newHeight, $srcHeight - $top);
1✔
547
                return [$left, $top, $newWidth, $newHeight];
1✔
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
1✔
570
        {
571
                $opacity = max(0, min(100, $opacity));
1✔
572
                if ($opacity === 0) {
1✔
573
                        return $this;
1✔
574
                }
575

576
                $width = $image->getWidth();
1✔
577
                $height = $image->getHeight();
1✔
578
                $left = (int) (self::isPercent($left) ? round(($this->getWidth() - $width) / 100 * $left) : $left);
1✔
579
                $top = (int) (self::isPercent($top) ? round(($this->getHeight() - $height) / 100 * $top) : $top);
1✔
580

581
                $output = $input = $image->image;
1✔
582
                if ($opacity < 100) {
1✔
583
                        $tbl = [];
1✔
584
                        for ($i = 0; $i < 128; $i++) {
1✔
585
                                $tbl[$i] = round(127 - (127 - $i) * $opacity / 100);
1✔
586
                        }
587

588
                        $output = imagecreatetruecolor($width, $height);
1✔
589
                        imagealphablending($output, false);
1✔
590
                        if (!$image->isTrueColor()) {
1✔
591
                                $input = $output;
1✔
592
                                imagefilledrectangle($output, 0, 0, $width, $height, (int) imagecolorallocatealpha($output, 0, 0, 0, 127));
1✔
593
                                imagecopy($output, $image->image, 0, 0, 0, 0, $width, $height);
1✔
594
                        }
595

596
                        for ($x = 0; $x < $width; $x++) {
1✔
597
                                for ($y = 0; $y < $height; $y++) {
1✔
598
                                        $c = \imagecolorat($input, $x, $y);
1✔
599
                                        $c = ($c & 0xFFFFFF) + ($tbl[$c >> 24] << 24);
1✔
600
                                        \imagesetpixel($output, $x, $y, $c);
1✔
601
                                }
602
                        }
603

604
                        imagealphablending($output, true);
1✔
605
                }
606

607
                imagecopy(
1✔
608
                        $this->image,
1✔
609
                        $output,
610
                        $left,
611
                        $top,
612
                        0,
1✔
613
                        0,
1✔
614
                        $width,
615
                        $height,
616
                );
617
                return $this;
1✔
618
        }
619

620

621
        /**
622
         * Calculates the bounding box for a TrueType text. Returns keys left, top, width and height.
623
         * @param  array<string, mixed>  $options
624
         * @return array{left: int, top: int, width: int, height: int}
625
         */
626
        public static function calculateTextBox(
627
                string $text,
628
                string $fontFile,
629
                float $size,
630
                float $angle = 0,
631
                array $options = [],
632
        ): array
633
        {
634
                self::ensureExtension();
×
635
                $box = imagettfbbox($size, $angle, $fontFile, $text, $options);
×
636
                return [
637
                        'left' => $minX = min([$box[0], $box[2], $box[4], $box[6]]),
×
638
                        'top' => $minY = min([$box[1], $box[3], $box[5], $box[7]]),
×
639
                        'width' => max([$box[0], $box[2], $box[4], $box[6]]) - $minX + 1,
×
640
                        'height' => max([$box[1], $box[3], $box[5], $box[7]]) - $minY + 1,
×
641
                ];
642
        }
643

644

645
        /**
646
         * Draw a rectangle.
647
         */
648
        public function rectangleWH(int $x, int $y, int $width, int $height, ImageColor $color): void
1✔
649
        {
650
                if ($width !== 0 && $height !== 0) {
1✔
651
                        $this->rectangle($x, $y, $x + $width + ($width > 0 ? -1 : 1), $y + $height + ($height > 0 ? -1 : 1), $color);
1✔
652
                }
653
        }
1✔
654

655

656
        /**
657
         * Draw a filled rectangle.
658
         */
659
        public function filledRectangleWH(int $x, int $y, int $width, int $height, ImageColor $color): void
1✔
660
        {
661
                if ($width !== 0 && $height !== 0) {
1✔
662
                        $this->filledRectangle($x, $y, $x + $width + ($width > 0 ? -1 : 1), $y + $height + ($height > 0 ? -1 : 1), $color);
1✔
663
                }
664
        }
1✔
665

666

667
        /**
668
         * 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).
669
         * @param  ?ImageType::*  $type
670
         * @throws ImageException
671
         */
672
        public function save(string $file, ?int $quality = null, ?int $type = null): void
1✔
673
        {
674
                $type ??= self::extensionToType(pathinfo($file, PATHINFO_EXTENSION));
1✔
675
                $this->output($type, $quality, $file);
1✔
676
        }
1✔
677

678

679
        /**
680
         * 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).
681
         * @param  ImageType::*  $type
682
         */
683
        public function toString(int $type = ImageType::JPEG, ?int $quality = null): string
1✔
684
        {
685
                return Helpers::capture(function () use ($type, $quality): void {
1✔
686
                        $this->output($type, $quality);
1✔
687
                });
1✔
688
        }
689

690

691
        /**
692
         * Outputs image to string.
693
         */
694
        public function __toString(): string
695
        {
696
                return $this->toString();
1✔
697
        }
698

699

700
        /**
701
         * 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).
702
         * @param  ImageType::*  $type
703
         * @throws ImageException
704
         */
705
        public function send(int $type = ImageType::JPEG, ?int $quality = null): void
1✔
706
        {
707
                header('Content-Type: ' . self::typeToMimeType($type));
1✔
708
                $this->output($type, $quality);
1✔
709
        }
1✔
710

711

712
        /**
713
         * Outputs image to browser or file.
714
         * @param  ImageType::*  $type
715
         * @throws ImageException
716
         */
717
        private function output(int $type, ?int $quality, ?string $file = null): void
1✔
718
        {
719
                [$defQuality, $min, $max] = match ($type) {
1✔
720
                        ImageType::JPEG => [85, 0, 100],
1✔
721
                        ImageType::PNG => [9, 0, 9],
1✔
722
                        ImageType::GIF => [null, null, null],
1✔
723
                        ImageType::WEBP => [80, 0, 100],
1✔
724
                        ImageType::AVIF => [30, 0, 100],
1✔
725
                        ImageType::BMP => [null, null, null],
1✔
726
                        default => throw new Nette\InvalidArgumentException("Unsupported image type '$type'."),
1✔
727
                };
728

729
                $args = [$this->image, $file];
1✔
730
                if ($defQuality !== null) {
1✔
731
                        $args[] = $quality === null ? $defQuality : max($min, min($max, $quality));
1✔
732
                }
733

734
                Callback::invokeSafe('image' . self::Formats[$type], $args, function (string $message) use ($file): void {
1✔
735
                        if ($file !== null) {
1✔
736
                                @unlink($file);
1✔
737
                        }
738
                        throw new ImageException($message);
1✔
739
                });
1✔
740
        }
1✔
741

742

743
        /**
744
         * Call to undefined method.
745
         * @param  mixed[]  $args
746
         * @throws Nette\MemberAccessException
747
         */
748
        public function __call(string $name, array $args): mixed
1✔
749
        {
750
                $function = 'image' . $name;
1✔
751
                if (!function_exists($function)) {
1✔
752
                        ObjectHelpers::strictCall(static::class, $name);
1✔
753
                }
754

755
                foreach ($args as $key => $value) {
1✔
756
                        if ($value instanceof self) {
1✔
757
                                $args[$key] = $value->getImageResource();
1✔
758

759
                        } elseif ($value instanceof ImageColor || (is_array($value) && isset($value['red']))) {
1✔
760
                                /** @var ImageColor|array{red: int, green: int, blue: int, alpha?: int} $value */
761
                                $args[$key] = $this->resolveColor($value);
1✔
762
                        }
763
                }
764

765
                $res = $function($this->image, ...$args);
1✔
766
                return $res instanceof \GdImage
1✔
767
                        ? $this->setImageResource($res)
1✔
768
                        : $res;
1✔
769
        }
770

771

772
        public function __clone()
773
        {
774
                ob_start(fn() => '');
1✔
775
                imagepng($this->image, null, 0);
1✔
776
                $this->setImageResource(imagecreatefromstring(ob_get_clean()) ?: throw new Nette\ShouldNotHappenException);
1✔
777
        }
1✔
778

779

780
        /** @param-out int|float $num */
781
        private static function isPercent(int|string &$num): bool
1✔
782
        {
783
                if (is_string($num) && str_ends_with($num, '%')) {
1✔
784
                        $num = (float) substr($num, 0, -1);
1✔
785
                        return true;
1✔
786
                } elseif (is_int($num) || $num === (string) (int) $num) {
1✔
787
                        $num = (int) $num;
1✔
788
                        return false;
1✔
789
                }
790

791
                throw new Nette\InvalidArgumentException("Expected dimension in int|string, '$num' given.");
×
792
        }
793

794

795
        /**
796
         * Prevents serialization.
797
         */
798
        public function __serialize(): array
799
        {
800
                throw new Nette\NotSupportedException('You cannot serialize or unserialize ' . self::class . ' instances.');
×
801
        }
802

803

804
        /**
805
         * @param  ImageColor|array{red: int, green: int, blue: int, alpha?: int}  $color
806
         */
807
        public function resolveColor(ImageColor|array $color): int
1✔
808
        {
809
                [$r, $g, $b, $a] = ImageColor::from($color)->toRGBA();
1✔
810
                return imagecolorallocatealpha($this->image, $r, $g, $b, $a)
1✔
811
                        ?: imagecolorresolvealpha($this->image, $r, $g, $b, $a);
1✔
812
        }
813

814

815
        private static function ensureExtension(): void
816
        {
817
                if (!extension_loaded('gd')) {
1✔
818
                        throw new Nette\NotSupportedException('PHP extension GD is not loaded.');
×
819
                }
820
        }
1✔
821
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc