1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Utils;
9:
10: use Nette;
11:
12:
13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87:
88: class Image
89: {
90: use Nette\SmartObject;
91:
92:
93: const SHRINK_ONLY = 0b0001;
94:
95:
96: const STRETCH = 0b0010;
97:
98:
99: const FIT = 0b0000;
100:
101:
102: const FILL = 0b0100;
103:
104:
105: const EXACT = 0b1000;
106:
107:
108: const JPEG = IMAGETYPE_JPEG,
109: PNG = IMAGETYPE_PNG,
110: GIF = IMAGETYPE_GIF;
111:
112: 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:
114:
115: const ENLARGE = 0;
116:
117:
118: private $image;
119:
120:
121: 122: 123: 124: 125: 126: 127: 128:
129: public static function rgb($red, $green, $blue, $transparency = 0)
130: {
131: return [
132: 'red' => max(0, min(255, (int) $red)),
133: 'green' => max(0, min(255, (int) $green)),
134: 'blue' => max(0, min(255, (int) $blue)),
135: 'alpha' => max(0, min(127, (int) $transparency)),
136: ];
137: }
138:
139:
140: 141: 142: 143: 144: 145: 146: 147:
148: public static function fromFile($file, & $format = NULL)
149: {
150: if (!extension_loaded('gd')) {
151: throw new Nette\NotSupportedException('PHP extension GD is not loaded.');
152: }
153:
154: static $funcs = [
155: self::JPEG => 'imagecreatefromjpeg',
156: self::PNG => 'imagecreatefrompng',
157: self::GIF => 'imagecreatefromgif',
158: ];
159: $format = @getimagesize($file)[2];
160:
161: if (!isset($funcs[$format])) {
162: throw new UnknownImageFileException(is_file($file) ? "Unknown type of file '$file'." : "File '$file' not found.");
163: }
164: return new static(Callback::invokeSafe($funcs[$format], [$file], function ($message) {
165: throw new ImageException($message);
166: }));
167: }
168:
169:
170: 171: 172: 173: 174: 175: 176:
177: public static function fromString($s, & $format = NULL)
178: {
179: if (!extension_loaded('gd')) {
180: throw new Nette\NotSupportedException('PHP extension GD is not loaded.');
181: }
182:
183: if (func_num_args() > 1) {
184: $tmp = @getimagesizefromstring($s)[2];
185: $format = in_array($tmp, [self::JPEG, self::PNG, self::GIF], TRUE) ? $tmp : NULL;
186: }
187:
188: return new static(Callback::invokeSafe('imagecreatefromstring', [$s], function ($message) {
189: throw new ImageException($message);
190: }));
191: }
192:
193:
194: 195: 196: 197: 198: 199: 200:
201: public static function fromBlank($width, $height, $color = NULL)
202: {
203: if (!extension_loaded('gd')) {
204: throw new Nette\NotSupportedException('PHP extension GD is not loaded.');
205: }
206:
207: $width = (int) $width;
208: $height = (int) $height;
209: if ($width < 1 || $height < 1) {
210: throw new Nette\InvalidArgumentException('Image width and height must be greater than zero.');
211: }
212:
213: $image = imagecreatetruecolor($width, $height);
214: if (is_array($color)) {
215: $color += ['alpha' => 0];
216: $color = imagecolorresolvealpha($image, $color['red'], $color['green'], $color['blue'], $color['alpha']);
217: imagealphablending($image, FALSE);
218: imagefilledrectangle($image, 0, 0, $width - 1, $height - 1, $color);
219: imagealphablending($image, TRUE);
220: }
221: return new static($image);
222: }
223:
224:
225: 226: 227: 228:
229: public function __construct($image)
230: {
231: $this->setImageResource($image);
232: imagesavealpha($image, TRUE);
233: }
234:
235:
236: 237: 238: 239:
240: public function getWidth()
241: {
242: return imagesx($this->image);
243: }
244:
245:
246: 247: 248: 249:
250: public function getHeight()
251: {
252: return imagesy($this->image);
253: }
254:
255:
256: 257: 258: 259: 260:
261: protected function setImageResource($image)
262: {
263: if (!is_resource($image) || get_resource_type($image) !== 'gd') {
264: throw new Nette\InvalidArgumentException('Image is not valid.');
265: }
266: $this->image = $image;
267: return $this;
268: }
269:
270:
271: 272: 273: 274:
275: public function getImageResource()
276: {
277: return $this->image;
278: }
279:
280:
281: 282: 283: 284: 285: 286: 287:
288: public function resize($width, $height, $flags = self::FIT)
289: {
290: if ($flags & self::EXACT) {
291: return $this->resize($width, $height, self::FILL)->crop('50%', '50%', $width, $height);
292: }
293:
294: list($newWidth, $newHeight) = static::calculateSize($this->getWidth(), $this->getHeight(), $width, $height, $flags);
295:
296: if ($newWidth !== $this->getWidth() || $newHeight !== $this->getHeight()) {
297: $newImage = static::fromBlank($newWidth, $newHeight, self::RGB(0, 0, 0, 127))->getImageResource();
298: imagecopyresampled(
299: $newImage, $this->image,
300: 0, 0, 0, 0,
301: $newWidth, $newHeight, $this->getWidth(), $this->getHeight()
302: );
303: $this->image = $newImage;
304: }
305:
306: if ($width < 0 || $height < 0) {
307: imageflip($this->image, $width < 0 ? ($height < 0 ? IMG_FLIP_BOTH : IMG_FLIP_HORIZONTAL) : IMG_FLIP_VERTICAL);
308: }
309: return $this;
310: }
311:
312:
313: 314: 315: 316: 317: 318: 319: 320: 321:
322: public static function calculateSize($srcWidth, $srcHeight, $newWidth, $newHeight, $flags = self::FIT)
323: {
324: if (is_string($newWidth) && substr($newWidth, -1) === '%') {
325: $newWidth = (int) round($srcWidth / 100 * abs($newWidth));
326: $percents = TRUE;
327: } else {
328: $newWidth = (int) abs($newWidth);
329: }
330:
331: if (is_string($newHeight) && substr($newHeight, -1) === '%') {
332: $newHeight = (int) round($srcHeight / 100 * abs($newHeight));
333: $flags |= empty($percents) ? 0 : self::STRETCH;
334: } else {
335: $newHeight = (int) abs($newHeight);
336: }
337:
338: if ($flags & self::STRETCH) {
339: if (empty($newWidth) || empty($newHeight)) {
340: throw new Nette\InvalidArgumentException('For stretching must be both width and height specified.');
341: }
342:
343: if ($flags & self::SHRINK_ONLY) {
344: $newWidth = (int) round($srcWidth * min(1, $newWidth / $srcWidth));
345: $newHeight = (int) round($srcHeight * min(1, $newHeight / $srcHeight));
346: }
347:
348: } else {
349: if (empty($newWidth) && empty($newHeight)) {
350: throw new Nette\InvalidArgumentException('At least width or height must be specified.');
351: }
352:
353: $scale = [];
354: if ($newWidth > 0) {
355: $scale[] = $newWidth / $srcWidth;
356: }
357:
358: if ($newHeight > 0) {
359: $scale[] = $newHeight / $srcHeight;
360: }
361:
362: if ($flags & self::FILL) {
363: $scale = [max($scale)];
364: }
365:
366: if ($flags & self::SHRINK_ONLY) {
367: $scale[] = 1;
368: }
369:
370: $scale = min($scale);
371: $newWidth = (int) round($srcWidth * $scale);
372: $newHeight = (int) round($srcHeight * $scale);
373: }
374:
375: return [max($newWidth, 1), max($newHeight, 1)];
376: }
377:
378:
379: 380: 381: 382: 383: 384: 385: 386:
387: public function crop($left, $top, $width, $height)
388: {
389: list($r['x'], $r['y'], $r['width'], $r['height'])
390: = static::calculateCutout($this->getWidth(), $this->getHeight(), $left, $top, $width, $height);
391: if (PHP_VERSION_ID > 50611) {
392: $this->image = imagecrop($this->image, $r);
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:
402: 403: 404: 405: 406: 407: 408: 409: 410: 411:
412: public static function calculateCutout($srcWidth, $srcHeight, $left, $top, $newWidth, $newHeight)
413: {
414: if (is_string($newWidth) && substr($newWidth, -1) === '%') {
415: $newWidth = (int) round($srcWidth / 100 * substr($newWidth, 0, -1));
416: }
417: if (is_string($newHeight) && substr($newHeight, -1) === '%') {
418: $newHeight = (int) round($srcHeight / 100 * substr($newHeight, 0, -1));
419: }
420: if (is_string($left) && substr($left, -1) === '%') {
421: $left = (int) round(($srcWidth - $newWidth) / 100 * substr($left, 0, -1));
422: }
423: if (is_string($top) && substr($top, -1) === '%') {
424: $top = (int) round(($srcHeight - $newHeight) / 100 * substr($top, 0, -1));
425: }
426: if ($left < 0) {
427: $newWidth += $left;
428: $left = 0;
429: }
430: if ($top < 0) {
431: $newHeight += $top;
432: $top = 0;
433: }
434: $newWidth = min($newWidth, $srcWidth - $left);
435: $newHeight = min($newHeight, $srcHeight - $top);
436: return [$left, $top, $newWidth, $newHeight];
437: }
438:
439:
440: 441: 442: 443:
444: public function sharpen()
445: {
446: imageconvolution($this->image, [
447: [-1, -1, -1],
448: [-1, 24, -1],
449: [-1, -1, -1],
450: ], 16, 0);
451: return $this;
452: }
453:
454:
455: 456: 457: 458: 459: 460: 461: 462:
463: public function place(Image $image, $left = 0, $top = 0, $opacity = 100)
464: {
465: $opacity = max(0, min(100, (int) $opacity));
466: if ($opacity === 0) {
467: return $this;
468: }
469:
470: $width = $image->getWidth();
471: $height = $image->getHeight();
472:
473: if (is_string($left) && substr($left, -1) === '%') {
474: $left = (int) round(($this->getWidth() - $width) / 100 * $left);
475: }
476:
477: if (is_string($top) && substr($top, -1) === '%') {
478: $top = (int) round(($this->getHeight() - $height) / 100 * $top);
479: }
480:
481: $output = $input = $image->image;
482: if ($opacity < 100) {
483: for ($i = 0; $i < 128; $i++) {
484: $tbl[$i] = round(127 - (127 - $i) * $opacity / 100);
485: }
486:
487: $output = imagecreatetruecolor($width, $height);
488: imagealphablending($output, FALSE);
489: if (!$image->isTrueColor()) {
490: $input = $output;
491: imagefilledrectangle($output, 0, 0, $width, $height, imagecolorallocatealpha($output, 0, 0, 0, 127));
492: imagecopy($output, $image->image, 0, 0, 0, 0, $width, $height);
493: }
494: for ($x = 0; $x < $width; $x++) {
495: for ($y = 0; $y < $height; $y++) {
496: $c = \imagecolorat($input, $x, $y);
497: $c = ($c & 0xFFFFFF) + ($tbl[$c >> 24] << 24);
498: \imagesetpixel($output, $x, $y, $c);
499: }
500: }
501: imagealphablending($output, TRUE);
502: }
503:
504: imagecopy(
505: $this->image, $output,
506: $left, $top, 0, 0, $width, $height
507: );
508: return $this;
509: }
510:
511:
512: 513: 514: 515: 516: 517: 518:
519: public function save($file = NULL, $quality = NULL, $type = NULL)
520: {
521: if ($type === NULL) {
522: switch (strtolower($ext = pathinfo($file, PATHINFO_EXTENSION))) {
523: case 'jpg':
524: case 'jpeg':
525: $type = self::JPEG;
526: break;
527: case 'png':
528: $type = self::PNG;
529: break;
530: case 'gif':
531: $type = self::GIF;
532: break;
533: default:
534: throw new Nette\InvalidArgumentException("Unsupported file extension '$ext'.");
535: }
536: }
537:
538: switch ($type) {
539: case self::JPEG:
540: $quality = $quality === NULL ? 85 : max(0, min(100, (int) $quality));
541: return imagejpeg($this->image, $file, $quality);
542:
543: case self::PNG:
544: $quality = $quality === NULL ? 9 : max(0, min(9, (int) $quality));
545: return imagepng($this->image, $file, $quality);
546:
547: case self::GIF:
548: return imagegif($this->image, $file);
549:
550: default:
551: throw new Nette\InvalidArgumentException("Unsupported image type '$type'.");
552: }
553: }
554:
555:
556: 557: 558: 559: 560: 561:
562: public function toString($type = self::JPEG, $quality = NULL)
563: {
564: ob_start(function () {});
565: $this->save(NULL, $quality, $type);
566: return ob_get_clean();
567: }
568:
569:
570: 571: 572: 573:
574: public function __toString()
575: {
576: try {
577: return $this->toString();
578: } catch (\Throwable $e) {
579: } catch (\Exception $e) {
580: }
581: if (isset($e)) {
582: if (func_num_args()) {
583: throw $e;
584: }
585: trigger_error("Exception in " . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR);
586: }
587: }
588:
589:
590: 591: 592: 593: 594: 595:
596: public function send($type = self::JPEG, $quality = NULL)
597: {
598: if (!in_array($type, [self::JPEG, self::PNG, self::GIF], TRUE)) {
599: throw new Nette\InvalidArgumentException("Unsupported image type '$type'.");
600: }
601: header('Content-Type: ' . image_type_to_mime_type($type));
602: return $this->save(NULL, $quality, $type);
603: }
604:
605:
606: 607: 608: 609: 610: 611: 612: 613:
614: public function __call($name, $args)
615: {
616: $function = 'image' . $name;
617: if (!function_exists($function)) {
618: ObjectMixin::strictCall(get_class($this), $name);
619: }
620:
621: foreach ($args as $key => $value) {
622: if ($value instanceof self) {
623: $args[$key] = $value->getImageResource();
624:
625: } elseif (is_array($value) && isset($value['red'])) {
626: $args[$key] = imagecolorallocatealpha(
627: $this->image,
628: $value['red'], $value['green'], $value['blue'], $value['alpha']
629: ) ?: imagecolorresolvealpha(
630: $this->image,
631: $value['red'], $value['green'], $value['blue'], $value['alpha']
632: );
633: }
634: }
635: $res = $function($this->image, ...$args);
636: return is_resource($res) && get_resource_type($res) === 'gd' ? $this->setImageResource($res) : $res;
637: }
638:
639:
640: public function __clone()
641: {
642: ob_start(function () {});
643: imagegd2($this->image);
644: $this->setImageResource(imagecreatefromstring(ob_get_clean()));
645: }
646:
647: }
648: