<?php
/**
 * Copyright 2002-2017 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file LICENSE for license information (LGPL). If you
 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
 *
 * @author    Chuck Hagenbuch <chuck@horde.org>
 * @author    Michael J. Rubinsky <mrubinsk@horde.org>
 * @author    Jan Schneider <jan@horde.org>
 * @category  Horde
 * @license   http://www.horde.org/licenses/lgpl21 LGPL-2.1
 * @package   Image
 */

/**
 * This class implements the Horde_Image API for the PHP GD extension.
 *
 * @author    Chuck Hagenbuch <chuck@horde.org>
 * @author    Michael J. Rubinsky <mrubinsk@horde.org>
 * @author    Jan Schneider <jan@horde.org>
 * @category  Horde
 * @copyright 2002-2017 Horde LLC
 * @license   http://www.horde.org/licenses/lgpl21 LGPL-2.1
 * @package   Image
 *
 * @property-read  resource $_im  The underlaying image resource.
 */
class Horde_Image_Gd extends Horde_Image_Base
{
    /**
     * Allocated color resources.
     *
     * @var int[]
     */
    protected $_colors = array();

    /**
     * Capabilites of this driver.
     *
     * @var string[]
     */
    protected $_capabilities = array(
        'canvas',
        'circle',
        'crop',
        'dashedLine',
        'flip',
        'grayscale',
        'line',
        'mirror',
        'polygon',
        'polyline',
        'rectangle',
        'resize',
        'rotate',
        'roundedRectangle',
        'sepia',
        'text',
        'yellowize',
    );

    /**
     * GD image resource for the current image data.
     *
     * @var resource
     */
    protected $_im;

    /**
     * Constructor.
     *
     * @see Horde_Image_Base::_construct
     */
    public function __construct($params, $context = array())
    {
        parent::__construct($params, $context);
        if (!empty($params['filename'])) {
            $this->loadFile($params['filename']);
        } elseif (!empty($params['data'])) {
            $this->loadString($params['data']);
        } elseif (!empty($params['width'])) {
            $this->_im = $this->create($this->_width, $this->_height);
            $this->call(
                'imageFill',
                array(
                    $this->_im,
                    0, 0,
                    $this->_allocateColor($this->_background)
                )
            );
        }
    }

    /**
     */
    public function __get($property)
    {
        switch ($property) {
        case '_im':
             return $this->_im;
        }
    }

    /**
     * Displays the current image.
     */
    public function display()
    {
        $this->headers();
        $this->call('image' . $this->_type, array($this->_im));
    }

    /**
     * Returns the raw data for this image.
     *
     * @param boolean $convert  Ignored for Gd driver.
     * @param array $options    Array of options:
     *     - stream: If true, return as a stream resource.
     *               DEFAULT: false.
     *
     * @return string  The raw image data.
     */
    public function raw($convert = false, $options = array())
    {
        if (!is_resource($this->_im)) {
            return '';
        }

        ob_start();
        call_user_func('image' . $this->_type, $this->_im);

        if (empty($options['stream'])) {
            return ob_get_clean();
        }

        $s = new Horde_Stream_Temp();
        $s->add(ob_get_clean(), true);

        return $s->stream;
    }

    /**
     * Resets the image data to defaults.
     */
    public function reset()
    {
        parent::reset();
        if (is_resource($this->_im)) {
            $this->call('imageDestroy', array($this->_im));
        }
    }

    /**
     * Returns the height and width of the current image data.
     *
     * @return array  A hash with 'width' containing the width, 'height'
     *                containing the height of the image.
     */
    public function getDimensions()
    {
        if (is_resource($this->_im) &&
            $this->_width == 0 &&
            $this->_height == 0) {
            $this->_width = $this->call('imageSX', array($this->_im));
            $this->_height = $this->call('imageSY', array($this->_im));
        }
        return array(
            'width' => $this->_width,
            'height' => $this->_height
        );
    }

    /**
     * Creates a color that can be accessed in this object.
     *
     * When a color is set, the integer resource of it is returned.
     *
     * @param string $name    The name of the color.
     * @param integer $alpha  Alpha transparency (0 - 127).
     *
     * @return integer  The resource of the color that can be passed to GD.
     */
    private function _allocateColor($name, $alpha = 0)
    {
        if (empty($this->_colors[$name])) {
            if ($name == 'none') {
                $this->_colors[$name] = $this->call(
                    'imageColorAllocateAlpha',
                    array($this->_im, 0, 0, 0, 127)
                );
            } else {
                list($r, $g, $b) = Horde_Image::getRGB($name);
                $this->_colors[$name] = $this->call(
                    'imageColorAllocateAlpha',
                    array($this->_im, $r, $g, $b, $alpha)
                );
            }
        }

        return $this->_colors[$name];
    }

    /**
     * Returns the numeric font size a from textual description.
     *
     * @param string $font  A textual size description.
     *
     * @return integer  The font size supported by GD.
     */
    private function _getFont($font)
    {
        switch ($font) {
        case 'tiny':
            return 1;

        case 'medium':
            return 3;

        case 'large':
            return 4;

        case 'giant':
            return 5;

        case 'small':
        default:
            return 2;
        }
    }

    /**
     * Loads the image data from a string.
     *
     * @param string $image_data  The data to use for the image.
     */
    public function loadString($image_data)
    {
        $this->_im = $this->call('imageCreateFromString', array($image_data));
    }

    /**
     * Loads the image data from a file.
     *
     * @param string $filename  The full path and filename to the file to load
     *                          the image data from.
     */
    public function loadFile($filename)
    {
        $info = $this->call('getimagesize', array($filename));
        if (is_array($info)) {
            switch ($info[2]) {
            case IMAGETYPE_GIF:
                if (function_exists('imagecreatefromgif')) {
                    $this->_im = $this->call(
                        'imagecreatefromgif',
                        array($filename)
                    );
                }
                break;
            case IMAGETYPE_JPEG:
                $this->_im = $this->call(
                    'imagecreatefromjpeg',
                    array($filename)
                );
                break;
            case IMAGETYPE_PNG:
                $this->_im = $this->call(
                    'imagecreatefrompng',
                    array($filename)
                );
                break;
            case IMAGETYPE_WBMP:
                if (function_exists('imagecreatefromgwbmp')) {
                    $this->_im = $this->call(
                        'imagecreatefromgwbmp',
                        array($filename)
                    );
                }
                break;
            case IMAGETYPE_XBM:
                $this->_im = $this->call(
                    'imagecreatefromxbm',
                    array($filename)
                );
                break;
            }
        }

        if (is_resource($this->_im)) {
            return;
        }

        parent::loadFile($filename);
        $this->_im = $this->call(
            'imageCreateFromString',
            array($this->_data->__toString())
        );
    }

    /**
     * Resizes the current image.
     *
     * @param integer $width        The new width.
     * @param integer $height       The new height.
     * @param boolean $ratio        Maintain original aspect ratio.
     * @param boolean $keepProfile  Keep the image meta data (unused).
     */
    public function resize($width, $height, $ratio = true, $keepProfile = false)
    {
        /* Abort if we're asked to divide by zero, truncate the image
         * completely in either direction, or there is no image data. */
        if (!$width || !$height || !is_resource($this->_im)) {
            throw new Horde_Image_Exception('Unable to resize image.');
        }

        if ($ratio) {
            if ($width / $height > $this->call('imageSX', array($this->_im)) / $this->call('imageSY', array($this->_im))) {
                $width = $height
                    * $this->call('imageSX', array($this->_im))
                    / $this->call('imageSY', array($this->_im));
            } else {
                $height = $width
                    * $this->call('imageSY', array($this->_im))
                    / $this->call('imageSX', array($this->_im));
            }
        }

        $im = $this->_im;
        $this->_im = $this->create($width, $height);

        /* Reset geometry since it will change. */
        $this->_width = 0;
        $this->_height = 0;

        try {
            $this->call(
                'imageCopyResampled',
                array(
                    $this->_im, $im,
                    0, 0, 0, 0,
                    $width, $height,
                    $this->call('imageSX', array($im)),
                    $this->call('imageSY', array($im))
                )
            );
        } catch (Horde_Image_Exception $e) {
            $this->call(
                'imageCopyResized',
                array(
                    $this->_im, $im,
                    0, 0, 0, 0,
                    $width, $height,
                    $this->call('imageSX', array($im)),
                    $this->call('imageSY', array($im))
                )
            );
        }
    }

    /**
     * Crops the current image.
     *
     * @param integer $x1  x for the top left corner.
     * @param integer $y1  y for the top left corner.
     * @param integer $x2  x for the bottom right corner.
     * @param integer $y2  y for the bottom right corner.
     */
    public function crop($x1, $y1, $x2, $y2)
    {
        $im = $this->_im;
        $this->_im = $this->create($x2 - $x1, $y2 - $y1);
        $this->_width = 0;
        $this->_height = 0;
        $this->call(
            'imageCopy',
            array($this->_im, $im, 0, 0, $x1, $y1, $x2 - $x1, $y2 - $y1)
        );
    }

    /**
     * Rotates the current image.
     *
     * @param integer $angle      The angle to rotate the image by, in the
     *                            clockwise direction.
     * @param string $background  The background color to fill any triangles.
     */
    public function rotate($angle, $background = 'white')
    {
        $background = $this->_allocateColor($background);

        $this->_width = 0;
        $this->_height = 0;

        switch ($angle) {
        case '90':
            $x = $this->call('imageSX', array($this->_im));
            $y = $this->call('imageSY', array($this->_im));
            $xymax = max($x, $y);

            $im = $this->create($xymax, $xymax);
            $im = $this->call('imageRotate', array($im, 270, $background));
            $this->_im = $im;
            $im = $this->create($y, $x);
            if ($x < $y) {
                $this->call(
                    'imageCopy',
                    array($im, $this->_im, 0, 0, 0, 0, $xymax, $xymax)
                );
            } elseif ($x > $y) {
                $this->call(
                    'imageCopy',
                    array(
                        $im, $this->_im,
                        0, 0, $xymax - $y, $xymax - $x,
                        $xymax, $xymax
                    )
                );
            }
            $this->_im = $im;
            break;

        default:
            $this->_im = $this->call(
                'imageRotate',
                array($this->_im, 360 - $angle, $background)
            );
            break;
        }
    }

    /**
     * Flips the current image.
     */
    public function flip()
    {
        $x = $this->call('imageSX', array($this->_im));
        $y = $this->call('imageSY', array($this->_im));

        $im = $this->create($x, $y);
        for ($curY = 0; $curY < $y; $curY++) {
            $this->call(
                'imageCopy',
                array($im, $this->_im, 0, $y - ($curY + 1), 0, $curY, $x, 1)
            );
        }

        $this->_im = $im;
    }

    /**
     * Mirrors the current image.
     */
    public function mirror()
    {
        $x = $this->call('imageSX', array($this->_im));
        $y = $this->call('imageSY', array($this->_im));

        $im = $this->create($x, $y);
        for ($curX = 0; $curX < $x; $curX++) {
            $this->call(
                'imageCopy',
                array($im, $this->_im, $x - ($curX + 1), 0, $curX, 0, 1, $y)
            );
        }

        $this->_im = $im;
    }

    /**
     * Converts the current image to grayscale.
     */
    public function grayscale()
    {
        $rateR = .229;
        $rateG = .587;
        $rateB = .114;
        $whiteness = 3;
        if ($this->call('imageIsTrueColor', array($this->_im)) === true) {
            $this->call('imageTrueColorToPalette', array($this->_im, true, 256));
        }
        $colors = min(256, $this->call('imageColorsTotal', array($this->_im)));
        for ($x = 0; $x < $colors; $x++) {
            $src = $this->call('imageColorsForIndex', array($this->_im, $x));
            $new = min(
                255,
                abs($src['red'] * $rateR + $src['green'] * $rateG + $src['blue'] * $rateB) + $whiteness);
            $this->call('imageColorSet', array($this->_im, $x, $new, $new, $new));
        }
    }

    /**
     * Applies a sepia filter.
     *
     * @param integer $threshold  Extent of sepia effect.
     */
    public function sepia($threshold = 85)
    {
        $tintR = 80;
        $tintG = 43;
        $tintB = -23;
        $rateR = .229;
        $rateG = .587;
        $rateB = .114;
        $whiteness = 3;

        if ($this->call('imageIsTrueColor', array($this->_im)) === true) {
            $this->call('imageTrueColorToPalette', array($this->_im, true, 256));
        }

        $colors = max(256, $this->call('imageColorsTotal', array($this->_im)));
        for ($x = 0; $x < $colors; $x++) {
            $src = $this->call('imageColorsForIndex', array($this->_im, $x));
            $new = min(
                255,
                abs($src['red'] * $rateR + $src['green'] * $rateG + $src['blue'] * $rateB) + $whiteness
            );
            $r = min(255, $new + $tintR);
            $g = min(255, $new + $tintG);
            $b = min(255, $new + $tintB);
            $this->call('imageColorSet', array($this->_im, $x, $r, $g, $b));
        }
    }

    /**
     * Applies a yellow filter.
     *
     * Adds a layer of yellow that can be transparent or solid. If $intensityY
     * is 255 the image will be 0% transparent (solid).
     *
     * @param integer $intensityY  How strong should the yellow (red and green)
     *                             be? (0-255)
     * @param integer $intensityB  How weak should the blue be? (>= 2, in the
     *                             positive limit it will be make BLUE 0)
     */
    public function yellowize($intensityY = 50, $intensityB = 3)
    {
        if ($this->call('imageIsTrueColor', array($this->_im)) === true) {
            $this->call('imageTrueColorToPalette', array($this->_im, true, 256));
        }

        $colors = max(256, $this->call('imageColorsTotal', array($this->_im)));
        for ($x = 0; $x < $colors; $x++) {
            $src = $this->call('imageColorsForIndex', array($this->_im, $x));
            $r = min($src['red'] + $intensityY, 255);
            $g = min($src['green'] + $intensityY, 255);
            $b = max(($r + $g) / max($intensityB, 2), 0);
            $this->call('imageColorSet', array($this->_im, $x, $r, $g, $b));
        }
    }

    /**
     * Draws a text string on the image in a specified location, with the
     * specified style information.
     *
     * @param string $text        The text to draw.
     * @param integer $x          The left x coordinate of the start of the
     *                            text string.
     * @param integer $y          The top y coordinate of the start of the text
     *                            string.
     * @param string $font        The font identifier you want to use for the
     *                            text.
     * @param string $color       The color that you want the text displayed in.
     * @param integer $direction  An integer that specifies the orientation of
     *                            the text.
     * @param string $fontsize    Size of the font (small, medium, large, giant)
     */
    public function text(
        $string, $x, $y, $font = '', $color = 'black', $direction = 0,
        $fontsize = 'small'
    )
    {
        $c = $this->_allocateColor($color);
        $f = $this->_getFont($fontsize);
        switch ($direction) {
        case -90:
        case 270:
            $this->call(
                'imageStringUp',
                array($this->_im, $f, $x, $y, $string, $c)
            );
            break;

        case 0:
        default:
            $this->call(
                'imageString',
                array($this->_im, $f, $x, $y, $string, $c)
            );
            break;
        }
    }

    /**
     * Draws a circle.
     *
     * @param integer $x     The x coordinate of the centre.
     * @param integer $y     The y coordinate of the centre.
     * @param integer $r     The radius of the circle.
     * @param string $color  The line color of the circle.
     * @param string $fill   The color to fill the circle.
     */
    public function circle($x, $y, $r, $color, $fill = null)
    {
        $c = $this->_allocateColor($color);
        if (is_null($fill)) {
            $result = $this->call(
                'imageEllipse',
                array($this->_im, $x, $y, $r * 2, $r * 2, $c)
            );
        } else {
            if ($fill !== $color) {
                $fillColor = $this->_allocateColor($fill);
                $this->call(
                    'imageFilledEllipse',
                    array($this->_im, $x, $y, $r * 2, $r * 2, $fillColor)
                );
                $this->call(
                    'imageEllipse',
                    array($this->_im, $x, $y, $r * 2, $r * 2, $c)
                );
            } else {
                $this->call(
                    'imageFilledEllipse',
                    array($this->_im, $x, $y, $r * 2, $r * 2, $c)
                );
            }
        }
    }

    /**
     * Draws a polygon based on a set of vertices.
     *
     * @param array $vertices  An array of x and y labeled arrays
     *                         (eg. $vertices[0]['x'], $vertices[0]['y'], ...).
     * @param string $color    The color you want to draw the polygon with.
     * @param string $fill     The color to fill the polygon.
     */
    public function polygon($verts, $color, $fill = 'none')
    {
        $vertices = array();
        foreach ($verts as $vert) {
            $vertices[] = $vert['x'];
            $vertices[] = $vert['y'];
        }

        if ($fill != 'none') {
            $f = $this->_allocateColor($fill);
            $this->call(
                'imageFilledPolygon',
                array($this->_im, $vertices, count($verts), $f)
            );
        }

        if ($fill == 'none' || $fill != $color) {
            $c = $this->_allocateColor($color);
            $this->call(
                'imagePolygon',
                array($this->_im, $vertices, count($verts), $c)
            );
        }
    }

    /**
     * Draws a rectangle.
     *
     * @param integer $x       The left x-coordinate of the rectangle.
     * @param integer $y       The top y-coordinate of the rectangle.
     * @param integer $width   The width of the rectangle.
     * @param integer $height  The height of the rectangle.
     * @param string $color    The line color of the rectangle.
     * @param string $fill     The color to fill the rectangle.
     */
    public function rectangle(
        $x, $y, $width, $height, $color = 'black', $fill = 'none'
    )
    {
        if ($fill != 'none') {
            $f = $this->_allocateColor($fill);
            $this->call(
                'imageFilledRectangle',
                array($this->_im, $x, $y, $x + $width, $y + $height, $f)
            );
        }

        if ($fill == 'none' || $fill != $color) {
            $c = $this->_allocateColor($color);
            $this->call(
                'imageRectangle',
                array($this->_im, $x, $y, $x + $width, $y + $height, $c)
            );
        }
    }

    /**
     * Draws a rounded rectangle.
     *
     * @param integer $x       The left x-coordinate of the rectangle.
     * @param integer $y       The top y-coordinate of the rectangle.
     * @param integer $width   The width of the rectangle.
     * @param integer $height  The height of the rectangle.
     * @param integer $round   The width of the corner rounding.
     * @param string  $color   The line color of the rectangle.
     * @param string  $fill    The color to fill the rounded rectangle with.
     */
    public function roundedRectangle(
        $x, $y, $width, $height, $round, $color = 'black', $fill = 'none'
    )
    {
        if ($round <= 0) {
            // Optimize out any calls with no corner rounding.
            $this->rectangle($x, $y, $width, $height, $color, $fill);
            return;
        }

        $c = $this->_allocateColor($color);

        // Set corner points to avoid lots of redundant math.
        $xul = $x + $round;
        $yul = $y + $round;

        $xur = $x + $width - $round;
        $yur = $y + $round;

        $xlr = $x + $width - $round;
        $ylr = $y + $height - $round;

        $xll = $x + $round;
        $yll = $y + $height - $round;

        $r = $round * 2;

        // Calculate the upper left arc.
        $pul = Horde_Image::arcPoints($round, 180, 270);

        // Calculate the upper right arc.
        $pur = Horde_Image::arcPoints($round, 270, 360);

        // Calculate the lower right arc.
        $plr = Horde_Image::arcPoints($round, 0, 90);

        // Calculate the lower left arc.
        $pll = Horde_Image::arcPoints($round, 90, 180);

        // Draw the corners - upper left, upper right, lower right, lower left.
        $this->call(
            'imageArc',
            array($this->_im, $xul, $yul, $r, $r, 180, 270, $c)
        );
        $this->call(
            'imageArc',
            array($this->_im, $xur, $yur, $r, $r, 270, 360, $c)
        );
        $this->call(
            'imageArc',
            array($this->_im, $xlr, $ylr, $r, $r, 0, 90, $c)
        );
        $this->call(
            'imageArc',
            array($this->_im, $xll, $yll, $r, $r, 90, 180, $c)
        );

        // Draw the connecting sides - top, right, bottom, left.
        $this->call(
            'imageLine',
            array($this->_im, $xul, $y, $xur, $y, $c)
        );
        $this->call(
            'imageLine',
            array($this->_im, $x + $width, $yur, $x + $width, $ylr, $c)
        );
        $this->call(
            'imageLine',
            array($this->_im, $xlr, $y + $height, $xll, $y + $height, $c)
        );
        $this->call(
            'imageLine',
            array($this->_im, $x, $yll, $x, $yul, $c)
        );

        if ($fill != 'none') {
            $f = $this->_allocateColor($fill);
            $this->call(
                'imageFillToBorder',
                array(
                    $this->_im,
                    $x + ($width / 2), $y + ($height / 2),
                    $c, $f
                )
            );
        }
    }

    /**
     * Draws a line.
     *
     * @param integer $x0    The x coordinate of the start.
     * @param integer $y0    The y coordinate of the start.
     * @param integer $x1    The x coordinate of the end.
     * @param integer $y1    The y coordinate of the end.
     * @param string $color  The line color.
     * @param string $width  The width of the line.
     */
    public function line($x1, $y1, $x2, $y2, $color = 'black', $width = 1)
    {
        $c = $this->_allocateColor($color);

        // Don't need to do anything special for single-width lines.
        if ($width == 1) {
            $this->call('imageLine', array($this->_im, $x1, $y1, $x2, $y2, $c));
        } elseif ($x1 == $x2) {
            // For vertical lines, we can just draw a vertical rectangle.
            $left = $x1 - floor(($width - 1) / 2);
            $right = $x1 + floor($width / 2);
            $this->call(
                'imageFilledRectangle',
                array($this->_im, $left, $y1, $right, $y2, $c)
            );
        } elseif ($y1 == $y2) {
            // For horizontal lines, we can just draw a horizontal filled
            // rectangle.
            $top = $y1 - floor($width / 2);
            $bottom = $y1 + floor(($width - 1) / 2);
            $this->call(
                'imageFilledRectangle',
                array($this->_im, $x1, $top, $x2, $bottom, $c)
            );
        } else {
            // Angled lines.

            // Make sure that the end points of the line are perpendicular to
            // the line itself.
            $a = atan2($y1 - $y2, $x2 - $x1);
            $dx = (sin($a) * $width / 2);
            $dy = (cos($a) * $width / 2);

            $verts = array(
                $x2 + $dx, $y2 + $dy,
                $x2 - $dx, $y2 - $dy,
                $x1 - $dx, $y1 - $dy,
                $x1 + $dx, $y1 + $dy
            );
            $this->call(
                'imageFilledPolygon',
                array($this->_im, $verts, count($verts) / 2, $c)
            );
        }
    }

    /**
     * Draws a dashed line.
     *
     * @param integer $x0           The x co-ordinate of the start.
     * @param integer $y0           The y co-ordinate of the start.
     * @param integer $x1           The x co-ordinate of the end.
     * @param integer $y1           The y co-ordinate of the end.
     * @param string $color         The line color.
     * @param string $width         The width of the line.
     * @param integer $dash_length  The length of a dash on the dashed line.
     * @param integer $dash_space   The length of a space in the dashed line.
     */
    public function dashedLine(
        $x0, $y0, $x1, $y1, $color = 'black', $width = 1, $dash_length = 2,
        $dash_space = 2
    )
    {
        $c = $this->_allocateColor($color);
        $w = $this->_allocateColor('white');

        // Set up the style array according to the $dash_* parameters.
        $style = array();
        for ($i = 0; $i < $dash_length; $i++) {
            $style[] = $c;
        }
        for ($i = 0; $i < $dash_space; $i++) {
            $style[] = $w;
        }

        $this->call('imageSetStyle', array($this->_im, $style));
        $this->call('imageSetThickness', array($this->_im, $width));
        $this->call(
            'imageLine',
            array($this->_im, $x0, $y0, $x1, $y1, IMG_COLOR_STYLED)
        );
    }

    /**
     * Draws a polyline (a non-closed, non-filled polygon) based on a set of
     * vertices.
     *
     * @param array $vertices  An array of x and y labeled arrays
     *                         (eg. $vertices[0]['x'], $vertices[0]['y'], ...).
     * @param string $color    The color you want to draw the line with.
     * @param string $width    The width of the line.
     */
    public function polyline($verts, $color, $width = 1)
    {
        $first = true;
        foreach ($verts as $vert) {
            if (!$first) {
                $this->line(
                    $lastX, $lastY,
                    $vert['x'], $vert['y'],
                    $color, $width
                );
            } else {
                $first = false;
            }
            $lastX = $vert['x'];
            $lastY = $vert['y'];
        }
    }

    /**
     * Draws an arc.
     *
     * @param integer $x      The x coordinate of the centre.
     * @param integer $y      The y coordinate of the centre.
     * @param integer $r      The radius of the arc.
     * @param integer $start  The start angle of the arc.
     * @param integer $end    The end angle of the arc.
     * @param string  $color  The line color of the arc.
     * @param string  $fill   The fill color of the arc (defaults to none).
     */
    public function arc(
        $x, $y, $r, $start, $end, $color = 'black', $fill = null
    )
    {
        $c = $this->_allocateColor($color);
        if (is_null($fill)) {
            $this->call(
                'imageArc',
                array($this->_im, $x, $y, $r * 2, $r * 2, $start, $end, $c)
            );
        } else {
            if ($fill !== $color) {
                $f = $this->_allocateColor($fill);
                $this->call(
                    'imageFilledArc',
                    array(
                        $this->_im,
                        $x, $y, $r * 2, $r * 2,
                        $start, $end,
                        $f, IMG_ARC_PIE
                    )
                );
                $this->call(
                    'imageFilledArc',
                    array(
                        $this->_im,
                        $x, $y, $r * 2, $r * 2,
                        $start, $end,
                        $c, IMG_ARC_EDGED | IMG_ARC_NOFILL
                    )
                );
            } else {
                $this->call(
                    'imageFilledArc',
                    array(
                        $this->_im,
                        $x, $y, $r * 2, $r * 2,
                        $start, $end,
                        $c, IMG_ARC_PIE
                    )
                );
            }
        }
    }

    /**
     * Creates an image of the given size.
     *
     * If possible the function returns a true color image.
     *
     * @param integer $width   The image width.
     * @param integer $height  The image height.
     *
     * @return resource  The image handler.
     * @throws Horde_Image_Exception
     */
    public function create($width, $height)
    {
        $result = $this->call('imageCreateTrueColor', array($width, $height));
        if (!is_resource($result)) {
            throw new Horde_Image_Exception('Could not create image.');
        }
        $this->call('imagesavealpha', array($result, true));
        $this->call('imagealphablending', array($result, false));
        if (function_exists('imageantialias')) {
            $this->call('imageantialias', array($result, true));
        }

        return $result;
    }

    /**
     * Wraps a call to a function of the gd extension.
     *
     * @param string $function  The name of the function to wrap.
     * @param array $params     An array with all parameters for that function.
     *
     * @return mixed  The result of the function call.
     * @throws Horde_Image_Exception
     */
    public function call($function, $params = null)
    {
        unset($php_errormsg);
        $track = ini_set('track_errors', 1);
        $result = @call_user_func_array($function, $params);
        if ($track !== false) {
            ini_set('track_errors', $track);
        }
        if (!empty($php_errormsg)) {
            $error_msg = $php_errormsg;
            throw new Horde_Image_Exception($error_msg);
        }
        return $result;
    }

    /**
     * Applies the specified mask to the image.
     *
     * @param resource $mask  The gd image resource representing the mask
     */
    public function applyMask($mask)
    {
        $imgX = round($this->call('imageSX', array($this->_im)));
        $imgY = round($this->call('imageSY', array($this->_im)));
        $mask_resized = $this->create($imgX, $imgY);
        $this->call(
            'imageCopyResampled',
            array(
                $mask_resized, $mask,
                0, 0, 0, 0,
                $imgX, $imgY,
                $this->call('imageSX', array($mask)),
                $this->call('imageSY', array($mask))
            )
        );

        $mask_blendtemp = $this->create($imgX, $imgY);
        $mbtX = $this->call('imageSX', array($mask_blendtemp));
        $mbtY = $this->call('imageSY', array($mask_blendtemp));

        $color_background = $this->call(
            'imageColorAllocate',
            array($mask_blendtemp, 0, 0, 0)
        );

        $this->call(
            'imageFilledRectangle',
            array($mask_blendtemp, 0, 0, $mbtX, $mbtY, $color_background)
        );

        $this->call('imageAlphaBlending', array($mask_blendtemp, false));
        $this->call('imageSaveAlpha', array($mask_blendtemp, true));

        for ($x = 0; $x < $imgX; $x++) {
            for ($y = 0; $y < $imgY; $y++) {
                $realPixel = $this->call(
                    'imageColorsForIndex',
                    array(
                        $this->_im,
                        $this->call('imageColorAt', array($this->_im, $x, $y))
                    )
                );
                $maskPixel = Horde_Image::grayscalePixel(
                    $this->call(
                        'imageColorsForIndex',
                        array(
                            $mask_resized,
                            $this->call(
                                'imageColorAt',
                                array($mask_resized, $x, $y)
                            )
                        )
                    )
                );
                $maskAlpha = 127
                    - floor($maskPixel['red'] / 2)
                    * (1 - ($realPixel['alpha'] / 127));
                $newcolor = $this->call(
                    'imageColorAllocateAlpha',
                    array(
                        $mask_blendtemp,
                        $realPixel['red'],
                        $realPixel['green'],
                        $realPixel['blue'],
                        intval($maskAlpha)
                    )
                );
                $this->call(
                    'imageSetPixel',
                    array($mask_blendtemp, $x, $y, $newcolor)
                );
            }
        }
        $this->call('imageAlphaBlending', array($this->_im, false));
        $this->call('imageSaveAlpha', array($this->_im, true));
        $this->call(
            'imageCopy',
            array($this->_im, $mask_blendtemp, 0, 0, 0, 0, $mbtX, $mbtY)
        );

        $this->call('imageDestroy', array($mask_blendtemp));
        $this->call('imageDestroy', array($mask_resized));
    }

}
