Image.php 22 KB

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