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