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