<?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
 */

/**
 * Effect for composing multiple images into a single image.
 *
 * The technique for the Polaroid-like stack using the Imagick extension is
 * credited to Mikko Koppanen and is documented at http://valokuva.org
 *
 * @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
 */
class Horde_Image_Effect_Imagick_PhotoStack extends Horde_Image_Effect
{
    /**
     * Valid parameters for the stack effect:
     *   - images: (array) An array of Horde_Image objects to stack. Images
     *             are stacked in a FIFO manner, so that the top-most image is
     *             the last one in this array.
     *   - type: (string) Determines the style for the composition.
     *           'plain' or 'polaroid' are supported.
     *   - resize_height: (integer) The height that each individual thumbnail
     *                    should be resized to before composing on the image.
     *   - padding: (integer) How much padding should we ensure is left around
     *              the active image area?
     *   - background: (string) The background canvas color - this is used as
     *                 the color to set any padding to.
     *   - bordercolor: (string) If using type 'plain' this sets the color of
     *                  the border that each individual thumbnail gets.
     *   - borderwidth: (integer) If using type 'plain' this sets the width of
     *                  the border on each individual thumbnail.
     *   - offset: (integer) If using type 'plain' this determines the amount
     *             of x and y offset to give each successive image when it is
     *             placed on the top of the stack.
     *
     * @var array
     */
    protected $_params = array(
        'type'           => 'plain',
        'resize_height'  => '150',
        'padding'        => 0,
        'background'     => 'none',
        'bordercolor'    => '#333',
        'borderwidth'    => 1,
        'borderrounding' => 10,
        'offset'         => 5
    );

    /**
     * Applies the effect.
     */
    public function apply()
    {
        $i = 1;
        $cnt = count($this->_params['images']);
        if ($cnt <= 0) {
            throw new Horde_Image_Exception('No Images provided.');
        }
        if (!method_exists($this->_image->imagick, 'polaroidImage') ||
            !method_exists($this->_image->imagick, 'trimImage')) {
            throw new Horde_Image_Exception('Your version of Imagick is not compiled against a recent enough ImageMagick library to use the PhotoStack effect.');
        }

        $imgs = array();
        $length = 0;

        try {
            switch ($this->_params['type']) {
            case 'plain':
            case 'rounded':
                $haveBottom = false;
                // First, we need to resize the top image to get the dimensions
                // for the rest of the stack.
                $topimg = new Imagick();
                $topimg->clear();
                $topimg->readImageBlob(
                    $this->_params['images'][$cnt - 1]->raw()
                );
                $topimg->thumbnailImage(
                    $this->_params['resize_height'],
                    $this->_params['resize_height'],
                    true);
                if ($this->_params['type'] == 'rounded') {
                    $topimg = $this->_roundBorder($topimg);
                }

                $size = $topimg->getImageGeometry();
                foreach ($this->_params['images'] as $image) {
                    $imgk= new Imagick();
                    $imgk->clear();
                    $imgk->readImageBlob($image->raw());
                    // Either resize the thumbnail to match the top image or we
                    // *are* the top image already.
                    if ($i++ <= $cnt) {
                        $imgk->thumbnailImage(
                            $size['width'], $size['height'], false
                        );
                    } else {
                        $imgk->destroy();
                        $imgk = $this->_image->cloneImagickObject($topimg);
                    }
                    if ($this->_params['type'] == 'rounded') {
                        $imgk = $this->_roundBorder($imgk);
                    } else {
                        $imgk->borderImage(
                            $this->_params['bordercolor'],
                            $this->_params['borderwidth'],
                            $this->_params['borderwidth']
                        );
                    }
                    // Only shadow the bottom image for 'plain' stacks
                    if (!$haveBottom) {
                        $shad = $this->_image->cloneImagickObject($imgk);
                        $shad->setImageBackgroundColor(
                            new ImagickPixel('black')
                        );
                        $shad->shadowImage(80, 4, 0, 0);
                        $shad->compositeImage(
                            $imgk, Imagick::COMPOSITE_OVER, 0, 0
                        );
                        $imgk->clear();
                        $imgk->addImage($shad);
                        $shad->destroy();
                        $haveBottom = true;
                    }
                    // Get the geometry of the image and remember the largest.
                    $geo = $imgk->getImageGeometry();
                    $length = max(
                        $length,
                        sqrt(pow($geo['height'], 2) + pow($geo['width'], 2))
                    );

                    $imgs[] = $imgk;
                }
                break;

            case 'polaroid':
                foreach ($this->_params['images'] as $image) {
                    // @TODO: instead of doing $image->raw(), we might be able
                    //        to clone the imagick object if we can do it
                    //        cleanly might be faster, less memory intensive?
                    $imgk = new Imagick();
                    $imgk->clear();
                    $imgk->readImageBlob($image->raw());
                    $imgk->thumbnailImage(
                        $this->_params['resize_height'],
                        $this->_params['resize_height'],
                        true
                    );
                    $imgk->setImageBackgroundColor('black');
                    if ($i++ == $cnt) {
                        $angle = 0;
                    } else {
                        $angle = mt_rand(1, 45);
                        if (mt_rand(1, 2) % 2 === 0) {
                            $angle = $angle * -1;
                        }
                    }
                    $result = $imgk->polaroidImage(new ImagickDraw(), $angle);

                    // Get the geometry of the image and remember the largest.
                    $geo = $imgk->getImageGeometry();
                    $length = max(
                        $length,
                        sqrt(pow($geo['height'], 2) + pow($geo['width'], 2))
                    );

                    $imgs[] = $imgk;
                }
                break;
            }

            // Make sure the background canvas is large enough to hold it all.
            $this->_image->imagick->thumbnailImage(
                $length * 1.5 + 20,
                $length * 1.5 + 20
            );

            // x and y offsets.
            $xo = $yo = (count($imgs) + 1) * $this->_params['offset'];
            foreach ($imgs as $image) {
                if ($this->_params['type'] == 'polaroid') {
                    $xo = mt_rand(1, $this->_params['resize_height'] / 2);
                    $yo = mt_rand(1, $this->_params['resize_height'] / 2);
                } elseif ($this->_params['type'] == 'plain' ||
                          $this->_params['type'] == 'rounded') {
                    $xo -= $this->_params['offset'];
                    $yo -= $this->_params['offset'];
                }
                $this->_image->imagick->compositeImage(
                    $image, Imagick::COMPOSITE_OVER, $xo, $yo
                );
                $image->removeImage();
                $image->destroy();
            }

            // Trim the canvas before resizing to keep the thumbnails as large
            // as possible.
            $this->_image->imagick->trimImage(0);
            if ($this->_params['padding'] ||
                $this->_params['background'] != 'none') {
                $this->_image->imagick->borderImage(
                    new ImagickPixel($this->_params['background']),
                    $this->_params['padding'],
                    $this->_params['padding']);
            }
        } catch (ImagickPixelException $e) {
            throw new Horde_Image_Exception($e);
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
    }

    private function _roundBorder($image)
    {
        $context = array('tmpdir' => $this->_image->getTmpDir());
        $size = $image->getImageGeometry();
        $new = new Horde_Image_Imagick(array(), $context);
        $new->loadString($image->getImageBlob());
        $image->destroy();
        $new->addEffect(
            'RoundCorners',
            array('border' => 2, 'bordercolor' => '#111')
        );
        $new->applyEffects();
        $return = new Imagick();
        $return->newImage(
            $size['width'] + $this->_params['borderwidth'],
            $size['height'] + $this->_params['borderwidth'],
            $this->_params['bordercolor']
        );
        $return->setImageFormat($this->_image->getType());
        $return->clear();
        $return->readImageBlob($new->raw());

        return $return;
    }
}
