<?php
/**
 * Pimcore
 *
 * This source file is subject to the GNU General Public License version 3 (GPLv3)
 * For the full copyright and license information, please view the LICENSE.md and gpl-3.0.txt
 * files that are distributed with this source code.
 *
 * @copyright  Copyright (c) 2009-2016 pimcore GmbH (http://www.pimcore.org)
 * @license    http://www.pimcore.org/license     GNU General Public License version 3 (GPLv3)
 */

namespace Pimcore\Image\Adapter;

use Pimcore\Image\Adapter;
use Pimcore\File; 
use Pimcore\Config;

class Imagick extends Adapter {


    /**
     * @var string
     */
    protected static $RGBColorProfile;

    /**
     * @var string
     */
    protected static $CMYKColorProfile;

    /**
     * @var \Imagick
     */
    protected $resource;

    /**
     * @var string
     */
    protected $imagePath;

    /**
     * @param $imagePath
     * @param array $options
     * @return $this|bool|self
     */
    public function load ($imagePath, $options = []) {

        // support image URLs
        if(preg_match("@^https?://@", $imagePath)) {
            $tmpFilename = "imagick_auto_download_" . md5($imagePath) . "." . File::getFileExtension($imagePath);
            $tmpFilePath = PIMCORE_SYSTEM_TEMP_DIRECTORY . "/" . $tmpFilename;

            $this->tmpFiles[] = $tmpFilePath;

            File::put($tmpFilePath, \Pimcore\Tool::getHttpData($imagePath));
            $imagePath = $tmpFilePath;
        }

        if($this->resource) {
            unset($this->resource);
            $this->resource = null;
        }

        try {
            $i = new \Imagick();
            $this->imagePath = $imagePath;

            if(method_exists($i, "setcolorspace")) {
                $i->setcolorspace(\Imagick::COLORSPACE_SRGB);
            }

            $i->setBackgroundColor(new \ImagickPixel('transparent')); //set .png transparent (print)

            if(isset($options["resolution"])) {
                // set the resolution to 2000x2000 for known vector formats
                // otherwise this will cause problems with eg. cropPercent in the image editable (select specific area)
                // maybe there's a better solution but for now this fixes the problem
                $i->setResolution($options["resolution"]["x"], $options["resolution"]["y"]);
            }

            $imagePathLoad = $imagePath;
            if(!defined("HHVM_VERSION")) {
                $imagePathLoad .= "[0]"; // not supported by HHVM implementation - selects the first layer/page in layered/pages file formats
            }

            if(!$i->readImage($imagePathLoad) || !filesize($imagePath)) {
                return false;
            }

            $this->resource = $i; // this is because of HHVM which has problems with $this->resource->readImage();

            // set dimensions
            $dimensions = $this->getDimensions();
            $this->setWidth($dimensions["width"]);
            $this->setHeight($dimensions["height"]);

            // check if image can have alpha channel
            if(!$this->reinitializing) {
                $alphaChannel = $i->getImageAlphaChannel();
                if($alphaChannel) {
                    $this->setIsAlphaPossible(true);
                }
            }

            $this->setColorspaceToRGB();

        } catch (\Exception $e) {
            \Logger::error("Unable to load image: " . $imagePath);
            \Logger::error($e);
            return false;
        }


        $this->setModified(false);

        return $this;
    }

    /**
     * @param $path
     * @param null $format
     * @param null $quality
     * @param null $colorProfile
     * @return $this|mixed
     * @throws \Exception
     */
    public function save ($path, $format = null, $quality = null, $colorProfile = null) {

        if(!$format) {
            $format = "png32";
        }
        $format = strtolower($format);

        if($format == "png") {
            // we need to force imagick to create png32 images, otherwise this can cause some strange effects
            // when used with gray-scale images
            $format = "png32";
        }

        $i = $this->resource; // this is because of HHVM which has problems with $this->resource->writeImage();

        $originalFilename = null;
        if(!$this->reinitializing) {
            if($this->getUseContentOptimizedFormat()) {
                $format = "jpeg";
                if($this->hasAlphaChannel()) {
                    $format = "png32";
                }
            }
        }

        $i->stripimage();
        $i->profileImage('*', null);
        $i->setImageFormat($format);

        if($quality) {
            $i->setCompressionQuality((int) $quality);
            $i->setImageCompressionQuality((int) $quality);
        }

        if($format == "tiff") {
            $i->setCompression(\Imagick::COMPRESSION_LZW);
        }

        // force progressive JPEG if filesize >= 10k
        // normally jpeg images are bigger than 10k so we avoid the double compression (baseline => filesize check => if necessary progressive)
        // and check the dimensions here instead to faster generate the image
        // progressive JPEG - better compression, smaller filesize, especially for web optimization
        if($format == "jpeg") {
            if( ($this->getWidth() * $this->getHeight()) > 35000) {
                $i->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
            }
        }

        if(defined("HHVM_VERSION")) {
            $success = $i->writeImage($path);
        } else {
            $success = $i->writeImage($format . ":" . $path);
        }

        if(!$success) {
            throw new \Exception("Unable to write image: " , $path);
        }

        return $this;
    }

    /**
     * @return $this
     */
    // @TODO: Needs further testing => speed improvement especially with bigger images
    /*protected function reinitializeImage() {

        $i = $this->resource;

        $i->writeImage("mpr:temp");
        $this->destroy();

        $i = new \Imagick();
        $i->readImage("mpr:temp");

        $this->resource = $i;

        $this->modified = false;

        return $this;
    }*/

    /**
     * @return  void
     */
    protected function destroy() {
        if($this->resource) {
            $this->resource->clear();
            $this->resource->destroy();
            $this->resource = null;
        }
    }

    /**
     * @return bool
     */
    protected function hasAlphaChannel() {

        if($this->isAlphaPossible) {
            $width = $this->resource->getImageWidth(); // Get the width of the image
            $height = $this->resource->getImageHeight(); // Get the height of the image

            // We run the image pixel by pixel and as soon as we find a transparent pixel we stop and return true.
            for($i = 0; $i < $width; $i++) {
                for($j = 0; $j < $height; $j++) {
                    $pixel = $this->resource->getImagePixelColor($i, $j);
                    $color = $pixel->getColor(true); // get the real alpha not just 1/0
                    if($color["a"] < 1) { // if there's an alpha pixel, return true
                        return true;
                    }
                }
            }
        }

        // If we dont find any pixel the function will return false.
        return false;
    }

    /**
     * @return $this
     */
    public function setColorspaceToRGB() {

        $imageColorspace = $this->resource->getImageColorspace();

        if ($imageColorspace == \Imagick::COLORSPACE_CMYK) {
            if(self::getCMYKColorProfile() && self::getRGBColorProfile()) {
                $profiles = $this->resource->getImageProfiles('*', false);
                // we're only interested if ICC profile(s) exist
                $has_icc_profile = (array_search('icc', $profiles) !== false);
                // if it doesn't have a CMYK ICC profile, we add one
                if ($has_icc_profile === false) {
                    $this->resource->profileImage('icc', self::getCMYKColorProfile());
                }
                // then we add an RGB profile
                $this->resource->profileImage('icc', self::getRGBColorProfile());
                $this->resource->setImageColorspace(\Imagick::COLORSPACE_SRGB); // we have to use SRGB here, no clue why but it works
            } else {
                $this->resource->setImageColorspace(\Imagick::COLORSPACE_SRGB);
            }
        } else if ($imageColorspace == \Imagick::COLORSPACE_GRAY) {
            $this->resource->setImageColorspace(\Imagick::COLORSPACE_SRGB);
        } else if (!in_array($imageColorspace, array(\Imagick::COLORSPACE_RGB, \Imagick::COLORSPACE_SRGB))) {
            $this->resource->setImageColorspace(\Imagick::COLORSPACE_SRGB);
        } else {
            // this is to handle embedded icc profiles in the RGB/sRGB colorspace
            $profiles = $this->resource->getImageProfiles('*', false);
            $has_icc_profile = (array_search('icc', $profiles) !== false);
            if($has_icc_profile) {
                try {
                    // if getImageColorspace() says SRGB but the embedded icc profile is CMYK profileImage() will throw an exception
                    $this->resource->profileImage('icc', self::getRGBColorProfile());
                } catch (\Exception $e) {
                    \Logger::warn($e);
                }
            }
        }

        // this is a HACK to force grayscale images to be real RGB - truecolor, this is important if you want to use
        // thumbnails in PDF's because they do not support "real" grayscale JPEGs or PNGs
        // problem is described here: http://imagemagick.org/Usage/basics/#type
        // and here: http://www.imagemagick.org/discourse-server/viewtopic.php?f=2&t=6888#p31891
        $currentLocale = setlocale(LC_ALL,"0"); // this locale hack thing is also a hack for imagick
        setlocale(LC_ALL,"en"); // Set locale to "en" for ImagickDraw::point() to ensure the involved tostring() methods keep the decimal point

        $draw = new \ImagickDraw();
        $draw->setFillColor("#ff0000");
        $draw->setfillopacity(.01);
        $draw->point(floor($this->getWidth()/2),floor($this->getHeight()/2)); // place it in the middle of the image
        $this->resource->drawImage($draw);

        setlocale(LC_ALL, $currentLocale); // see setlocale() above, for details ;-)

        return $this;
    }

    /**
     * @param string $CMYKColorProfile
     */
    public static function setCMYKColorProfile($CMYKColorProfile)
    {
        self::$CMYKColorProfile = $CMYKColorProfile;
    }

    /**
     * @return string
     */
    public static function getCMYKColorProfile()
    {
        if(!self::$CMYKColorProfile) {
            $path = Config::getSystemConfig()->assets->icc_cmyk_profile;
            if(!$path || !file_exists($path)) {
                $path = __DIR__ . "/../icc-profiles/ISOcoated_v2_eci.icc"; // default profile
            }

            if($path && file_exists($path)) {
                self::$CMYKColorProfile = file_get_contents($path);
            }

        }

        return self::$CMYKColorProfile;
    }

    /**
     * @param string $RGBColorProfile
     */
    public static function setRGBColorProfile($RGBColorProfile)
    {
        self::$RGBColorProfile = $RGBColorProfile;
    }

    /**
     * @return string
     */
    public static function getRGBColorProfile()
    {
        if(!self::$RGBColorProfile) {
            $path = Config::getSystemConfig()->assets->icc_rgb_profile;
            if(!$path || !file_exists($path)) {
                $path = __DIR__ . "/../icc-profiles/sRGB_IEC61966-2-1_black_scaled.icc"; // default profile
            }

            if(file_exists($path)) {
                self::$RGBColorProfile = file_get_contents($path);
            }
        }

        return self::$RGBColorProfile;
    }

    /**
     * @param  $width
     * @param  $height
     * @return self
     */
    public function resize ($width, $height) {

        $this->preModify();

        // this is the check for vector formats because they need to have a resolution set
        // this does only work if "resize" is the first step in the image-pipeline

        if($this->isVectorGraphic()) {
            // the resolution has to be set before loading the image, that's why we have to destroy the instance and load it again
            $res = $this->resource->getImageResolution();
            $x_ratio = $res['x'] / $this->getWidth();
            $y_ratio = $res['y'] / $this->getHeight();
            $this->resource->removeImage();

            $newRes = ["x" => $width * $x_ratio, "y" => $height * $y_ratio];

            // only use the calculated resolution if we need a higher one that the one we got from the metadata (getImageResolution)
            // this is because sometimes the quality is much better when using the "native" resolution from the metadata
            if($newRes["x"] > $res["x"] && $newRes["y"] > $res["y"]) {
                $this->resource->setResolution($newRes["x"], $newRes["y"]);
            } else {
                $this->resource->setResolution($res["x"], $res["y"]);
            }

            $this->resource->readImage($this->imagePath);
            $this->setColorspaceToRGB();
        }

        $width  = (int)$width;
        $height = (int)$height;

        $this->resource->resizeimage($width, $height, \Imagick::FILTER_UNDEFINED, 1, false);

        $this->setWidth($width);
        $this->setHeight($height);

        $this->postModify();

        return $this;
    }

    /**
     * @param  $x
     * @param  $y
     * @param  $width
     * @param  $height
     * @return self
     */
    public function crop($x, $y, $width, $height) {

        $this->preModify();

        $this->resource->cropImage($width, $height, $x, $y);
        $this->resource->setImagePage($width, $height, 0, 0);

        $this->setWidth($width);
        $this->setHeight($height);

        $this->postModify();

        return $this;
    }


    /**
     * @param $width
     * @param $height
     * @return $this
     */
    public function frame ($width, $height) {

        $this->preModify();

        $this->contain($width, $height);

        $x = ($width - $this->getWidth()) / 2;
        $y = ($height - $this->getHeight()) / 2;


        $newImage = $this->createImage($width, $height);
        $newImage->compositeImage($this->resource, \Imagick::COMPOSITE_DEFAULT , $x, $y);
        $this->resource = $newImage;

        $this->setWidth($width);
        $this->setHeight($height);

        $this->postModify();

        $this->setIsAlphaPossible(true);

        return $this;
    }

    /**
     * @param  $tolerance
     * @return self
     */
    public function trim ($tolerance) {

        $this->preModify();

        $this->resource->trimimage($tolerance);

        $dimensions = $this->getDimensions();
        $this->setWidth($dimensions['width']);
        $this->setHeight($dimensions['height']);

        $this->postModify();

        return $this;
    }

    /**
     * @param  $color
     * @return self
     */
    public function setBackgroundColor ($color) {

        $this->preModify();

        $newImage = $this->createImage($this->getWidth(), $this->getHeight(), $color);
        $newImage->compositeImage($this->resource, \Imagick::COMPOSITE_DEFAULT , 0, 0);
        $this->resource = $newImage;

        $this->postModify();

        $this->setIsAlphaPossible(false);

        return $this;
    }

    /**
     * @param $width
     * @param $height
     * @return Imagick
     */
    protected  function createImage ($width, $height, $color = "transparent") {
        $newImage = new \Imagick();
        $newImage->newimage($width, $height, $color);

        return $newImage;
    }


    /**
     * @param $angle
     * @return $this
     */
    public function rotate ($angle) {

        $this->preModify();

        $this->resource->rotateImage(new \ImagickPixel('none'), $angle);
        $this->setWidth($this->resource->getimagewidth());
        $this->setHeight($this->resource->getimageheight());

        $this->postModify();

        $this->setIsAlphaPossible(true);

        return $this;
    }


    /**
     * @param  $x
     * @param  $y
     * @return self
     */
    public function roundCorners ($x, $y) {

        $this->preModify();

        $this->resource->roundCorners($x, $y);

        $this->postModify();

        $this->setIsAlphaPossible(true);

        return $this;
    }


    /**
     * @param $image
     * @return $this|Adapter
     */
    public function setBackgroundImage ($image) {

        $this->preModify();

        $image = ltrim($image,"/");
        $image = PIMCORE_DOCUMENT_ROOT . "/" . $image;

        if(is_file($image)) {
            $newImage = new \Imagick();
            $newImage->readimage($image);
            $newImage->resizeimage($this->getWidth(), $this->getHeight(), \Imagick::FILTER_UNDEFINED, 1, false);
            $newImage->compositeImage($this->resource, \Imagick::COMPOSITE_DEFAULT, 0 ,0);
            $this->resource = $newImage;
        }

        $this->postModify();

        return $this;
    }

    /**
     * @param string $image
     * @param int $x Amount of horizontal pixels the overlay should be offset from the origin
     * @param int $y Amount of vertical pixels the overlay should be offset from the origin
     * @param int $alpha Opacity in a scale of 0 (transparent) to 100 (opaque)
     * @param string $origin Origin of the X and Y coordinates (top-left, top-right, bottom-left, bottom-right or center)
     * @return self
     */
    public function  addOverlay ($image, $x = 0, $y = 0, $alpha = 100, $composite = "COMPOSITE_DEFAULT", $origin = 'top-left') {

        $this->preModify();

        // 100 alpha is default
        if(empty($alpha)) {
            $alpha = 100;
        }
        $alpha = round($alpha / 100, 1);

        //Make sure the composite constant exists.
        if(is_null(constant("Imagick::" . $composite))) {
            $composite = "COMPOSITE_DEFAULT";
        }

        $newImage = null;

        if(is_string($image)) {

            $image = ltrim($image,"/");
            $image = PIMCORE_DOCUMENT_ROOT . "/" . $image;

            $newImage = new \Imagick();
            $newImage->readimage($image);
        } else if ($image instanceof \Imagick) {
            $newImage = $image;
        }

        if($newImage) {
            if($origin == 'top-right') {
                $x = $this->resource->getImageWidth() - $newImage->getImageWidth() - $x;
            } elseif($origin == 'bottom-left') {
                $y = $this->resource->getImageHeight() - $newImage->getImageHeight() - $y;
            } elseif($origin == 'bottom-right') {
                $x = $this->resource->getImageWidth() - $newImage->getImageWidth() - $x;
                $y = $this->resource->getImageHeight() - $newImage->getImageHeight() - $y;
            } elseif($origin == 'center') {
                $x = round($this->resource->getImageWidth() / 2) - round($newImage->getImageWidth() / 2) + $x;
                $y = round($this->resource->getImageHeight() / 2) -round($newImage->getImageHeight() / 2) + $y;
            }

            $newImage->evaluateImage(\Imagick::EVALUATE_MULTIPLY, $alpha, \Imagick::CHANNEL_ALPHA);
            $this->resource->compositeImage($newImage, constant("Imagick::" . $composite), $x ,$y);
        }

        $this->postModify();

        return $this;
    }

    /**
     * @param $image
     * @param string $composite
     * @return $this
     */
    public function addOverlayFit($image, $composite = "COMPOSITE_DEFAULT") {

        $image = ltrim($image,"/");
        $image = PIMCORE_DOCUMENT_ROOT . "/" . $image;

        $newImage = new \Imagick();
        $newImage->readimage($image);
        $newImage->resizeimage($this->getWidth(), $this->getHeight(), \Imagick::FILTER_UNDEFINED, 1, false);

        $this->addOverlay($newImage, 0, 0, 100, $composite);

        return $this;
    }

    /**
     * @param  $image
     * @return self
     */
    public function applyMask ($image) {

        $this->preModify();
        $image = ltrim($image,"/");
        $image = PIMCORE_DOCUMENT_ROOT . "/" . $image;

        if(is_file($image)) {
            $this->resource->setImageMatte(1);
            $newImage = new \Imagick();
            $newImage->readimage($image);
            $newImage->resizeimage($this->getWidth(), $this->getHeight(), \Imagick::FILTER_UNDEFINED, 1, false);
            $this->resource->compositeImage($newImage, \Imagick::COMPOSITE_COPYOPACITY, 0 ,0, \Imagick::CHANNEL_ALPHA);
        }

        $this->postModify();

        $this->setIsAlphaPossible(true);

        return $this;
    }


    /**
     * @return self
     */
    public function grayscale () {

        $this->preModify();
        $this->resource->setImageType(\Imagick::IMGTYPE_GRAYSCALEMATTE);
        $this->postModify();

        return $this;
    }

    /**
     * @return self
     */
    public function sepia () {

        $this->preModify();
        $this->resource->sepiatoneimage(85);
        $this->postModify();

        return $this;
    }

    /**
     * @param int $radius
     * @param float $sigma
     * @param float $amount
     * @param float $threshold
     * @return $this|Adapter
     */
    public function sharpen ($radius = 0, $sigma = 1.0, $amount = 1.0, $threshold = 0.05) {

        $this->preModify();
        $this->resource->normalizeImage();
        $this->resource->unsharpMaskImage($radius, $sigma, $amount, $threshold);
        $this->postModify();

        return $this;
    }

    /**
     * @param int $radius
     * @param float $sigma
     * @return $this|Adapter
     */
    public function gaussianBlur($radius = 0, $sigma = 1.0) {
        $this->preModify();
        $this->resource->gaussianBlurImage($radius, $sigma);
        $this->postModify();

        return $this;
    }

    /**
     * @param int $brightness
     * @param int $saturation
     * @param int $hue
     * @return $this
     */
    public function brightnessSaturation($brightness = 100, $saturation = 100, $hue = 100) {
        $this->preModify();
        $this->resource->modulateImage($brightness, $saturation, $hue);
        $this->postModify();

        return $this;
    }

    /**
     * @param $mode
     * @return $this|Adapter
     */
    public function mirror($mode) {

        $this->preModify();

        if($mode == "vertical") {
            $this->resource->flipImage();
        } else if ($mode == "horizontal") {
            $this->resource->flopImage();
        }

        $this->postModify();

        return $this;
    }

    /**
     * @param null $imagePath
     * @return bool
     */
    public function isVectorGraphic ($imagePath = null) {

        if(!$imagePath) {
            $imagePath = $this->imagePath;
        }

        // we need to do this check first, because ImageMagick using the inkscape delegate returns "PNG" when calling
        // getimageformat() onto SVG graphics, this is a workaround to avoid problems
        if(in_array(File::getFileExtension($imagePath), ["svg","svgz","eps","pdf","ps"])) {
            // use file-extension if filename is provided
            return true;
        }

        try {
            $type = $this->resource->getimageformat();
            $vectorTypes = array("EPT","EPDF","EPI","EPS","EPS2","EPS3","EPSF","EPSI","EPT","PDF","PFA","PFB","PFM","PS","PS2","PS3","SVG","SVGZ","MVG");

            if(in_array(strtoupper($type),$vectorTypes)) {
                return true;
            }
        } catch (\Exception $e) {
            \Logger::err($e);
        }

        return false;
    }

    /**
     * @return array
     */
    public function getDimensions() {

        if($vectorDimensions = $this->getVectorFormatEmbeddedRasterDimensions()) {
            return $vectorDimensions;
        }

        return [
            "width" => $this->resource->getImageWidth(),
            "height" => $this->resource->getImageHeight()
        ];
    }

    /**
     * @return array|null
     */
    public function getVectorFormatEmbeddedRasterDimensions() {
        if(in_array($this->resource->getimageformat(), ["EPT","EPDF","EPI","EPS","EPS2","EPS3","EPSF","EPSI","EPT","PDF","PFA","PFB","PFM","PS","PS2","PS3"])) {
            // we need a special handling for PhotoShop EPS
            $i = 0;

            ini_set("auto_detect_line_endings", true); // we need to turn this on, as the damn f****** Mac has different line endings in EPS files, Prost Mahlzeit!

            $epsFile = fopen($this->imagePath, 'r');
            while (($eps_line = fgets($epsFile)) && ($i < 100)) {
                if(preg_match("/%ImageData: ([0-9]+) ([0-9]+)/i", $eps_line,$matches)) {
                    return [
                        "width" => $matches[1],
                        "height" => $matches[2]
                    ];
                    break;
                }
                $i++;
            }
        }

        return null;
    }

    /**
     * @return array
     */
    public function getVectorRasterDimensions() {

        if($vectorDimensions = $this->getVectorFormatEmbeddedRasterDimensions()) {
            return $vectorDimensions;
        }

        return parent::getVectorRasterDimensions();
    }
}
