Image.php 21 KB

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