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