Namespaces

  • Nette
    • Application
    • Caching
    • Collections
    • Config
    • Forms
    • IO
    • Loaders
    • Mail
    • Reflection
    • Security
    • Templates
    • Web
  • None
  • PHP

Classes

  • ArrayTools
  • Callback
  • Component
  • ComponentContainer
  • Configurator
  • DateTime
  • Debug
  • Environment
  • Framework
  • FreezableObject
  • GenericRecursiveIterator
  • Image
  • ImageMagick
  • InstanceFilterIterator
  • Object
  • ObjectMixin
  • Paginator
  • RecursiveComponentIterator
  • ServiceLocator
  • SmartCachingIterator
  • String
  • Tools

Interfaces

  • IComponent
  • IComponentContainer
  • IDebuggable
  • IServiceLocator
  • ITranslator

Exceptions

  • AmbiguousServiceException
  • Overview
  • Namespace
  • Class
  • Tree
  • Other releases
  1: <?php
  2: 
  3: /**
  4:  * This file is part of the Nette Framework (https://nette.org)
  5:  *
  6:  * Copyright (c) 2004 David Grudl (http://davidgrudl.com)
  7:  *
  8:  * For the full copyright and license information, please view
  9:  * the file license.txt that was distributed with this source code.
 10:  */
 11: 
 12: namespace Nette;
 13: 
 14: use Nette;
 15: 
 16: 
 17: 
 18: /**
 19:  * Basic manipulation with images.
 20:  *
 21:  * <code>
 22:  * $image = Image::fromFile('nette.jpg');
 23:  * $image->resize(150, 100);
 24:  * $image->sharpen();
 25:  * $image->send();
 26:  * </code>
 27:  *
 28:  * @author     David Grudl
 29:  *
 30:  * @property-read int $width
 31:  * @property-read int $height
 32:  * @property-read resource $imageResource
 33:  */
 34: class Image extends Object
 35: {
 36:     /** {@link resize()} allows enlarging image (it only shrinks images by default) */
 37:     const ENLARGE = 1;
 38: 
 39:     /** {@link resize()} will ignore aspect ratio */
 40:     const STRETCH = 2;
 41: 
 42:     /** {@link resize()} fits in given area */
 43:     const FIT = 0;
 44: 
 45:     /** {@link resize()} fills (and even overflows) given area */
 46:     const FILL = 4;
 47: 
 48:     /**#@+ @int image types {@link send()} */
 49:     const JPEG = IMAGETYPE_JPEG;
 50:     const PNG = IMAGETYPE_PNG;
 51:     const GIF = IMAGETYPE_GIF;
 52:     /**#@-*/
 53: 
 54:     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;";
 55: 
 56:     /** @var bool */
 57:     public static $useImageMagick = FALSE;
 58: 
 59:     /** @var resource */
 60:     private $image;
 61: 
 62: 
 63: 
 64:     /**
 65:      * Returns RGB color.
 66:      * @param  int  red 0..255
 67:      * @param  int  green 0..255
 68:      * @param  int  blue 0..255
 69:      * @param  int  transparency 0..127
 70:      * @return array
 71:      */
 72:     public static function rgb($red, $green, $blue, $transparency = 0)
 73:     {
 74:         return array(
 75:             'red' => max(0, min(255, (int) $red)),
 76:             'green' => max(0, min(255, (int) $green)),
 77:             'blue' => max(0, min(255, (int) $blue)),
 78:             'alpha' => max(0, min(127, (int) $transparency)),
 79:         );
 80:     }
 81: 
 82: 
 83: 
 84:     /**
 85:      * Opens image from file.
 86:      * @param  string
 87:      * @param  mixed  detected image format
 88:      * @return Image
 89:      */
 90:     public static function fromFile($file, & $format = NULL)
 91:     {
 92:         if (!extension_loaded('gd')) {
 93:             throw new \Exception("PHP extension GD is not loaded.");
 94:         }
 95: 
 96:         $info = @getimagesize($file); // @ - files smaller than 12 bytes causes read error
 97:         if (self::$useImageMagick && (empty($info) || $info[0] * $info[1] > 9e5)) { // cca 1024x768
 98:             return new ImageMagick($file, $format);
 99:         }
100: 
101:         switch ($format = $info[2]) {
102:         case self::JPEG:
103:             return new static(imagecreatefromjpeg($file));
104: 
105:         case self::PNG:
106:             return new static(imagecreatefrompng($file));
107: 
108:         case self::GIF:
109:             return new static(imagecreatefromgif($file));
110: 
111:         default:
112:             if (self::$useImageMagick) {
113:                 return new ImageMagick($file, $format);
114:             }
115:             throw new \Exception("Unknown image type or file '$file' not found.");
116:         }
117:     }
118: 
119: 
120: 
121:     /**
122:      * Create a new image from the image stream in the string.
123:      * @param  string
124:      * @param  mixed  detected image format
125:      * @return Image
126:      */
127:     public static function fromString($s, & $format = NULL)
128:     {
129:         if (!extension_loaded('gd')) {
130:             throw new \Exception("PHP extension GD is not loaded.");
131:         }
132: 
133:         if (strncmp($s, "\xff\xd8", 2) === 0) {
134:             $format = self::JPEG;
135: 
136:         } elseif (strncmp($s, "\x89PNG", 4) === 0) {
137:             $format = self::PNG;
138: 
139:         } elseif (strncmp($s, "GIF", 3) === 0) {
140:             $format = self::GIF;
141: 
142:         } else {
143:             $format = NULL;
144:         }
145:         return new static(imagecreatefromstring($s));
146:     }
147: 
148: 
149: 
150:     /**
151:      * Creates blank image.
152:      * @param  int
153:      * @param  int
154:      * @param  array
155:      * @return Image
156:      */
157:     public static function fromBlank($width, $height, $color = NULL)
158:     {
159:         if (!extension_loaded('gd')) {
160:             throw new \Exception("PHP extension GD is not loaded.");
161:         }
162: 
163:         $width = (int) $width;
164:         $height = (int) $height;
165:         if ($width < 1 || $height < 1) {
166:             throw new \InvalidArgumentException('Image width and height must be greater than zero.');
167:         }
168: 
169:         $image = imagecreatetruecolor($width, $height);
170:         if (is_array($color)) {
171:             $color += array('alpha' => 0);
172:             $color = imagecolorallocatealpha($image, $color['red'], $color['green'], $color['blue'], $color['alpha']);
173:             imagealphablending($image, FALSE);
174:             imagefilledrectangle($image, 0, 0, $width - 1, $height - 1, $color);
175:             imagealphablending($image, TRUE);
176:         }
177:         return new static($image);
178:     }
179: 
180: 
181: 
182:     /**
183:      * Wraps GD image.
184:      * @param  resource
185:      */
186:     public function __construct($image)
187:     {
188:         $this->setImageResource($image);
189:     }
190: 
191: 
192: 
193:     /**
194:      * Returns image width.
195:      * @return int
196:      */
197:     public function getWidth()
198:     {
199:         return imagesx($this->image);
200:     }
201: 
202: 
203: 
204:     /**
205:      * Returns image height.
206:      * @return int
207:      */
208:     public function getHeight()
209:     {
210:         return imagesy($this->image);
211:     }
212: 
213: 
214: 
215:     /**
216:      * Sets image resource.
217:      * @param  resource
218:      * @return Image  provides a fluent interface
219:      */
220:     protected function setImageResource($image)
221:     {
222:         if (!is_resource($image) || get_resource_type($image) !== 'gd') {
223:             throw new \InvalidArgumentException('Image is not valid.');
224:         }
225:         $this->image = $image;
226:         return $this;
227:     }
228: 
229: 
230: 
231:     /**
232:      * Returns image GD resource.
233:      * @return resource
234:      */
235:     public function getImageResource()
236:     {
237:         return $this->image;
238:     }
239: 
240: 
241: 
242:     /**
243:      * Resizes image.
244:      * @param  mixed  width in pixels or percent
245:      * @param  mixed  height in pixels or percent
246:      * @param  int    flags
247:      * @return Image  provides a fluent interface
248:      */
249:     public function resize($width, $height, $flags = self::FIT)
250:     {
251:         list($newWidth, $newHeight) = self::calculateSize($this->getWidth(), $this->getHeight(), $width, $height, $flags);
252: 
253:         if ($newWidth !== $this->getWidth() || $newHeight !== $this->getHeight()) { // resize
254:             $newImage = self::fromBlank($newWidth, $newHeight, self::RGB(0, 0, 0, 127))->getImageResource();
255:             imagecopyresampled(
256:                 $newImage, $this->getImageResource(),
257:                 0, 0, 0, 0,
258:                 $newWidth, $newHeight, $this->getWidth(), $this->getHeight()
259:             );
260:             $this->image = $newImage;
261:         }
262: 
263:         if ($width < 0 || $height < 0) { // flip is processed in two steps for better quality
264:             $newImage = self::fromBlank($newWidth, $newHeight, self::RGB(0, 0, 0, 127))->getImageResource();
265:             imagecopyresampled(
266:                 $newImage, $this->getImageResource(),
267:                 0, 0, $width < 0 ? $newWidth - 1 : 0, $height < 0 ? $newHeight - 1 : 0,
268:                 $newWidth, $newHeight, $width < 0 ? -$newWidth : $newWidth, $height < 0 ? -$newHeight : $newHeight
269:             );
270:             $this->image = $newImage;
271:         }
272:         return $this;
273:     }
274: 
275: 
276: 
277:     /**
278:      * Calculates dimensions of resized image.
279:      * @param  mixed  source width
280:      * @param  mixed  source height
281:      * @param  mixed  width in pixels or percent
282:      * @param  mixed  height in pixels or percent
283:      * @param  int    flags
284:      * @return array
285:      */
286:     public static function calculateSize($srcWidth, $srcHeight, $newWidth, $newHeight, $flags = self::FIT)
287:     {
288:         if (substr($newWidth, -1) === '%') {
289:             $newWidth = round($srcWidth / 100 * abs($newWidth));
290:             $flags |= self::ENLARGE;
291:             $percents = TRUE;
292:         } else {
293:             $newWidth = (int) abs($newWidth);
294:         }
295: 
296:         if (substr($newHeight, -1) === '%') {
297:             $newHeight = round($srcHeight / 100 * abs($newHeight));
298:             $flags |= empty($percents) ? self::ENLARGE : self::STRETCH;
299:         } else {
300:             $newHeight = (int) abs($newHeight);
301:         }
302: 
303:         if ($flags & self::STRETCH) { // non-proportional
304:             if (empty($newWidth) || empty($newHeight)) {
305:                 throw new \InvalidArgumentException('For stretching must be both width and height specified.');
306:             }
307: 
308:             if (($flags & self::ENLARGE) === 0) {
309:                 $newWidth = round($srcWidth * min(1, $newWidth / $srcWidth));
310:                 $newHeight = round($srcHeight * min(1, $newHeight / $srcHeight));
311:             }
312: 
313:         } else {  // proportional
314:             if (empty($newWidth) && empty($newHeight)) {
315:                 throw new \InvalidArgumentException('At least width or height must be specified.');
316:             }
317: 
318:             $scale = array();
319:             if ($newWidth > 0) { // fit width
320:                 $scale[] = $newWidth / $srcWidth;
321:             }
322: 
323:             if ($newHeight > 0) { // fit height
324:                 $scale[] = $newHeight / $srcHeight;
325:             }
326: 
327:             if ($flags & self::FILL) {
328:                 $scale = array(max($scale));
329:             }
330: 
331:             if (($flags & self::ENLARGE) === 0) {
332:                 $scale[] = 1;
333:             }
334: 
335:             $scale = min($scale);
336:             $newWidth = round($srcWidth * $scale);
337:             $newHeight = round($srcHeight * $scale);
338:         }
339: 
340:         return array(max((int) $newWidth, 1), max((int) $newHeight, 1));
341:     }
342: 
343: 
344: 
345:     /**
346:      * Crops image.
347:      * @param  mixed  x-offset in pixels or percent
348:      * @param  mixed  y-offset in pixels or percent
349:      * @param  mixed  width in pixels or percent
350:      * @param  mixed  height in pixels or percent
351:      * @return Image  provides a fluent interface
352:      */
353:     public function crop($left, $top, $width, $height)
354:     {
355:         list($left, $top, $width, $height) = self::calculateCutout($this->getWidth(), $this->getHeight(), $left, $top, $width, $height);
356:         $newImage = self::fromBlank($width, $height, self::RGB(0, 0, 0, 127))->getImageResource();
357:         imagecopy($newImage, $this->getImageResource(), 0, 0, $left, $top, $width, $height);
358:         $this->image = $newImage;
359:         return $this;
360:     }
361: 
362: 
363: 
364:     /**
365:      * Calculates dimensions of cutout in image.
366:      * @param  mixed  source width
367:      * @param  mixed  source height
368:      * @param  mixed  x-offset in pixels or percent
369:      * @param  mixed  y-offset in pixels or percent
370:      * @param  mixed  width in pixels or percent
371:      * @param  mixed  height in pixels or percent
372:      * @return array
373:      */
374:     public static function calculateCutout($srcWidth, $srcHeight, $left, $top, $newWidth, $newHeight)
375:     {
376:         if (substr($newWidth, -1) === '%') {
377:             $newWidth = round($srcWidth / 100 * $newWidth);
378:         }
379:         if (substr($newHeight, -1) === '%') {
380:             $newHeight = round($srcHeight / 100 * $newHeight);
381:         }
382:         if (substr($left, -1) === '%') {
383:             $left = round(($srcWidth - $newWidth) / 100 * $left);
384:         }
385:         if (substr($top, -1) === '%') {
386:             $top = round(($srcHeight - $newHeight) / 100 * $top);
387:         }
388:         if ($left < 0) {
389:             $newWidth += $left; $left = 0;
390:         }
391:         if ($top < 0) {
392:             $newHeight += $top; $top = 0;
393:         }
394:         $newWidth = min((int) $newWidth, $srcWidth - $left);
395:         $newHeight = min((int) $newHeight, $srcHeight - $top);
396:         return array($left, $top, $newWidth, $newHeight);
397:     }
398: 
399: 
400: 
401:     /**
402:      * Sharpen image.
403:      * @return Image  provides a fluent interface
404:      */
405:     public function sharpen()
406:     {
407:         imageconvolution($this->getImageResource(), array( // my magic numbers ;)
408:             array( -1, -1, -1 ),
409:             array( -1, 24, -1 ),
410:             array( -1, -1, -1 ),
411:         ), 16, 0);
412:         return $this;
413:     }
414: 
415: 
416: 
417:     /**
418:      * Puts another image into this image.
419:      * @param  Image
420:      * @param  mixed  x-coordinate in pixels or percent
421:      * @param  mixed  y-coordinate in pixels or percent
422:      * @param  int  opacity 0..100
423:      * @return Image  provides a fluent interface
424:      */
425:     public function place(Image $image, $left = 0, $top = 0, $opacity = 100)
426:     {
427:         $opacity = max(0, min(100, (int) $opacity));
428: 
429:         if (substr($left, -1) === '%') {
430:             $left = round(($this->getWidth() - $image->getWidth()) / 100 * $left);
431:         }
432: 
433:         if (substr($top, -1) === '%') {
434:             $top = round(($this->getHeight() - $image->getHeight()) / 100 * $top);
435:         }
436: 
437:         if ($opacity === 100) {
438:             imagecopy($this->getImageResource(), $image->getImageResource(), $left, $top, 0, 0, $image->getWidth(), $image->getHeight());
439: 
440:         } elseif ($opacity <> 0) {
441:             imagecopymerge($this->getImageResource(), $image->getImageResource(), $left, $top, 0, 0, $image->getWidth(), $image->getHeight(), $opacity);
442:         }
443:         return $this;
444:     }
445: 
446: 
447: 
448:     /**
449:      * Saves image to the file.
450:      * @param  string  filename
451:      * @param  int  quality 0..100 (for JPEG and PNG)
452:      * @param  int  optional image type
453:      * @return bool TRUE on success or FALSE on failure.
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); // PHP bug #44591
482: 
483:         default:
484:             throw new \Exception("Unsupported image type.");
485:         }
486:     }
487: 
488: 
489: 
490:     /**
491:      * Outputs image to string.
492:      * @param  int  image type
493:      * @param  int  quality 0..100 (for JPEG and PNG)
494:      * @return string
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:      * Outputs image to string.
507:      * @return string
508:      */
509:     public function __toString()
510:     {
511:         try {
512:             return $this->toString();
513: 
514:         } catch (\Exception $e) {
515:             Debug::toStringException($e);
516:         }
517:     }
518: 
519: 
520: 
521:     /**
522:      * Outputs image to browser.
523:      * @param  int  image type
524:      * @param  int  quality 0..100 (for JPEG and PNG)
525:      * @return bool TRUE on success or FALSE on failure.
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 \Exception("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:      * Call to undefined method.
540:      * @param  string  method name
541:      * @param  array   arguments
542:      * @return mixed
543:      * @throws \MemberAccessException
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'])) { // rgb
554:                     $args[$key] = imagecolorallocatealpha($this->getImageResource(), $value['red'], $value['green'], $value['blue'], $value['alpha']);
555:                 }
556:             }
557:             array_unshift($args, $this->getImageResource());
558: 
559:             $res = call_user_func_array($function, $args);
560:             return is_resource($res) ? new static($res) : $res;
561:         }
562: 
563:         return parent::__call($name, $args);
564:     }
565: 
566: }
567: 
Nette Framework 0.9.7 API documentation generated by ApiGen 2.3.0