<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * Image_Canvas
 *
 * Class for handling output in GD compatible format.
 * 
 * Supported formats are PNG, JPEG, GIF and WBMP.
 *
 * Requires PHP extension GD
 *
 * PHP versions 4 and 5
 *
 * LICENSE: This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or (at your
 * option) any later version. This library is distributed in the hope that it
 * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
 * General Public License for more details. You should have received a copy of
 * the GNU Lesser General Public License along with this library; if not, see
 * <http://www.gnu.org/licenses/>
 *
 * @category  Images
 * @package   Image_Canvas
 * @author    Jesper Veggerby <pear.nosey@veggerby.dk>
 * @author    Stefan Neufeind <pear.neufeind@speedpartner.de>
 * @copyright 2003-2009 The PHP Group
 * @license   http://www.gnu.org/copyleft/lesser.html  LGPL License 2.1
 * @version   SVN: $Id$
 * @link      http://pear.php.net/package/Image_Canvas
 */

/**
 * Include file Image/Canvas.php
 */
require_once 'Image/Canvas/WithMap.php';

/**
 * Include file Image/Canvas/Color.php
 */
require_once 'Image/Canvas/Color.php';

/**
 * Canvas class to output using PHP GD support.
 * 
 * @category  Images
 * @package   Image_Canvas
 * @author    Jesper Veggerby <pear.nosey@veggerby.dk>
 * @author    Stefan Neufeind <pear.neufeind@speedpartner.de>
 * @copyright 2003-2009 The PHP Group
 * @license   http://www.gnu.org/copyleft/lesser.html  LGPL License 2.1
 * @version   Release: @package_version@
 * @link      http://pear.php.net/package/Image_Canvas
 * @abstract
 */
class Image_Canvas_GD extends Image_Canvas_WithMap
{

    /**
     * The canvas of the graph
     * @var resource
     * @access private
     */
    var $_canvas;

    /**
     * The canvas to use for tiled filling
     * @var resource
     * @access private
     */
    var $_tileImage = null;

    /**
     * Is version GD2 installed?
     * @var bool
     * @access private
     */
    var $_gd2 = true;

    /**
     * Do we need a factor to convert size in pixels to points?
     * GD2 uses "pt" for font-sizes
     * @var float
     * @access private
     */
    var $_pxToPtFactor = 1;

    /**
     * Antialiasing?
     * 
     * Possible values 'off', 'driver' and 'native'
     * 
     * @var string
     * @access private
     */
    var $_antialias = 'off';
    
    var $_alpha = false;
        
    var $_clipping = array();

    /**
     * Create the GD canvas.
     *
     * Parameters available:
     *
     * 'width' The width of the graph on the canvas
     *
     * 'height' The height of the graph on the canvas
     *
     * 'left' The left offset of the graph on the canvas
     *
     * 'top' The top offset of the graph on the canvas
     * 
     * 'antialias' = 'native' enables native GD antialiasing - this
     * method has no severe impact on performance (approx +5%). Requires PHP
     * 4.3.2 (with bundled GD2)
     * 
     * 'antialias' = {true|'driver'} Image_Graph implemented method. This method
     * has a severe impact on performance, drawing an antialiased line this
     * way is about XX times slower, with an overall performance impact of
     * about +40%. The justification for this method is that if native support
     * is not available this can be used, it is also a future feature that this
     * method for antialiasing will support line styles.
     * 
     * Use antialiased for best results with a line/area chart having just a few
     * datapoints. Native antialiasing does not provide a good appearance with
     * short lines, as for example with smoothed charts. Antialiasing does not
     * (currently) work with linestyles, neither native nor driver method!
     * 
     * 'noalpha' = true If alpha blending is to be disabled
     *
     * 'filename' An image to open, on which the graph is created on
     *
     * 'gd' A GD resource to add the image to, use this option to continue
     * working on an already existing GD resource. Make sure this is passed 'by-
     * reference' (using &amp;)
     * 
     * 'usemap' Initialize an image map
     *
     * 'gd' and 'filename' are mutually exclusive with 'gd' as preference
     *
     * 'width' and 'height' are required unless 'filename' or 'gd' are
     * specified, in which case the width and height are taken as the actual
     * image width/height. If the latter is the case and 'left' and/or 'top' was
     * also specified, the actual 'width'/'height' are altered so that the graph
     * fits inside the canvas (i.e 'height' = actual height - top, etc.)
     *
     * @param array $param Parameter array
     */
    function __construct($param)
    {
        include_once 'Image/Canvas/Color.php';

        parent::__construct($param);
        
        $this->_gd2 = ($this->_version() == 2);
        $this->_pxToPtFactor = ($this->_gd2 ? (72/96) : 1);
        $this->_font = array('font' => 1, 'color' => 'black');

        if ((isset($param['gd'])) && (is_resource($param['gd']))) {
            $this->_canvas =& $param['gd'];
        } elseif (isset($param['filename'])) {
            $this->_canvas =& $this->_getGD($param['filename']);
        } else {
            if ($this->_gd2) {
                $this->_canvas = ImageCreateTrueColor(
                    $this->_width,
                    $this->_height
                );
                if ((!isset($param['noalpha'])) || ($param['noalpha'] !== true)) {
                    ImageAlphaBlending($this->_canvas, true);
                    $this->_alpha = true;
                }
            } else {
                $this->_canvas = ImageCreate($this->_width, $this->_height);
            }
        }
        
        if (isset($param['antialias'])) {
            $this->_antialias = $param['antialias'];
        }
        
        if ($this->_antialias === true) {
            $this->_antialias = 'driver';
        }
        
        if (($this->_gd2) && ($this->_antialias === 'native') && (function_exists('ImageAntialias'))) {
            ImageAntialias($this->_canvas, true);
        }
    }

    /**
     * Get an GD image resource from a file
     *
     * @param string $filename File to fetch image from
     *
     * @return mixed The GD image resource
     * @access private
     */
    function &_getGD($filename)
    {
        $info = getimagesize($filename);
        
        $result = null;
        switch($info[2]) {
        case IMG_PNG:
            $result =& ImageCreateFromPNG($filename);
            break;
            
        case IMG_JPG:
            $result =& ImageCreateFromJPEG($filename);
            break;
            
        case IMG_GIF:
            $result =& ImageCreateFromGIF($filename);
            break;
        }
        return $result;
    }

    /**
     * Get the color index for the RGB color
     *
     * @param int $color The color
     *
     * @return int The GD image index of the color
     * @access private
     */
    function _color($color = false)
    {
        if (($color === false) || ($color === 'opague') || ($color === 'transparent')) {
            return ImageColorTransparent($this->_canvas);
        } else {
            return Image_Canvas_Color::allocateColor($this->_canvas, $color);
        }
    }

    /**
     * Get the GD applicable linestyle
     *
     * @param mixed $lineStyle The line style to return, false if the one
     *   explicitly set
     *
     * @return mixed A GD compatible linestyle
     * @access private
     */
    function _getLineStyle($lineStyle = false)
    {
        if ($this->_gd2) {
            ImageSetThickness($this->_canvas, $this->_thickness);
        }

        if ($lineStyle == 'transparent') {
            return false;
        } elseif ($lineStyle === false) {
            if (is_array($this->_lineStyle)) {
                $colors = array();
                foreach ($this->_lineStyle as $color) {
                    if ($color === 'transparent') {
                        $color = false;
                    }
                    $colors[] = $this->_color($color);
                }
                ImageSetStyle($this->_canvas, $colors);
                return IMG_COLOR_STYLED;
            } else {
                return $this->_color($this->_lineStyle);
            }
        } else {
            return $this->_color($lineStyle);
        }
    }

    /**
     * Get the GD applicable fillstyle
     *
     * @param mixed $fillStyle The fillstyle to return, false if the one
     *   explicitly set
     * @param int   $x0        ???
     * @param int   $y0        ???
     * @param int   $x1        ???
     * @param int   $y1        ???
     *
     * @return mixed A GD compatible fillstyle
     * @access private
     */
    function _getFillStyle($fillStyle = false, $x0 = 0, $y0 = 0, $x1 = 0, $y1 = 0)
    {
        if ($this->_tileImage != null) {
            ImageDestroy($this->_tileImage);
            $this->_tileImage = null;
        }
        if ($fillStyle == 'transparent') {
            return false;
        } elseif ($fillStyle === false) {
            if (is_resource($this->_fillStyle)) {
                $x = min($x0, $x1);
                $y = min($y0, $y1);
                $w = abs($x1 - $x0) + 1;
                $h = abs($y1 - $y0) + 1;
                if ($this->_gd2) {
                    $this->_tileImage = ImageCreateTrueColor(
                        $this->getWidth(),
                        $this->getHeight()
                    );

                    ImageCopyResampled(
                        $this->_tileImage,
                        $this->_fillStyle,
                        $x,
                        $y,
                        0,
                        0,
                        $w,
                        $h,
                        ImageSX($this->_fillStyle),
                        ImageSY($this->_fillStyle)
                    );
                } else {
                    $this->_tileImage = ImageCreate(
                        $this->getWidth(),
                        $this->getHeight()
                    );

                    ImageCopyResized(
                        $this->_tileImage,
                        $this->_fillStyle,
                        $x,
                        $y,
                        0,
                        0,
                        $w,
                        $h,
                        ImageSX($this->_fillStyle),
                        ImageSY($this->_fillStyle)
                    );
                }
                ImageSetTile($this->_canvas, $this->_tileImage);
                return IMG_COLOR_TILED;
            } elseif ((is_array($this->_fillStyle)) && (isset($this->_fillStyle['direction']))) {
                $width = abs($x1 - $x0) + 1;
                $height = abs($y1 - $y0) + 1;

                switch ($this->_fillStyle['direction']) {
                case 'horizontal':
                    $count = $width;
                    break;

                case 'vertical':
                    $count = $height;
                    break;

                case 'horizontal_mirror':
                    $count = $width / 2;
                    break;

                case 'vertical_mirror':
                    $count = $height / 2;
                    break;

                case 'diagonal_tl_br':
                case 'diagonal_bl_tr':
                    $count = sqrt($width * $width + $height * $height);
                    break;

                case 'radial':
                    $count = max($width, $height, sqrt($width * $width + $height * $height)) + 1;
                    break;

                }

                $count = round($count);

                if ($this->_gd2) {
                    $this->_tileImage = ImageCreateTrueColor(
                        $this->getWidth(),
                        $this->getHeight()
                    );
                } else {
                    $this->_tileImage = ImageCreate(
                        $this->getWidth(),
                        $this->getHeight()
                    );
                }


                $startColor = Image_Canvas_Color::color2RGB(
                    ($this->_fillStyle['direction'] == 'radial' ?
                        $this->_fillStyle['end'] :
                        $this->_fillStyle['start']
                    )
                );
                $endColor = Image_Canvas_Color::color2RGB(
                    ($this->_fillStyle['direction'] == 'radial' ?
                        $this->_fillStyle['start'] :
                        $this->_fillStyle['end']
                    )
                );

                $redIncrement = ($endColor[0] - $startColor[0]) / $count;
                $greenIncrement = ($endColor[1] - $startColor[1]) / $count;
                $blueIncrement = ($endColor[2] - $startColor[2]) / $count;

                $color = false;
                for ($i = 0; $i < $count; $i ++) {
                    unset($color);
                    if ($i == 0) {
                        $color = $startColor;
                        unset($color[3]);
                    } else {
                        $color[0] = round(
                            ($redIncrement * $i) +
                            $redIncrement + $startColor[0]
                        );
                        $color[1] = round(
                            ($greenIncrement * $i) +
                            $greenIncrement + $startColor[1]
                        );
                        $color[2] = round(
                            ($blueIncrement * $i) +
                            $blueIncrement + $startColor[2]
                        );
                    }
                    $color = Image_Canvas_Color::allocateColor(
                        $this->_tileImage,
                        $color
                    );

                    switch ($this->_fillStyle['direction']) {
                    case 'horizontal':
                        ImageLine(
                            $this->_tileImage,
                            $x0 + $i,
                            $y0,
                            $x0 + $i,
                            $y1, $color
                        );
                        break;

                    case 'vertical':
                        ImageLine(
                            $this->_tileImage,
                            $x0,
                            $y1 - $i,
                            $x1,
                            $y1 - $i, $color
                        );
                        break;

                    case 'horizontal_mirror':
                        if (($x0 + $i) <= ($x1 - $i)) {
                            ImageLine(
                                $this->_tileImage,
                                $x0 + $i,
                                $y0,
                                $x0 + $i,
                                $y1, $color
                            );

                            ImageLine(
                                $this->_tileImage,
                                $x1 - $i,
                                $y0,
                                $x1 - $i,
                                $y1, $color
                            );
                        }
                        break;

                    case 'vertical_mirror':
                        if (($y0 + $i) <= ($y1 - $i)) {
                            ImageLine(
                                $this->_tileImage,
                                $x0,
                                $y0 + $i,
                                $x1,
                                $y0 + $i, $color
                            );
                            ImageLine(
                                $this->_tileImage,
                                $x0,
                                $y1 - $i,
                                $x1,
                                $y1 - $i, $color
                            );
                        }
                        break;

                    case 'diagonal_tl_br':
                        if (($i > $width) && ($i > $height)) {
                            $polygon = array (
                                $x1, $y0 + $i - $width - 1,
                                $x1, $y1,
                                $x0 + $i - $height - 1, $y1);
                        } elseif ($i > $width) {
                            $polygon = array (
                                $x0, $y0 + $i,
                                $x0, $y1,
                                $x1, $y1,
                                $x1, $y0 + $i - $width - 1);
                        } elseif ($i > $height) {
                            $polygon = array (
                                $x0 + $i - $height - 1, $y1,
                                $x1, $y1,
                                $x1, $y0,
                                $x0 + $i, $y0);
                        } else {
                            $polygon = array (
                                $x0, $y0 + $i,
                                $x0, $y1,
                                $x1, $y1,
                                $x1, $y0,
                                $x0 + $i, $y0);
                        }
                        ImageFilledPolygon(
                            $this->_tileImage,
                            $polygon,
                            count($polygon) / 2,
                            $color
                        );
                        break;

                    case 'diagonal_bl_tr':
                        if (($i > $width) && ($i > $height)) {
                            $polygon = array (
                                $x1, $y1 - $i + $width - 1,
                                $x1, $y0,
                                $x0 + $i - $height - 1, $y0);
                        } elseif ($i > $width) {
                            $polygon = array (
                                $x0, $y1 - $i,
                                $x0, $y0,
                                $x1, $y0,
                                $x1, $y1 - $i + $width - 1);
                        } elseif ($i > $height) {
                            $polygon = array (
                                $x0 + $i - $height - 1, $y0,
                                $x1, $y0,
                                $x1, $y1,
                                $x0 + $i, $y1);
                        } else {
                            $polygon = array (
                                $x0, $y1 - $i,
                                $x0, $y0,
                                $x1, $y0,
                                $x1, $y1,
                                $x0 + $i, $y1);
                        }
                        ImageFilledPolygon(
                            $this->_tileImage,
                            $polygon,
                            count($polygon) / 2,
                            $color
                        );
                        break;

                    case 'radial':
                        if (($this->_gd2) && ($i < $count)) {
                            ImageFilledEllipse(
                                $this->_tileImage,
                                $x0 + $width / 2,
                                $y0 + $height / 2,
                                $count - $i,
                                $count - $i,
                                $color
                            );
                        }
                        break;
                    }
                }
                ImageSetTile($this->_canvas, $this->_tileImage);
                return IMG_COLOR_TILED;
            } else {
                return $this->_color($this->_fillStyle);
            }
        } else {
            return $this->_color($fillStyle);
        }
    }

    /**
     * Sets an image that should be used for filling
     *
     * @param string $filename The filename of the image to fill with
     *
     * @return void
     */
    function setFillImage($filename)
    {
        $this->_fillStyle =& $this->_getGD($filename);
    }

    /**
     * Sets the font options.
     *
     * The $font array may have the following entries:
     *
     * 'ttf' = the .ttf file (either the basename, filename or full path)
     * If 'ttf' is specified, then the following can be specified
     *
     * 'size' = size in pixels
     *
     * 'angle' = the angle with which to write the text
     *
     * @param array $fontOptions The font options.
     *
     * @return void
     */
    function setFont($fontOptions)
    {
        parent::setFont($fontOptions);

        if (isset($this->_font['ttf'])) {
            $this->_font['file'] = str_replace('\\', '/', Image_Canvas_Tool::fontMap($this->_font['ttf']));
        } elseif (!isset($this->_font['font'])) {
            $this->_font['font'] = 1;
        }

        if (!isset($this->_font['color'])) {
            $this->_font['color'] = 'black';
        }

        if ((isset($this->_font['angle'])) && ($this->_font['angle'] === false)) {
            $this->_font['angle'] = 0;
        }
    }

    /**
     * Calculate pixels on a line
     *
     * @param int $x0 X start point
     * @param int $y0 Y start point
     * @param int $x1 X end point
     * @param int $y1 Y end point
     *
     * @return array An associated array of x,y points with all pixels on the
     *   line    
     * @access private
     */
    function &_linePixels($x0, $y0, $x1, $y1)
    {
        $pixels = array();
        if (abs($x0 - $x1) > abs($y0 - $y1)) {
            if ($x1 != $x0) {
                $m = ($y1 - $y0) / ($x1 - $x0);
            } else {
                $m = 0;
            }  
            $b = $y0 - $m * $x0;
            $strx = min($x0, $x1);
            $endx = max($x0, $x1);
            for ($x = $strx; $x <= $endx; $x++) {
                $pixels[] = array('X' => $x, 'Y' => ($m * $x + $b));                
            }
        } else {
            if ($y1 != $y0) {
                $m = ($x1 - $x0) / ($y1 - $y0);
            } else {
                $m = 0;
            }
            $b = $x0 - $m * $y0;
            $stry = min($y0, $y1);
            $endy = max($y0, $y1);
            for ($y = $stry; $y <= $endy; $y++) {
                $pixels[] = array('X' => ($m * $y + $b), 'Y' => $y);                
            }
        }
        return $pixels;
    }

    /**
     * Draws an antialiased line
     *
     * @param int   $x0    X start point
     * @param int   $y0    Y start point
     * @param int   $x1    X end point
     * @param int   $y1    Y end point
     * @param mixed $color The line color, can be omitted
     *
     * @return void
     * @access private
     */
    function _antialiasedLine($x0, $y0, $x1, $y1, $color = false)
    {
        if (($line = $this->_getLineStyle($color)) !== false) {
            if ($line >= 0) {
                $line = ImageColorsForIndex($this->_canvas, $line);
                $pixels = &$this->_linePixels($x0, $y0, $x1, $y1);
                foreach ($pixels as $point) {
                    $this->_antialiasedPixel($point['X'], $point['Y'], $line);
                }
                unset($pixels);
            }
        }
    }


    /**
     * Draws an antialiased pixel
     *
     * @param int   $x     X point
     * @param int   $y     Y point
     * @param mixed $color The pixel color
     *
     * @return void
     * @access private
     */
    function _antialiasedPixel($x, $y, $color)
    {
        $fx = floor($x);
        $fy = floor($y);
        $cx = ceil($x);
        $cy = ceil($y);
        $xa = $x - $fx;
        $xb = $cx - $x;
        $ya = $y - $fy;
        $yb = $cy - $y;
        if (($cx == $fx) && ($cy == $fy)) {
            $this->_antialisedSubPixel($fx, $fy, 0.0, 1.0, $color);
        } else {
            $this->_antialisedSubPixel($fx, $fy, $xa + $ya, $xb + $yb, $color);
            if ($cy != $fy) {
                $this->_antialisedSubPixel($fx, $cy, $xa + $yb, $xb + $ya, $color);
            }
            if ($cx != $fx) {
                $this->_antialisedSubPixel($cx, $fy, $xb + $ya, $xa + $yb, $color);
                if ($cy != $fy) {
                    $this->_antialisedSubPixel($cx, $cy, $xb + $yb, $xa + $ya, $color);
                }
            }
        }
    }

    /**
     * Antialias'es the pixel around x,y with weights a,b
     *
     * @param int   $x     X point
     * @param int   $y     Y point
     * @param int   $a     The weight of the current color
     * @param int   $b     The weight of the applied/wanted color
     * @param mixed $color The pixel color
     *
     * @return void
     * @access private
     */
    function _antialisedSubPixel($x, $y, $a, $b, $color)
    {
        $x = $this->_getX($x);
        $y = $this->_getX($y);
        if (($x >=0 ) && ($y >= 0) && ($x < $this->getWidth()) && ($y < $this->getHeight())) {
            $tempColor = ImageColorsForIndex($this->_canvas, ImageColorAt($this->_canvas, $x, $y));                
            
            $newColor[0] = min(255, round($tempColor['red'] * $a + $color['red'] * $b));        
            $newColor[1] = min(255, round($tempColor['green'] * $a + $color['green'] * $b));        
            $newColor[2] = min(255, round($tempColor['blue'] * $a + $color['blue'] * $b));
            //$newColor[3] = 0;
            $color = '#';
            foreach ($newColor as $acolor) {
                $color .= sprintf('%02s', dechex($acolor));
            }
            $newColor = $this->_color($color);//,'rgb(' . $newColor[0] . ',' . $newColor[1] . ','  . $newColor[2] .')';        
    
            ImageSetPixel($this->_canvas, $x, $y, $newColor);
        }
    }
    
    
    /**
     * Draw a line end
     *
     * Parameter array:
     * 
     * 'x': int X point
     * 
     * 'y': int Y point
     * 
     * 'end': string The end type of the end
     * 
     * 'size': int The size of the end
     * 
     * 'color': string The color of the end
     * 
     * 'angle': int [optional] The angle with which to draw the end
     * 
     * @param array $params Parameter array
     *
     * @return void
     */
    function drawEnd($params) 
    {        
        $x = $this->_getX($params['x']);
        $y = $this->_getY($params['y']);
        $size = $params['size'];
        //var_dump($params);
        $angle = deg2rad((isset($params['angle']) ? $params['angle'] : 0));
        $pi2 = pi() / 2;
        switch ($params['end']) {
        case 'lollipop':
        case 'circle':
            $this->ellipse(
                array(
                    'x' => $x,
                    'y' => $y,
                    'rx' => $size / 2,
                    'ry' => $size / 2,
                    'fill' => $params['color'],
                    'line' => $params['color']                    
                )
            );
            break;
        case 'diamond':
            $x0 = round($params['x'] + cos($angle) * $size * 0.65);
            $y0 = round($params['y'] - sin($angle) * $size * 0.65);
            $shape = array(
                $x0 + round(cos($angle) * $size * 0.65),
                $y0 - round(sin($angle) * $size * 0.65),
                $x0 + round(cos($angle + $pi2) * $size * 0.65),
                $y0 - round(sin($angle + $pi2) * $size * 0.65),
                $x0 + round(cos($angle + pi()) * $size * 0.65),
                $y0 - round(sin($angle + pi()) * $size * 0.65),
                $x0 + round(cos($angle + 3 * $pi2) * $size * 0.65),
                $y0 - round(sin($angle + 3 * $pi2) * $size * 0.65)
            );
            break;
        case 'line':
            $this->line(
                array(
                    'x0' => $x + round(cos($angle + $pi2) * $size / 2),
                    'y0' => $y - round(sin($angle + $pi2) * $size / 2),
                    'x1' => $x + round(cos($angle + 3 * $pi2) * $size / 2),
                    'y1' => $y - round(sin($angle + 3 * $pi2) * $size / 2),
                    'color' => $params['color']
                )
            );
            break;
        case 'box':
        case 'rectangle':
            $x0 = round($params['x'] + cos($angle) * $size / 2);
            $y0 = round($params['y'] - sin($angle) * $size / 2);
            $pi4 = pi() / 4;            
            $shape = array(
                $x0 + round(cos($angle + $pi4) * $size / 2),
                $y0 - round(sin($angle + $pi4) * $size / 2),
                $x0 + round(cos($angle + $pi2 + $pi4) * $size / 2),
                $y0 - round(sin($angle + $pi2 + $pi4) * $size / 2),
                $x0 + round(cos($angle + pi() + $pi4) * $size / 2),
                $y0 - round(sin($angle + pi() + $pi4) * $size / 2),
                $x0 + round(cos($angle + 3 * $pi2 + $pi4) * $size / 2),
                $y0 - round(sin($angle + 3 * $pi2 + $pi4) * $size / 2)
            );
            break;
        case 'arrow':  
            $shape = array(
                $x + cos($angle) * $size,
                $y - sin($angle) * $size,
                $x + cos($angle + $pi2) * $size * 0.4,
                $y - sin($angle + $pi2) * $size * 0.4,
                $x + cos($angle + 3 * $pi2) * $size * 0.4,
                $y - sin($angle + 3 * $pi2) * $size * 0.4,                
            );
            break;
        case 'arrow2':  
            $shape = array(
                $x + round(cos($angle) * $size),
                $y - round(sin($angle) * $size),
                $x + round(cos($angle + $pi2 + deg2rad(45)) * $size),
                $y - round(sin($angle + $pi2 + deg2rad(45)) * $size),
                $x,
                $y,
                $x + round(cos($angle + 3 * $pi2 - deg2rad(45)) * $size),
                $y - round(sin($angle + 3 * $pi2 - deg2rad(45)) * $size),                
            );
            break;
        }
        
        if (isset($shape)) {
            // output the shape
            if (($fill = $this->_getFillStyle($params['color'])) !== false) {
                ImageFilledPolygon($this->_canvas, $shape, count($shape)/2, $fill);
            }
        }
        parent::drawEnd($params);
    }    
    
    /**
     * Draw a line
     *
     * Parameter array:
     * 
     * 'x0': int X start point
     * 
     * 'y0': int Y start point
     * 
     * 'x1': int X end point
     * 
     * 'y1': int Y end point
     * 
     * 'color': mixed [optional] The line color
     * 
     * @param array $params Parameter array
     *
     * @return void
     */
    function line($params)
    {
        $x0 = $this->_getX($params['x0']);
        $y0 = $this->_getY($params['y0']);
        $x1 = $this->_getX($params['x1']);
        $y1 = $this->_getY($params['y1']);
        $color = (isset($params['color']) ? $params['color'] : false);
        
        $x0 = $this->_getX($x0);
        $y0 = $this->_getY($y0);
        $x1 = $this->_getX($x1);
        $y1 = $this->_getY($y1);
        if (($this->_antialias === 'driver') && ($x0 != $x1) && ($y0 != $y1)) {
            $this->_antialiasedLine($x0, $y0, $x1, $y1, $color);
        } elseif (($line = $this->_getLineStyle($color)) !== false) {            
            ImageLine($this->_canvas, $x0, $y0, $x1, $y1, $line);
        }
        parent::line($params);
    }

    /**
     * Get the line thickness
     *
     * @param mixed $lineStyle The line style to return the thickness of,
     * false if the one explicitly set
     *
     * @return float A line thickness
     * @access private
     */
    function _getLineThickness($lineStyle = false)
    {
        return $this->_thickness;
    }
 
    /**
     * Parameter array:
     * 
     * 'connect': bool [optional] Specifies whether the start point should be
     * connected  to the endpoint (closed polygon) or not (connected line)
     * 
     * 'fill': mixed [optional] The fill color
     * 
     * 'line': mixed [optional] The line color
     *
     * @param array $params Parameter array
     *
     * @return void
     */
    function polygon($params)
    {
        include_once 'Image/Canvas/Tool.php';

        $connectEnds = (isset($params['connect']) ? $params['connect'] : false);
        $fillColor = (isset($params['fill']) ? $params['fill'] : false);
        $lineColor = (isset($params['line']) ? $params['line'] : false);

        if (!$connectEnds) {
            $fillColor = 'transparent';
        }
        $style = $this->_getLineStyle($lineColor) . $this->_getFillStyle($fillColor);

        $lastPoint = false;
        foreach ($this->_polygon as $point) {
            if (($lastPoint) && (isset($lastPoint['P1X']))
                && (isset($lastPoint['P1Y'])) && (isset($lastPoint['P2X']))
                && (isset($lastPoint['P2Y']))
            ) {
                $dx = abs($point['X'] - $lastPoint['X']);
                $dy = abs($point['Y'] - $lastPoint['Y']);
                $d = sqrt($dx * $dx + $dy * $dy);
                if ($d > 0) {
                    $interval = 1 / $d;
                    for ($t = 0; $t <= 1; $t = $t + $interval) {
                        $x = Image_Canvas_Tool::bezier(
                            $t,
                            $lastPoint['X'],
                            $lastPoint['P1X'],
                            $lastPoint['P2X'],
                            $point['X']
                        );
    
                        $y = Image_Canvas_Tool::bezier(
                            $t,
                            $lastPoint['Y'],
                            $lastPoint['P1Y'],
                            $lastPoint['P2Y'],
                            $point['Y']
                        );
    
                        if (!isset($low['X'])) {
                            $low['X'] = $x;
                        } else {
                            $low['X'] = min($x, $low['X']);
                        }
                        if (!isset($high['X'])) {
                            $high['X'] = $x;
                        } else {
                            $high['X'] = max($x, $high['X']);
                        }
                        if (!isset($low['Y'])) {
                            $low['Y'] = $y;
                        } else {
                            $low['Y'] = min($y, $low['Y']);
                        }
                        if (!isset($high['Y'])) {
                            $high['Y'] = $y;
                        } else {
                            $high['Y'] = max($y, $high['Y']);
                        }
                        $polygon[] = $x;
                        $polygon[] = $y;
                    }
                    if (($t - $interval) < 1) {
                        $x = Image_Canvas_Tool::bezier(
                            1,
                            $lastPoint['X'],
                            $lastPoint['P1X'],
                            $lastPoint['P2X'],
                            $point['X']
                        );
    
                        $y = Image_Canvas_Tool::bezier(
                            1,
                            $lastPoint['Y'],
                            $lastPoint['P1Y'],
                            $lastPoint['P2Y'],
                            $point['Y']
                        );
    
                        $polygon[] = $x;
                        $polygon[] = $y;
                    }
                }
            } else {
                if (!isset($low['X'])) {
                    $low['X'] = $point['X'];
                } else {
                    $low['X'] = min($point['X'], $low['X']);
                }
                if (!isset($high['X'])) {
                    $high['X'] = $point['X'];
                } else {
                    $high['X'] = max($point['X'], $high['X']);
                }
                if (!isset($low['Y'])) {
                    $low['Y'] = $point['Y'];
                } else {
                    $low['Y'] = min($point['Y'], $low['Y']);
                }
                if (!isset($high['Y'])) {
                    $high['Y'] = $point['Y'];
                } else {
                    $high['Y'] = max($point['Y'], $high['Y']);
                }

                $polygon[] = $point['X'];
                $polygon[] = $point['Y'];
            }
            $lastPoint = $point;
        }

        if ((isset($polygon)) && (is_array($polygon))) {
            if ($connectEnds) {
                if (($fill = $this->_getFillStyle($fillColor, $low['X'], $low['Y'], $high['X'], $high['Y'])) !== false) {
                    ImageFilledPolygon($this->_canvas, $polygon, count($polygon)/2, $fill);
                }
                if ($this->_antialias === 'driver') {
                    $pfirst = $p0 = false; 
                    reset($polygon);
                    
                    while (list(, $x) = each($polygon)) {
                        list(, $y) = each($polygon);
                        if ($p0 !== false) {
                            $this->_antialiasedLine($p0['X'], $p0['Y'], $x, $y, $lineColor);
                        }
                        if ($pfirst === false) {
                            $pfirst = array('X' => $x, 'Y' => $y);
                        }                        
                        $p0 = array('X' => $x, 'Y' => $y);;
                    }
                    
                    $this->_antialiasedLine($p0['X'], $p0['Y'], $pfirst['X'], $pfirst['Y'], $lineColor);
                } elseif (($line = $this->_getLineStyle($lineColor)) !== false) {
                    ImagePolygon($this->_canvas, $polygon, count($polygon)/2, $line);
                }
            } else {
                $prev_point = false;
                if ($this->_getLineThickness() !== '0') {
                    if ($this->_antialias === 'driver' && $this->_getLineThickness() == '') {
                        reset($polygon);
                        while (list(, $x) = each($polygon)) {
                            list(, $y) = each($polygon);
                            if ($prev_point) {
                                $this->_antialiasedLine(
                                    $prev_point['X'],
                                    $prev_point['Y'],
                                    $x,
                                    $y,
                                    $lineColor
                                );                            
                            }
                            $prev_point = array('X' => $x, 'Y' => $y);;
                        }
                    }
                } elseif (($line = $this->_getLineStyle($lineColor)) !== false) {
                    reset($polygon);
                    while (list(, $x) = each($polygon)) {
                        list(, $y) = each($polygon);
                        if ($prev_point) {
                            ImageLine(
                                $this->_canvas,
                                $prev_point['X'],
                                $prev_point['Y'],
                                $x,
                                $y,
                                $line
                            );
                        }
                        $prev_point = array('X' => $x, 'Y' => $y);;
                    }
                }
            }
        }

        parent::polygon($params);
    }

    /**
     * Draw a rectangle
     *
     * Parameter array:
     * 
     * 'x0': int X start point
     * 
     * 'y0': int Y start point
     * 
     * 'x1': int X end point
     * 
     * 'y1': int Y end point
     * 
     * 'fill': mixed [optional] The fill color
     * 
     * 'line': mixed [optional] The line color
     * 
     * @param array $params Parameter array
     *
     * @return void
     */
    function rectangle($params)
    {
        $x0 = $this->_getX($params['x0']);
        $y0 = $this->_getY($params['y0']);
        $x1 = $this->_getX($params['x1']);
        $y1 = $this->_getY($params['y1']);
        $fillColor = (isset($params['fill']) ? $params['fill'] : false);
        $lineColor = (isset($params['line']) ? $params['line'] : false);

        if (($fill = $this->_getFillStyle($fillColor, $x0, $y0, $x1, $y1)) !== false) {
            ImageFilledRectangle($this->_canvas, $x0, $y0, $x1, $y1, $fill);
        }

        if (($line = $this->_getLineStyle($lineColor)) !== false) {
            ImageRectangle($this->_canvas, $x0, $y0, $x1, $y1, $line);
        }

        parent::rectangle($params);
    }

    /**
     * Draw an ellipse
     *
     * Parameter array:
     * 
     * 'x': int X center point
     * 
     * 'y': int Y center point
     * 
     * 'rx': int X radius
     * 
     * 'ry': int Y radius
     * 
     * 'fill': mixed [optional] The fill color
     * 
     * 'line': mixed [optional] The line color
     * 
     * @param array $params Parameter array
     *
     * @return void
     */
    function ellipse($params)
    {
        $x = $this->_getX($params['x']);
        $y = $this->_getY($params['y']);
        $rx = $this->_getX($params['rx']);
        $ry = $this->_getY($params['ry']);
        $fillColor = (isset($params['fill']) ? $params['fill'] : false);
        $lineColor = (isset($params['line']) ? $params['line'] : false);

        if (($fill = $this->_getFillStyle($fillColor, $x - $rx, $y - $ry, $x + $rx, $y + $ry)) !== false) {
            ImageFilledEllipse($this->_canvas, $x, $y, $rx * 2, $ry * 2, $fill);
        }

        if (($line = $this->_getLineStyle($lineColor)) !== false) {
            ImageEllipse($this->_canvas, $x, $y, $rx * 2, $ry * 2, $line);
        }
        parent::ellipse($params);
    }

    /**
     * Draw a pie slice
     *
     * Parameter array:
     * 
     * 'x': int X center point
     * 
     * 'y': int Y center point
     * 
     * 'rx': int X radius
     * 
     * 'ry': int Y radius
     * 
     * 'v1': int The starting angle (in degrees)
     * 
     * 'v2': int The end angle (in degrees)
     * 
     * 'srx': int [optional] Starting X-radius of the pie slice (i.e. for a doughnut)
     * 
     * 'sry': int [optional] Starting Y-radius of the pie slice (i.e. for a doughnut)
     * 
     * 'fill': mixed [optional] The fill color
     * 
     * 'line': mixed [optional] The line color
     * 
     * @param array $params Parameter array
     *
     * @return void
     */
    function pieslice($params)
    {
        $x = $this->_getX($params['x']);
        $y = $this->_getY($params['y']);
        $rx = $params['rx'];
        $ry = $params['ry'];
        $v1 = $params['v1'];
        $v2 = $params['v2'];
        $srx = (isset($params['srx']) ? $params['srx'] : 0);
        $sry = (isset($params['sry']) ? $params['sry'] : 0);
        $fillColor = (isset($params['fill']) ? $params['fill'] : false);
        $lineColor = (isset($params['line']) ? $params['line'] : false);

        $dA = 0.1;

        if (($srx !== false) && ($sry !== false)) {
            $angle = max($v1, $v2);
            while ($angle >= min($v1, $v2)) {
                $polygon[] = ($x + $srx * cos(deg2rad($angle % 360)));
                $polygon[] = ($y + $sry * sin(deg2rad($angle % 360)));
                $angle -= $dA;
            }
            if (($angle + $dA) > min($v1, $v2)) {
                $polygon[] = ($x + $srx * cos(deg2rad(min($v1, $v2) % 360)));
                $polygon[] = ($y + $sry * sin(deg2rad(min($v1, $v2) % 360)));
            }
        } else {
            $polygon[] = $x;
            $polygon[] = $y;
        }

        $angle = min($v1, $v2);
        while ($angle <= max($v1, $v2)) {
            $polygon[] = ($x + $rx * cos(deg2rad($angle % 360)));
            $polygon[] = ($y + $ry * sin(deg2rad($angle % 360)));
            $angle += $dA;
        }

        if (($angle - $dA) < max($v1, $v2)) {
            $polygon[] = ($x + $rx * cos(deg2rad(max($v1, $v2) % 360)));
            $polygon[] = ($y + $ry * sin(deg2rad(max($v1, $v2) % 360)));
        }

        if ((($fill = $this->_getFillStyle($fillColor, $x - $rx - 1, $y - $ry - 1, $x + $rx + 1, $y + $ry + 1)) !== false) && (count($polygon) > 2)) {
            ImageFilledPolygon($this->_canvas, $polygon, count($polygon) / 2, $fill);
        }

        if (($line = $this->_getLineStyle($lineColor)) !== false) {
            ImagePolygon($this->_canvas, $polygon, count($polygon) / 2, $line);
        }

        parent::pieSlice($params);
    }

    /**
     * Get the width of a text,
     *
     * @param string $text The text to get the width of
     *
     * @return int The width of the text
     */
    function textWidth($text)
    {
        if (isset($this->_font['file'])) {
            $angle = 0;
            if (isset($this->_font['angle'])) {
                $angle = $this->_font['angle'];
            }
            
            $width = 0;
            $lines = explode("\n", $text);
            foreach ($lines as $line) {
                $bounds = ImageTTFBBox(
                    $this->_font['size']*$this->_pxToPtFactor,
                    $angle,
                    $this->_font['file'],
                    $text
                );
    
                $x0 = min($bounds[0], $bounds[2], $bounds[4], $bounds[6]);
                $x1 = max($bounds[0], $bounds[2], $bounds[4], $bounds[6]);
                $width = max(abs($x0 - $x1), $width);
            }
            return $width;
        } else {
            if ((isset($this->_font['vertical'])) && ($this->_font['vertical'])) {
                return ImageFontHeight($this->_font['font']) * (substr_count($text, "\n") + 1);
            } else {
                $width = 0;
                $lines = explode("\n", $text);
                foreach ($lines as $line) {
                    $width = max($width, ImageFontWidth($this->_font['font']) * strlen($line));
                }
                return $width;
            }
        }
    }

    /**
     * Get the height of a text.
     * 
     * Note! This method can give some peculiar results, since ImageTTFBBox() returns the total
     * bounding box of a text, where ImageTTF() writes the text on the baseline of the text, that
     * is 'g', 'p', 'q' and other letters that dig under the baseline will appear to have a larger
     * height than they actually do. Have a look at the tests/text.php test case - the first two
     * columns, 'left and 'center', both look alright, whereas the last column, 'right', appear
     * with a larger space between the first text and the second. This is because the total height
     * is actually smaller by exactly the number of pixels that the 'g' digs under the baseline.
     * Remove the 'g' from the text and they appear correct. 
     *
     * @param string $text  The text to get the height of
     * @param bool   $force Force the method to calculate the size
     *
     * @return int The height of the text
     */
    function textHeight($text, $force = false)
    {
        if (isset($this->_font['file'])) {
            $angle = 0;
            if (isset($this->_font['angle'])) {
                $angle = $this->_font['angle'];
            }
            
            $linebreaks = substr_count($text, "\n"); 
            if (($angle == 0) && ($force === false)) {
                /*
                 * if the angle is 0 simply return the size, due to different
                 * heights for example for x-axis labels, making the labels
                 * _not_ appear as written on the same baseline
                 */ 
                return $this->_font['size'] + ($this->_font['size'] + 2) * $linebreaks;
            }

            $height = 0;
            $lines = explode("\n", $text);
            foreach ($lines as $line) {            
                $bounds = ImageTTFBBox(
                    $this->_font['size']*$this->_pxToPtFactor,
                    $angle,
                    $this->_font['file'],
                    $line
                );
    
                $y0 = min($bounds[1], $bounds[3], $bounds[5], $bounds[7]);
                $y1 = max($bounds[1], $bounds[3], $bounds[5], $bounds[7]);
                $height += abs($y0 - $y1);
            }
            return $height + $linebreaks * 2;
        } else {
            if ((isset($this->_font['vertical'])) && ($this->_font['vertical'])) {
                $width = 0;
                $lines = explode("\n", $text);
                foreach ($lines as $line) {
                    $width = max($width, ImageFontWidth($this->_font['font']) * strlen($line));
                }
                return $width;
            } else {
                return ImageFontHeight($this->_font['font']) * (substr_count($text, "\n") + 1);
            }
        }
    }
    
    /**
     * Calculated the absolute bottom-left position of the text, by simulating
     * the calculation of the baseline drop.
     *
     * @param int    $x     The relative x position to write the text 
     * @param int    $y     The relative y position to write the text
     * @param string $text  The text to write 
     * @param array  $align The alignment of the text relative to (x, y)
     *
     * @return array An array containing the absolute x and y positions
     * @access private
     */
    function _getAbsolutePosition($x, $y, $text, $align) 
    {
        if ($this->_font['angle'] > 0) {
            $w0 = $this->textWidth($text);
            $h0 = $this->textHeight($text);
            
            if ($align['vertical'] == 'bottom') {
                $dy = $y - $h0;
            } else if ($align['vertical'] == 'center') {
                $dy = $y - $h0 / 2;
            } else if ($align['vertical'] == 'top') {
                $dy = $y;
            }

            if ($align['horizontal'] == 'right') {
                $dx = $x - $w0;
            } else if ($align['horizontal'] == 'center') {
                $dx = $x - $w0 / 2;
            } else if ($align['horizontal'] == 'left') {
                $dx = $x;
            }
            
            if (($this->_font['angle'] < 180) && ($this->_font['angle'] >= 0)) {
                $dy += $h0;
            }
            if (($this->_font['angle'] >= 90) && ($this->_font['angle'] < 270)) {
                $dx += $w0;
            }            
        } else {
            // get the maximum size of normal text above base line - sampled by 'Al'
            $size1 = imagettfbbox($this->_font['size']*$this->_pxToPtFactor, 0, $this->_font['file'], 'Al');    
            $height1 = abs($size1[7] - $size1[1]);
            
            // get the maximum size of all text above base and below line - sampled by 'AlgjpqyQ'
            $size2 = imagettfbbox($this->_font['size']*$this->_pxToPtFactor, 0, $this->_font['file'], 'AlgjpqyQ');
            $height2 = abs($size2[7] - $size2[1]);
        
            // get the size of the text, simulating height above baseline beinh max, by sampling using 'Al'
            $size = imagettfbbox($this->_font['size']*$this->_pxToPtFactor, 0, $this->_font['file'], 'Al' . $text);
            $height = abs($size[7] - $size[1]);
            
            // if all text is above baseline, i.e. height of text compares to max height above (within 10%)     
            if (abs($height - $height1)/$height1 < 0.1) { 
                $dHeight = 0;        
            } else {
                $dHeight = abs($height2 - $height1);
            }

            // specifies the bottom-left corner!
            $dx = $x + sin(deg2rad($this->_font['angle'])) * $dHeight;
            $dy = $y - cos(deg2rad($this->_font['angle'])) * $dHeight;

            if ($align['vertical'] == 'top') {
                $dy += $height;
            } else if ($align['vertical'] == 'center') {
                $dy += ($height + $dHeight) / 2;
            } else if ($align['vertical'] == 'bottom') {
                $dy += $dHeight;
            }
        
            if ($align['horizontal'] == 'center') {
                $factor = 0.5;
            } else if ($align['horizontal'] == 'right') {
                $factor = 1;
            } else {
                $factor = 0;
            }
            
            if ($factor != 0) {
                $size = imagettfbbox($this->_font['size']*$this->_pxToPtFactor, 0, $this->_font['file'], $text);
                $w0 = abs($size[2] - $size[0]);                
                $dx -= cos(deg2rad($this->_font['angle'])) * $w0 * $factor;
                $dy += sin(deg2rad($this->_font['angle'])) * $w0 * $factor;
            }
        }
                
        return array('x' => $dx, 'y' => $dy);  
    }

    /**
     * Writes text
     *
     * Parameter array:
     * 
     * 'x': int X-point of text
     * 
     * 'y': int Y-point of text
     * 
     * 'text': string The text to add
     * 
     * 'alignment': array [optional] Alignment
     * 
     * 'color': mixed [optional] The color of the text
     *
     * @param array $params Parameter array
     *
     * @return void
     */
    function addText($params)
    {
        $x0 = $this->_getX($params['x']);
        $y0 = $this->_getY($params['y']);
        $text = $params['text'];
        $color = (isset($params['color']) ? $params['color'] : false);
        $alignment = (isset($params['alignment']) ? $params['alignment'] : false);

        $text = str_replace("\r", '', $text);

        if (!is_array($alignment)) {
            $alignment = array('vertical' => 'top', 'horizontal' => 'left');
        }

        if (!isset($alignment['vertical'])) {
            $alignment['vertical'] = 'top';
        }
        
        if (!isset($alignment['horizontal'])) {
            $alignment['horizontal'] = 'left';
        }

        if (isset($this->_font['size'])) {            
            $textHeight = $this->_font['size'] + 2;
        } else {
            $textHeight = $this->textHeight('A');
        }
        $lines = explode("\n", $text);                
        foreach ($lines as $line) {          
            $x = $x0; 
            $y = $y0;
            
            $y0 += $textHeight + 2;
                    
            if (($color === false) && (isset($this->_font['color']))) {
                $color = $this->_font['color'];
            }

            if ($color != 'transparent') {
                if (isset($this->_font['file'])) {                   
                    $result = $this->_getAbsolutePosition($x, $y, $line, $alignment);                    
                    ImageTTFText(
                        $this->_canvas,
                        $this->_font['size']*$this->_pxToPtFactor,
                        $this->_font['angle'],
                        $result['x'],
                        $result['y'],
                        $this->_color($color),
                        $this->_font['file'],
                        $line
                    );
                } else {
                    $width = $this->textWidth($line);
                    $height = $this->textHeight($line);
                    if ($alignment['horizontal'] == 'right') {
                        $x -= $width;
                    } else if ($alignment['horizontal'] == 'center') {
                        $x -= $width / 2;
                    }
                    if ($alignment['vertical'] == 'bottom') {
                        $y -= $height;
                    } else if ($alignment['vertical'] == 'center') {
                        $y -= $height / 2;
                    }
                    if ((isset($this->_font['vertical'])) && ($this->_font['vertical'])) {                      
                        ImageStringUp(
                            $this->_canvas,
                            $this->_font['font'],
                            $x,
                            $y + $this->textHeight($text),
                            $line,
                            $this->_color($color)
                        );
                    } else {
                        ImageString(
                            $this->_canvas,
                            $this->_font['font'],
                            $x,
                            $y,
                            $line,
                            $this->_color($color)
                        );
                    }
                }
            }
        }
        parent::addText($params);
    }

    /**
     * Overlay image
     *
     * Parameter array:
     * 
     * 'x': int X-point of overlayed image
     * 
     * 'y': int Y-point of overlayed image
     * 
     * 'filename': string The filename of the image to overlay
     * 
     * 'width': int [optional] The width of the overlayed image (resizing if possible)
     * 
     * 'height': int [optional] The height of the overlayed image (resizing if possible)
     * 
     * 'alignment': array [optional] Alignment
     *
     * @param array $params Parameter array
     *
     * @return void
     */
    function image($params)
    {
        $x = $this->_getX($params['x']);
        $y = $this->_getY($params['y']);
        $filename = $params['filename'];
        $width = (isset($params['width']) ? $params['width'] : false);
        $height = (isset($params['height']) ? $params['height'] : false);
        $alignment = (isset($params['alignment']) ? $params['alignment'] : false);

        if (!is_array($alignment)) {
            $alignment = array('vertical' => 'top', 'horizontal' => 'left');
        }

        if (!isset($alignment['vertical'])) {
            $alignment['vertical'] = 'top';
        }
        
        if (!isset($alignment['horizontal'])) {
            $alignment['horizontal'] = 'left';
        }

        if (file_exists($filename)) {
            if (strtolower(substr($filename, -4)) == '.png') {
                $image = ImageCreateFromPNG($filename);
            } elseif (strtolower(substr($filename, -4)) == '.gif') {
                $image = ImageCreateFromGIF($filename);
            } else {
                $image = ImageCreateFromJPEG($filename);
            }

            $imgWidth = ImageSX($image);
            $imgHeight = ImageSY($image);

            $outputWidth = ($width !== false ? $width : $imgWidth);
            $outputHeight = ($height !== false ? $height : $imgHeight);
            
            if ($alignment['horizontal'] == 'right') {
                $x -= $outputWidth;
            } elseif ($alignment['horizontal'] == 'center') {
                $x -= $outputWidth / 2;
            }

            if ($alignment['vertical'] == 'bottom') {
                $y -= $outputHeight;
            } elseif ($alignment['vertical'] == 'center') {
                $y -= $outputHeight / 2;
            }

            if ((($width !== false) && ($width != $imgWidth))
                || (($height !== false) && ($height != $imgHeight))
            ) {
                if ($this->_gd2) {
                    ImageCopyResampled(
                        $this->_canvas,
                        $image,
                        $x,
                        $y,
                        0,
                        0,
                        $width,
                        $height,
                        $imgWidth,
                        $imgHeight
                    );
                } else {
                    ImageCopyResized(
                        $this->_canvas,
                        $image,
                        $x,
                        $y,
                        0,
                        0,
                        $width,
                        $height,
                        $imgWidth,
                        $imgHeight
                    );
                }
            } else {
                ImageCopy(
                    $this->_canvas,
                    $image,
                    $x,
                    $y,
                    0,
                    0,
                    $imgWidth,
                    $imgHeight
                );
            }
            ImageDestroy($image);
        }
        parent::image($params);
    }
    
    /**
     * Set clipping to occur
     * 
     * Parameter array:
     * 
     * 'x0': int X point of Upper-left corner
     * 'y0': int X point of Upper-left corner
     * 'x1': int X point of lower-right corner
     * 'y1': int Y point of lower-right corner
     *
     * @param array $params Parameter array
     *
     * @return void
     */
    function setClipping($params = false) 
    {
        if ($params === false) {
            $index = count($this->_clipping) - 1;
            if (isset($this->_clipping[$index])) {                
                $params = $this->_clipping[$index];
                $canvas = $params['canvas'];
                ImageCopy(
                    $canvas, 
                    $this->_canvas,
                    min($params['x0'], $params['x1']),
                    min($params['y0'], $params['y1']),
                    min($params['x0'], $params['x1']),
                    min($params['y0'], $params['y1']),
                    abs($params['x1'] - $params['x0'] + 1),
                    abs($params['y1'] - $params['y0'] + 1)
                );
                $this->_canvas = $canvas;
                unset($this->_clipping[$index]);
            }
        } else {
            $params['canvas'] = $this->_canvas;

            if ($this->_gd2) {
                $this->_canvas = ImageCreateTrueColor(
                    $this->_width,
                    $this->_height
                );
                if ($this->_alpha) {
                    ImageAlphaBlending($this->_canvas, true);
                }
            } else {
                $this->_canvas = ImageCreate($this->_width, $this->_height);
            }
            
            if (($this->_gd2) && ($this->_antialias === 'native')) {
                ImageAntialias($this->_canvas, true);
            }
            
            ImageCopy($this->_canvas, $params['canvas'], 0, 0, 0, 0, $this->_width, $this->_height);                
            
            $this->_clipping[count($this->_clipping)] = $params;
        }
    }
    
    /**
     * Get a canvas specific HTML tag.
     * 
     * This method implicitly saves the canvas to the filename in the 
     * filesystem path specified and parses it as URL specified by URL path
     * 
     * Parameter array:
     * 
     * 'filename' string
     * 
     * 'filepath': string Path to the file on the file system. Remember the final slash
     * 
     * 'urlpath': string Path to the file available through an URL. Remember the final slash
     * 
     * 'alt': string [optional] Alternative text on image
     * 
     * 'cssclass': string [optional] The CSS Stylesheet class
     * 
     * 'border': int [optional] The border width on the image 
     *
     * @param array $params Parameter array
     *
     * @return string HTML code
     */
    function toHtml($params)
    {
        parent::toHtml($params);
        return '<img src="' . $params['urlpath'] . $params['filename'] . '"' .
            (isset($params['alt']) ? ' alt="' . $params['alt'] . '"' : '') .
            (isset($params['cssclass']) ? ' class="' . $params['cssclass'] . '"' : '') .
            (isset($params['border']) ? ' border="' . $params['border'] . '"' : '') .
            (isset($this->_imageMap) ? ' usemap="#' . $params['filename'] . '"' : '') . ' />' .
            (isset($this->_imageMap) ? "\n" . $this->_imageMap->toHtml(array('name' => $params['filename'])) : '');
    }

    /**
     * Resets the canvas.
     *
     * Include fillstyle, linestyle, thickness and polygon
     *
     * @return void
     * @access private
     */
    function _reset()
    {
        if ($this->_gd2) {
            ImageSetThickness($this->_canvas, 1);
        }
        if ($this->_tileImage != null) {
            ImageDestroy($this->_tileImage);
            $this->_tileImage = null;
        }
        parent::_reset();
        $this->_font = array('font' => 1, 'color' => 'black');
    }

    /**
     * Check which version of GD is installed
     *
     * @return int 0 if GD isn't installed, 1 if GD 1.x is installed and 2 if GD
     * 2.x is installed
     * @access private
     */
    function _version()
    {
        $result = false;
        if (function_exists('gd_info')) {
            $info = gd_info();
            $version = $info['GD Version'];
        } else {
            ob_start();
            phpinfo(8);
            $php_info = ob_get_contents();
            ob_end_clean();

            if (preg_match("/<td[^>]*>GD Version *<\/td><td[^>]*>([^<]*)<\/td>/", $php_info, $result)
            ) {
                $version = $result[1];
            } else {
                $version = null;
            }
        }

        preg_match('/(\d+)\./', $version, $result );
        return $result[1];

    }

}

?>
