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