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: class Image extends Object
33: {
34:
35: const ENLARGE = 1;
36:
37:
38: const STRETCH = 2;
39:
40:
41: const FIT = 0;
42:
43:
44: const FILL = 4;
45:
46:
47: const JPEG = IMAGETYPE_JPEG,
48: PNG = IMAGETYPE_PNG,
49: GIF = IMAGETYPE_GIF;
50:
51: 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;";
52:
53:
54: private $image;
55:
56:
57:
58: 59: 60: 61: 62: 63: 64: 65:
66: public static function rgb($red, $green, $blue, $transparency = 0)
67: {
68: return array(
69: 'red' => max(0, min(255, (int) $red)),
70: 'green' => max(0, min(255, (int) $green)),
71: 'blue' => max(0, min(255, (int) $blue)),
72: 'alpha' => max(0, min(127, (int) $transparency)),
73: );
74: }
75:
76:
77:
78: 79: 80: 81: 82: 83:
84: public static function fromFile($file, & $format = NULL)
85: {
86: if (!extension_loaded('gd')) {
87: throw new NotSupportedException("PHP extension GD is not loaded.");
88: }
89:
90: $info = @getimagesize($file);
91:
92: switch ($format = $info[2]) {
93: case self::JPEG:
94: return new self(imagecreatefromjpeg($file));
95:
96: case self::PNG:
97: return new self(imagecreatefrompng($file));
98:
99: case self::GIF:
100: return new self(imagecreatefromgif($file));
101:
102: default:
103: throw new UnknownImageFileException("Unknown image type or file '$file' not found.");
104: }
105: }
106:
107:
108:
109: 110: 111: 112: 113:
114: public static function getFormatFromString($s)
115: {
116: $types = array('image/jpeg' => self::JPEG, 'image/gif' => self::GIF, 'image/png' => self::PNG);
117: $type = MimeTypeDetector::fromString($s);
118: return isset($types[$type]) ? $types[$type] : NULL;
119: }
120:
121:
122:
123: 124: 125: 126: 127: 128:
129: public static function fromString($s, & $format = NULL)
130: {
131: if (!extension_loaded('gd')) {
132: throw new NotSupportedException("PHP extension GD is not loaded.");
133: }
134:
135: $format = self::getFormatFromString($s);
136:
137: return new self(imagecreatefromstring($s));
138: }
139:
140:
141:
142: 143: 144: 145: 146: 147: 148:
149: public static function fromBlank($width, $height, $color = NULL)
150: {
151: if (!extension_loaded('gd')) {
152: throw new NotSupportedException("PHP extension GD is not loaded.");
153: }
154:
155: $width = (int) $width;
156: $height = (int) $height;
157: if ($width < 1 || $height < 1) {
158: throw new InvalidArgumentException('Image width and height must be greater than zero.');
159: }
160:
161: $image = imagecreatetruecolor($width, $height);
162: if (is_array($color)) {
163: $color += array('alpha' => 0);
164: $color = imagecolorallocatealpha($image, $color['red'], $color['green'], $color['blue'], $color['alpha']);
165: imagealphablending($image, FALSE);
166: imagefilledrectangle($image, 0, 0, $width - 1, $height - 1, $color);
167: imagealphablending($image, TRUE);
168: }
169: return new self($image);
170: }
171:
172:
173:
174: 175: 176: 177:
178: public function __construct($image)
179: {
180: $this->setImageResource($image);
181: imagesavealpha($image, TRUE);
182: }
183:
184:
185:
186: 187: 188: 189:
190: public function getWidth()
191: {
192: return imagesx($this->image);
193: }
194:
195:
196:
197: 198: 199: 200:
201: public function getHeight()
202: {
203: return imagesy($this->image);
204: }
205:
206:
207:
208: 209: 210: 211: 212:
213: protected function setImageResource($image)
214: {
215: if (!is_resource($image) || get_resource_type($image) !== 'gd') {
216: throw new InvalidArgumentException('Image is not valid.');
217: }
218: $this->image = $image;
219: return $this;
220: }
221:
222:
223:
224: 225: 226: 227:
228: public function getImageResource()
229: {
230: return $this->image;
231: }
232:
233:
234:
235: 236: 237: 238: 239: 240: 241:
242: public function resize($width, $height, $flags = self::FIT)
243: {
244: list($newWidth, $newHeight) = self::calculateSize($this->getWidth(), $this->getHeight(), $width, $height, $flags);
245:
246: if ($newWidth !== $this->getWidth() || $newHeight !== $this->getHeight()) {
247: $newImage = self::fromBlank($newWidth, $newHeight, self::RGB(0, 0, 0, 127))->getImageResource();
248: imagecopyresampled(
249: $newImage, $this->getImageResource(),
250: 0, 0, 0, 0,
251: $newWidth, $newHeight, $this->getWidth(), $this->getHeight()
252: );
253: $this->image = $newImage;
254: }
255:
256: if ($width < 0 || $height < 0) {
257: $newImage = self::fromBlank($newWidth, $newHeight, self::RGB(0, 0, 0, 127))->getImageResource();
258: imagecopyresampled(
259: $newImage, $this->getImageResource(),
260: 0, 0, $width < 0 ? $newWidth - 1 : 0, $height < 0 ? $newHeight - 1 : 0,
261: $newWidth, $newHeight, $width < 0 ? -$newWidth : $newWidth, $height < 0 ? -$newHeight : $newHeight
262: );
263: $this->image = $newImage;
264: }
265: return $this;
266: }
267:
268:
269:
270: 271: 272: 273: 274: 275: 276: 277: 278:
279: public static function calculateSize($srcWidth, $srcHeight, $newWidth, $newHeight, $flags = self::FIT)
280: {
281: if (substr($newWidth, -1) === '%') {
282: $newWidth = round($srcWidth / 100 * abs($newWidth));
283: $flags |= self::ENLARGE;
284: $percents = TRUE;
285: } else {
286: $newWidth = (int) abs($newWidth);
287: }
288:
289: if (substr($newHeight, -1) === '%') {
290: $newHeight = round($srcHeight / 100 * abs($newHeight));
291: $flags |= empty($percents) ? self::ENLARGE : self::STRETCH;
292: } else {
293: $newHeight = (int) abs($newHeight);
294: }
295:
296: if ($flags & self::STRETCH) {
297: if (empty($newWidth) || empty($newHeight)) {
298: throw new InvalidArgumentException('For stretching must be both width and height specified.');
299: }
300:
301: if (($flags & self::ENLARGE) === 0) {
302: $newWidth = round($srcWidth * min(1, $newWidth / $srcWidth));
303: $newHeight = round($srcHeight * min(1, $newHeight / $srcHeight));
304: }
305:
306: } else {
307: if (empty($newWidth) && empty($newHeight)) {
308: throw new InvalidArgumentException('At least width or height must be specified.');
309: }
310:
311: $scale = array();
312: if ($newWidth > 0) {
313: $scale[] = $newWidth / $srcWidth;
314: }
315:
316: if ($newHeight > 0) {
317: $scale[] = $newHeight / $srcHeight;
318: }
319:
320: if ($flags & self::FILL) {
321: $scale = array(max($scale));
322: }
323:
324: if (($flags & self::ENLARGE) === 0) {
325: $scale[] = 1;
326: }
327:
328: $scale = min($scale);
329: $newWidth = round($srcWidth * $scale);
330: $newHeight = round($srcHeight * $scale);
331: }
332:
333: return array(max((int) $newWidth, 1), max((int) $newHeight, 1));
334: }
335:
336:
337:
338: 339: 340: 341: 342: 343: 344: 345:
346: public function crop($left, $top, $width, $height)
347: {
348: list($left, $top, $width, $height) = self::calculateCutout($this->getWidth(), $this->getHeight(), $left, $top, $width, $height);
349: $newImage = self::fromBlank($width, $height, self::RGB(0, 0, 0, 127))->getImageResource();
350: imagecopy($newImage, $this->getImageResource(), 0, 0, $left, $top, $width, $height);
351: $this->image = $newImage;
352: return $this;
353: }
354:
355:
356:
357: 358: 359: 360: 361: 362: 363: 364: 365: 366:
367: public static function calculateCutout($srcWidth, $srcHeight, $left, $top, $newWidth, $newHeight)
368: {
369: if (substr($newWidth, -1) === '%') {
370: $newWidth = round($srcWidth / 100 * $newWidth);
371: }
372: if (substr($newHeight, -1) === '%') {
373: $newHeight = round($srcHeight / 100 * $newHeight);
374: }
375: if (substr($left, -1) === '%') {
376: $left = round(($srcWidth - $newWidth) / 100 * $left);
377: }
378: if (substr($top, -1) === '%') {
379: $top = round(($srcHeight - $newHeight) / 100 * $top);
380: }
381: if ($left < 0) {
382: $newWidth += $left; $left = 0;
383: }
384: if ($top < 0) {
385: $newHeight += $top; $top = 0;
386: }
387: $newWidth = min((int) $newWidth, $srcWidth - $left);
388: $newHeight = min((int) $newHeight, $srcHeight - $top);
389: return array($left, $top, $newWidth, $newHeight);
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(
432: $this->getImageResource(), $image->getImageResource(),
433: $left, $top, 0, 0, $image->getWidth(), $image->getHeight()
434: );
435:
436: } elseif ($opacity <> 0) {
437: imagecopymerge(
438: $this->getImageResource(), $image->getImageResource(),
439: $left, $top, 0, 0, $image->getWidth(), $image->getHeight(),
440: $opacity
441: );
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 InvalidArgumentException("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: Debugger::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 InvalidArgumentException("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:
546: public function __call($name, $args)
547: {
548: $function = 'image' . $name;
549: if (function_exists($function)) {
550: foreach ($args as $key => $value) {
551: if ($value instanceof self) {
552: $args[$key] = $value->getImageResource();
553:
554: } elseif (is_array($value) && isset($value['red'])) {
555: $args[$key] = imagecolorallocatealpha(
556: $this->getImageResource(),
557: $value['red'], $value['green'], $value['blue'], $value['alpha']
558: );
559: }
560: }
561: array_unshift($args, $this->getImageResource());
562:
563: $res = call_user_func_array($function, $args);
564: return is_resource($res) && get_resource_type($res) === 'gd' ? $this->setImageResource($res) : $res;
565: }
566:
567: return parent::__call($name, $args);
568: }
569:
570: }
571:
572:
573:
574: 575: 576: 577: 578:
579: class UnknownImageFileException extends Exception
580: {
581: }
582: