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