PluginProbe
Packeta / 2.1
Packeta v2.1
2.3.2 2.3.1 trunk 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3.0 1.3.1 1.3.2 1.4 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 All 56 releases
packeta / deps / nette / utils / src / Utils / Image.php

Image.php in Packeta 2.1, at deps/nette/utils/src/Utils/Image.php

608 lines 24.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 Packetery\Nette\Utils;
9
10 use Packetery\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 int $width
90 * @property-read int $height
91 * @property-read resource|\GdImage $imageResource
92 */
93 class Image
94 {
95 use \Packetery\Nette\SmartObject;
96 /** {@link resize()} only shrinks images */
97 public const SHRINK_ONLY = 0b1;
98 /** {@link resize()} will ignore aspect ratio */
99 public const STRETCH = 0b10;
100 /** {@link resize()} fits in given area so its dimensions are less than or equal to the required dimensions */
101 public const FIT = 0b0;
102 /** {@link resize()} fills given area so its dimensions are greater than or equal to the required dimensions */
103 public const FILL = 0b100;
104 /** {@link resize()} fills given area exactly */
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 \Packetery\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 \Packetery\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 \Packetery\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 \Packetery\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 * @return static
169 * @throws \Packetery\Nette\NotSupportedException if gd extension is not loaded
170 */
171 public static function fromBlank(int $width, int $height, ?array $color = null)
172 {
173 if (!\extension_loaded('gd')) {
174 throw new \Packetery\Nette\NotSupportedException('PHP extension GD is not loaded.');
175 }
176 if ($width < 1 || $height < 1) {
177 throw new \Packetery\Nette\InvalidArgumentException('Image width and height must be greater than zero.');
178 }
179 $image = \imagecreatetruecolor($width, $height);
180 if ($color) {
181 $color += ['alpha' => 0];
182 $color = \imagecolorresolvealpha($image, $color['red'], $color['green'], $color['blue'], $color['alpha']);
183 \imagealphablending($image, \false);
184 \imagefilledrectangle($image, 0, 0, $width - 1, $height - 1, $color);
185 \imagealphablending($image, \true);
186 }
187 return new static($image);
188 }
189 /**
190 * Returns the type of image from file.
191 */
192 public static function detectTypeFromFile(string $file, &$width = null, &$height = null) : ?int
193 {
194 [$width, $height, $type] = @\getimagesize($file);
195 // @ - files smaller than 12 bytes causes read error
196 return isset(self::Formats[$type]) ? $type : null;
197 }
198 /**
199 * Returns the type of image from string.
200 */
201 public static function detectTypeFromString(string $s, &$width = null, &$height = null) : ?int
202 {
203 [$width, $height, $type] = @\getimagesizefromstring($s);
204 // @ - strings smaller than 12 bytes causes read error
205 return isset(self::Formats[$type]) ? $type : null;
206 }
207 /**
208 * Returns the file extension for the given `Image::XXX` constant.
209 */
210 public static function typeToExtension(int $type) : string
211 {
212 if (!isset(self::Formats[$type])) {
213 throw new \Packetery\Nette\InvalidArgumentException("Unsupported image type '{$type}'.");
214 }
215 return self::Formats[$type];
216 }
217 /**
218 * Returns the `Image::XXX` constant for given file extension.
219 */
220 public static function extensionToType(string $extension) : int
221 {
222 $extensions = \array_flip(self::Formats) + ['jpg' => self::JPEG];
223 $extension = \strtolower($extension);
224 if (!isset($extensions[$extension])) {
225 throw new \Packetery\Nette\InvalidArgumentException("Unsupported file extension '{$extension}'.");
226 }
227 return $extensions[$extension];
228 }
229 /**
230 * Returns the mime type for the given `Image::XXX` constant.
231 */
232 public static function typeToMimeType(int $type) : string
233 {
234 return 'image/' . self::typeToExtension($type);
235 }
236 /**
237 * Wraps GD image.
238 * @param resource|\GdImage $image
239 */
240 public function __construct($image)
241 {
242 $this->setImageResource($image);
243 \imagesavealpha($image, \true);
244 }
245 /**
246 * Returns image width.
247 */
248 public function getWidth() : int
249 {
250 return \imagesx($this->image);
251 }
252 /**
253 * Returns image height.
254 */
255 public function getHeight() : int
256 {
257 return \imagesy($this->image);
258 }
259 /**
260 * Sets image resource.
261 * @param resource|\GdImage $image
262 * @return static
263 */
264 protected function setImageResource($image)
265 {
266 if (!$image instanceof \GdImage && !(\is_resource($image) && \get_resource_type($image) === 'gd')) {
267 throw new \Packetery\Nette\InvalidArgumentException('Image is not valid.');
268 }
269 $this->image = $image;
270 return $this;
271 }
272 /**
273 * Returns image GD resource.
274 * @return resource|\GdImage
275 */
276 public function getImageResource()
277 {
278 return $this->image;
279 }
280 /**
281 * Scales an image.
282 * @param int|string|null $width in pixels or percent
283 * @param int|string|null $height in pixels or percent
284 * @return static
285 */
286 public function resize($width, $height, int $flags = self::FIT)
287 {
288 if ($flags & self::EXACT) {
289 return $this->resize($width, $height, self::FILL)->crop('50%', '50%', $width, $height);
290 }
291 [$newWidth, $newHeight] = static::calculateSize($this->getWidth(), $this->getHeight(), $width, $height, $flags);
292 if ($newWidth !== $this->getWidth() || $newHeight !== $this->getHeight()) {
293 // resize
294 $newImage = static::fromBlank($newWidth, $newHeight, self::rgb(0, 0, 0, 127))->getImageResource();
295 \imagecopyresampled($newImage, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->getWidth(), $this->getHeight());
296 $this->image = $newImage;
297 }
298 if ($width < 0 || $height < 0) {
299 \imageflip($this->image, $width < 0 ? $height < 0 ? \IMG_FLIP_BOTH : \IMG_FLIP_HORIZONTAL : \IMG_FLIP_VERTICAL);
300 }
301 return $this;
302 }
303 /**
304 * Calculates dimensions of resized image.
305 * @param int|string|null $newWidth in pixels or percent
306 * @param int|string|null $newHeight in pixels or percent
307 */
308 public static function calculateSize(int $srcWidth, int $srcHeight, $newWidth, $newHeight, int $flags = self::FIT) : array
309 {
310 if ($newWidth === null) {
311 } elseif (self::isPercent($newWidth)) {
312 $newWidth = (int) \round($srcWidth / 100 * \abs($newWidth));
313 $percents = \true;
314 } else {
315 $newWidth = \abs($newWidth);
316 }
317 if ($newHeight === null) {
318 } elseif (self::isPercent($newHeight)) {
319 $newHeight = (int) \round($srcHeight / 100 * \abs($newHeight));
320 $flags |= empty($percents) ? 0 : self::STRETCH;
321 } else {
322 $newHeight = \abs($newHeight);
323 }
324 if ($flags & self::STRETCH) {
325 // non-proportional
326 if (!$newWidth || !$newHeight) {
327 throw new \Packetery\Nette\InvalidArgumentException('For stretching must be both width and height specified.');
328 }
329 if ($flags & self::SHRINK_ONLY) {
330 $newWidth = (int) \round($srcWidth * \min(1, $newWidth / $srcWidth));
331 $newHeight = (int) \round($srcHeight * \min(1, $newHeight / $srcHeight));
332 }
333 } else {
334 // proportional
335 if (!$newWidth && !$newHeight) {
336 throw new \Packetery\Nette\InvalidArgumentException('At least width or height must be specified.');
337 }
338 $scale = [];
339 if ($newWidth > 0) {
340 // fit width
341 $scale[] = $newWidth / $srcWidth;
342 }
343 if ($newHeight > 0) {
344 // fit height
345 $scale[] = $newHeight / $srcHeight;
346 }
347 if ($flags & self::FILL) {
348 $scale = [\max($scale)];
349 }
350 if ($flags & self::SHRINK_ONLY) {
351 $scale[] = 1;
352 }
353 $scale = \min($scale);
354 $newWidth = (int) \round($srcWidth * $scale);
355 $newHeight = (int) \round($srcHeight * $scale);
356 }
357 return [\max($newWidth, 1), \max($newHeight, 1)];
358 }
359 /**
360 * Crops image.
361 * @param int|string $left in pixels or percent
362 * @param int|string $top in pixels or percent
363 * @param int|string $width in pixels or percent
364 * @param int|string $height in pixels or percent
365 * @return static
366 */
367 public function crop($left, $top, $width, $height)
368 {
369 [$r['x'], $r['y'], $r['width'], $r['height']] = static::calculateCutout($this->getWidth(), $this->getHeight(), $left, $top, $width, $height);
370 if (\gd_info()['GD Version'] === 'bundled (2.1.0 compatible)') {
371 $this->image = \imagecrop($this->image, $r);
372 \imagesavealpha($this->image, \true);
373 } else {
374 $newImage = static::fromBlank($r['width'], $r['height'], self::RGB(0, 0, 0, 127))->getImageResource();
375 \imagecopy($newImage, $this->image, 0, 0, $r['x'], $r['y'], $r['width'], $r['height']);
376 $this->image = $newImage;
377 }
378 return $this;
379 }
380 /**
381 * Calculates dimensions of cutout in image.
382 * @param int|string $left in pixels or percent
383 * @param int|string $top in pixels or percent
384 * @param int|string $newWidth in pixels or percent
385 * @param int|string $newHeight in pixels or percent
386 */
387 public static function calculateCutout(int $srcWidth, int $srcHeight, $left, $top, $newWidth, $newHeight) : array
388 {
389 if (self::isPercent($newWidth)) {
390 $newWidth = (int) \round($srcWidth / 100 * $newWidth);
391 }
392 if (self::isPercent($newHeight)) {
393 $newHeight = (int) \round($srcHeight / 100 * $newHeight);
394 }
395 if (self::isPercent($left)) {
396 $left = (int) \round(($srcWidth - $newWidth) / 100 * $left);
397 }
398 if (self::isPercent($top)) {
399 $top = (int) \round(($srcHeight - $newHeight) / 100 * $top);
400 }
401 if ($left < 0) {
402 $newWidth += $left;
403 $left = 0;
404 }
405 if ($top < 0) {
406 $newHeight += $top;
407 $top = 0;
408 }
409 $newWidth = \min($newWidth, $srcWidth - $left);
410 $newHeight = \min($newHeight, $srcHeight - $top);
411 return [$left, $top, $newWidth, $newHeight];
412 }
413 /**
414 * Sharpens image a little bit.
415 * @return static
416 */
417 public function sharpen()
418 {
419 \imageconvolution($this->image, [
420 // my magic numbers ;)
421 [-1, -1, -1],
422 [-1, 24, -1],
423 [-1, -1, -1],
424 ], 16, 0);
425 return $this;
426 }
427 /**
428 * Puts another image into this image.
429 * @param int|string $left in pixels or percent
430 * @param int|string $top in pixels or percent
431 * @param int $opacity 0..100
432 * @return static
433 */
434 public function place(self $image, $left = 0, $top = 0, int $opacity = 100)
435 {
436 $opacity = \max(0, \min(100, $opacity));
437 if ($opacity === 0) {
438 return $this;
439 }
440 $width = $image->getWidth();
441 $height = $image->getHeight();
442 if (self::isPercent($left)) {
443 $left = (int) \round(($this->getWidth() - $width) / 100 * $left);
444 }
445 if (self::isPercent($top)) {
446 $top = (int) \round(($this->getHeight() - $height) / 100 * $top);
447 }
448 $output = $input = $image->image;
449 if ($opacity < 100) {
450 $tbl = [];
451 for ($i = 0; $i < 128; $i++) {
452 $tbl[$i] = \round(127 - (127 - $i) * $opacity / 100);
453 }
454 $output = \imagecreatetruecolor($width, $height);
455 \imagealphablending($output, \false);
456 if (!$image->isTrueColor()) {
457 $input = $output;
458 \imagefilledrectangle($output, 0, 0, $width, $height, \imagecolorallocatealpha($output, 0, 0, 0, 127));
459 \imagecopy($output, $image->image, 0, 0, 0, 0, $width, $height);
460 }
461 for ($x = 0; $x < $width; $x++) {
462 for ($y = 0; $y < $height; $y++) {
463 $c = \imagecolorat($input, $x, $y);
464 $c = ($c & 0xffffff) + ($tbl[$c >> 24] << 24);
465 \imagesetpixel($output, $x, $y, $c);
466 }
467 }
468 \imagealphablending($output, \true);
469 }
470 \imagecopy($this->image, $output, $left, $top, 0, 0, $width, $height);
471 return $this;
472 }
473 /**
474 * 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).
475 * @throws ImageException
476 */
477 public function save(string $file, ?int $quality = null, ?int $type = null) : void
478 {
479 $type = $type ?? self::extensionToType(\pathinfo($file, \PATHINFO_EXTENSION));
480 $this->output($type, $quality, $file);
481 }
482 /**
483 * 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).
484 */
485 public function toString(int $type = self::JPEG, ?int $quality = null) : string
486 {
487 return Helpers::capture(function () use($type, $quality) {
488 $this->output($type, $quality);
489 });
490 }
491 /**
492 * Outputs image to string.
493 */
494 public function __toString() : string
495 {
496 try {
497 return $this->toString();
498 } catch (\Throwable $e) {
499 if (\func_num_args() || \PHP_VERSION_ID >= 70400) {
500 throw $e;
501 }
502 \trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", \E_USER_ERROR);
503 return '';
504 }
505 }
506 /**
507 * 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).
508 * @throws ImageException
509 */
510 public function send(int $type = self::JPEG, ?int $quality = null) : void
511 {
512 \header('Content-Type: ' . self::typeToMimeType($type));
513 $this->output($type, $quality);
514 }
515 /**
516 * Outputs image to browser or file.
517 * @throws ImageException
518 */
519 private function output(int $type, ?int $quality, ?string $file = null) : void
520 {
521 switch ($type) {
522 case self::JPEG:
523 $quality = $quality === null ? 85 : \max(0, \min(100, $quality));
524 $success = @\imagejpeg($this->image, $file, $quality);
525 // @ is escalated to exception
526 break;
527 case self::PNG:
528 $quality = $quality === null ? 9 : \max(0, \min(9, $quality));
529 $success = @\imagepng($this->image, $file, $quality);
530 // @ is escalated to exception
531 break;
532 case self::GIF:
533 $success = @\imagegif($this->image, $file);
534 // @ is escalated to exception
535 break;
536 case self::WEBP:
537 $quality = $quality === null ? 80 : \max(0, \min(100, $quality));
538 $success = @\imagewebp($this->image, $file, $quality);
539 // @ is escalated to exception
540 break;
541 case self::AVIF:
542 $quality = $quality === null ? 30 : \max(0, \min(100, $quality));
543 $success = @\imageavif($this->image, $file, $quality);
544 // @ is escalated to exception
545 break;
546 case self::BMP:
547 $success = @\imagebmp($this->image, $file);
548 // @ is escalated to exception
549 break;
550 default:
551 throw new \Packetery\Nette\InvalidArgumentException("Unsupported image type '{$type}'.");
552 }
553 if (!$success) {
554 throw new ImageException(Helpers::getLastError() ?: 'Unknown error');
555 }
556 }
557 /**
558 * Call to undefined method.
559 * @return mixed
560 * @throws \Packetery\Nette\MemberAccessException
561 */
562 public function __call(string $name, array $args)
563 {
564 $function = 'image' . $name;
565 if (!\function_exists($function)) {
566 ObjectHelpers::strictCall(static::class, $name);
567 }
568 foreach ($args as $key => $value) {
569 if ($value instanceof self) {
570 $args[$key] = $value->getImageResource();
571 } elseif (\is_array($value) && isset($value['red'])) {
572 // rgb
573 $args[$key] = \imagecolorallocatealpha($this->image, $value['red'], $value['green'], $value['blue'], $value['alpha']) ?: \imagecolorresolvealpha($this->image, $value['red'], $value['green'], $value['blue'], $value['alpha']);
574 }
575 }
576 $res = $function($this->image, ...$args);
577 return $res instanceof \GdImage || \is_resource($res) && \get_resource_type($res) === 'gd' ? $this->setImageResource($res) : $res;
578 }
579 public function __clone()
580 {
581 \ob_start(function () {
582 });
583 \imagepng($this->image, null, 0);
584 $this->setImageResource(\imagecreatefromstring(\ob_get_clean()));
585 }
586 /**
587 * @param int|string $num in pixels or percent
588 */
589 private static function isPercent(&$num) : bool
590 {
591 if (\is_string($num) && \substr($num, -1) === '%') {
592 $num = (float) \substr($num, 0, -1);
593 return \true;
594 } elseif (\is_int($num) || $num === (string) (int) $num) {
595 $num = (int) $num;
596 return \false;
597 }
598 throw new \Packetery\Nette\InvalidArgumentException("Expected dimension in int|string, '{$num}' given.");
599 }
600 /**
601 * Prevents serialization.
602 */
603 public function __sleep() : array
604 {
605 throw new \Packetery\Nette\NotSupportedException('You cannot serialize or unserialize ' . self::class . ' instances.');
606 }
607 }
608