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