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: class Image extends Object
103: {
104:
105: const SHRINK_ONLY = 1;
106:
107:
108: const STRETCH = 2;
109:
110:
111: const FIT = 0;
112:
113:
114: const FILL = 4;
115:
116:
117: const EXACT = 8;
118:
119:
120: const JPEG = IMAGETYPE_JPEG,
121: PNG = IMAGETYPE_PNG,
122: GIF = IMAGETYPE_GIF;
123:
124: 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;";
125:
126:
127: const ENLARGE = 0;
128:
129:
130: private $image;
131:
132:
133: 134: 135: 136: 137: 138: 139: 140:
141: public static function rgb($red, $green, $blue, $transparency = 0)
142: {
143: return array(
144: 'red' => max(0, min(255, (int) $red)),
145: 'green' => max(0, min(255, (int) $green)),
146: 'blue' => max(0, min(255, (int) $blue)),
147: 'alpha' => max(0, min(127, (int) $transparency)),
148: );
149: }
150:
151:
152: 153: 154: 155: 156: 157:
158: public static function fromFile($file, & $format = NULL)
159: {
160: if (!extension_loaded('gd')) {
161: throw new NotSupportedException("PHP extension GD is not loaded.");
162: }
163:
164: $info = @getimagesize($file);
165:
166: switch ($format = $info[2]) {
167: case self::JPEG:
168: return new static(imagecreatefromjpeg($file));
169:
170: case self::PNG:
171: return new static(imagecreatefrompng($file));
172:
173: case self::GIF:
174: return new static(imagecreatefromgif($file));
175:
176: default:
177: throw new UnknownImageFileException("Unknown image type or file '$file' not found.");
178: }
179: }
180:
181:
182: 183: 184: 185: 186:
187: public static function getFormatFromString($s)
188: {
189: $types = array('image/jpeg' => self::JPEG, 'image/gif' => self::GIF, 'image/png' => self::PNG);
190: $type = Utils\MimeTypeDetector::fromString($s);
191: return isset($types[$type]) ? $types[$type] : NULL;
192: }
193:
194:
195: 196: 197: 198: 199: 200:
201: public static function fromString($s, & $format = NULL)
202: {
203: if (!extension_loaded('gd')) {
204: throw new NotSupportedException("PHP extension GD is not loaded.");
205: }
206:
207: $format = static::getFormatFromString($s);
208:
209: return new static(imagecreatefromstring($s));
210: }
211:
212:
213: 214: 215: 216: 217: 218: 219:
220: public static function fromBlank($width, $height, $color = NULL)
221: {
222: if (!extension_loaded('gd')) {
223: throw new NotSupportedException("PHP extension GD is not loaded.");
224: }
225:
226: $width = (int) $width;
227: $height = (int) $height;
228: if ($width < 1 || $height < 1) {
229: throw new InvalidArgumentException('Image width and height must be greater than zero.');
230: }
231:
232: $image = imagecreatetruecolor($width, $height);
233: if (is_array($color)) {
234: $color += array('alpha' => 0);
235: $color = imagecolorallocatealpha($image, $color['red'], $color['green'], $color['blue'], $color['alpha']);
236: imagealphablending($image, FALSE);
237: imagefilledrectangle($image, 0, 0, $width - 1, $height - 1, $color);
238: imagealphablending($image, TRUE);
239: }
240: return new static($image);
241: }
242:
243:
244: 245: 246: 247:
248: public function __construct($image)
249: {
250: $this->setImageResource($image);
251: imagesavealpha($image, TRUE);
252: }
253:
254:
255: 256: 257: 258:
259: public function getWidth()
260: {
261: return imagesx($this->image);
262: }
263:
264:
265: 266: 267: 268:
269: public function getHeight()
270: {
271: return imagesy($this->image);
272: }
273:
274:
275: 276: 277: 278: 279:
280: protected function setImageResource($image)
281: {
282: if (!is_resource($image) || get_resource_type($image) !== 'gd') {
283: throw new InvalidArgumentException('Image is not valid.');
284: }
285: $this->image = $image;
286: return $this;
287: }
288:
289:
290: 291: 292: 293:
294: public function getImageResource()
295: {
296: return $this->image;
297: }
298:
299:
300: 301: 302: 303: 304: 305: 306:
307: public function resize($width, $height, $flags = self::FIT)
308: {
309: if ($flags & self::EXACT) {
310: return $this->resize($width, $height, self::FILL)->crop('50%', '50%', $width, $height);
311: }
312:
313: list($newWidth, $newHeight) = static::calculateSize($this->getWidth(), $this->getHeight(), $width, $height, $flags);
314:
315: if ($newWidth !== $this->getWidth() || $newHeight !== $this->getHeight()) {
316: $newImage = static::fromBlank($newWidth, $newHeight, self::RGB(0, 0, 0, 127))->getImageResource();
317: imagecopyresampled(
318: $newImage, $this->getImageResource(),
319: 0, 0, 0, 0,
320: $newWidth, $newHeight, $this->getWidth(), $this->getHeight()
321: );
322: $this->image = $newImage;
323: }
324:
325: if ($width < 0 || $height < 0) {
326: $newImage = static::fromBlank($newWidth, $newHeight, self::RGB(0, 0, 0, 127))->getImageResource();
327: imagecopyresampled(
328: $newImage, $this->getImageResource(),
329: 0, 0, $width < 0 ? $newWidth - 1 : 0, $height < 0 ? $newHeight - 1 : 0,
330: $newWidth, $newHeight, $width < 0 ? -$newWidth : $newWidth, $height < 0 ? -$newHeight : $newHeight
331: );
332: $this->image = $newImage;
333: }
334: return $this;
335: }
336:
337:
338: 339: 340: 341: 342: 343: 344: 345: 346:
347: public static function calculateSize($srcWidth, $srcHeight, $newWidth, $newHeight, $flags = self::FIT)
348: {
349: if (substr($newWidth, -1) === '%') {
350: $newWidth = round($srcWidth / 100 * abs($newWidth));
351: $percents = TRUE;
352: } else {
353: $newWidth = (int) abs($newWidth);
354: }
355:
356: if (substr($newHeight, -1) === '%') {
357: $newHeight = round($srcHeight / 100 * abs($newHeight));
358: $flags |= empty($percents) ? 0 : self::STRETCH;
359: } else {
360: $newHeight = (int) abs($newHeight);
361: }
362:
363: if ($flags & self::STRETCH) {
364: if (empty($newWidth) || empty($newHeight)) {
365: throw new InvalidArgumentException('For stretching must be both width and height specified.');
366: }
367:
368: if ($flags & self::SHRINK_ONLY) {
369: $newWidth = round($srcWidth * min(1, $newWidth / $srcWidth));
370: $newHeight = round($srcHeight * min(1, $newHeight / $srcHeight));
371: }
372:
373: } else {
374: if (empty($newWidth) && empty($newHeight)) {
375: throw new InvalidArgumentException('At least width or height must be specified.');
376: }
377:
378: $scale = array();
379: if ($newWidth > 0) {
380: $scale[] = $newWidth / $srcWidth;
381: }
382:
383: if ($newHeight > 0) {
384: $scale[] = $newHeight / $srcHeight;
385: }
386:
387: if ($flags & self::FILL) {
388: $scale = array(max($scale));
389: }
390:
391: if ($flags & self::SHRINK_ONLY) {
392: $scale[] = 1;
393: }
394:
395: $scale = min($scale);
396: $newWidth = round($srcWidth * $scale);
397: $newHeight = round($srcHeight * $scale);
398: }
399:
400: return array(max((int) $newWidth, 1), max((int) $newHeight, 1));
401: }
402:
403:
404: 405: 406: 407: 408: 409: 410: 411:
412: public function crop($left, $top, $width, $height)
413: {
414: list($left, $top, $width, $height) = static::calculateCutout($this->getWidth(), $this->getHeight(), $left, $top, $width, $height);
415: $newImage = static::fromBlank($width, $height, self::RGB(0, 0, 0, 127))->getImageResource();
416: imagecopy($newImage, $this->getImageResource(), 0, 0, $left, $top, $width, $height);
417: $this->image = $newImage;
418: return $this;
419: }
420:
421:
422: 423: 424: 425: 426: 427: 428: 429: 430: 431:
432: public static function calculateCutout($srcWidth, $srcHeight, $left, $top, $newWidth, $newHeight)
433: {
434: if (substr($newWidth, -1) === '%') {
435: $newWidth = round($srcWidth / 100 * $newWidth);
436: }
437: if (substr($newHeight, -1) === '%') {
438: $newHeight = round($srcHeight / 100 * $newHeight);
439: }
440: if (substr($left, -1) === '%') {
441: $left = round(($srcWidth - $newWidth) / 100 * $left);
442: }
443: if (substr($top, -1) === '%') {
444: $top = round(($srcHeight - $newHeight) / 100 * $top);
445: }
446: if ($left < 0) {
447: $newWidth += $left; $left = 0;
448: }
449: if ($top < 0) {
450: $newHeight += $top; $top = 0;
451: }
452: $newWidth = min((int) $newWidth, $srcWidth - $left);
453: $newHeight = min((int) $newHeight, $srcHeight - $top);
454: return array($left, $top, $newWidth, $newHeight);
455: }
456:
457:
458: 459: 460: 461:
462: public function sharpen()
463: {
464: imageconvolution($this->getImageResource(), array(
465: array( -1, -1, -1 ),
466: array( -1, 24, -1 ),
467: array( -1, -1, -1 ),
468: ), 16, 0);
469: return $this;
470: }
471:
472:
473: 474: 475: 476: 477: 478: 479: 480:
481: public function place(Image $image, $left = 0, $top = 0, $opacity = 100)
482: {
483: $opacity = max(0, min(100, (int) $opacity));
484:
485: if (substr($left, -1) === '%') {
486: $left = round(($this->getWidth() - $image->getWidth()) / 100 * $left);
487: }
488:
489: if (substr($top, -1) === '%') {
490: $top = round(($this->getHeight() - $image->getHeight()) / 100 * $top);
491: }
492:
493: if ($opacity === 100) {
494: imagecopy(
495: $this->getImageResource(), $image->getImageResource(),
496: $left, $top, 0, 0, $image->getWidth(), $image->getHeight()
497: );
498:
499: } elseif ($opacity <> 0) {
500: imagecopymerge(
501: $this->getImageResource(), $image->getImageResource(),
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->getImageResource(), $file, $quality);
537:
538: case self::PNG:
539: $quality = $quality === NULL ? 9 : max(0, min(9, (int) $quality));
540: return imagepng($this->getImageResource(), $file, $quality);
541:
542: case self::GIF:
543: return $file === NULL ? imagegif($this->getImageResource()) : imagegif($this->getImageResource(), $file);
544:
545: default:
546: throw new InvalidArgumentException("Unsupported image 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:
574: } catch (\Exception $e) {
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: