<?php
/**
 * This class defines the Horde_Image:: API, and also provides some
 * utility functions, such as generating highlights of a color.
 *
 * $Horde: horde/lib/Image.php,v 1.17 2003/08/04 12:45:05 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.17 $
 * @since   Horde 3.0
 * @package Horde_Image
 */
class Horde_Image {

    /**
     * Current color.
     * @var string $_rgb
     */
    var $_rgb = 'ff0000';

    /**
     * Observers.
     * @var array $_observers
     */
    var $_observers = array();

    /**
     * Capabilites of this driver.
     * @var array $_capabilities
     */
    var $_capabilities = array();

    /**
     * The current image data.
     * @var string $_data
     */
    var $_data = '';

    /**
     * The current image id.
     * @var string $_id
     */
    var $_id = '';

    var $_width;
    var $_height;

    /**
     * Constructor.
     *
     * @param string $rgb  The base color for generated pixels/images.
     */
    function Horde_Image($params)
    {
        if (!empty($params['rgb'])) {
            $this->_rgb = $params['rgb'];
        }
    }

    function getCapabilites()
    {
        return $this->_capabilities;
    }

    function hasCapability($capability)
    {
        return in_array($capability, $this->_capabilities);
    }

    function getLink($url, $title = '')
    {
        return Horde::img($url, $title, '', '');
    }

    /**
     * Generate image headers.
     */
    function headers()
    {
        header('Content-type: ' . $this->getContentType());
    }

    /**
     * Calculate a lighter (or darker) version of a color.
     * @static
     *
     * @param string $color  An HTML color, e.g.: #ffffcc.
     *
     * @return string  A modified HTML color.
     */
    function modifyColor($color, $factor = 0x11)
    {
        $r = hexdec(substr($color, 1, 2));
        $g = hexdec(substr($color, 3, 2));
        $b = hexdec(substr($color, 5, 2));

        if ($r >= $g && $r >= $b) {
            $g = $g / $r;
            $b = $b / $r;

            $r = $r + $factor;
            $g = floor($g * $r);
            $b = floor($b * $r);
        } elseif ($g >= $r && $g >= $b) {
            $r = $r / $g;
            $b = $b / $g;

            $g = $g + $factor;
            $r = floor($r * $g);
            $b = floor($b * $g);
        } else {
            $r = $r / $b;
            $g = $g / $b;

            $b = $b + $factor;
            $r = floor($r * $b);
            $g = floor($g * $b);
        }

        return '#' . str_pad(dechex(min($r, 255)), 2, '0', STR_PAD_LEFT) . str_pad(dechex(min($g, 255)), 2, '0', STR_PAD_LEFT) . str_pad(dechex(min($b, 255)), 2, '0', STR_PAD_LEFT);
    }

    function getRGB($colorname)
    {
        require_once dirname(__FILE__) . '/Image/rgb.php';
        return isset($GLOBALS['horde_image_rgb_colors'][$colorname]) ?
            $GLOBALS['horde_image_rgb_colors'][$colorname] :
            array(0, 0, 0);
    }

    function getHexColor($colorname)
    {
        require_once dirname(__FILE__) . '/Image/rgb.php';
        if (isset($GLOBALS['horde_image_rgb_colors'][$colorname])) {
            list($r, $g, $b) = $GLOBALS['horde_image_rgb_colors'][$colorname];
            return '#' . str_pad(dechex(min($r, 255)), 2, '0', STR_PAD_LEFT) . str_pad(dechex(min($g, 255)), 2, '0', STR_PAD_LEFT) . str_pad(dechex(min($b, 255)), 2, '0', STR_PAD_LEFT);
        } else {
            return 'black';
        }
    }

    /**
     * Draw a shaped point at the specified (x,y) point. Useful for
     * scatter diagrams, debug points, etc. Draws squares, circles,
     * diamonds, and triangles.
     *
     * @param integer $x      The x coordinate of the point to brush.
     * @param integer $y      The y coordinate of the point to brush.
     * @param string  $color  The color to brush the point with.
     * @param string  $shape  What brush to use? Defaults to a square.
     */
    function brush($x, $y, $color = 'black', $shape = 'square')
    {
        switch ($shape) {
        case 'triangle':
            $verts[0] = array('x' => $x + 3, 'y' => $y + 3);
            $verts[1] = array('x' => $x, 'y' => $y - 3);
            $verts[2] = array('x' => $x - 3, 'y' => $y + 3);
            $this->polygon($verts, $color, $color);
            break;

        case 'circle':
            $this->circle($x, $y, 3, $color, $color);
            break;

        case 'diamond':
            $verts[0] = array('x' => $x - 3, 'y' => $y);
            $verts[1] = array('x' => $x, 'y' => $y + 3);
            $verts[2] = array('x' => $x + 3, 'y' => $y);
            $verts[3] = array('x' => $x, 'y' => $y - 3);
            $this->polygon($verts, $color, $color);
            break;

        case 'square':
        default:
            $this->rectangle($x - 2, $y - 2, 4, 4, $color, $color);
            break;
        }
    }

    /**
     * Add an observer to this image. The observer will be notified
     * when the image's changes.
     */
    function addObserver($method, &$object)
    {
        $this->_observers[] = array($method, &$object);
    }

    /**
     * Let observers know that something happened worth acting on.
     */
    function notifyObservers()
    {
        for ($i = 0; $i < count($this->_observers); ++$i) {
            $obj = &$this->_observers[$i][1];
            $method = $this->_observers[$i][0];
            $obj->$method($this);
        }
    }

    /**
     * Get the height and width of the current image data.
     *
     * @return array  An hash with 'width' containing the width,
     *                'height' containing the height of the image.
     */
    function getDimensions()
    {
        $tmp = $this->toFile();
        $details = getimagesize($tmp);
        unlink($tmp);

        return array('width' => $details[0],
                     'height' => $details[1]);
    }

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

    function toFile()
    {
        $tmp = Horde::getTempFile('img', false);
        $fp = @fopen($tmp, 'wb');
        fwrite($fp, $this->_data);
        fclose($fp);
        return $tmp;
    }

    /**
     * Get an x,y pair on circle, assuming center is 0,0.
     *
     * @param double $degrees   The degrees of arc to get the point for.
     * @param int    $diameter  The diameter of the circle.
     *
     * @return array (x coordinate, y coordinate) of the point.
     */
    function getCirclePoint($degrees, $diameter)
    {
        // Avoid problems with doubles.
        $degrees += 0.0001;

        $x = cos(deg2rad($degrees)) * ($diameter / 2);
        $y = sin(deg2rad($degrees)) * ($diameter / 2);

        return array($x, $y);
    }

    /**
     * Get point coordinates at the limits of an arc. Only valid for
     * angles ($end - $start) <= 45 degrees.
     * @access private
     *
     * @param integer $r      The radius of the arc.
     * @param integer $start  The starting angle.
     * @param integer $end    The ending angle.
     */
    function _getArcPoints($r, $start, $end)
    {
        // Start point.
        $pts['x1'] = $r * cos(deg2rad($start));
        $pts['y1'] = $r * sin(deg2rad($start));

        // End point.
        $pts['x2'] = $r * cos(deg2rad($end));
        $pts['y2'] = $r * sin(deg2rad($end));

        // Anchor point.
        $a3 = ($start + $end) / 2;
        $r3 = $r / cos(deg2rad(($end - $start) / 2));
        $pts['x3'] = $r3 * cos(deg2rad($a3));
        $pts['y3'] = $r3 * sin(deg2rad($a3));

        return $pts;
    }

    /**
     * Attempts to return a concrete Horde_Image instance based on
     * $driver.
     *
     * @param mixed $driver  The type of concrete Horde_Image subclass to return.
     *                       This is based on the storage driver ($driver). The
     *                       code is dynamically included. If $driver is an array,
     *                       then we will look in $driver[0]/lib/Image/ for
     *                       the subclass implementation named $driver[1].php.
     * @param array $params  (optional) A hash containing any additional
     *                       configuration or connection parameters a subclass
     *                       might need.
     *
     * @return object Horde_Image  The newly created concrete Horde_Image instance,
     *                             or false on an error.
     */
    function &factory($driver, $params)
    {
        if (is_array($driver)) {
            list($app, $driver) = $driver;
        }

        if (!empty($app)) {
            require_once $GLOBALS['registry']->getParam('fileroot', $app) . '/lib/Image/' . $driver . '.php';
        } elseif (@file_exists(dirname(__FILE__) . '/Image/' . $driver . '.php')) {
            require_once dirname(__FILE__) . '/Image/' . $driver . '.php';
        } else {
            @include_once 'Horde/Image/' . $driver . '.php';
        }
        $class = 'Horde_Image_' . $driver;
        if (class_exists($class)) {
            return new $class($params);
        } else {
            return PEAR::raiseError('Class definition of ' . $class . ' not found.');
        }
    }

    /**
     * Attempts to return a reference to a concrete Horde_Image
     * instance based on $driver. It will only create a new instance
     * if no Horde_Image instance with the same parameters currently
     * exists.
     *
     * This should be used if multiple types of image renderers (and,
     * thus, multiple Horde_Image instances) are required.
     *
     * This method must be invoked as: $var = &Horde_Image::singleton()
     *
     * @param mixed $driver  The type of concrete Horde_Image subclass to return.
     *                       This is based on the storage driver ($driver). The
     *                       code is dynamically included. If $driver is an array,
     *                       then we will look in $driver[0]/lib/Image/ for
     *                       the subclass implementation named $driver[1].php.
     * @param array $params  (optional) A hash containing any additional
     *                       configuration or connection parameters a subclass
     *                       might need.
     *
     * @return object Horde_Image  The concrete Horde_Image reference, or false
     *                             on an error.
     */
    function &singleton($driver, $params = array())
    {
        static $instances;
        if (!isset($instances)) {
            $instances = array();
        }

        $signature = serialize(array($driver, $params));
        if (!isset($instances[$signature])) {
            $instances[$signature] = &Horde_Image::factory($driver, $params);
        }

        return $instances[$signature];
    }

}
