<?php
/**
 * Copyright 2007-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    Michael J. Rubinsky <mrubinsk@horde.org>
 * @category  Horde
 * @license   http://www.horde.org/licenses/lgpl21 LGPL-2.1
 * @package   Image
 */

/**
 * Imagick driver for the Horde_Image API.
 *
 * @author    Michael J. Rubinsky <mrubinsk@horde.org>
 * @category  Horde
 * @copyright 2007-2017 Horde LLC
 * @license   http://www.horde.org/licenses/lgpl21 LGPL-2.1
 * @package   Image
 *
 * @property-read  Imagick $imagick  The underlaying Imagick object.
 */
class Horde_Image_Imagick extends Horde_Image_Base
{
    /**
     * The underlaying Imagick object.
     *
     * @var Imagick
     */
    protected $_imagick;

    /**
     * Flag for iterator, since calling nextImage on Imagick would result in a
     * fatal error if there are no more images.
     *
     * @var boolean
     */
    private $_noMoreImages = false;

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

    /**
     * Constructor.
     *
     * @see Horde_Image_Base::_construct
     */
    public function __construct($params, $context = array())
    {
        if (!Horde_Util::loadExtension('imagick')) {
            throw new Horde_Image_Exception(
                'Required PECL Imagick extension not found.'
            );
        }

        parent::__construct($params, $context);
        ini_set('imagick.locale_fix', 1);

        $this->_imagick = new Imagick();
        if (!empty($params['filename'])) {
            $this->loadFile($params['filename']);
        } elseif(!empty($params['data'])) {
            $this->loadString($params['data']);
        } else {
            $this->_width = max(array($this->_width, 1));
            $this->_height = max(array($this->_height, 1));
            try {
                $color = new ImagickPixel($this->_background);
            } catch (ImagickPixelException $e) {
                throw new Horde_Image_Exception($e);
            }
            try {
                $this->_imagick->newImage(
                    $this->_width, $this->_height, $color
                );
            } catch (ImagickException $e) {
                throw new Horde_Image_Exception($e);
            }
        }

        try {
            $this->_imagick->setImageFormat($this->_type);
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
    }

    /**
     * Loads the image data from a string.
     *
     * @param string $image_data  The data to use for the image.
     *
     * @throws Horde_Image_Exception
     */
    public function loadString($image_data)
    {
        parent::loadString($image_data);
        $this->_imagick->clear();
        try {
            $this->_data->rewind();
            $this->_imagick->readImageFile($this->_data->stream);
            $this->_imagick->setImageFormat($this->_type);
            $this->_imagick->setIteratorIndex(0);
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
        $this->_data->close();
        unset($this->_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.
     *
     * @throws Horde_Image_Exception
     */
    public function loadFile($filename)
    {
        $this->reset();
        try {
            $this->_imagick->readImage($filename);
            $this->_imagick->setImageFormat($this->_type);
            $this->_imagick->setIteratorIndex(0);
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
        // // Parent function loads image data into $this->_data
        // parent::loadFile($filename);
        // $this->loadString($this->_data);
    }

    /**
     * Sets the output image type.
     *
     * @param string $type  An image type (png, jpg, etc.)
     *
     * @return string  The previous image type.
     */
    public function setType($type)
    {
        $old = parent::setType($type);
        try {
            $this->_imagick->setImageFormat($this->_type);
        } catch (ImagickException $e) {
            // Don't care about an empty wand here.
        }
        return $old;
    }

    /**
     * Returns the raw data for this image.
     *
     * @param boolean $convert  Ignored for Imagick driver.
     * @param array $options    Array of options:
     *     - stream: If true, return as a stream resource.
     *               DEFAULT: false.
     *
     * @return mixed  The raw image data as a string or stream resource.
     */
    public function raw($convert = false, $options = array())
    {
        try {
            $this->_imagick->stripImage();
            if (empty($options['stream'])) {
                return $this->_imagick->getImageBlob();
            }
            $s = new Horde_Stream_Temp();
            $s->add($this->_imagick->getImageBlob(), true);
            return $s->stream;
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
    }

    /**
     * Resets the image data to defaults.
     */
    public function reset()
    {
        parent::reset();
        $this->_imagick->clear();
        $this->_noMoreImages = false;
    }

    /**
     * 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()
    {
        if ($this->_height == 0 && $this->_width == 0) {
            try {
                $size = $this->_imagick->getImageGeometry();
            } catch (ImagickException $e) {
                return array('width' => 0, 'height' => 0);
            }

            $this->_height = $size['height'];
            $this->_width = $size['width'];
        }

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

    /**
     * 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.
     */
    public function resize($width, $height, $ratio = true, $keepProfile = false)
    {
        try {
            if ($keepProfile) {
                $this->_imagick->resizeImage($width, $height, $ratio);
            } else {
                $this->_imagick->thumbnailImage($width, $height, $ratio);
            }
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
        $this->clearGeometry();
    }

    /**
     * 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)
    {
        try {
            $result = $this->_imagick->cropImage($x2 - $x1, $y2 - $y1, $x1, $y1);
            $this->_imagick->setImagePage(0, 0, 0, 0);
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
        $this->clearGeometry();
    }

    /**
     * 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')
    {
        try {
            $this->_imagick->rotateImage($background, $angle);
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
        $this->clearGeometry();
    }

    /**
     * Flips the current image.
     */
    public function flip()
    {
        try {
            $this->_imagick->flipImage();
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
    }

    /**
     * Mirrors the current image.
     */
    public function mirror()
    {
        try {
            $this->_imagick->flopImage();
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
    }

    /**
     * Converts the current image to grayscale.
     */
    public function grayscale()
    {
        try {
            $this->_imagick->setImageType(Imagick::IMGTYPE_GRAYSCALE);
        } catch (ImageException $e) {
            throw new Horde_Image_Exception($e);
        }
    }

    /**
     * Applies a sepia filter.
     *
     * @param integer $threshold  Extent of sepia effect.
     */
    public function sepia($threshold = 85)
    {
        try {
            $this->_imagick->sepiaToneImage($threshold);
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
    }

    /**
     * Draws a text string on the image in a specified location, with the
     * specified style information.
     *
     * @TODO: Need to differentiate between the stroke (border) and the fill
     *        color, but this is a BC break, since we were just not providing a
     *        border.
     *
     * @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'
    )
    {
        $fontsize = Horde_Image::getFontSize($fontsize);
        try {
            $pixel = new ImagickPixel($color);
        } catch (ImagickPixelException $e) {
            throw new Horde_Image_Exception($e);
        }
        try {
            $draw = new ImagickDraw();
            $draw->setFillColor($pixel);
            if (!empty($font)) {
                $draw->setFont($font);
            }
            $draw->setFontSize($fontsize);
            $draw->setGravity(Imagick::GRAVITY_NORTHWEST);
        } catch (ImagickDrawException $e) {
            throw new Horde_Image_Exception($e);
        }
        try {
            $this->_imagick->annotateImage($draw, $x, $y, $direction, $string);
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
        $draw->destroy();
    }

    /**
     * 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 = 'none')
    {
        try {
            $draw = new ImagickDraw();
            $draw->setFillColor(new ImagickPixel($fill));
            $draw->setStrokeColor(new ImagickPixel($color));
            $draw->circle($x, $y, $r + $x, $y);
        } catch (ImagickDrawException $e) {
            throw new Horde_Image_Exception($e);
        } catch (ImagickPixelException $e) {
            throw new Horde_Image_Exception($e);
        }
        try {
            $this->_imagick->drawImage($draw);
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
        $draw->destroy();
    }

    /**
     * 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')
    {
        try {
            $draw = new ImagickDraw();
            $draw->setFillColor(new ImagickPixel($fill));
            $draw->setStrokeColor(new ImagickPixel($color));
            $draw->polygon($verts);
        } catch (ImagickDrawException $e) {
            throw new Horde_Image_Exception($e);
        } catch (ImagickPixelException $e) {
            throw new Horde_Image_Exception($e);
        }
        try {
            $this->_imagick->drawImage($draw);
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
        $draw->destroy();
    }

    /**
     * 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, $fill = 'none')
    {
        try {
            $draw = new ImagickDraw();
            $draw->setStrokeColor(new ImagickPixel($color));
            $draw->setFillColor(new ImagickPixel($fill));
            $draw->rectangle($x, $y, $x + $width, $y + $height);
        } catch (ImagickDrawException $e) {
            throw new Horde_Image_Exception($e);
        } catch (ImagickPixelException $e) {
            throw new Horde_Image_Exception($e);
        }
        try {
            $this->_imagick->drawImage($draw);
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
        $draw->destroy();
    }

    /**
     * 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, $fill
    )
    {
        try {
            $draw = new ImagickDraw();
            $draw->setStrokeColor(new ImagickPixel($color));
            $draw->setFillColor(new ImagickPixel($fill));
            $draw->roundRectangle($x, $y, $x + $width, $y + $height, $round, $round);
        } catch (ImagickDrawException $e) {
            throw new Horde_Image_Exception($e);
        } catch (ImagickPixelException $e) {
            throw new Horde_Image_Exception($e);
        }
        try {
            $this->_imagick->drawImage($draw);
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
        $draw->destroy();
    }

    /**
     * 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($x0, $y0, $x1, $y1, $color = 'black', $width = 1)
    {
        try {
            $draw = new ImagickDraw();
            $draw->setStrokeColor(new ImagickPixel($color));
            $draw->setStrokeWidth($width);
            $draw->line($x0, $y0, $x1, $y1);
        } catch (ImagickDrawException $e) {
            throw new Horde_Image_Exception($e);
        } catch (ImagickPixelException $e) {
            throw new Horde_Image_Exception($e);
        }
        try {
            $this->_imagick->drawImage($draw);
        } catch (ImagickException $e) {
            throw Horde_Image_Exception($e);
        }
        $draw->destroy();
    }

    /**
     * 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
    )
    {
        try {
            $draw = new ImagickDraw();
            $draw->setStrokeColor(new ImagickPixel($color));
            $draw->setStrokeWidth($width);
            $draw->setStrokeDashArray(array($dash_length, $dash_space));
            $draw->line($x0, $y0, $x1, $y1);
        } catch (ImagickDrawException $e) {
            throw new Horde_Image_Exception($e);
        } catch (ImagickPixelException $e) {
            throw new Horde_Image_Exception($e);
        }
        try {
            $this->_imagick->drawImage($draw);
        } catch (ImageException $e) {
            throw new Horde_Image_Exception($e);
        }
        $draw->destroy();
    }

    /**
     * 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)
    {
        try {
            $draw = new ImagickDraw();
            $draw->setStrokeColor(new ImagickPixel($color));
            $draw->setStrokeWidth($width);
            $draw->setFillColor(new ImagickPixel('none'));
            $draw->polyline($verts);
        } catch (ImagickDrawException $e) {
            throw new Horde_Image_Exception($e);
        } catch (ImagickPixelException $e) {
            throw new Horde_Image_Exception($e);
        }
        try {
            $this->_imagick->drawImage($draw);
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
        $draw->destroy();
    }

    /**
     * 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 = 'none'
    )
    {
        $points = Horde_Image::arcPoints($r, $start, $end);
        $points['x1'] += $x;
        $points['x2'] += $x;
        $points['x3'] += $x;
        $points['y1'] += $y;
        $points['y2'] += $y;
        $points['y3'] += $y;

        try {
            $draw = new ImagickDraw();
            $draw->setStrokeColor(new ImagickPixel($color));
            $draw->setFillColor(new ImagickPixel($fill));
            $draw->arc($x - $r, $y - $r, $x + $r, $y + $r, $start, $end);
        } catch (ImagickDrawException $e) {
            throw new Horde_Image_Exception($e);
        } catch (ImagickPixelException $e) {
            throw new Horde_Image_Exception($e);
        }

        // If filled, draw the outline.
        if (!empty($fill)) {
            $mid = round(($start + $end) / 2);
            list($x1, $y1) = Horde_Image::circlePoint($start, $r * 2);
            list($x2, $y2) = Horde_Image::circlePoint($mid, $r * 2);
            list($x3, $y3) = Horde_Image::circlePoint($end, $r * 2);

            $verts = array(
                array('x' => $x + round($x3), 'y' => $y + round($y3)),
                array('x' => $x, 'y' => $y),
                array('x' => $x + round($x1), 'y' => $y + round($y1))
            );

            if ($mid > 90) {
                $verts1 = array(
                    array('x' => $x + round($x2), 'y' => $y + round($y2)),
                    array('x' => $x, 'y' => $y),
                    array('x' => $x + round($x1), 'y' => $y + round($y1))
                );
                $verts2 = array(
                    array('x' => $x + round($x3), 'y' => $y + round($y3)),
                    array('x' => $x, 'y' => $y),
                    array('x' => $x + round($x2), 'y' => $y + round($y2))
                );

                $this->polygon($verts1, $fill, $fill);
                $this->polygon($verts2, $fill, $fill);
            } else {
                $this->polygon($verts, $fill, $fill);
            }

            $this->polyline($verts, $color);
        }
        try {
            $this->_imagick->drawImage($draw);
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
        $draw->destroy();
    }

    /**
     * Applies any effects in the effect queue.
     */
    public function applyEffects()
    {
        // noop for this driver.
    }

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

    /**
     * Utility function to wrap Imagick::borderImage.
     *
     * Use when you don't want to replace all pixels in the clipping area with
     * the border color i.e.  you want to "frame" the existing image. Preserves
     * transparency etc.
     *
     * @param Imagick &$image  The Imagick object to border.
     * @param string $color    The border color.
     * @param integer $width   The image width including the border.
     * @param integer $height  The image height including the border.
     *
     * @todo  Make non-static for H6.
     */
    public static function frameImage(&$image, $color, $width, $height)
    {
        // Need to jump through these hoops in order to preserve any
        // transparency.
        try {
            // @todo Use clone or $this->_cloneImagickObject().
            $border = $image->clone();
            $border->borderImage(new ImagickPixel($color), $width, $height);
            $border->compositeImage($image, Imagick::COMPOSITE_COPY, $width, $height);
            $image->clear();
            $image->addImage($border);
        } catch (ImagickPixelException $e) {
            throw new Horde_Image_Exception($e);
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
        $border->destroy();
    }

    /**
     * Resets the Imagick iterator to the first image in the set.
     */
    public function rewind()
    {
        $this->_logDebug('Horde_Image_Imagick#rewind');
        $this->_imagick->setFirstIterator();
        $this->_noMoreImages = false;
    }

    /**
     * Returns the current image from the internal iterator.
     *
     * @return Horde_Image_Imagick
     */
    public function current()
    {
        $this->_logDebug('Horde_Image_Imagick#current');
        $params = array('data' => $this->raw(false, array('stream' => true)));
        $image = new Horde_Image_Imagick($params, $this->_context);
        return $image;
    }

    /**
     * Returns the index of the internal iterator.
     *
     * @return integer
     */
    public function key()
    {
        $this->_logDebug('Horde_Image_Imagick#key: ' . $this->_imagick->getIteratorIndex());
        return $this->_imagick->getIteratorIndex();
    }

    /**
     * Advances the iterator.
     *
     * @return Horde_Image_Imagick
     */
    public function next()
    {
        if ($this->_imagick->hasNextImage()) {
            $this->_imagick->nextImage();
            return $this->current();
        } else {
            $this->_noMoreImages = true;
            return false;
        }
    }

    /**
     * Deterimines if the current iterator item is valid.
     *
     * @return boolean
     */
    public function valid()
    {
        $this->_logDebug('Horde_Image_Imagick#valid:' . print_r(!$this->_noMoreImages, true));
        return !$this->_noMoreImages;
    }

    /**
     * Returns a specific image from the pages of images.
     *
     * @param integer $index  The index to return.
     *
     * @return Horde_Image_Imagick  The requested image
     */
    public function getImageAtIndex($index)
    {
        if ($index >= $this->_imagick->getNumberImages()) {
            throw new Horde_Image_Exception('Image index out of bounds.');
        }

        $currentIndex = $this->_imagick->getIteratorIndex();
        $this->_imagick->setIteratorIndex($index);
        $image = $this->current();
        $this->_imagick->setIteratorIndex($currentIndex);

        return $image;
    }

    /**
     * Returns the number of image pages available in the image object.
     *
     * @return integer  The number of images.
     */
    public function getImagePageCount()
    {
        return $this->_imagick->getNumberImages();
    }


    /**
     * Wrapper around cloning the imagick resource object.
     *
     * @param  Imagick $imagick  A imagick resource object to clone. If empty
     *                           will clone the imagick object associated with
     *                           this Horde_Imagice_Imagick object.
     *
     * @todo  Remove in H6 when we can increase version dependency of Imagick.
     *
     * @return Imagick
     * @since  2.4.0
     */
    public function cloneImagickObject($imagick = null)
    {
        if (version_compare(phpversion('imagick'), '3.1.0') >= 0) {
            return empty($imagick)
                ? clone $this->_imagick
                : clone $imagick;
        }

        return empty($imagick)
            ? $this->_imagick->clone()
            : $imagick->clone();
    }

 }
