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