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