PluginProbe
Packeta / 1.6.2
Packeta v1.6.2
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 1.6.2, at deps/nette/utils/src/Utils/Image.php

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