<?php

require_once dirname(__FILE__) . '/../Image.php';

/**
 * This class implements the Horde_Image:: API for the PHP GD
 * extension. It mainly provides some utility functions, such as the
 * ability to make pixels, for now.
 *
 * $Horde: horde/lib/Image/gd.php,v 1.24 2003/07/29 16:53:39 chuck Exp $
 *
 * Copyright 2002-2003 Chuck Hagenbuch <chuck@horde.org>
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
 *
 * @author  Chuck Hagenbuch <chuck@horde.org>
 * @version $Revision: 1.24 $
 * @since   Horde 3.0
 * @package Horde_Image
 */
class Horde_Image_gd extends Horde_Image {

    /**
     * Capabilites of this driver.
     * @var array $_capabilities
     */
    var $_capabilities = array('resize',
                               'rotate',
                               'flip',
                               'mirror',
                               'grayscale',
                               'sepia',
                               'yellowize',
                               'canvas',
                         );

    /**
     * What kind of images should GD generate? Defaults to 'png'.
     * @var string $_type
     */
    var $_type = 'png';

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

    /**
     * String identifier of the current image. New image data will not
     * be loaded if the same id is already loaded.
     * @var string $_id
     */
    var $_id = '';

    function Horde_Image_gd($params)
    {
        parent::Horde_Image($params);
        if (!empty($params['type'])) {
            $this->_type = $params['type'];
        }

        if (!empty($params['width'])) {
            $this->_width = $params['width'];
            $this->_height = $params['height'];
            $this->_im = imageCreateTrueColor($this->_width, $this->_height);

            if (!empty($params['background'])) {
                $rgb = $this->getRGB($params['background']);
            } else {
                $rgb = array(255, 255, 255);
            }
            $background = imageColorAllocate($this->_im, $rgb[0], $rgb[1], $rgb[2]);
            imageFill($this->_im, 0, 0, $background);
        }
    }

    function getContentType()
    {
        return 'image/' . $this->_type;
    }

    /**
     * Display the current image.
     */
    function display()
    {
        $this->headers();
        $function = 'image' . $this->_type;
        $function($this->_im);
    }

    /**
     * Return the raw data for the current image.
     *
     * @return string  Image data.
     */
    function raw()
    {
        return Horde::bufferOutput('image' . $this->_type, $this->_im);
    }

    /**
     * Get the height and width of an image.
     *
     * @return array  An hash with 'width' containing the width,
     *                'height' containing the height of the image.
     */
    function getDimensions()
    {
        return array('width' => imageSX($this->_im),
                     'height' => imageSY($this->_im));
    }

    /**
     * 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.
     *
     * @return integer  The resource of the color that can be passed to GD.
     */
    function allocateColor($name)
    {
        list($r, $g, $b) = $this->getRGB($name);
        return imageColorAllocate($this->_im, $r, $g, $b);
    }

    function getFont($font)
    {
        return 2;
    }

    function loadString($id, $image_data)
    {
        if ($id != $this->_id) {
            if ($this->_im) {
                imageDestroy($this->_im);
            }
            $this->_im = imageCreateFromString($image_data);
            $this->_id = $id;
        }
    }

    function makePixel($rgb = null)
    {
        if (!is_null($rgb)) {
            $this->_rgb = $rgb;
        }
        $rgb = str_replace('#', '', $this->_rgb);

        $r = hexdec(substr($rgb, 0, 2));
        $g = hexdec(substr($rgb, 2, 2));
        $b = hexdec(substr($rgb, 4, 2));

        $i = imageCreate(1, 1);
        $c = imageColorAllocate($i, $r, $g, $b);
        imageSetPixel($i, 0, 0, $c);

    }

    function makeBarcode($text)
    {
        $barcodeheight = 40;
        $barcodethinwidth = 2;
        $barcodethickwidth = $barcodethinwidth * 3;
        $codingmap = array('0' => '000110100', '1' => '100100001',
                           '2' => '001100001', '3' => '101100000', '4' => '000110001',
                           '5' => '100110000', '6' => '001110000', '7' => '000100101',
                           '8' => '100100100', '9' => '001100100', 'A' => '100001001',
                           'B' => '001001001', 'C' => '101001000', 'D' => '000011001',
                           'E' => '100011000', 'F' => '001011000', 'G' => '000001101',
                           'H' => '100001100', 'I' => '001001100', 'J' => '000011100',
                           'K' => '100000011', 'L' => '001000011', 'M' => '101000010',
                           'N' => '000010011', 'O' => '100010010', 'P' => '001010010',
                           'Q' => '000000111', 'R' => '100000110', 'S' => '001000110',
                           'T' => '000010110', 'U' => '110000001', 'V' => '011000001',
                           'W' => '111000000', 'X' => '010010001', 'Y' => '110010000',
                           'Z' => '011010000', ' ' => '011000100', '$' => '010101000',
                           '%' => '000101010', '*' => '010010100', '+' => '010001010',
                           '-' => '010000101', '.' => '110000100', '/' => '010100010');
        $text = String::upper($text, true);

        // Add start/stop chars.
        $text = "*$text*";

        $textlen = strlen($text);
        $barcodewidth = ($textlen) * (7 * $barcodethinwidth
                                      + 3 * $barcodethickwidth) - $barcodethinwidth;
        $this->_im = imageCreate($barcodewidth, $barcodeheight);
        $black = imageColorAllocate($this->_im, 0, 0, 0);
        $white = imageColorAllocate($this->_im, 255, 255, 255);
        imageFill($this->_im, 0, 0, $white);
        $xpos = 0;
        for ($idx = 0; $idx < $textlen; $idx++) {
            $char = substr($text, $idx, 1);
            // Make unknown chars a '-'.
            if (!isset($codingmap[$char])) {
                $char = '-';
            }
            for ($baridx = 0; $baridx <= 8; $baridx++) {
                $elementwidth = (substr($codingmap[$char], $baridx, 1)) ?
                    $barcodethickwidth : $barcodethinwidth;
                if (($baridx + 1) % 2) {
                    imageFilledRectangle($this->_im, $xpos, 0, $xpos + $elementwidth - 1,
                                         $barcodeheight, $black);
                }
                $xpos += $elementwidth;
            }
            $xpos += $barcodethinwidth;
        }
    }

    /**
     * Resize an image.
     *
     * @param integer $width      The new width.
     * @param integer $height     The new height.
     * @param boolean $ratio      Maintain original aspect ratio.
     *
     * @return string The image resized.
     */
    function resize($width, $height, $ratio = true)
    {
        if ($ratio) {
            if ($width / $height > imageSX($this->_im) / imageSY($this->_im)) {
                $width = $height * imageSX($this->_im) / imageSY($this->_im);
            } else {
                $height = $width * imageSY($this->_im) / imageSX($this->_im);
            }
        }

        $im = $this->_im;
        $this->_im = imageCreateTrueColor($width, $height);
        imageFill($this->_im, 0, 0, imageColorAllocate($this->_im, 255, 255, 255));
        imageCopyResampled($this->_im, $im, 0, 0, 0, 0, $width, $height, imageSX($im), imageSY($im));
    }

    /**
     * Rotate an image.
     *
     * @param integer $angle       The angle to rotate the image by,
     *                             in the clockwise direction
     * @param integer $background  The background color to fill any triangles
     *
     * @return string  The rotated image.
     */
    function rotate($angle, $background = 'white')
    {
        $background = $this->allocateColor($background);

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

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

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

    /**
     * Flip an image.
     *
     * @return string  The flipped image.
     */
    function flip()
    {
        $x = imageSX($this->_im);
        $y = imageSY($this->_im);

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

        $this->_im = $im;
    }

    /**
     * Mirror an image.
     *
     * @return string  The mirrored image.
     */
    function mirror()
    {
        $x = imageSX($this->_im);
        $y = imageSY($this->_im);

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

        $this->_im = $im;
    }

    /**
     * Convert an image to grayscale.
     *
     * @return string  The grayscale image.
     */
    function grayscale()
    {
        $rateR = .229;
        $rateG = .587;
        $rateB = .114;
        $whiteness = 3;

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

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

    /**
     * Sepia filter.
     *
     * Basically turns the image to grayscale and then adds some
     * defined tint on it (R += 30, G += 43, B += -23) so it will
     * appear to be a very old picture.
     */
    function sepia()
    {
        $tintR = 80;
        $tintG = 43;
        $tintB = -23;
        $rateR = .229;
        $rateG = .587;
        $rateB = .114;
        $whiteness = 3;

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

        $colors = max(256, imageColorsTotal($this->_im));
        for ($x = 0; $x < $colors; $x++) {
            $src = imageColorsForIndex($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);
            imageColorSet($this->_im, $x, $r, $g, $b);
        }
    }

    /**
     * Yellowize filter.
     *
     * Adds a layer of yellow that can be transparent or solid.  If
     * $intensityA 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)
     */
    function yellowize($intensityY = 50, $intensityB = 3)
    {
        if (imageIsTrueColor($this->_im)) {
            imageTrueColorToPalette($this->_im, true, 256);
        }

        $colors = max(256, imageColorsTotal($this->_im));
        for ($x = 0; $x < $colors; $x++) {
            $src = imageColorsForIndex($this->_im, $x);
            $r = min($src['red'] + $intensityY, 255);
            $g = min($src['green'] + $intensityY, 255);
            $b = max(($r + $g) / max($intensityB, 2), 0);
            imageColorSet($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.
     */
    function text($string, $x, $y, $font = 'monospace', $color = 'black', $direction = 0)
    {
        $c = $this->allocateColor($color);
        $f = $this->getFont($font);
        switch ($direction) {
        case -90:
        case 270:
            imageStringUp($this->_im, $f, $x, $y, $string, $c);
            break;

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

    /**
     * Draw a circle.
     *
     * @param integer $x      The x co-ordinate of the centre.
     * @param integer $y      The y co-ordinate of the centre.
     * @param integer $r      The radius of the circle.
     * @param string  $color  The line color of the circle.
     * @param string  $fill   (optional) The color to fill the circle.
     */
    function circle($x, $y, $r, $color, $fill = null)
    {
        $c = $this->allocateColor($color);
        if (is_null($fill)) {
            imageEllipse($this->_im, $x, $y, $r * 2, $r * 2, $c);
        } else {
            if ($fill !== $color) {
                $fillColor = $this->allocateColor($fill);
                imageFilledEllipse($this->_im, $x, $y, $r * 2, $r * 2, $fillColor);
                imageEllipse($this->_im, $x, $y, $r * 2, $r * 2, $c);
            } else {
                imageFilledEllipse($this->_im, $x, $y, $r * 2, $r * 2, $c);
            }
        }
    }

    /**
     * Draw 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      (optional) The color to fill the polygon.
     */
    function polygon($verts, $color, $fill = 'none')
    {
        $vertices = array();
        foreach ($verts as $vert) {
            $vertices[] = $vert['x'];
            $vertices[] = $vert['y'];
        }

        $c = $this->allocateColor($color);
        if ($fill != 'none') {
            $f = $this->allocateColor($fill);
            imageFilledPolygon($this->_im, $vertices, count($verts), $f);
        }

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

    /**
     * Draw 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    (optional) The color to fill the rectangle.
     */
    function rectangle($x, $y, $width, $height, $color, $fill = 'none')
    {
        $c = $this->allocateColor($color);
        if ($fill != 'none') {
            $f = $this->allocateColor($fill);
            imageFilledRectangle($this->_im, $x, $y, $x + $width, $y + $height, $f);
        }

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

    /**
     * Draw a 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  (optional) The line color.
     * @param string  $width  (optional) The width of the line.
     */
    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) {
            imageLine($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);
            imageFilledRectangle($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);
            imageFilledRectangle($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);
            imageFilledPolygon($this->_im, $verts, count($verts) / 2, $c);
        }
    }

    /**
     * Draw 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        (optional) The line color.
     * @param string  $width        (optional) 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
     */
    function dashedLine($x1, $y1, $x2, $y2, $color = 'black', $width = 1, $dash_length = 2, $dash_space = 2)
    {
        $color = $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[] = $color;
        }
        for ($i = 0; $i < $dash_space; $i++) {
            $style[] = $w;
        }

        imageSetStyle($this->_im, $style);
        imageSetThickness($this->_im, $width);
        imageLine($this->_im, $x1, $y1, $x2, $y2, IMG_COLOR_STYLED);
    }

    /**
     * Draw 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     (optional) The width of the line.
     */
    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'];
        }
    }

    /**
     * Draw an arc.
     *
     * @param integer $x      The x co-ordinate of the centre.
     * @param integer $y      The y co-ordinate 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).
     */
    function arc($x, $y, $r, $start, $end, $color = 'black', $fill = null)
    {
        $c = $this->allocateColor($color);
        if (is_null($fill)) {
            imageArc($this->_im, $x, $y, $r * 2, $r * 2, $start, $end, $c);
        } else {
            if ($fill !== $color) {
                $fillColor = $this->allocateColor($fill);
                imageFilledArc($this->_im, $x, $y, $r * 2, $r * 2, $start, $end, $fillColor, IMG_ARC_PIE);
                imageFilledArc($this->_im, $x, $y, $r * 2, $r * 2, $start, $end, $c, IMG_ARC_EDGED | IMG_ARC_NOFILL);
            } else {
                imageFilledArc($this->_im, $x, $y, $r * 2, $r * 2, $start, $end, $c, IMG_ARC_PIE);
            }
        }
    }

}
