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