<?php
/**
 * Copyright 2002-2017 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file COPYING 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 defines the Horde_Image API, and also provides some utility
 * functions, such as generating highlights of a color.
 *
 * @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
 */
abstract class Horde_Image_Base extends EmptyIterator
{
    /**
     * Background color.
     *
     * @var string
     */
    protected $_background = 'white';

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

    /**
     * The current image data.
     *
     * @var Horde_Stream
     */
    protected $_data;

    /**
     * Logger.
     */
    protected $_logger;

    /**
     * The current width of the image data.
     *
     * @var integer
     */
    protected $_width = 0;

    /**
     * The current height of the image data.
     *
     * @var integer
     */
    protected $_height = 0;

    /**
     * A directory for temporary files.
     *
     * @var string
     */
    protected $_tmpdir;

    /**
     * Array containing available Effects
     *
     * @var array
     */
    protected $_loadedEffects = array();

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

    /**
     * Cache the context
     *
     * @param array
     */
    protected $_context;

    /**
     * Constructor.
     *
     * @param array $params   The image object parameters. Values include:
     *                        - background: (string) The background color.
     *                                      DEFAULT: white.
     *                        - data: (string) The image binary data.
     *                        - height: (integer) The desired image height.
     *                        - type: (string) The output image type (png, jpeg
     *                                etc.). DEFAULT: png.
     *                        - width: (integer) The desired image width.
     * @param array $context  The object context - configuration, injected
     *                        objects:
     *                        - logger: (Horde_Log_Logger) A logger.
     *                        - tmpdir: [REQUIRED] (string) Temporary directory.
     *
     * @throws InvalidArgumentException
     */
    protected function __construct($params, $context = array())
    {
        $this->_params = $params;
        $this->_context = $context;

        if (empty($context['tmpdir'])) {
            throw new InvalidArgumentException(
                'A path to a temporary directory is required.'
            );
        }
        $this->_tmpdir = $context['tmpdir'];

        $this->_logger = !empty($context['logger'])
            ? $context['logger']
            : new Horde_Support_Stub();

        if (isset($params['width'])) {
            $this->_width = (integer)$params['width'];
        }
        if (isset($params['height'])) {
            $this->_height = (integer)$params['height'];
        }
        if (!empty($params['type'])) {
            // We only want the extension, not the full mimetype.
            if (strpos($params['type'], 'image/') !== false) {
                $params['type'] = substr($params['type'], 6);
            }
            $this->_type = $params['type'];
        }
        if (!empty($params['background'])) {
            $this->_background = $params['background'];
        }
    }

    /**
     * Catch-all method so that we don't error out when calling an unsupported
     * manipulation method.
     */
    public function __call($method, $args)
    {
    }

    /**
     * Returns the capabilities.
     *
     * @return array  A list of backend capabilities.
     */
    public function getCapabilities()
    {
        return $this->_capabilities;
    }

    /**
     * Checks the existence of a particular capability.
     *
     * @param string $capability  The capability to check for.
     *
     * @return boolean  True if the backend has this capability.
     */
    public function hasCapability($capability)
    {
        return in_array($capability, $this->_capabilities);
    }

    /**
     * Sends HTTP headers for the image.
     */
    public function headers()
    {
        header('Content-type: ' . $this->getContentType());
    }

    /**
     * Returns the MIME type for this image.
     *
     * @return string  The MIME type for this image.
     */
    public function getContentType()
    {
        return 'image/' . $this->_type;
    }

    /**
     * Returns the image type.
     *
     * @return string  The type of this image (png, jpg, etc.).
     */
    public function getType()
    {
        return $this->_type;
    }

    /**
     * Sets the output image type.
     *
     * @param string $type  An image type (png, jpg, etc.)
     *
     * @return string  The previous image type.
     */
    public function setType($type)
    {
        // We only want the extension, not the full mimetype.
        if (strpos($type, 'image/') !== false) {
            $type = substr($type, 6);
        }
        $old = $this->_type;
        $this->_type = $type;

        return $old;
    }

    /**
     * Draws 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.
     */
    public 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;
        }
    }

    /**
     * Resets the image data to defaults.
     */
    public function reset()
    {
        if ($this->_data) {
            $this->_data->close();
        }
        $this->_data = null;
        $this->_width = null;
        $this->_height = null;
        $this->_background = 'white';
    }

    /**
     * Returns 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.
     */
    public function getDimensions()
    {
        // Check if we know it already
        if ($this->_width == 0 && $this->_height == 0) {
            $tmp = $this->toFile();
            $details = @getimagesize($tmp);
            list($this->_width, $this->_height) = $details;
            unlink($tmp);
        }

        return array('width' => $this->_width, 'height' => $this->_height);
    }

    /**
     * Loads the image data from a string.
     *
     * @param mixed $image_data  The data to use for the image as a string,
     *                           Horde_Stream, or stream resource.
     */
    public function loadString($image_data)
    {
        $this->reset();
        $this->_data = new Horde_Stream_Temp();
        $this->_data->add($image_data, true);
    }

    /**
     * Loads the image data from a file.
     *
     * @param string $filename  The full path and filename to the file to load
     *                          the image data from.
     *
     * @throws Horde_Image_Exception
     */
    public function loadFile($filename)
    {
        $this->reset();
        if (!file_exists($filename)) {
            throw new Horde_Image_Exception(
                sprintf("The image file, %s, does not exist.", $filename)
            );
        }
        $fp = fopen($filename, 'r');
        $this->_data = new Horde_Stream_Temp();
        $this->_data->add($fp, true);

        if (!$this->_data->length()) {
            throw new Horde_Image_Exception(
                sprintf("Could not load the image file %s", $filename)
            );
        }
    }

    /**
     * Saves image data to file.
     *
     * If $data is false-ish, saves current image data after performing pending
     * operations on the data.  If $data contains raw image data, saves that
     * data to file without regard for the current image data.
     *
     * @param mixed  String or stream resource of binary image data.
     *
     * @return string  Path to temporary file.
     * @throws  Horde_Image_Exception
     */
    public function toFile($data = null)
    {
        if (empty($data)) {
            if ($data = $this->raw(false, array('stream' => true))) {
                return $this->toFile($data);
            }
            throw new Horde_Image_Exception('Unable to copy to file.');
        }

        $tmp = Horde_Util::getTempFile('img', false, $this->_tmpdir);
        $fp = fopen($tmp, 'wb');

        if (is_resource($data)) {
            rewind($data);
            while (!feof($data)) {
                fwrite($fp, fread($data, 8192));
            }
        } elseif ($data) {
            fwrite($fp, $data);
        }

        fclose($fp);
        return $tmp;
    }

    /**
     * Displays the current image.
     */
    public function display()
    {
        $this->headers();
        $data = $this->raw(true, array('stream' => true));
        $output = fopen('php://output', 'w');
        while (!feof($data)) {
            fwrite($output, fread($data, 8192));
        }
    }

    /**
     * Returns the raw data for this image.
     *
     * @param boolean $convert  If true, the image data will be returned in the
     *                          target format, independently from any image
     *                          operations.
     * @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 (empty($options['stream'])) {
            return $this->_data->__toString();
        }

        return $this->_data->stream;
    }

    /**
     * Attempts to apply requested effect to this image.
     *
     * @param string $type   The type of effect to apply.
     * @param array $params  Any parameters for the effect.
     */
    public function addEffect($type, $params)
    {
        $class = str_replace('Horde_Image_', '', get_class($this));
        $params['logger'] = $this->_logger;
        $effect = Horde_Image_Effect::factory($type, $class, $params);
        $effect->setImageObject($this);
        $effect->apply();
    }

    /**
     * Returns a list of available effects for this driver.
     */
    public function getLoadedEffects()
    {
        if (!count($this->_loadedEffects)) {
            $class = str_replace('Horde_Image_', '', get_class($this));
            $this->_loadedEffects = array();
            // First, load the driver-agnostic Effects.
            $path = __DIR__ . '/Effect/';
            if (is_dir($path)) {
                if ($handle = opendir($path)) {
                    while (($file = readdir($handle)) !== false) {
                        if (substr($file, -4, 4) == '.php') {
                            $this->_loadedEffects[] = substr(
                                $file, 0, strlen($file) - 4
                            );
                        }
                    }
                }
            }

            // Driver specific effects.
            $path = $path . $class;
            if (is_dir($path)) {
                if ($handle = opendir($path)) {
                    while (($file = readdir($handle)) !== false) {
                        if (substr($file, -4, 4) == '.php') {
                            $this->_loadedEffects[] = substr(
                                $file, 0, strlen($file) - 4
                            );
                        }
                    }
                }
            }
        }

        return $this->_loadedEffects;
    }

    /**
     * Applies any effects in the effect queue.
     */
    public function applyEffects()
    {
        $this->raw();
    }

    /**
     * Returns the current temporary directory.
     *
     * @return string  The current temporary directory.
     */
    public function getTmpDir()
    {
        return $this->_tmpdir;
    }

    /**
     * Utility function to zero out cached geometry information.
     *
     * Shouldn't really be called from client code, but is needed since effects
     * may need to clear these.
     */
    public function clearGeometry()
    {
        $this->_height = 0;
        $this->_width = 0;
    }

    /**
     * Logs a message at debug level.
     *
     * @param string $message  The log message.
     */
    protected function _logDebug($message)
    {
        $this->_logger->debug($message);
    }

    /**
     * Logs a message at error level.
     *
     * @param string $message  The log message.
     */
    protected function _logErr($message)
    {
        $this->_logger->err($message);
    }

    /**
     * Returns a specific image from the pages of images.
     *
     * @param integer $index  The index to return.
     *
     * @return Horde_Image_Base  The requested image
     */
    public function getImageAtIndex($index)
    {
        if ($index > 0) {
            throw new Horde_Image_Exception('Image index out of bounds.');
        }
        $class = get_class($this);
        return new $class(array('data' => $this->raw()), $this->_context);
    }

    /**
     * Returns the number of image pages available in the image object.
     *
     * @return integer  The number of images.
     */
    public function getImagePageCount()
    {
        return 1;
    }

}
