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