<?php
/**
 * Copyright 2010-2017 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file LICENSE 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
 */

/**
 * Image effect for determining the best crop based on the center of edginess.
 *
 * Based on ideas and code by Jue Wang <jue@jueseph.com>
 * http://jueseph.com/2010/06/opticrop-usage-and-implementation/
 *
 * @author    Michael J. Rubinsky <mrubinsk@horde.org>
 * @category  Horde
 * @copyright 2010-2017 Horde LLC
 * @license   http://www.horde.org/licenses/lgpl21 LGPL-2.1
 * @package   Image
 */
class Horde_Image_Effect_Imagick_SmartCrop extends Horde_Image_Effect
{
    /**
     * Valid parameters:
     *   - width: (integer) Target width.
     *   - height: (integer) Target height.
     *
     * @var array
     */
    protected $_params = array();

    /**
     * Applies the effect.
     */
    public function apply()
    {
        $this->_params = new Horde_Support_Array($this->_params);

        // Existing geometry
        $geometry = $this->_image->getDimensions();
        $w0 = $geometry['width'];
        $h0 = $geometry['height'];

        $w = $this->_params->width;
        $h = $this->_params->height;

        // @TODO: Parameterize these
        $r = 1;         // radius of edge filter
        $nk = 9;        // scale count: number of crop sizes to try
        $gamma = 0.2;   // edge normalization parameter -- see documentation

        // Target AR
        $ar = $this->_params->width / $this->_params->height;

        // Existing AR
        $ar0 = $w0 / $h0;

        $this->_logger->debug(
            sprintf("SmartCrop: %d x %d => %d x %d ", $w0, $h0, $w, $h)
        );
        $this->_logger->debug('OAR: ' . $ar0);
        $this->_logger->debug('TAR: ' . $ar);


        try {
            // Compute COE
            $img = $this->_image->cloneImagickObject();
            $img->edgeImage($r);
            $img->modulateImage(100,0,100);
            $img->blackThresholdImage("#0f0f0f");

            $xcenter = $ycenter = $sum = 0;
            $n = 100000;
            for ($k = 0; $k < $n; $k++) {
                $i = mt_rand(0, $w0 - 1);
                $j = mt_rand(0, $h0 - 1);
                $pixel = $img->getImagePixelColor($i, $j);
                $val = $pixel->getColor();
                $val = $val['b'];
                $sum += $val;
                $xcenter = $xcenter + ($i + 1) * $val;
                $ycenter = $ycenter + ($j + 1) * $val;
            }
        } catch (ImagickPixelException $e) {
            throw new Horde_Image_Exception($e);
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
        $xcenter /= $sum;
        $ycenter /= $sum;
        $this->_logger->debug('COE: ' . $xcenter . 'x' . $ycenter);

        // crop source img to target AR
        if ($w0 / $h0 > $ar) {
            // source AR wider than target
            // crop width to target AR
            $wcrop0 = round($ar * $h0);
            $hcrop0 = $h0;
        } else {
            // crop height to target AR
            $wcrop0 = $w0;
            $hcrop0 = round($w0 / $ar);
        }

        // crop parameters for all scales and translations
        $params = array();

        // crop at different scales
        $hgap = $hcrop0 - $h;
        $hinc = ($nk == 1) ? 0 : $hgap / ($nk - 1);
        $wgap = $wcrop0 - $w;
        $winc = ($nk == 1) ? 0 : $wgap / ($nk - 1);

        // find window with highest normalized edginess
        $n = 10000;
        $maxbetanorm = 0;
        $maxfile = '';
        $maxparam = array('w' => 0, 'h' => 0, 'x' => 0, 'y' => 0);

        try {
            for ($k = 0; $k < $nk; $k++) {
                $hcrop = round($hcrop0 - $k * $hinc);
                $wcrop = round($wcrop0 - $k * $winc);
                $xcrop = $xcenter - $wcrop / 2;
                $ycrop = $ycenter - $hcrop / 2;
                if ($xcrop < 0) {
                    $xcrop = 0;
                }
                if ($xcrop + $wcrop > $w0) {
                    $xcrop = $w0 - $wcrop;
                }
                if ($ycrop < 0) {
                    $ycrop = 0;
                }
                if ($ycrop+$hcrop > $h0) {
                    $ycrop = $h0 - $hcrop;
                }
                $this->_logger->debug("crop: $wcrop, $hcrop, $xcrop, $ycrop");

                $beta = 0;
                for ($c = 0; $c < $n; $c++) {
                    $i = mt_rand(0, $wcrop - 1);
                    $j = mt_rand(0, $hcrop - 1);
                    $pixel = $img->getImagePixelColor($xcrop + $i, $ycrop + $j);
                    $val = $pixel->getColor();
                    $beta += $val['b'];// & 0xFF;
                }

                $area = $wcrop * $hcrop;
                $betanorm = $beta / ($n * pow($area, $gamma - 1));

                // best image found, save the params
                if ($betanorm > $maxbetanorm) {
                    $this->_logger->debug('Found best');
                    $maxbetanorm = $betanorm;
                    $maxparam['w'] = $wcrop;
                    $maxparam['h'] = $hcrop;
                    $maxparam['x'] = $xcrop;
                    $maxparam['y'] = $ycrop;
                }
            }

            $this->_logger->debug('Cropping');
            // Crop to best
            $this->_image->imagick->cropImage($maxparam['w'],
                                              $maxparam['h'],
                                              $maxparam['x'],
                                              $maxparam['y']);
            $this->_image->imagick->scaleImage($w, $h);
        } catch (ImagickPixelException $e) {
            throw new Horde_Image_Exception($e);
        } catch (ImagickException $e) {
            throw new Horde_Image_Exception($e);
        }
        $img->destroy();
    }
}
