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