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