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