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