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