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