Namespaces

  • Nette
    • Application
      • Diagnostics
      • Responses
      • Routers
      • UI
    • Caching
      • Storages
    • ComponentModel
    • Database
      • Diagnostics
      • Drivers
      • Reflection
      • Table
    • DI
      • Config
        • Adapters
      • Diagnostics
      • Extensions
    • Diagnostics
    • Forms
      • Controls
      • Rendering
    • Http
      • Diagnostics
    • Iterators
    • Latte
      • Macros
    • Loaders
    • Localization
    • Mail
    • PhpGenerator
    • Reflection
    • Security
      • Diagnostics
    • Templating
    • Utils
  • NetteModule
  • none

Classes

  • ArrayHash
  • ArrayList
  • Callback
  • Configurator
  • DateTime
  • Environment
  • Framework
  • FreezableObject
  • Image
  • Object
  • ObjectMixin

Interfaces

  • IFreezable

Exceptions

  • ArgumentOutOfRangeException
  • DeprecatedException
  • DirectoryNotFoundException
  • FatalErrorException
  • FileNotFoundException
  • InvalidArgumentException
  • InvalidStateException
  • IOException
  • MemberAccessException
  • NotImplementedException
  • NotSupportedException
  • OutOfRangeException
  • StaticClassException
  • UnexpectedValueException
  • UnknownImageFileException
  • Overview
  • Namespace
  • Class
  • Tree
  • Deprecated
  • Other releases
  • Nette homepage
  1: <?php
  2: 
  3: /**
  4:  * This file is part of the Nette Framework (https://nette.org)
  5:  * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
  6:  */
  7: 
  8: namespace Nette;
  9: 
 10: use Nette;
 11: 
 12: 
 13: /**
 14:  * Basic manipulation with images.
 15:  *
 16:  * <code>
 17:  * $image = Image::fromFile('nette.jpg');
 18:  * $image->resize(150, 100);
 19:  * $image->sharpen();
 20:  * $image->send();
 21:  * </code>
 22:  *
 23:  * @author     David Grudl
 24:  *
 25:  * @method void alphaBlending(bool $on)
 26:  * @method void antialias(bool $on)
 27:  * @method void arc($x, $y, $w, $h, $start, $end, $color)
 28:  * @method void char(int $font, $x, $y, string $char, $color)
 29:  * @method void charUp(int $font, $x, $y, string $char, $color)
 30:  * @method int colorAllocate($red, $green, $blue)
 31:  * @method int colorAllocateAlpha($red, $green, $blue, $alpha)
 32:  * @method int colorAt($x, $y)
 33:  * @method int colorClosest($red, $green, $blue)
 34:  * @method int colorClosestAlpha($red, $green, $blue, $alpha)
 35:  * @method int colorClosestHWB($red, $green, $blue)
 36:  * @method void colorDeallocate($color)
 37:  * @method int colorExact($red, $green, $blue)
 38:  * @method int colorExactAlpha($red, $green, $blue, $alpha)
 39:  * @method void colorMatch(Image $image2)
 40:  * @method int colorResolve($red, $green, $blue)
 41:  * @method int colorResolveAlpha($red, $green, $blue, $alpha)
 42:  * @method void colorSet($index, $red, $green, $blue)
 43:  * @method array colorsForIndex($index)
 44:  * @method int colorsTotal()
 45:  * @method int colorTransparent($color = NULL)
 46:  * @method void convolution(array $matrix, float $div, float $offset)
 47:  * @method void copy(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH)
 48:  * @method void copyMerge(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH, $opacity)
 49:  * @method void copyMergeGray(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH, $opacity)
 50:  * @method void copyResampled(Image $src, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH)
 51:  * @method void copyResized(Image $src, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH)
 52:  * @method void dashedLine($x1, $y1, $x2, $y2, $color)
 53:  * @method void ellipse($cx, $cy, $w, $h, $color)
 54:  * @method void fill($x, $y, $color)
 55:  * @method void filledArc($cx, $cy, $w, $h, $s, $e, $color, $style)
 56:  * @method void filledEllipse($cx, $cy, $w, $h, $color)
 57:  * @method void filledPolygon(array $points, $numPoints, $color)
 58:  * @method void filledRectangle($x1, $y1, $x2, $y2, $color)
 59:  * @method void fillToBorder($x, $y, $border, $color)
 60:  * @method void filter($filtertype)
 61:  * @method array ftText($size, $angle, $x, $y, $col, string $fontFile, string $text, array $extrainfo = NULL)
 62:  * @method void gammaCorrect(float $inputgamma, float $outputgamma)
 63:  * @method int interlace($interlace = NULL)
 64:  * @method bool isTrueColor()
 65:  * @method void layerEffect($effect)
 66:  * @method void line($x1, $y1, $x2, $y2, $color)
 67:  * @method void paletteCopy(Image $source)
 68:  * @method void polygon(array $points, $numPoints, $color)
 69:  * @method array psText(string $text, $font, $size, $color, $backgroundColor, $x, $y, $space = NULL, $tightness = NULL, float $angle = NULL, $antialiasSteps = NULL)
 70:  * @method void rectangle($x1, $y1, $x2, $y2, $col)
 71:  * @method Image rotate(float $angle, $backgroundColor)
 72:  * @method void saveAlpha(bool $saveflag)
 73:  * @method void setBrush(Image $brush)
 74:  * @method void setPixel($x, $y, $color)
 75:  * @method void setStyle(array $style)
 76:  * @method void setThickness($thickness)
 77:  * @method void setTile(Image $tile)
 78:  * @method void string($font, $x, $y, string $s, $col)
 79:  * @method void stringUp($font, $x, $y, string $s, $col)
 80:  * @method void trueColorToPalette(bool $dither, $ncolors)
 81:  * @method array ttfText($size, $angle, $x, $y, $color, string $fontfile, string $text)
 82:  * @property-read int $width
 83:  * @property-read int $height
 84:  * @property-read resource $imageResource
 85:  */
 86: class Image extends Object
 87: {
 88:     /** {@link resize()} only shrinks images */
 89:     const SHRINK_ONLY = 1;
 90: 
 91:     /** {@link resize()} will ignore aspect ratio */
 92:     const STRETCH = 2;
 93: 
 94:     /** {@link resize()} fits in given area so its dimensions are less than or equal to the required dimensions */
 95:     const FIT = 0;
 96: 
 97:     /** {@link resize()} fills given area so its dimensions are greater than or equal to the required dimensions */
 98:     const FILL = 4;
 99: 
100:     /** {@link resize()} fills given area exactly */
101:     const EXACT = 8;
102: 
103:     /** @int image types {@link send()} */
104:     const JPEG = IMAGETYPE_JPEG,
105:         PNG = IMAGETYPE_PNG,
106:         GIF = IMAGETYPE_GIF;
107: 
108:     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;";
109: 
110:     /** @deprecated */
111:     const ENLARGE = 0;
112: 
113:     /** @var resource */
114:     private $image;
115: 
116: 
117:     /**
118:      * Returns RGB color.
119:      * @param  int  red 0..255
120:      * @param  int  green 0..255
121:      * @param  int  blue 0..255
122:      * @param  int  transparency 0..127
123:      * @return array
124:      */
125:     public static function rgb($red, $green, $blue, $transparency = 0)
126:     {
127:         return array(
128:             'red' => max(0, min(255, (int) $red)),
129:             'green' => max(0, min(255, (int) $green)),
130:             'blue' => max(0, min(255, (int) $blue)),
131:             'alpha' => max(0, min(127, (int) $transparency)),
132:         );
133:     }
134: 
135: 
136:     /**
137:      * Opens image from file.
138:      * @param  string
139:      * @param  mixed  detected image format
140:      * @throws Nette\NotSupportedException if gd extension is not loaded
141:      * @throws UnknownImageFileException if file not found or file type is not known
142:      * @return Image
143:      */
144:     public static function fromFile($file, & $format = NULL)
145:     {
146:         if (!extension_loaded('gd')) {
147:             throw new NotSupportedException('PHP extension GD is not loaded.');
148:         }
149: 
150:         $info = @getimagesize($file); // @ - files smaller than 12 bytes causes read error
151: 
152:         switch ($format = $info[2]) {
153:             case self::JPEG:
154:                 return new static(imagecreatefromjpeg($file));
155: 
156:             case self::PNG:
157:                 return new static(imagecreatefrompng($file));
158: 
159:             case self::GIF:
160:                 return new static(imagecreatefromgif($file));
161: 
162:             default:
163:                 throw new UnknownImageFileException("Unknown image type or file '$file' not found.");
164:         }
165:     }
166: 
167: 
168:     /**
169:      * Get format from the image stream in the string.
170:      * @param  string
171:      * @return mixed  detected image format
172:      */
173:     public static function getFormatFromString($s)
174:     {
175:         $types = array('image/jpeg' => self::JPEG, 'image/gif' => self::GIF, 'image/png' => self::PNG);
176:         $type = Utils\MimeTypeDetector::fromString($s);
177:         return isset($types[$type]) ? $types[$type] : NULL;
178:     }
179: 
180: 
181:     /**
182:      * Create a new image from the image stream in the string.
183:      * @param  string
184:      * @param  mixed  detected image format
185:      * @return Image
186:      */
187:     public static function fromString($s, & $format = NULL)
188:     {
189:         if (!extension_loaded('gd')) {
190:             throw new NotSupportedException('PHP extension GD is not loaded.');
191:         }
192: 
193:         $format = static::getFormatFromString($s);
194: 
195:         return new static(imagecreatefromstring($s));
196:     }
197: 
198: 
199:     /**
200:      * Creates blank image.
201:      * @param  int
202:      * @param  int
203:      * @param  array
204:      * @return Image
205:      */
206:     public static function fromBlank($width, $height, $color = NULL)
207:     {
208:         if (!extension_loaded('gd')) {
209:             throw new NotSupportedException('PHP extension GD is not loaded.');
210:         }
211: 
212:         $width = (int) $width;
213:         $height = (int) $height;
214:         if ($width < 1 || $height < 1) {
215:             throw new InvalidArgumentException('Image width and height must be greater than zero.');
216:         }
217: 
218:         $image = imagecreatetruecolor($width, $height);
219:         if (is_array($color)) {
220:             $color += array('alpha' => 0);
221:             $color = imagecolorresolvealpha($image, $color['red'], $color['green'], $color['blue'], $color['alpha']);
222:             imagealphablending($image, FALSE);
223:             imagefilledrectangle($image, 0, 0, $width - 1, $height - 1, $color);
224:             imagealphablending($image, TRUE);
225:         }
226:         return new static($image);
227:     }
228: 
229: 
230:     /**
231:      * Wraps GD image.
232:      * @param  resource
233:      */
234:     public function __construct($image)
235:     {
236:         $this->setImageResource($image);
237:         imagesavealpha($image, TRUE);
238:     }
239: 
240: 
241:     /**
242:      * Returns image width.
243:      * @return int
244:      */
245:     public function getWidth()
246:     {
247:         return imagesx($this->image);
248:     }
249: 
250: 
251:     /**
252:      * Returns image height.
253:      * @return int
254:      */
255:     public function getHeight()
256:     {
257:         return imagesy($this->image);
258:     }
259: 
260: 
261:     /**
262:      * Sets image resource.
263:      * @param  resource
264:      * @return self
265:      */
266:     protected function setImageResource($image)
267:     {
268:         if (!is_resource($image) || get_resource_type($image) !== 'gd') {
269:             throw new InvalidArgumentException('Image is not valid.');
270:         }
271:         $this->image = $image;
272:         return $this;
273:     }
274: 
275: 
276:     /**
277:      * Returns image GD resource.
278:      * @return resource
279:      */
280:     public function getImageResource()
281:     {
282:         return $this->image;
283:     }
284: 
285: 
286:     /**
287:      * Resizes image.
288:      * @param  mixed  width in pixels or percent
289:      * @param  mixed  height in pixels or percent
290:      * @param  int    flags
291:      * @return self
292:      */
293:     public function resize($width, $height, $flags = self::FIT)
294:     {
295:         if ($flags & self::EXACT) {
296:             return $this->resize($width, $height, self::FILL)->crop('50%', '50%', $width, $height);
297:         }
298: 
299:         list($newWidth, $newHeight) = static::calculateSize($this->getWidth(), $this->getHeight(), $width, $height, $flags);
300: 
301:         if ($newWidth !== $this->getWidth() || $newHeight !== $this->getHeight()) { // resize
302:             $newImage = static::fromBlank($newWidth, $newHeight, self::RGB(0, 0, 0, 127))->getImageResource();
303:             imagecopyresampled(
304:                 $newImage, $this->getImageResource(),
305:                 0, 0, 0, 0,
306:                 $newWidth, $newHeight, $this->getWidth(), $this->getHeight()
307:             );
308:             $this->image = $newImage;
309:         }
310: 
311:         if ($width < 0 || $height < 0) { // flip is processed in two steps for better quality
312:             $newImage = static::fromBlank($newWidth, $newHeight, self::RGB(0, 0, 0, 127))->getImageResource();
313:             imagecopyresampled(
314:                 $newImage, $this->getImageResource(),
315:                 0, 0, $width < 0 ? $newWidth - 1 : 0, $height < 0 ? $newHeight - 1 : 0,
316:                 $newWidth, $newHeight, $width < 0 ? -$newWidth : $newWidth, $height < 0 ? -$newHeight : $newHeight
317:             );
318:             $this->image = $newImage;
319:         }
320:         return $this;
321:     }
322: 
323: 
324:     /**
325:      * Calculates dimensions of resized image.
326:      * @param  mixed  source width
327:      * @param  mixed  source height
328:      * @param  mixed  width in pixels or percent
329:      * @param  mixed  height in pixels or percent
330:      * @param  int    flags
331:      * @return array
332:      */
333:     public static function calculateSize($srcWidth, $srcHeight, $newWidth, $newHeight, $flags = self::FIT)
334:     {
335:         if (substr($newWidth, -1) === '%') {
336:             $newWidth = round($srcWidth / 100 * abs($newWidth));
337:             $percents = TRUE;
338:         } else {
339:             $newWidth = (int) abs($newWidth);
340:         }
341: 
342:         if (substr($newHeight, -1) === '%') {
343:             $newHeight = round($srcHeight / 100 * abs($newHeight));
344:             $flags |= empty($percents) ? 0 : self::STRETCH;
345:         } else {
346:             $newHeight = (int) abs($newHeight);
347:         }
348: 
349:         if ($flags & self::STRETCH) { // non-proportional
350:             if (empty($newWidth) || empty($newHeight)) {
351:                 throw new InvalidArgumentException('For stretching must be both width and height specified.');
352:             }
353: 
354:             if ($flags & self::SHRINK_ONLY) {
355:                 $newWidth = round($srcWidth * min(1, $newWidth / $srcWidth));
356:                 $newHeight = round($srcHeight * min(1, $newHeight / $srcHeight));
357:             }
358: 
359:         } else {  // proportional
360:             if (empty($newWidth) && empty($newHeight)) {
361:                 throw new InvalidArgumentException('At least width or height must be specified.');
362:             }
363: 
364:             $scale = array();
365:             if ($newWidth > 0) { // fit width
366:                 $scale[] = $newWidth / $srcWidth;
367:             }
368: 
369:             if ($newHeight > 0) { // fit height
370:                 $scale[] = $newHeight / $srcHeight;
371:             }
372: 
373:             if ($flags & self::FILL) {
374:                 $scale = array(max($scale));
375:             }
376: 
377:             if ($flags & self::SHRINK_ONLY) {
378:                 $scale[] = 1;
379:             }
380: 
381:             $scale = min($scale);
382:             $newWidth = round($srcWidth * $scale);
383:             $newHeight = round($srcHeight * $scale);
384:         }
385: 
386:         return array(max((int) $newWidth, 1), max((int) $newHeight, 1));
387:     }
388: 
389: 
390:     /**
391:      * Crops image.
392:      * @param  mixed  x-offset in pixels or percent
393:      * @param  mixed  y-offset in pixels or percent
394:      * @param  mixed  width in pixels or percent
395:      * @param  mixed  height in pixels or percent
396:      * @return self
397:      */
398:     public function crop($left, $top, $width, $height)
399:     {
400:         list($left, $top, $width, $height) = static::calculateCutout($this->getWidth(), $this->getHeight(), $left, $top, $width, $height);
401:         $newImage = static::fromBlank($width, $height, self::RGB(0, 0, 0, 127))->getImageResource();
402:         imagecopy($newImage, $this->getImageResource(), 0, 0, $left, $top, $width, $height);
403:         $this->image = $newImage;
404:         return $this;
405:     }
406: 
407: 
408:     /**
409:      * Calculates dimensions of cutout in image.
410:      * @param  mixed  source width
411:      * @param  mixed  source height
412:      * @param  mixed  x-offset in pixels or percent
413:      * @param  mixed  y-offset in pixels or percent
414:      * @param  mixed  width in pixels or percent
415:      * @param  mixed  height in pixels or percent
416:      * @return array
417:      */
418:     public static function calculateCutout($srcWidth, $srcHeight, $left, $top, $newWidth, $newHeight)
419:     {
420:         if (substr($newWidth, -1) === '%') {
421:             $newWidth = round($srcWidth / 100 * $newWidth);
422:         }
423:         if (substr($newHeight, -1) === '%') {
424:             $newHeight = round($srcHeight / 100 * $newHeight);
425:         }
426:         if (substr($left, -1) === '%') {
427:             $left = round(($srcWidth - $newWidth) / 100 * $left);
428:         }
429:         if (substr($top, -1) === '%') {
430:             $top = round(($srcHeight - $newHeight) / 100 * $top);
431:         }
432:         if ($left < 0) {
433:             $newWidth += $left; $left = 0;
434:         }
435:         if ($top < 0) {
436:             $newHeight += $top; $top = 0;
437:         }
438:         $newWidth = min((int) $newWidth, $srcWidth - $left);
439:         $newHeight = min((int) $newHeight, $srcHeight - $top);
440:         return array($left, $top, $newWidth, $newHeight);
441:     }
442: 
443: 
444:     /**
445:      * Sharpen image.
446:      * @return self
447:      */
448:     public function sharpen()
449:     {
450:         imageconvolution($this->getImageResource(), array( // my magic numbers ;)
451:             array(-1, -1, -1),
452:             array(-1, 24, -1),
453:             array(-1, -1, -1),
454:         ), 16, 0);
455:         return $this;
456:     }
457: 
458: 
459:     /**
460:      * Puts another image into this image.
461:      * @param  Image
462:      * @param  mixed  x-coordinate in pixels or percent
463:      * @param  mixed  y-coordinate in pixels or percent
464:      * @param  int  opacity 0..100
465:      * @return self
466:      */
467:     public function place(Image $image, $left = 0, $top = 0, $opacity = 100)
468:     {
469:         $opacity = max(0, min(100, (int) $opacity));
470: 
471:         if (substr($left, -1) === '%') {
472:             $left = round(($this->getWidth() - $image->getWidth()) / 100 * $left);
473:         }
474: 
475:         if (substr($top, -1) === '%') {
476:             $top = round(($this->getHeight() - $image->getHeight()) / 100 * $top);
477:         }
478: 
479:         if ($opacity === 100) {
480:             imagecopy(
481:                 $this->getImageResource(), $image->getImageResource(),
482:                 $left, $top, 0, 0, $image->getWidth(), $image->getHeight()
483:             );
484: 
485:         } elseif ($opacity <> 0) {
486:             imagecopymerge(
487:                 $this->getImageResource(), $image->getImageResource(),
488:                 $left, $top, 0, 0, $image->getWidth(), $image->getHeight(),
489:                 $opacity
490:             );
491:         }
492:         return $this;
493:     }
494: 
495: 
496:     /**
497:      * Saves image to the file.
498:      * @param  string  filename
499:      * @param  int  quality 0..100 (for JPEG and PNG)
500:      * @param  int  optional image type
501:      * @return bool TRUE on success or FALSE on failure.
502:      */
503:     public function save($file = NULL, $quality = NULL, $type = NULL)
504:     {
505:         if ($type === NULL) {
506:             switch (strtolower(pathinfo($file, PATHINFO_EXTENSION))) {
507:                 case 'jpg':
508:                 case 'jpeg':
509:                     $type = self::JPEG;
510:                     break;
511:                 case 'png':
512:                     $type = self::PNG;
513:                     break;
514:                 case 'gif':
515:                     $type = self::GIF;
516:             }
517:         }
518: 
519:         switch ($type) {
520:             case self::JPEG:
521:                 $quality = $quality === NULL ? 85 : max(0, min(100, (int) $quality));
522:                 return imagejpeg($this->getImageResource(), $file, $quality);
523: 
524:             case self::PNG:
525:                 $quality = $quality === NULL ? 9 : max(0, min(9, (int) $quality));
526:                 return imagepng($this->getImageResource(), $file, $quality);
527: 
528:             case self::GIF:
529:                 return imagegif($this->getImageResource(), $file);
530: 
531:             default:
532:                 throw new InvalidArgumentException('Unsupported image type.');
533:         }
534:     }
535: 
536: 
537:     /**
538:      * Outputs image to string.
539:      * @param  int  image type
540:      * @param  int  quality 0..100 (for JPEG and PNG)
541:      * @return string
542:      */
543:     public function toString($type = self::JPEG, $quality = NULL)
544:     {
545:         ob_start();
546:         $this->save(NULL, $quality, $type);
547:         return ob_get_clean();
548:     }
549: 
550: 
551:     /**
552:      * Outputs image to string.
553:      * @return string
554:      */
555:     public function __toString()
556:     {
557:         try {
558:             return $this->toString();
559:         } catch (\Throwable $e) {
560:         } catch (\Exception $e) {
561:         }
562:         if (isset($e)) {
563:             if (func_num_args()) {
564:                 throw $e;
565:             }
566:             trigger_error("Exception in " . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR);
567:         }
568:     }
569: 
570: 
571:     /**
572:      * Outputs image to browser.
573:      * @param  int  image type
574:      * @param  int  quality 0..100 (for JPEG and PNG)
575:      * @return bool TRUE on success or FALSE on failure.
576:      */
577:     public function send($type = self::JPEG, $quality = NULL)
578:     {
579:         if ($type !== self::GIF && $type !== self::PNG && $type !== self::JPEG) {
580:             throw new InvalidArgumentException('Unsupported image type.');
581:         }
582:         header('Content-Type: ' . image_type_to_mime_type($type));
583:         return $this->save(NULL, $quality, $type);
584:     }
585: 
586: 
587:     /**
588:      * Call to undefined method.
589:      *
590:      * @param  string  method name
591:      * @param  array   arguments
592:      * @return mixed
593:      * @throws MemberAccessException
594:      */
595:     public function __call($name, $args)
596:     {
597:         $function = 'image' . $name;
598:         if (function_exists($function)) {
599:             foreach ($args as $key => $value) {
600:                 if ($value instanceof self) {
601:                     $args[$key] = $value->getImageResource();
602: 
603:                 } elseif (is_array($value) && isset($value['red'])) { // rgb
604:                     $args[$key] = imagecolorallocatealpha(
605:                         $this->getImageResource(),
606:                         $value['red'], $value['green'], $value['blue'], $value['alpha']
607:                     ) ?: imagecolorresolvealpha(
608:                         $this->image,
609:                         $value['red'], $value['green'], $value['blue'], $value['alpha']
610:                     );
611:                 }
612:             }
613:             array_unshift($args, $this->getImageResource());
614: 
615:             $res = call_user_func_array($function, $args);
616:             return is_resource($res) && get_resource_type($res) === 'gd' ? $this->setImageResource($res) : $res;
617:         }
618: 
619:         return parent::__call($name, $args);
620:     }
621: 
622: 
623:     public function __clone()
624:     {
625:         ob_start();
626:         imagegd2($this->getImageResource());
627:         $this->setImageResource(imagecreatefromstring(ob_get_clean()));
628:     }
629: 
630: }
631: 
632: 
633: /**
634:  * The exception that indicates invalid image file.
635:  */
636: class UnknownImageFileException extends \Exception
637: {
638: }
639: 
Nette 2.1 API documentation generated by ApiGen 2.8.0