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