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: class Image extends Nette\Object
87: {
88:
89: const SHRINK_ONLY = 1;
90:
91:
92: const STRETCH = 2;
93:
94:
95: const FIT = 0;
96:
97:
98: const FILL = 4;
99:
100:
101: const EXACT = 8;
102:
103:
104: const JPEG = IMAGETYPE_JPEG,
105: PNG = IMAGETYPE_PNG,
106: GIF = IMAGETYPE_GIF;
107:
108: 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:
110:
111: const ENLARGE = 0;
112:
113:
114: private $image;
115:
116:
117: 118: 119: 120: 121: 122: 123: 124:
125: public static function rgb($red, $green, $blue, $transparency = 0)
126: {
127: return array(
128: 'red' => max(0, min(255, (int) $red)),
129: 'green' => max(0, min(255, (int) $green)),
130: 'blue' => max(0, min(255, (int) $blue)),
131: 'alpha' => max(0, min(127, (int) $transparency)),
132: );
133: }
134:
135:
136: 137: 138: 139: 140: 141: 142: 143:
144: public static function fromFile($file, & $format = NULL)
145: {
146: if (!extension_loaded('gd')) {
147: throw new Nette\NotSupportedException('PHP extension GD is not loaded.');
148: }
149:
150: $info = @getimagesize($file);
151:
152: switch ($format = $info[2]) {
153: case self::JPEG:
154: return new static(imagecreatefromjpeg($file));
155:
156: case self::PNG:
157: return new static(imagecreatefrompng($file));
158:
159: case self::GIF:
160: return new static(imagecreatefromgif($file));
161:
162: default:
163: throw new UnknownImageFileException(is_file($file) ? "Unknown type of file '$file'." : "File '$file' not found.");
164: }
165: }
166:
167:
168: 169: 170: 171: 172:
173: public static function getFormatFromString($s)
174: {
175: $types = array('image/jpeg' => self::JPEG, 'image/gif' => self::GIF, 'image/png' => self::PNG);
176: $type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $s);
177: return isset($types[$type]) ? $types[$type] : NULL;
178: }
179:
180:
181: 182: 183: 184: 185: 186: 187:
188: public static function fromString($s, & $format = NULL)
189: {
190: if (!extension_loaded('gd')) {
191: throw new Nette\NotSupportedException('PHP extension GD is not loaded.');
192: }
193:
194: $format = static::getFormatFromString($s);
195:
196: return new static(imagecreatefromstring($s));
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($newWidth));
338: $percents = TRUE;
339: } else {
340: $newWidth = (int) abs($newWidth);
341: }
342:
343: if (substr($newHeight, -1) === '%') {
344: $newHeight = round($srcHeight / 100 * abs($newHeight));
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 * $newWidth);
423: }
424: if (substr($newHeight, -1) === '%') {
425: $newHeight = round($srcHeight / 100 * $newHeight);
426: }
427: if (substr($left, -1) === '%') {
428: $left = round(($srcWidth - $newWidth) / 100 * $left);
429: }
430: if (substr($top, -1) === '%') {
431: $top = round(($srcHeight - $newHeight) / 100 * $top);
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 * $left);
476: }
477:
478: if (substr($top, -1) === '%') {
479: $top = round(($this->getHeight() - $image->getHeight()) / 100 * $top);
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(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: }
530: }
531:
532: switch ($type) {
533: case self::JPEG:
534: $quality = $quality === NULL ? 85 : max(0, min(100, (int) $quality));
535: return imagejpeg($this->image, $file, $quality);
536:
537: case self::PNG:
538: $quality = $quality === NULL ? 9 : max(0, min(9, (int) $quality));
539: return imagepng($this->image, $file, $quality);
540:
541: case self::GIF:
542: return imagegif($this->image, $file);
543:
544: default:
545: throw new Nette\InvalidArgumentException('Unsupported image type.');
546: }
547: }
548:
549:
550: 551: 552: 553: 554: 555:
556: public function toString($type = self::JPEG, $quality = NULL)
557: {
558: ob_start();
559: $this->save(NULL, $quality, $type);
560: return ob_get_clean();
561: }
562:
563:
564: 565: 566: 567:
568: public function __toString()
569: {
570: try {
571: return $this->toString();
572: } catch (\Throwable $e) {
573: } catch (\Exception $e) {
574: }
575: if (isset($e)) {
576: if (func_num_args()) {
577: throw $e;
578: }
579: trigger_error("Exception in " . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR);
580: }
581: }
582:
583:
584: 585: 586: 587: 588: 589:
590: public function send($type = self::JPEG, $quality = NULL)
591: {
592: if ($type !== self::GIF && $type !== self::PNG && $type !== self::JPEG) {
593: throw new Nette\InvalidArgumentException('Unsupported image type.');
594: }
595: header('Content-Type: ' . image_type_to_mime_type($type));
596: return $this->save(NULL, $quality, $type);
597: }
598:
599:
600: 601: 602: 603: 604: 605: 606: 607:
608: public function __call($name, $args)
609: {
610: $function = 'image' . $name;
611: if (!function_exists($function)) {
612: return parent::__call($name, $args);
613: }
614:
615: foreach ($args as $key => $value) {
616: if ($value instanceof self) {
617: $args[$key] = $value->getImageResource();
618:
619: } elseif (is_array($value) && isset($value['red'])) {
620: $args[$key] = imagecolorallocatealpha(
621: $this->image,
622: $value['red'], $value['green'], $value['blue'], $value['alpha']
623: ) ?: imagecolorresolvealpha(
624: $this->image,
625: $value['red'], $value['green'], $value['blue'], $value['alpha']
626: );
627: }
628: }
629: array_unshift($args, $this->image);
630:
631: $res = call_user_func_array($function, $args);
632: return is_resource($res) && get_resource_type($res) === 'gd' ? $this->setImageResource($res) : $res;
633: }
634:
635:
636: public function __clone()
637: {
638: ob_start();
639: imagegd2($this->image);
640: $this->setImageResource(imagecreatefromstring(ob_get_clean()));
641: }
642:
643: }
644:
645:
646: 647: 648:
649: class UnknownImageFileException extends \Exception
650: {
651: }
652: