<?php
/**
 * Resize and crop images on the fly, store generated images in a cache.
 *
 * This version is a all-in-one version of img.php, it is not dependant an any other file
 * so you can simply copy it to any place you want it.
 *
 * @author  Mikael Roos mos@dbwebb.se
 * @example http://dbwebb.se/opensource/cimage
 * @link    https://github.com/mosbth/cimage
 *
 */


/**
 * Change configuration details in the array below or create a separate file
 * where you store the configuration details.
 *
 * The configuration file should be named the same name as this file and then
 * add '_config.php'. If this file is named 'img.php' then name the
 * config file should be named 'img_config.php'.
 *
 * The settings below are only a few of the available ones. Check the file in
 * webroot/img_config.php for a complete list of configuration options.
 */
$config = array(

    'mode'         => 'development',               // 'production', 'development', 'strict'
    //'image_path'   =>  __DIR__ . '/img/',
    //'cache_path'   =>  __DIR__ . '/../cache/',
    //'alias_path'   =>  __DIR__ . '/img/alias/',
    //'remote_allow' => true,
    //'password'     => false,                      // "secret-password",

);



/**
 * Get a image from a remote server using HTTP GET and If-Modified-Since.
 *
 */
class CHttpGet
{
    private $request  = array();
    private $response = array();



    /**
    * Constructor
    *
    */
    public function __construct()
    {
        $this->request['header'] = array();
    }



    /**
     * Build an encoded url.
     *
     * @param string $baseUrl This is the original url which will be merged.
     * @param string $merge   Thse parts should be merged into the baseUrl,
     *                        the format is as parse_url.
     *
     * @return string $url as the modified url.
     */
    public function buildUrl($baseUrl, $merge)
    {
        $parts = parse_url($baseUrl);
        $parts = array_merge($parts, $merge);

        $url  = $parts['scheme'];
        $url .= "://";
        $url .= $parts['host'];
        $url .= isset($parts['port'])
            ? ":" . $parts['port']
            : "" ;
        $url .= $parts['path'];

        return $url;
    }



    /**
     * Set the url for the request.
     *
     * @param string $url
     *
     * @return $this
     */
    public function setUrl($url)
    {
        $parts = parse_url($url);
        
        $path = "";
        if (isset($parts['path'])) {
            $pathParts = explode('/', $parts['path']);
            unset($pathParts[0]);
            foreach ($pathParts as $value) {
                $path .= "/" . rawurlencode($value);
            }
        }
        $url = $this->buildUrl($url, array("path" => $path));

        $this->request['url'] = $url;
        return $this;
    }



    /**
     * Set custom header field for the request.
     *
     * @param string $field
     * @param string $value
     *
     * @return $this
     */
    public function setHeader($field, $value)
    {
        $this->request['header'][] = "$field: $value";
        return $this;
    }



    /**
     * Set header fields for the request.
     *
     * @param string $field
     * @param string $value
     *
     * @return $this
     */
    public function parseHeader()
    {
        //$header = explode("\r\n", rtrim($this->response['headerRaw'], "\r\n"));
        
        $rawHeaders = rtrim($this->response['headerRaw'], "\r\n");
        # Handle multiple responses e.g. with redirections (proxies too)
        $headerGroups = explode("\r\n\r\n", $rawHeaders);
        # We're only interested in the last one
        $header = explode("\r\n", end($headerGroups));

        $output = array();

        if ('HTTP' === substr($header[0], 0, 4)) {
            list($output['version'], $output['status']) = explode(' ', $header[0]);
            unset($header[0]);
        }

        foreach ($header as $entry) {
            $pos = strpos($entry, ':');
            $output[trim(substr($entry, 0, $pos))] = trim(substr($entry, $pos + 1));
        }

        $this->response['header'] = $output;
        return $this;
    }



    /**
     * Perform the request.
     *
     * @param boolean $debug set to true to dump headers.
     *
     * @throws Exception when curl fails to retrieve url.
     *
     * @return boolean
     */
    public function doGet($debug = false)
    {
        $options = array(
            CURLOPT_URL             => $this->request['url'],
            CURLOPT_HEADER          => 1,
            CURLOPT_HTTPHEADER      => $this->request['header'],
            CURLOPT_AUTOREFERER     => true,
            CURLOPT_RETURNTRANSFER  => true,
            CURLINFO_HEADER_OUT     => $debug,
            CURLOPT_CONNECTTIMEOUT  => 5,
            CURLOPT_TIMEOUT         => 5,
            CURLOPT_FOLLOWLOCATION  => true,
            CURLOPT_MAXREDIRS       => 2,
        );

        $ch = curl_init();
        curl_setopt_array($ch, $options);
        $response = curl_exec($ch);

        if (!$response) {
            throw new Exception("Failed retrieving url, details follows: " . curl_error($ch));
        }

        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
        $this->response['headerRaw'] = substr($response, 0, $headerSize);
        $this->response['body']      = substr($response, $headerSize);

        $this->parseHeader();

        if ($debug) {
            $info = curl_getinfo($ch);
            echo "Request header<br><pre>", var_dump($info['request_header']), "</pre>";
            echo "Response header (raw)<br><pre>", var_dump($this->response['headerRaw']), "</pre>";
            echo "Response header (parsed)<br><pre>", var_dump($this->response['header']), "</pre>";
        }

        curl_close($ch);
        return true;
    }



    /**
     * Get HTTP code of response.
     *
     * @return integer as HTTP status code or null if not available.
     */
    public function getStatus()
    {
        return isset($this->response['header']['status'])
            ? (int) $this->response['header']['status']
            : null;
    }



    /**
     * Get file modification time of response.
     *
     * @return int as timestamp.
     */
    public function getLastModified()
    {
        return isset($this->response['header']['Last-Modified'])
            ? strtotime($this->response['header']['Last-Modified'])
            : null;
    }



    /**
     * Get content type.
     *
     * @return string as the content type or null if not existing or invalid.
     */
    public function getContentType()
    {
        $type = isset($this->response['header']['Content-Type'])
            ? $this->response['header']['Content-Type']
            : null;

        return preg_match('#[a-z]+/[a-z]+#', $type)
            ? $type
            : null;
    }



    /**
     * Get file modification time of response.
     *
     * @param mixed $default as default value (int seconds) if date is
     *                       missing in response header.
     *
     * @return int as timestamp or $default if Date is missing in
     *             response header.
     */
    public function getDate($default = false)
    {
        return isset($this->response['header']['Date'])
            ? strtotime($this->response['header']['Date'])
            : $default;
    }



    /**
     * Get max age of cachable item.
     *
     * @param mixed $default as default value if date is missing in response
     *                       header.
     *
     * @return int as timestamp or false if not available.
     */
    public function getMaxAge($default = false)
    {
        $cacheControl = isset($this->response['header']['Cache-Control'])
            ? $this->response['header']['Cache-Control']
            : null;

        $maxAge = null;
        if ($cacheControl) {
            // max-age=2592000
            $part = explode('=', $cacheControl);
            $maxAge = ($part[0] == "max-age")
                ? (int) $part[1]
                : null;
        }

        if ($maxAge) {
            return $maxAge;
        }

        $expire = isset($this->response['header']['Expires'])
            ? strtotime($this->response['header']['Expires'])
            : null;

        return $expire ? $expire : $default;
    }



    /**
     * Get body of response.
     *
     * @return string as body.
     */
    public function getBody()
    {
        return $this->response['body'];
    }
}



/**
 * Get a image from a remote server using HTTP GET and If-Modified-Since.
 *
 */
class CRemoteImage
{
    /**
     * Path to cache files.
     */
    private $saveFolder = null;



    /**
     * Use cache or not.
     */
    private $useCache = true;



    /**
     * HTTP object to aid in download file.
     */
    private $http;



    /**
     * Status of the HTTP request.
     */
    private $status;



    /**
     * Defalt age for cached items 60*60*24*7.
     */
    private $defaultMaxAge = 604800;



    /**
     * Url of downloaded item.
     */
    private $url;



    /**
     * Base name of cache file for downloaded item and name of image.
     */
    private $fileName;



    /**
     * Filename for json-file with details of cached item.
     */
    private $fileJson;



    /**
     * Cache details loaded from file.
     */
    private $cache;



    /**
     * Get status of last HTTP request.
     *
     * @return int as status
     */
    public function getStatus()
    {
        return $this->status;
    }



    /**
     * Get JSON details for cache item.
     *
     * @return array with json details on cache.
     */
    public function getDetails()
    {
        return $this->cache;
    }



    /**
     * Set the path to the cache directory.
     *
     * @param boolean $use true to use the cache and false to ignore cache.
     *
     * @return $this
     */
    public function setCache($path)
    {
        $this->saveFolder = rtrim($path, "/") . "/";
        return $this;
    }



    /**
     * Check if cache is writable or throw exception.
     *
     * @return $this
     *
     * @throws Exception if cahce folder is not writable.
     */
    public function isCacheWritable()
    {
        if (!is_writable($this->saveFolder)) {
            throw new Exception("Cache folder is not writable for downloaded files.");
        }
        return $this;
    }



    /**
     * Decide if the cache should be used or not before trying to download
     * a remote file.
     *
     * @param boolean $use true to use the cache and false to ignore cache.
     *
     * @return $this
     */
    public function useCache($use = true)
    {
        $this->useCache = $use;
        return $this;
    }



    /**
     * Set header fields.
     *
     * @return $this
     */
    public function setHeaderFields()
    {
        $cimageVersion = "CImage";
        if (defined("CIMAGE_USER_AGENT")) {
            $cimageVersion = CIMAGE_USER_AGENT;
        }
        
        $this->http->setHeader("User-Agent", "$cimageVersion (PHP/". phpversion() . " cURL)");
        $this->http->setHeader("Accept", "image/jpeg,image/png,image/gif");

        if ($this->useCache) {
            $this->http->setHeader("Cache-Control", "max-age=0");
        } else {
            $this->http->setHeader("Cache-Control", "no-cache");
            $this->http->setHeader("Pragma", "no-cache");
        }
    }



    /**
     * Save downloaded resource to cache.
     *
     * @return string as path to saved file or false if not saved.
     */
    public function save()
    {
        $this->cache = array();
        $date         = $this->http->getDate(time());
        $maxAge       = $this->http->getMaxAge($this->defaultMaxAge);
        $lastModified = $this->http->getLastModified();
        $type         = $this->http->getContentType();

        $this->cache['Date']           = gmdate("D, d M Y H:i:s T", $date);
        $this->cache['Max-Age']        = $maxAge;
        $this->cache['Content-Type']   = $type;
        $this->cache['Url']            = $this->url;

        if ($lastModified) {
            $this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified);
        }

        // Save only if body is a valid image
        $body = $this->http->getBody();
        $img = imagecreatefromstring($body);

        if ($img !== false) {
            file_put_contents($this->fileName, $body);
            file_put_contents($this->fileJson, json_encode($this->cache));
            return $this->fileName;
        }

        return false;
    }



    /**
     * Got a 304 and updates cache with new age.
     *
     * @return string as path to cached file.
     */
    public function updateCacheDetails()
    {
        $date         = $this->http->getDate(time());
        $maxAge       = $this->http->getMaxAge($this->defaultMaxAge);
        $lastModified = $this->http->getLastModified();

        $this->cache['Date']    = gmdate("D, d M Y H:i:s T", $date);
        $this->cache['Max-Age'] = $maxAge;

        if ($lastModified) {
            $this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified);
        }

        file_put_contents($this->fileJson, json_encode($this->cache));
        return $this->fileName;
    }



    /**
     * Download a remote file and keep a cache of downloaded files.
     *
     * @param string $url a remote url.
     *
     * @throws Exception when status code does not match 200 or 304.
     *
     * @return string as path to downloaded file or false if failed.
     */
    public function download($url)
    {
        $this->http = new CHttpGet();
        $this->url = $url;

        // First check if the cache is valid and can be used
        $this->loadCacheDetails();

        if ($this->useCache) {
            $src = $this->getCachedSource();
            if ($src) {
                $this->status = 1;
                return $src;
            }
        }

        // Do a HTTP request to download item
        $this->setHeaderFields();
        $this->http->setUrl($this->url);
        $this->http->doGet();

        $this->status = $this->http->getStatus();
        if ($this->status === 200) {
            $this->isCacheWritable();
            return $this->save();
        } elseif ($this->status === 304) {
            $this->isCacheWritable();
            return $this->updateCacheDetails();
        }

        throw new Exception("Unknown statuscode when downloading remote image: " . $this->status);
    }



    /**
     * Get the path to the cached image file if the cache is valid.
     *
     * @return $this
     */
    public function loadCacheDetails()
    {
        $cacheFile = md5($this->url);
        $this->fileName = $this->saveFolder . $cacheFile;
        $this->fileJson = $this->fileName . ".json";
        if (is_readable($this->fileJson)) {
            $this->cache = json_decode(file_get_contents($this->fileJson), true);
        }
    }



    /**
     * Get the path to the cached image file if the cache is valid.
     *
     * @return string as the path ot the image file or false if no cache.
     */
    public function getCachedSource()
    {
        $imageExists = is_readable($this->fileName);

        // Is cache valid?
        $date   = strtotime($this->cache['Date']);
        $maxAge = $this->cache['Max-Age'];
        $now    = time();
        
        if ($imageExists && $date + $maxAge > $now) {
            return $this->fileName;
        }

        // Prepare for a 304 if available
        if ($imageExists && isset($this->cache['Last-Modified'])) {
            $this->http->setHeader("If-Modified-Since", $this->cache['Last-Modified']);
        }

        return false;
    }
}



/**
 * Act as whitelist (or blacklist).
 *
 */
class CWhitelist
{
    /**
     * Array to contain the whitelist options.
     */
    private $whitelist = array();



    /**
     * Set the whitelist from an array of strings, each item in the
     * whitelist should be a regexp without the surrounding / or #.
     *
     * @param array $whitelist with all valid options,
     *                         default is to clear the whitelist.
     *
     * @return $this
     */
    public function set($whitelist = array())
    {
        if (!is_array($whitelist)) {
            throw new Exception("Whitelist is not of a supported format.");
        }

        $this->whitelist = $whitelist;
        return $this;
    }



    /**
     * Check if item exists in the whitelist.
     *
     * @param string $item      string to check.
     * @param array  $whitelist optional with all valid options, default is null.
     *
     * @return boolean true if item is in whitelist, else false.
     */
    public function check($item, $whitelist = null)
    {
        if ($whitelist !== null) {
            $this->set($whitelist);
        }
        
        if (empty($item) or empty($this->whitelist)) {
            return false;
        }
        
        foreach ($this->whitelist as $regexp) {
            if (preg_match("#$regexp#", $item)) {
                return true;
            }
        }

        return false;
    }
}



/**
 * Create an ASCII version of an image.
 *
 */
class CAsciiArt
{
    /**
     * Character set to use.
     */
    private $characterSet = array(
        'one' => "#0XT|:,.' ",
        'two' => "@%#*+=-:. ",
        'three' => "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "
    );



    /**
     * Current character set.
     */
    private $characters = null;



    /**
     * Length of current character set.
     */
    private $charCount = null;



    /**
     * Scale of the area to swap to a character.
     */
    private $scale = null;



    /**
     * Strategy to calculate luminance.
     */
    private $luminanceStrategy = null;



    /**
     * Constructor which sets default options.
     */
    public function __construct()
    {
        $this->setOptions();
    }



    /**
     * Add a custom character set.
     *
     * @param string $key   for the character set.
     * @param string $value for the character set.
     *
     * @return $this
     */
    public function addCharacterSet($key, $value)
    {
        $this->characterSet[$key] = $value;
        return $this;
    }



    /**
     * Set options for processing, defaults are available.
     *
     * @param array $options to use as default settings.
     *
     * @return $this
     */
    public function setOptions($options = array())
    {
        $default = array(
            "characterSet" => 'two',
            "scale" => 14,
            "luminanceStrategy" => 3,
            "customCharacterSet" => null,
        );
        $default = array_merge($default, $options);
        
        if (!is_null($default['customCharacterSet'])) {
            $this->addCharacterSet('custom', $default['customCharacterSet']);
            $default['characterSet'] = 'custom';
        }
        
        $this->scale = $default['scale'];
        $this->characters = $this->characterSet[$default['characterSet']];
        $this->charCount = strlen($this->characters);
        $this->luminanceStrategy = $default['luminanceStrategy'];
        
        return $this;
    }



    /**
     * Create an Ascii image from an image file.
     *
     * @param string $filename of the image to use.
     *
     * @return string $ascii with the ASCII image.
     */
    public function createFromFile($filename)
    {
        $img = imagecreatefromstring(file_get_contents($filename));
        list($width, $height) = getimagesize($filename);

        $ascii = null;
        $incY = $this->scale;
        $incX = $this->scale / 2;
        
        for ($y = 0; $y < $height - 1; $y += $incY) {
            for ($x = 0; $x < $width - 1; $x += $incX) {
                $toX = min($x + $this->scale / 2, $width - 1);
                $toY = min($y + $this->scale, $height - 1);
                $luminance = $this->luminanceAreaAverage($img, $x, $y, $toX, $toY);
                $ascii .= $this->luminance2character($luminance);
            }
            $ascii .= PHP_EOL;
        }

        return $ascii;
    }



    /**
     * Get the luminance from a region of an image using average color value.
     *
     * @param string  $img the image.
     * @param integer $x1  the area to get pixels from.
     * @param integer $y1  the area to get pixels from.
     * @param integer $x2  the area to get pixels from.
     * @param integer $y2  the area to get pixels from.
     *
     * @return integer $luminance with a value between 0 and 100.
     */
    public function luminanceAreaAverage($img, $x1, $y1, $x2, $y2)
    {
        $numPixels = ($x2 - $x1 + 1) * ($y2 - $y1 + 1);
        $luminance = 0;
        
        for ($x = $x1; $x <= $x2; $x++) {
            for ($y = $y1; $y <= $y2; $y++) {
                $rgb   = imagecolorat($img, $x, $y);
                $red   = (($rgb >> 16) & 0xFF);
                $green = (($rgb >> 8) & 0xFF);
                $blue  = ($rgb & 0xFF);
                $luminance += $this->getLuminance($red, $green, $blue);
            }
        }
        
        return $luminance / $numPixels;
    }



    /**
     * Calculate luminance value with different strategies.
     *
     * @param integer $red   The color red.
     * @param integer $green The color green.
     * @param integer $blue  The color blue.
     *
     * @return float $luminance with a value between 0 and 1.
     */
    public function getLuminance($red, $green, $blue)
    {
        switch ($this->luminanceStrategy) {
            case 1:
                $luminance = ($red * 0.2126 + $green * 0.7152 + $blue * 0.0722) / 255;
                break;
            case 2:
                $luminance = ($red * 0.299 + $green * 0.587 + $blue * 0.114) / 255;
                break;
            case 3:
                $luminance = sqrt(0.299 * pow($red, 2) + 0.587 * pow($green, 2) + 0.114 * pow($blue, 2)) / 255;
                break;
            case 0:
            default:
                $luminance = ($red + $green + $blue) / (255 * 3);
        }

        return $luminance;
    }



    /**
     * Translate the luminance value to a character.
     *
     * @param string $position a value between 0-100 representing the
     *                         luminance.
     *
     * @return string with the ascii character.
     */
    public function luminance2character($luminance)
    {
        $position = (int) round($luminance * ($this->charCount - 1));
        $char = $this->characters[$position];
        return $char;
    }
}



/**
 * Resize and crop images on the fly, store generated images in a cache.
 *
 * @author  Mikael Roos mos@dbwebb.se
 * @example http://dbwebb.se/opensource/cimage
 * @link    https://github.com/mosbth/cimage
 */
class CImage
{

    /**
     * Constants type of PNG image
     */
    const PNG_GREYSCALE         = 0;
    const PNG_RGB               = 2;
    const PNG_RGB_PALETTE       = 3;
    const PNG_GREYSCALE_ALPHA   = 4;
    const PNG_RGB_ALPHA         = 6;



    /**
     * Constant for default image quality when not set
     */
    const JPEG_QUALITY_DEFAULT = 60;



    /**
     * Quality level for JPEG images.
     */
    private $quality;



    /**
     * Is the quality level set from external use (true) or is it default (false)?
     */
    private $useQuality = false;



    /**
     * Constant for default image quality when not set
     */
    const PNG_COMPRESSION_DEFAULT = -1;



    /**
     * Compression level for PNG images.
     */
    private $compress;



    /**
     * Is the compress level set from external use (true) or is it default (false)?
     */
    private $useCompress = false;




    /**
     * Add HTTP headers for outputing image.
     */
    private $HTTPHeader = array();



    /**
     * Default background color, red, green, blue, alpha.
     *
     * @todo remake when upgrading to PHP 5.5
     */
    /*
    const BACKGROUND_COLOR = array(
        'red'   => 0,
        'green' => 0,
        'blue'  => 0,
        'alpha' => null,
    );*/



    /**
     * Default background color to use.
     *
     * @todo remake when upgrading to PHP 5.5
     */
    //private $bgColorDefault = self::BACKGROUND_COLOR;
    private $bgColorDefault = array(
        'red'   => 0,
        'green' => 0,
        'blue'  => 0,
        'alpha' => null,
    );


    /**
     * Background color to use, specified as part of options.
     */
    private $bgColor;



    /**
     * Where to save the target file.
     */
    private $saveFolder;



    /**
     * The working image object.
     */
    private $image;



    /**
     * Image filename, may include subdirectory, relative from $imageFolder
     */
    private $imageSrc;



    /**
     * Actual path to the image, $imageFolder . '/' . $imageSrc
     */
    private $pathToImage;



    /**
     * File type for source image, as provided by getimagesize()
     */
    private $fileType;



    /**
     * File extension to use when saving image.
     */
    private $extension;



    /**
     * Output format, supports null (image) or json.
     */
    private $outputFormat = null;



    /**
     * Verbose mode to print out a trace and display the created image
     */
    private $verbose = false;



    /**
     * Keep a log/trace on what happens
     */
    private $log = array();



    /**
     * Handle image as palette image
     */
    private $palette;



    /**
     * Target filename, with path, to save resulting image in.
     */
    private $cacheFileName;



    /**
     * Set a format to save image as, or null to use original format.
     */
    private $saveAs;


    /**
     * Path to command for filter optimize, for example optipng or null.
     */
    private $pngFilter;
    private $pngFilterCmd;



    /**
     * Path to command for deflate optimize, for example pngout or null.
     */
    private $pngDeflate;
    private $pngDeflateCmd;



    /**
     * Path to command to optimize jpeg images, for example jpegtran or null.
     */
     private $jpegOptimize;
     private $jpegOptimizeCmd;



    /**
     * Image dimensions, calculated from loaded image.
     */
    private $width;  // Calculated from source image
    private $height; // Calculated from source image


    /**
     * New image dimensions, incoming as argument or calculated.
     */
    private $newWidth;
    private $newWidthOrig;  // Save original value
    private $newHeight;
    private $newHeightOrig; // Save original value


    /**
     * Change target height & width when different dpr, dpr 2 means double image dimensions.
     */
    private $dpr = 1;


    /**
     * Always upscale images, even if they are smaller than target image.
     */
    const UPSCALE_DEFAULT = true;
    private $upscale = self::UPSCALE_DEFAULT;



    /**
     * Array with details on how to crop, incoming as argument and calculated.
     */
    public $crop;
    public $cropOrig; // Save original value


    /**
     * String with details on how to do image convolution. String
     * should map a key in the $convolvs array or be a string of
     * 11 float values separated by comma. The first nine builds
     * up the matrix, then divisor and last offset.
     */
    private $convolve;


    /**
     * Custom convolution expressions, matrix 3x3, divisor and offset.
     */
    private $convolves = array(
        'lighten'       => '0,0,0, 0,12,0, 0,0,0, 9, 0',
        'darken'        => '0,0,0, 0,6,0, 0,0,0, 9, 0',
        'sharpen'       => '-1,-1,-1, -1,16,-1, -1,-1,-1, 8, 0',
        'sharpen-alt'   => '0,-1,0, -1,5,-1, 0,-1,0, 1, 0',
        'emboss'        => '1,1,-1, 1,3,-1, 1,-1,-1, 3, 0',
        'emboss-alt'    => '-2,-1,0, -1,1,1, 0,1,2, 1, 0',
        'blur'          => '1,1,1, 1,15,1, 1,1,1, 23, 0',
        'gblur'         => '1,2,1, 2,4,2, 1,2,1, 16, 0',
        'edge'          => '-1,-1,-1, -1,8,-1, -1,-1,-1, 9, 0',
        'edge-alt'      => '0,1,0, 1,-4,1, 0,1,0, 1, 0',
        'draw'          => '0,-1,0, -1,5,-1, 0,-1,0, 0, 0',
        'mean'          => '1,1,1, 1,1,1, 1,1,1, 9, 0',
        'motion'        => '1,0,0, 0,1,0, 0,0,1, 3, 0',
    );


    /**
     * Resize strategy to fill extra area with background color.
     * True or false.
     */
    private $fillToFit;



    /**
     * To store value for option scale.
     */
    private $scale;



    /**
     * To store value for option.
     */
    private $rotateBefore;



    /**
     * To store value for option.
     */
    private $rotateAfter;



    /**
     * To store value for option.
     */
    private $autoRotate;



    /**
     * To store value for option.
     */
    private $sharpen;



    /**
     * To store value for option.
     */
    private $emboss;



    /**
     * To store value for option.
     */
    private $blur;



    /**
     * Used with option area to set which parts of the image to use.
     */
    private $offset;



    /**
     * Calculate target dimension for image when using fill-to-fit resize strategy.
     */
    private $fillWidth;
    private $fillHeight;



    /**
     * Allow remote file download, default is to disallow remote file download.
     */
    private $allowRemote = false;



    /**
     * Path to cache for remote download.
     */
    private $remoteCache;



    /**
     * Pattern to recognize a remote file.
     */
    //private $remotePattern = '#^[http|https]://#';
    private $remotePattern = '#^https?://#';



    /**
     * Use the cache if true, set to false to ignore the cached file.
     */
    private $useCache = true;



    /*
     * Set whitelist for valid hostnames from where remote source can be
     * downloaded.
     */
    private $remoteHostWhitelist = null;



    /*
     * Do verbose logging to file by setting this to a filename.
     */
    private $verboseFileName = null;



    /*
     * Output to ascii can take som options as an array.
     */
    private $asciiOptions = array();



    /*
     * Image copy strategy, defaults to RESAMPLE.
     */
     const RESIZE = 1;
     const RESAMPLE = 2;
     private $copyStrategy = NULL;



    /**
     * Properties, the class is mutable and the method setOptions()
     * decides (partly) what properties are created.
     *
     * @todo Clean up these and check if and how they are used
     */

    public $keepRatio;
    public $cropToFit;
    private $cropWidth;
    private $cropHeight;
    public $crop_x;
    public $crop_y;
    public $filters;
    private $attr; // Calculated from source image




    /**
     * Constructor, can take arguments to init the object.
     *
     * @param string $imageSrc    filename which may contain subdirectory.
     * @param string $imageFolder path to root folder for images.
     * @param string $saveFolder  path to folder where to save the new file or null to skip saving.
     * @param string $saveName    name of target file when saveing.
     */
    public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null)
    {
        $this->setSource($imageSrc, $imageFolder);
        $this->setTarget($saveFolder, $saveName);
    }



    /**
     * Set verbose mode.
     *
     * @param boolean $mode true or false to enable and disable verbose mode,
     *                      default is true.
     *
     * @return $this
     */
    public function setVerbose($mode = true)
    {
        $this->verbose = $mode;
        return $this;
    }



    /**
     * Set save folder, base folder for saving cache files.
     *
     * @todo clean up how $this->saveFolder is used in other methods.
     *
     * @param string $path where to store cached files.
     *
     * @return $this
     */
    public function setSaveFolder($path)
    {
        $this->saveFolder = $path;
        return $this;
    }



    /**
     * Use cache or not.
     *
     * @param boolean $use true or false to use cache.
     *
     * @return $this
     */
    public function useCache($use = true)
    {
        $this->useCache = $use;
        return $this;
    }



    /**
     * Create and save a dummy image. Use dimensions as stated in
     * $this->newWidth, or $width or default to 100 (same for height.
     *
     * @param integer $width  use specified width for image dimension.
     * @param integer $height use specified width for image dimension.
     *
     * @return $this
     */
    public function createDummyImage($width = null, $height = null)
    {
        $this->newWidth  = $this->newWidth  ?: $width  ?: 100;
        $this->newHeight = $this->newHeight ?: $height ?: 100;

        $this->image = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);

        return $this;
    }



    /**
     * Allow or disallow remote image download.
     *
     * @param boolean $allow   true or false to enable and disable.
     * @param string  $cache   path to cache dir.
     * @param string  $pattern to use to detect if its a remote file.
     *
     * @return $this
     */
    public function setRemoteDownload($allow, $cache, $pattern = null)
    {
        $this->allowRemote = $allow;
        $this->remoteCache = $cache;
        $this->remotePattern = is_null($pattern) ? $this->remotePattern : $pattern;

        $this->log(
            "Set remote download to: "
            . ($this->allowRemote ? "true" : "false")
            . " using pattern "
            . $this->remotePattern
        );

        return $this;
    }



    /**
     * Check if the image resource is a remote file or not.
     *
     * @param string $src check if src is remote.
     *
     * @return boolean true if $src is a remote file, else false.
     */
    public function isRemoteSource($src)
    {
        $remote = preg_match($this->remotePattern, $src);
        $this->log("Detected remote image: " . ($remote ? "true" : "false"));
        return !!$remote;
    }



    /**
     * Set whitelist for valid hostnames from where remote source can be
     * downloaded.
     *
     * @param array $whitelist with regexp hostnames to allow download from.
     *
     * @return $this
     */
    public function setRemoteHostWhitelist($whitelist = null)
    {
        $this->remoteHostWhitelist = $whitelist;
        $this->log(
            "Setting remote host whitelist to: "
            . (is_null($whitelist) ? "null" : print_r($whitelist, 1))
        );
        return $this;
    }



    /**
     * Check if the hostname for the remote image, is on a whitelist,
     * if the whitelist is defined.
     *
     * @param string $src the remote source.
     *
     * @return boolean true if hostname on $src is in the whitelist, else false.
     */
    public function isRemoteSourceOnWhitelist($src)
    {
        if (is_null($this->remoteHostWhitelist)) {
            $this->log("Remote host on whitelist not configured - allowing.");
            return true;
        }

        $whitelist = new CWhitelist();
        $hostname = parse_url($src, PHP_URL_HOST);
        $allow = $whitelist->check($hostname, $this->remoteHostWhitelist);

        $this->log(
            "Remote host is on whitelist: "
            . ($allow ? "true" : "false")
        );
        return $allow;
    }



    /**
     * Check if file extension is valid as a file extension.
     *
     * @param string $extension of image file.
     *
     * @return $this
     */
    private function checkFileExtension($extension)
    {
        $valid = array('jpg', 'jpeg', 'png', 'gif');

        in_array(strtolower($extension), $valid)
            or $this->raiseError('Not a valid file extension.');

        return $this;
    }



    /**
     * Normalize the file extension.
     *
     * @param string $extension of image file or skip to use internal.
     *
     * @return string $extension as a normalized file extension.
     */
    private function normalizeFileExtension($extension = null)
    {
        $extension = strtolower($extension ? $extension : $this->extension);

        if ($extension == 'jpeg') {
                $extension = 'jpg';
            }

        return $extension;
    }



    /**
     * Download a remote image and return path to its local copy.
     *
     * @param string $src remote path to image.
     *
     * @return string as path to downloaded remote source.
     */
    public function downloadRemoteSource($src)
    {
        if (!$this->isRemoteSourceOnWhitelist($src)) {
            throw new Exception("Hostname is not on whitelist for remote sources.");
        }

        $remote = new CRemoteImage();

        if (!is_writable($this->remoteCache)) {
            $this->log("The remote cache is not writable.");
        }

        $remote->setCache($this->remoteCache);
        $remote->useCache($this->useCache);
        $src = $remote->download($src);

        $this->log("Remote HTTP status: " . $remote->getStatus());
        $this->log("Remote item is in local cache: $src");
        $this->log("Remote details on cache:" . print_r($remote->getDetails(), true));

        return $src;
    }



    /**
     * Set source file to use as image source.
     *
     * @param string $src of image.
     * @param string $dir as optional base directory where images are.
     *
     * @return $this
     */
    public function setSource($src, $dir = null)
    {
        if (!isset($src)) {
            $this->imageSrc = null;
            $this->pathToImage = null;
            return $this;
        }

        if ($this->allowRemote && $this->isRemoteSource($src)) {
            $src = $this->downloadRemoteSource($src);
            $dir = null;
        }

        if (!isset($dir)) {
            $dir = dirname($src);
            $src = basename($src);
        }

        $this->imageSrc     = ltrim($src, '/');
        $imageFolder        = rtrim($dir, '/');
        $this->pathToImage  = $imageFolder . '/' . $this->imageSrc;

        return $this;
    }



    /**
     * Set target file.
     *
     * @param string $src of target image.
     * @param string $dir as optional base directory where images are stored.
     *                    Uses $this->saveFolder if null.
     *
     * @return $this
     */
    public function setTarget($src = null, $dir = null)
    {
        if (!isset($src)) {
            $this->cacheFileName = null;
            return $this;
        }

        if (isset($dir)) {
            $this->saveFolder = rtrim($dir, '/');
        }

        $this->cacheFileName  = $this->saveFolder . '/' . $src;

        // Sanitize filename
        $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName);
        $this->log("The cache file name is: " . $this->cacheFileName);

        return $this;
    }



    /**
     * Get filename of target file.
     *
     * @return Boolean|String as filename of target or false if not set.
     */
    public function getTarget()
    {
        return $this->cacheFileName;
    }



    /**
     * Set options to use when processing image.
     *
     * @param array $args used when processing image.
     *
     * @return $this
     */
    public function setOptions($args)
    {
        $this->log("Set new options for processing image.");

        $defaults = array(
            // Options for calculate dimensions
            'newWidth'    => null,
            'newHeight'   => null,
            'aspectRatio' => null,
            'keepRatio'   => true,
            'cropToFit'   => false,
            'fillToFit'   => null,
            'crop'        => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0),
            'area'        => null, //'0,0,0,0',
            'upscale'     => self::UPSCALE_DEFAULT,

            // Options for caching or using original
            'useCache'    => true,
            'useOriginal' => true,

            // Pre-processing, before resizing is done
            'scale'        => null,
            'rotateBefore' => null,
            'autoRotate'  => false,

            // General options
            'bgColor'     => null,

            // Post-processing, after resizing is done
            'palette'     => null,
            'filters'     => null,
            'sharpen'     => null,
            'emboss'      => null,
            'blur'        => null,
            'convolve'       => null,
            'rotateAfter' => null,

            // Output format
            'outputFormat' => null,
            'dpr'          => 1,
        );

        // Convert crop settings from string to array
        if (isset($args['crop']) && !is_array($args['crop'])) {
            $pices = explode(',', $args['crop']);
            $args['crop'] = array(
                'width'   => $pices[0],
                'height'  => $pices[1],
                'start_x' => $pices[2],
                'start_y' => $pices[3],
            );
        }

        // Convert area settings from string to array
        if (isset($args['area']) && !is_array($args['area'])) {
                $pices = explode(',', $args['area']);
                $args['area'] = array(
                    'top'    => $pices[0],
                    'right'  => $pices[1],
                    'bottom' => $pices[2],
                    'left'   => $pices[3],
                );
        }

        // Convert filter settings from array of string to array of array
        if (isset($args['filters']) && is_array($args['filters'])) {
            foreach ($args['filters'] as $key => $filterStr) {
                $parts = explode(',', $filterStr);
                $filter = $this->mapFilter($parts[0]);
                $filter['str'] = $filterStr;
                for ($i=1; $i<=$filter['argc']; $i++) {
                    if (isset($parts[$i])) {
                        $filter["arg{$i}"] = $parts[$i];
                    } else {
                        throw new Exception(
                            'Missing arg to filter, review how many arguments are needed at
                            http://php.net/manual/en/function.imagefilter.php'
                        );
                    }
                }
                $args['filters'][$key] = $filter;
            }
        }

        // Merge default arguments with incoming and set properties.
        //$args = array_merge_recursive($defaults, $args);
        $args = array_merge($defaults, $args);
        foreach ($defaults as $key => $val) {
            $this->{$key} = $args[$key];
        }

        if ($this->bgColor) {
            $this->setDefaultBackgroundColor($this->bgColor);
        }

        // Save original values to enable re-calculating
        $this->newWidthOrig  = $this->newWidth;
        $this->newHeightOrig = $this->newHeight;
        $this->cropOrig      = $this->crop;

        return $this;
    }



    /**
     * Map filter name to PHP filter and id.
     *
     * @param string $name the name of the filter.
     *
     * @return array with filter settings
     * @throws Exception
     */
    private function mapFilter($name)
    {
        $map = array(
            'negate'          => array('id'=>0,  'argc'=>0, 'type'=>IMG_FILTER_NEGATE),
            'grayscale'       => array('id'=>1,  'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE),
            'brightness'      => array('id'=>2,  'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS),
            'contrast'        => array('id'=>3,  'argc'=>1, 'type'=>IMG_FILTER_CONTRAST),
            'colorize'        => array('id'=>4,  'argc'=>4, 'type'=>IMG_FILTER_COLORIZE),
            'edgedetect'      => array('id'=>5,  'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT),
            'emboss'          => array('id'=>6,  'argc'=>0, 'type'=>IMG_FILTER_EMBOSS),
            'gaussian_blur'   => array('id'=>7,  'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR),
            'selective_blur'  => array('id'=>8,  'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR),
            'mean_removal'    => array('id'=>9,  'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL),
            'smooth'          => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH),
            'pixelate'        => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE),
        );

        if (isset($map[$name])) {
            return $map[$name];
        } else {
            throw new Exception('No such filter.');
        }
    }



    /**
     * Load image details from original image file.
     *
     * @param string $file the file to load or null to use $this->pathToImage.
     *
     * @return $this
     * @throws Exception
     */
    public function loadImageDetails($file = null)
    {
        $file = $file ? $file : $this->pathToImage;

        is_readable($file)
            or $this->raiseError('Image file does not exist.');

        // Get details on image
        $info = list($this->width, $this->height, $this->fileType, $this->attr) = getimagesize($file);
        if (empty($info)) {
            throw new Exception("The file doesn't seem to be a valid image.");
        }

        if ($this->verbose) {
            $this->log("Loading image details for: {$file}");
            $this->log(" Image width x height (type): {$this->width} x {$this->height} ({$this->fileType}).");
            $this->log(" Image filesize: " . filesize($file) . " bytes.");
            $this->log(" Image mimetype: " . image_type_to_mime_type($this->fileType));
        }

        return $this;
    }



    /**
     * Init new width and height and do some sanity checks on constraints, before any
     * processing can be done.
     *
     * @return $this
     * @throws Exception
     */
    public function initDimensions()
    {
        $this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");

        // width as %
        if ($this->newWidth[strlen($this->newWidth)-1] == '%') {
            $this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100;
            $this->log("Setting new width based on % to {$this->newWidth}");
        }

        // height as %
        if ($this->newHeight[strlen($this->newHeight)-1] == '%') {
            $this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100;
            $this->log("Setting new height based on % to {$this->newHeight}");
        }

        is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range');

        // width & height from aspect ratio
        if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) {
            if ($this->aspectRatio >= 1) {
                $this->newWidth   = $this->width;
                $this->newHeight  = $this->width / $this->aspectRatio;
                $this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}");

            } else {
                $this->newHeight  = $this->height;
                $this->newWidth   = $this->height * $this->aspectRatio;
                $this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}");
            }

        } elseif ($this->aspectRatio && is_null($this->newWidth)) {
            $this->newWidth   = $this->newHeight * $this->aspectRatio;
            $this->log("Setting new width based on aspect ratio to {$this->newWidth}");

        } elseif ($this->aspectRatio && is_null($this->newHeight)) {
            $this->newHeight  = $this->newWidth / $this->aspectRatio;
            $this->log("Setting new height based on aspect ratio to {$this->newHeight}");
        }

        // Change width & height based on dpr
        if ($this->dpr != 1) {
            if (!is_null($this->newWidth)) {
                $this->newWidth  = round($this->newWidth * $this->dpr);
                $this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}");
            }
            if (!is_null($this->newHeight)) {
                $this->newHeight = round($this->newHeight * $this->dpr);
                $this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}");
            }
        }

        // Check values to be within domain
        is_null($this->newWidth)
            or is_numeric($this->newWidth)
            or $this->raiseError('Width not numeric');

        is_null($this->newHeight)
            or is_numeric($this->newHeight)
            or $this->raiseError('Height not numeric');

        $this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");

        return $this;
    }



    /**
     * Calculate new width and height of image, based on settings.
     *
     * @return $this
     */
    public function calculateNewWidthAndHeight()
    {
        // Crop, use cropped width and height as base for calulations
        $this->log("Calculate new width and height.");
        $this->log("Original width x height is {$this->width} x {$this->height}.");
        $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");

        // Check if there is an area to crop off
        if (isset($this->area)) {
            $this->offset['top']    = round($this->area['top'] / 100 * $this->height);
            $this->offset['right']  = round($this->area['right'] / 100 * $this->width);
            $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height);
            $this->offset['left']   = round($this->area['left'] / 100 * $this->width);
            $this->offset['width']  = $this->width - $this->offset['left'] - $this->offset['right'];
            $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom'];
            $this->width  = $this->offset['width'];
            $this->height = $this->offset['height'];
            $this->log("The offset for the area to use is top {$this->area['top']}%, right {$this->area['right']}%, bottom {$this->area['bottom']}%, left {$this->area['left']}%.");
            $this->log("The offset for the area to use is top {$this->offset['top']}px, right {$this->offset['right']}px, bottom {$this->offset['bottom']}px, left {$this->offset['left']}px, width {$this->offset['width']}px, height {$this->offset['height']}px.");
        }

        $width  = $this->width;
        $height = $this->height;

        // Check if crop is set
        if ($this->crop) {
            $width  = $this->crop['width']  = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width'];
            $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height'];

            if ($this->crop['start_x'] == 'left') {
                $this->crop['start_x'] = 0;
            } elseif ($this->crop['start_x'] == 'right') {
                $this->crop['start_x'] = $this->width - $width;
            } elseif ($this->crop['start_x'] == 'center') {
                $this->crop['start_x'] = round($this->width / 2) - round($width / 2);
            }

            if ($this->crop['start_y'] == 'top') {
                $this->crop['start_y'] = 0;
            } elseif ($this->crop['start_y'] == 'bottom') {
                $this->crop['start_y'] = $this->height - $height;
            } elseif ($this->crop['start_y'] == 'center') {
                $this->crop['start_y'] = round($this->height / 2) - round($height / 2);
            }

            $this->log("Crop area is width {$width}px, height {$height}px, start_x {$this->crop['start_x']}px, start_y {$this->crop['start_y']}px.");
        }

        // Calculate new width and height if keeping aspect-ratio.
        if ($this->keepRatio) {

            $this->log("Keep aspect ratio.");

            // Crop-to-fit and both new width and height are set.
            if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) {

                // Use newWidth and newHeigh as width/height, image should fit in box.
                $this->log("Use newWidth and newHeigh as width/height, image should fit in box.");

            } elseif (isset($this->newWidth) && isset($this->newHeight)) {

                // Both new width and height are set.
                // Use newWidth and newHeigh as max width/height, image should not be larger.
                $ratioWidth  = $width  / $this->newWidth;
                $ratioHeight = $height / $this->newHeight;
                $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight;
                $this->newWidth  = round($width  / $ratio);
                $this->newHeight = round($height / $ratio);
                $this->log("New width and height was set.");

            } elseif (isset($this->newWidth)) {

                // Use new width as max-width
                $factor = (float)$this->newWidth / (float)$width;
                $this->newHeight = round($factor * $height);
                $this->log("New width was set.");

            } elseif (isset($this->newHeight)) {

                // Use new height as max-hight
                $factor = (float)$this->newHeight / (float)$height;
                $this->newWidth = round($factor * $width);
                $this->log("New height was set.");

            }

            // Get image dimensions for pre-resize image.
            if ($this->cropToFit || $this->fillToFit) {

                // Get relations of original & target image
                $ratioWidth  = $width  / $this->newWidth;
                $ratioHeight = $height / $this->newHeight;

                if ($this->cropToFit) {

                    // Use newWidth and newHeigh as defined width/height,
                    // image should fit the area.
                    $this->log("Crop to fit.");
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight;
                    $this->cropWidth  = round($width  / $ratio);
                    $this->cropHeight = round($height / $ratio);
                    $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio).");

                } elseif ($this->fillToFit) {

                    // Use newWidth and newHeigh as defined width/height,
                    // image should fit the area.
                    $this->log("Fill to fit.");
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth;
                    $this->fillWidth  = round($width  / $ratio);
                    $this->fillHeight = round($height / $ratio);
                    $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio).");
                }
            }
        }

        // Crop, ensure to set new width and height
        if ($this->crop) {
            $this->log("Crop.");
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
        }

        // Fill to fit, ensure to set new width and height
        /*if ($this->fillToFit) {
            $this->log("FillToFit.");
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
        }*/

        // No new height or width is set, use existing measures.
        $this->newWidth  = round(isset($this->newWidth) ? $this->newWidth : $this->width);
        $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height);
        $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}.");

        return $this;
    }



    /**
     * Re-calculate image dimensions when original image dimension has changed.
     *
     * @return $this
     */
    public function reCalculateDimensions()
    {
        $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight);

        $this->newWidth  = $this->newWidthOrig;
        $this->newHeight = $this->newHeightOrig;
        $this->crop      = $this->cropOrig;

        $this->initDimensions()
             ->calculateNewWidthAndHeight();

        return $this;
    }



    /**
     * Set extension for filename to save as.
     *
     * @param string $saveas extension to save image as
     *
     * @return $this
     */
    public function setSaveAsExtension($saveAs = null)
    {
        if (isset($saveAs)) {
            $saveAs = strtolower($saveAs);
            $this->checkFileExtension($saveAs);
            $this->saveAs = $saveAs;
            $this->extension = $saveAs;
        }

        $this->log("Prepare to save image as: " . $this->extension);

        return $this;
    }



    /**
     * Set JPEG quality to use when saving image
     *
     * @param int $quality as the quality to set.
     *
     * @return $this
     */
    public function setJpegQuality($quality = null)
    {
        if ($quality) {
            $this->useQuality = true;
        }

        $this->quality = isset($quality)
            ? $quality
            : self::JPEG_QUALITY_DEFAULT;

        (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100)
            or $this->raiseError('Quality not in range.');

        $this->log("Setting JPEG quality to {$this->quality}.");

        return $this;
    }



    /**
     * Set PNG compressen algorithm to use when saving image
     *
     * @param int $compress as the algorithm to use.
     *
     * @return $this
     */
    public function setPngCompression($compress = null)
    {
        if ($compress) {
            $this->useCompress = true;
        }

        $this->compress = isset($compress)
            ? $compress
            : self::PNG_COMPRESSION_DEFAULT;

        (is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9)
            or $this->raiseError('Quality not in range.');

        $this->log("Setting PNG compression level to {$this->compress}.");

        return $this;
    }



    /**
     * Use original image if possible, check options which affects image processing.
     *
     * @param boolean $useOrig default is to use original if possible, else set to false.
     *
     * @return $this
     */
    public function useOriginalIfPossible($useOrig = true)
    {
        if ($useOrig
            && ($this->newWidth == $this->width)
            && ($this->newHeight == $this->height)
            && !$this->area
            && !$this->crop
            && !$this->cropToFit
            && !$this->fillToFit
            && !$this->filters
            && !$this->sharpen
            && !$this->emboss
            && !$this->blur
            && !$this->convolve
            && !$this->palette
            && !$this->useQuality
            && !$this->useCompress
            && !$this->saveAs
            && !$this->rotateBefore
            && !$this->rotateAfter
            && !$this->autoRotate
            && !$this->bgColor
            && ($this->upscale === self::UPSCALE_DEFAULT)
        ) {
            $this->log("Using original image.");
            $this->output($this->pathToImage);
        }

        return $this;
    }



    /**
     * Generate filename to save file in cache.
     *
     * @param string  $base      as optional basepath for storing file.
     * @param boolean $useSubdir use or skip the subdir part when creating the
     *                           filename.
     * @param string  $prefix    to add as part of filename
     *
     * @return $this
     */
    public function generateFilename($base = null, $useSubdir = true, $prefix = null)
    {
        $filename     = basename($this->pathToImage);
        $cropToFit    = $this->cropToFit    ? '_cf'                      : null;
        $fillToFit    = $this->fillToFit    ? '_ff'                      : null;
        $crop_x       = $this->crop_x       ? "_x{$this->crop_x}"        : null;
        $crop_y       = $this->crop_y       ? "_y{$this->crop_y}"        : null;
        $scale        = $this->scale        ? "_s{$this->scale}"         : null;
        $bgColor      = $this->bgColor      ? "_bgc{$this->bgColor}"     : null;
        $quality      = $this->quality      ? "_q{$this->quality}"       : null;
        $compress     = $this->compress     ? "_co{$this->compress}"     : null;
        $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null;
        $rotateAfter  = $this->rotateAfter  ? "_ra{$this->rotateAfter}"  : null;

        $saveAs = $this->normalizeFileExtension();
        $saveAs = $saveAs ? "_$saveAs" : null;

        $copyStrat = null;
        if ($this->copyStrategy === self::RESIZE) {
            $copyStrat = "_rs";
        }

        $width  = $this->newWidth  ? '_' . $this->newWidth  : null;
        $height = $this->newHeight ? '_' . $this->newHeight : null;

        $offset = isset($this->offset)
            ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left']
            : null;

        $crop = $this->crop
            ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y']
            : null;

        $filters = null;
        if (isset($this->filters)) {
            foreach ($this->filters as $filter) {
                if (is_array($filter)) {
                    $filters .= "_f{$filter['id']}";
                    for ($i=1; $i<=$filter['argc']; $i++) {
                        $filters .= "-".$filter["arg{$i}"];
                    }
                }
            }
        }

        $sharpen = $this->sharpen ? 's' : null;
        $emboss  = $this->emboss  ? 'e' : null;
        $blur    = $this->blur    ? 'b' : null;
        $palette = $this->palette ? 'p' : null;

        $autoRotate = $this->autoRotate ? 'ar' : null;

        $optimize  = $this->jpegOptimize ? 'o' : null;
        $optimize .= $this->pngFilter    ? 'f' : null;
        $optimize .= $this->pngDeflate   ? 'd' : null;

        $convolve = null;
        if ($this->convolve) {
            $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve);
        }

        $upscale = null;
        if ($this->upscale !== self::UPSCALE_DEFAULT) {
            $upscale = '_nu';
        }

        $subdir = null;
        if ($useSubdir === true) {
            $subdir = str_replace('/', '-', dirname($this->imageSrc));
            $subdir = ($subdir == '.') ? '_.' : $subdir;
            $subdir .= '_';
        }

        $file = $prefix . $subdir . $filename . $width . $height
            . $offset . $crop . $cropToFit . $fillToFit
            . $crop_x . $crop_y . $upscale
            . $quality . $filters . $sharpen . $emboss . $blur . $palette
            . $optimize . $compress
            . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor
            . $convolve . $copyStrat . $saveAs;

        return $this->setTarget($file, $base);
    }



    /**
     * Use cached version of image, if possible.
     *
     * @param boolean $useCache is default true, set to false to avoid using cached object.
     *
     * @return $this
     */
    public function useCacheIfPossible($useCache = true)
    {
        if ($useCache && is_readable($this->cacheFileName)) {
            $fileTime   = filemtime($this->pathToImage);
            $cacheTime  = filemtime($this->cacheFileName);

            if ($fileTime <= $cacheTime) {
                if ($this->useCache) {
                    if ($this->verbose) {
                        $this->log("Use cached file.");
                        $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
                    }
                    $this->output($this->cacheFileName, $this->outputFormat);
                } else {
                    $this->log("Cache is valid but ignoring it by intention.");
                }
            } else {
                $this->log("Original file is modified, ignoring cache.");
            }
        } else {
            $this->log("Cachefile does not exists or ignoring it.");
        }

        return $this;
    }



    /**
     * Load image from disk. Try to load image without verbose error message,
     * if fail, load again and display error messages.
     *
     * @param string $src of image.
     * @param string $dir as base directory where images are.
     *
     * @return $this
     *
     */
    public function load($src = null, $dir = null)
    {
        if (isset($src)) {
            $this->setSource($src, $dir);
        }

        $this->loadImageDetails($this->pathToImage);

        $this->image = imagecreatefromstring(file_get_contents($this->pathToImage));
        if ($this->image === false) {
            throw new Exception("Could not load image.");
        }

        /* Removed v0.7.7
        if (image_type_to_mime_type($this->fileType) == 'image/png') {
            $type = $this->getPngType();
            $hasFewColors = imagecolorstotal($this->image);

            if ($type == self::PNG_RGB_PALETTE || ($hasFewColors > 0 && $hasFewColors <= 256)) {
                if ($this->verbose) {
                    $this->log("Handle this image as a palette image.");
                }
                $this->palette = true;
            }
        }
        */

        if ($this->verbose) {
            $this->log("### Image successfully loaded from file.");
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
            $this->log(" Number of colors in image = " . $this->colorsTotal($this->image));
            $index = imagecolortransparent($this->image);
            $this->log(" Detected transparent color = " . ($index >= 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
        }

        return $this;
    }



    /**
     * Get the type of PNG image.
     *
     * @param string $filename to use instead of default.
     *
     * @return int as the type of the png-image
     *
     */
    public function getPngType($filename = null)
    {
        $filename = $filename ? $filename : $this->pathToImage;

        $pngType = ord(file_get_contents($filename, false, null, 25, 1));

        if ($this->verbose) {
            $this->log("Checking png type of: " . $filename);
            $this->log($this->getPngTypeAsString($pngType));
        }

        return $pngType;
    }



    /**
     * Get the type of PNG image as a verbose string.
     *
     * @param integer $type     to use, default is to check the type.
     * @param string  $filename to use instead of default.
     *
     * @return int as the type of the png-image
     *
     */
    private function getPngTypeAsString($pngType = null, $filename = null)
    {
        if ($filename || !$pngType) {
            $pngType = $this->getPngType($filename);
        }

        $index = imagecolortransparent($this->image);
        $transparent = null;
        if ($index != -1) {
            $transparent = " (transparent)";
        }

        switch ($pngType) {

            case self::PNG_GREYSCALE:
                $text = "PNG is type 0, Greyscale$transparent";
                break;

            case self::PNG_RGB:
                $text = "PNG is type 2, RGB$transparent";
                break;

            case self::PNG_RGB_PALETTE:
                $text = "PNG is type 3, RGB with palette$transparent";
                break;

            case self::PNG_GREYSCALE_ALPHA:
                $text = "PNG is type 4, Greyscale with alpha channel";
                break;

            case self::PNG_RGB_ALPHA:
                $text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)";
                break;

            default:
                $text = "PNG is UNKNOWN type, is it really a PNG image?";
        }

        return $text;
    }




    /**
     * Calculate number of colors in an image.
     *
     * @param resource $im the image.
     *
     * @return int
     */
    private function colorsTotal($im)
    {
        if (imageistruecolor($im)) {
            $this->log("Colors as true color.");
            $h = imagesy($im);
            $w = imagesx($im);
            $c = array();
            for ($x=0; $x < $w; $x++) {
                for ($y=0; $y < $h; $y++) {
                    @$c['c'.imagecolorat($im, $x, $y)]++;
                }
            }
            return count($c);
        } else {
            $this->log("Colors as palette.");
            return imagecolorstotal($im);
        }
    }



    /**
     * Preprocess image before rezising it.
     *
     * @return $this
     */
    public function preResize()
    {
        $this->log("### Pre-process before resizing");

        // Rotate image
        if ($this->rotateBefore) {
            $this->log("Rotating image.");
            $this->rotate($this->rotateBefore, $this->bgColor)
                 ->reCalculateDimensions();
        }

        // Auto-rotate image
        if ($this->autoRotate) {
            $this->log("Auto rotating image.");
            $this->rotateExif()
                 ->reCalculateDimensions();
        }

        // Scale the original image before starting
        if (isset($this->scale)) {
            $this->log("Scale by {$this->scale}%");
            $newWidth  = $this->width * $this->scale / 100;
            $newHeight = $this->height * $this->scale / 100;
            $img = $this->CreateImageKeepTransparency($newWidth, $newHeight);
            imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height);
            $this->image = $img;
            $this->width = $newWidth;
            $this->height = $newHeight;
        }

        return $this;
    }



    /**
     * Resize or resample the image while resizing.
     *
     * @param int $strategy as CImage::RESIZE or CImage::RESAMPLE
     *
     * @return $this
     */
     public function setCopyResizeStrategy($strategy)
     {
         $this->copyStrategy = $strategy;
         return $this;
     }



    /**
     * Resize and or crop the image.
     *
     * @return void
     */
    public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h)
    {
        if($this->copyStrategy == self::RESIZE) {
            $this->log("Copy by resize");
            imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
        } else {
            $this->log("Copy by resample");
            imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
        }
    }



    /**
     * Resize and or crop the image.
     *
     * @return $this
     */
    public function resize()
    {

        $this->log("### Starting to Resize()");
        $this->log("Upscale = '$this->upscale'");

        // Only use a specified area of the image, $this->offset is defining the area to use
        if (isset($this->offset)) {

            $this->log("Offset for area to use, cropping it width={$this->offset['width']}, height={$this->offset['height']}, start_x={$this->offset['left']}, start_y={$this->offset['top']}");
            $img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']);
            imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']);
            $this->image = $img;
            $this->width = $this->offset['width'];
            $this->height = $this->offset['height'];
        }

        if ($this->crop) {

            // Do as crop, take only part of image
            $this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}");
            $img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']);
            imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']);
            $this->image = $img;
            $this->width = $this->crop['width'];
            $this->height = $this->crop['height'];
        }

        if (!$this->upscale) {
            // Consider rewriting the no-upscale code to fit within this if-statement,
            // likely to be more readable code.
            // The code is more or leass equal in below crop-to-fit, fill-to-fit and stretch
        }

        if ($this->cropToFit) {

            // Resize by crop to fit
            $this->log("Resizing using strategy - Crop to fit");

            if (!$this->upscale && ($this->width < $this->newWidth || $this->height < $this->newHeight)) {
                $this->log("Resizing - smaller image, do not upscale.");

                $cropX = round(($this->cropWidth/2) - ($this->newWidth/2));
                $cropY = round(($this->cropHeight/2) - ($this->newHeight/2));

                $posX = 0;
                $posY = 0;

                if ($this->newWidth > $this->width) {
                    $posX = round(($this->newWidth - $this->width) / 2);
                }

                if ($this->newHeight > $this->height) {
                    $posY = round(($this->newHeight - $this->height) / 2);
                }

                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
                imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->newWidth, $this->newHeight);
            } else {
                $cropX = round(($this->cropWidth/2) - ($this->newWidth/2));
                $cropY = round(($this->cropHeight/2) - ($this->newHeight/2));
                $imgPreCrop   = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight);
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
                $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height);
                imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight);
            }

            $this->image = $imageResized;
            $this->width = $this->newWidth;
            $this->height = $this->newHeight;

        } elseif ($this->fillToFit) {

            // Resize by fill to fit
            $this->log("Resizing using strategy - Fill to fit");

            $posX = 0;
            $posY = 0;

            $ratioOrig = $this->width / $this->height;
            $ratioNew  = $this->newWidth / $this->newHeight;

            // Check ratio for landscape or portrait
            if ($ratioOrig < $ratioNew) {
                $posX = round(($this->newWidth - $this->fillWidth) / 2);
            } else {
                $posY = round(($this->newHeight - $this->fillHeight) / 2);
            }

            if (!$this->upscale
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)
            ) {

                $this->log("Resizing - smaller image, do not upscale.");
                $posX = round(($this->fillWidth - $this->width) / 2);
                $posY = round(($this->fillHeight - $this->height) / 2);
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
                imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight);

            } else {
                $imgPreFill   = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight);
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
                $this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height);
                imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight);
            }

            $this->image = $imageResized;
            $this->width = $this->newWidth;
            $this->height = $this->newHeight;

        } elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) {

            // Resize it
            $this->log("Resizing, new height and/or width");

            if (!$this->upscale
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)
            ) {
                $this->log("Resizing - smaller image, do not upscale.");

                if (!$this->keepRatio) {
                    $this->log("Resizing - stretch to fit selected.");

                    $posX = 0;
                    $posY = 0;
                    $cropX = 0;
                    $cropY = 0;

                    if ($this->newWidth > $this->width && $this->newHeight > $this->height) {
                        $posX = round(($this->newWidth - $this->width) / 2);
                        $posY = round(($this->newHeight - $this->height) / 2);
                    } elseif ($this->newWidth > $this->width) {
                        $posX = round(($this->newWidth - $this->width) / 2);
                        $cropY = round(($this->height - $this->newHeight) / 2);
                    } elseif ($this->newHeight > $this->height) {
                        $posY = round(($this->newHeight - $this->height) / 2);
                        $cropX = round(($this->width - $this->newWidth) / 2);
                    }

                    //$this->log("posX=$posX, posY=$posY, cropX=$cropX, cropY=$cropY.");
                    $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
                    imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->newWidth, $this->newHeight);
                    $this->image = $imageResized;
                    $this->width = $this->newWidth;
                    $this->height = $this->newHeight;
                }
            } else {
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
                $this->imageCopyResampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height);
                $this->image = $imageResized;
                $this->width = $this->newWidth;
                $this->height = $this->newHeight;
            }
        }

        return $this;
    }



    /**
     * Postprocess image after rezising image.
     *
     * @return $this
     */
    public function postResize()
    {
        $this->log("### Post-process after resizing");

        // Rotate image
        if ($this->rotateAfter) {
            $this->log("Rotating image.");
            $this->rotate($this->rotateAfter, $this->bgColor);
        }

        // Apply filters
        if (isset($this->filters) && is_array($this->filters)) {

            foreach ($this->filters as $filter) {
                $this->log("Applying filter {$filter['type']}.");

                switch ($filter['argc']) {

                    case 0:
                        imagefilter($this->image, $filter['type']);
                        break;

                    case 1:
                        imagefilter($this->image, $filter['type'], $filter['arg1']);
                        break;

                    case 2:
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']);
                        break;

                    case 3:
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']);
                        break;

                    case 4:
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']);
                        break;
                }
            }
        }

        // Convert to palette image
        if ($this->palette) {
            $this->log("Converting to palette image.");
            $this->trueColorToPalette();
        }

        // Blur the image
        if ($this->blur) {
            $this->log("Blur.");
            $this->blurImage();
        }

        // Emboss the image
        if ($this->emboss) {
            $this->log("Emboss.");
            $this->embossImage();
        }

        // Sharpen the image
        if ($this->sharpen) {
            $this->log("Sharpen.");
            $this->sharpenImage();
        }

        // Custom convolution
        if ($this->convolve) {
            //$this->log("Convolve: " . $this->convolve);
            $this->imageConvolution();
        }

        return $this;
    }



    /**
     * Rotate image using angle.
     *
     * @param float $angle        to rotate image.
     * @param int   $anglebgColor to fill image with if needed.
     *
     * @return $this
     */
    public function rotate($angle, $bgColor)
    {
        $this->log("Rotate image " . $angle . " degrees with filler color.");

        $color = $this->getBackgroundColor();
        $this->image = imagerotate($this->image, $angle, $color);

        $this->width  = imagesx($this->image);
        $this->height = imagesy($this->image);

        $this->log("New image dimension width x height: " . $this->width . " x " . $this->height);

        return $this;
    }



    /**
     * Rotate image using information in EXIF.
     *
     * @return $this
     */
    public function rotateExif()
    {
        if (!in_array($this->fileType, array(IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM))) {
            $this->log("Autorotate ignored, EXIF not supported by this filetype.");
            return $this;
        }

        $exif = exif_read_data($this->pathToImage);

        if (!empty($exif['Orientation'])) {
            switch ($exif['Orientation']) {
                case 3:
                    $this->log("Autorotate 180.");
                    $this->rotate(180, $this->bgColor);
                    break;

                case 6:
                    $this->log("Autorotate -90.");
                    $this->rotate(-90, $this->bgColor);
                    break;

                case 8:
                    $this->log("Autorotate 90.");
                    $this->rotate(90, $this->bgColor);
                    break;

                default:
                    $this->log("Autorotate ignored, unknown value as orientation.");
            }
        } else {
            $this->log("Autorotate ignored, no orientation in EXIF.");
        }

        return $this;
    }



    /**
     * Convert true color image to palette image, keeping alpha.
     * http://stackoverflow.com/questions/5752514/how-to-convert-png-to-8-bit-png-using-php-gd-library
     *
     * @return void
     */
    public function trueColorToPalette()
    {
        $img = imagecreatetruecolor($this->width, $this->height);
        $bga = imagecolorallocatealpha($img, 0, 0, 0, 127);
        imagecolortransparent($img, $bga);
        imagefill($img, 0, 0, $bga);
        imagecopy($img, $this->image, 0, 0, 0, 0, $this->width, $this->height);
        imagetruecolortopalette($img, false, 255);
        imagesavealpha($img, true);

        if (imageistruecolor($this->image)) {
            $this->log("Matching colors with true color image.");
            imagecolormatch($this->image, $img);
        }

        $this->image = $img;
    }



    /**
     * Sharpen image using image convolution.
     *
     * @return $this
     */
    public function sharpenImage()
    {
        $this->imageConvolution('sharpen');
        return $this;
    }



    /**
     * Emboss image using image convolution.
     *
     * @return $this
     */
    public function embossImage()
    {
        $this->imageConvolution('emboss');
        return $this;
    }



    /**
     * Blur image using image convolution.
     *
     * @return $this
     */
    public function blurImage()
    {
        $this->imageConvolution('blur');
        return $this;
    }



    /**
     * Create convolve expression and return arguments for image convolution.
     *
     * @param string $expression constant string which evaluates to a list of
     *                           11 numbers separated by komma or such a list.
     *
     * @return array as $matrix (3x3), $divisor and $offset
     */
    public function createConvolveArguments($expression)
    {
        // Check of matching constant
        if (isset($this->convolves[$expression])) {
            $expression = $this->convolves[$expression];
        }

        $part = explode(',', $expression);
        $this->log("Creating convolution expressen: $expression");

        // Expect list of 11 numbers, split by , and build up arguments
        if (count($part) != 11) {
            throw new Exception(
                "Missmatch in argument convolve. Expected comma-separated string with
                11 float values. Got $expression."
            );
        }

        array_walk($part, function ($item, $key) {
            if (!is_numeric($item)) {
                throw new Exception("Argument to convolve expression should be float but is not.");
            }
        });

        return array(
            array(
                array($part[0], $part[1], $part[2]),
                array($part[3], $part[4], $part[5]),
                array($part[6], $part[7], $part[8]),
            ),
            $part[9],
            $part[10],
        );
    }



    /**
     * Add custom expressions (or overwrite existing) for image convolution.
     *
     * @param array $options Key value array with strings to be converted
     *                       to convolution expressions.
     *
     * @return $this
     */
    public function addConvolveExpressions($options)
    {
        $this->convolves = array_merge($this->convolves, $options);
        return $this;
    }



    /**
     * Image convolution.
     *
     * @param string $options A string with 11 float separated by comma.
     *
     * @return $this
     */
    public function imageConvolution($options = null)
    {
        // Use incoming options or use $this.
        $options = $options ? $options : $this->convolve;

        // Treat incoming as string, split by +
        $this->log("Convolution with '$options'");
        $options = explode(":", $options);

        // Check each option if it matches constant value
        foreach ($options as $option) {
            list($matrix, $divisor, $offset) = $this->createConvolveArguments($option);
            imageconvolution($this->image, $matrix, $divisor, $offset);
        }

        return $this;
    }



    /**
     * Set default background color between 000000-FFFFFF or if using
     * alpha 00000000-FFFFFF7F.
     *
     * @param string $color as hex value.
     *
     * @return $this
    */
    public function setDefaultBackgroundColor($color)
    {
        $this->log("Setting default background color to '$color'.");

        if (!(strlen($color) == 6 || strlen($color) == 8)) {
            throw new Exception(
                "Background color needs a hex value of 6 or 8
                digits. 000000-FFFFFF or 00000000-FFFFFF7F.
                Current value was: '$color'."
            );
        }

        $red    = hexdec(substr($color, 0, 2));
        $green  = hexdec(substr($color, 2, 2));
        $blue   = hexdec(substr($color, 4, 2));

        $alpha = (strlen($color) == 8)
            ? hexdec(substr($color, 6, 2))
            : null;

        if (($red < 0 || $red > 255)
            || ($green < 0 || $green > 255)
            || ($blue < 0 || $blue > 255)
            || ($alpha < 0 || $alpha > 127)
        ) {
            throw new Exception(
                "Background color out of range. Red, green blue
                should be 00-FF and alpha should be 00-7F.
                Current value was: '$color'."
            );
        }

        $this->bgColor = strtolower($color);
        $this->bgColorDefault = array(
            'red'   => $red,
            'green' => $green,
            'blue'  => $blue,
            'alpha' => $alpha
        );

        return $this;
    }



    /**
     * Get the background color.
     *
     * @param resource $img the image to work with or null if using $this->image.
     *
     * @return color value or null if no background color is set.
    */
    private function getBackgroundColor($img = null)
    {
        $img = isset($img) ? $img : $this->image;

        if ($this->bgColorDefault) {

            $red   = $this->bgColorDefault['red'];
            $green = $this->bgColorDefault['green'];
            $blue  = $this->bgColorDefault['blue'];
            $alpha = $this->bgColorDefault['alpha'];

            if ($alpha) {
                $color = imagecolorallocatealpha($img, $red, $green, $blue, $alpha);
            } else {
                $color = imagecolorallocate($img, $red, $green, $blue);
            }

            return $color;

        } else {
            return 0;
        }
    }



    /**
     * Create a image and keep transparency for png and gifs.
     *
     * @param int $width of the new image.
     * @param int $height of the new image.
     *
     * @return image resource.
    */
    private function createImageKeepTransparency($width, $height)
    {
        $this->log("Creating a new working image width={$width}px, height={$height}px.");
        $img = imagecreatetruecolor($width, $height);
        imagealphablending($img, false);
        imagesavealpha($img, true);

        $index = $this->image
            ? imagecolortransparent($this->image)
            : -1;

        if ($index != -1) {

            imagealphablending($img, true);
            $transparent = imagecolorsforindex($this->image, $index);
            $color = imagecolorallocatealpha($img, $transparent['red'], $transparent['green'], $transparent['blue'], $transparent['alpha']);
            imagefill($img, 0, 0, $color);
            $index = imagecolortransparent($img, $color);
            $this->Log("Detected transparent color = " . implode(", ", $transparent) . " at index = $index");

        } elseif ($this->bgColorDefault) {

            $color = $this->getBackgroundColor($img);
            imagefill($img, 0, 0, $color);
            $this->Log("Filling image with background color.");
        }

        return $img;
    }



    /**
     * Set optimizing  and post-processing options.
     *
     * @param array $options with config for postprocessing with external tools.
     *
     * @return $this
     */
    public function setPostProcessingOptions($options)
    {
        if (isset($options['jpeg_optimize']) && $options['jpeg_optimize']) {
            $this->jpegOptimizeCmd = $options['jpeg_optimize_cmd'];
        } else {
            $this->jpegOptimizeCmd = null;
        }

        if (isset($options['png_filter']) && $options['png_filter']) {
            $this->pngFilterCmd = $options['png_filter_cmd'];
        } else {
            $this->pngFilterCmd = null;
        }

        if (isset($options['png_deflate']) && $options['png_deflate']) {
            $this->pngDeflateCmd = $options['png_deflate_cmd'];
        } else {
            $this->pngDeflateCmd = null;
        }

        return $this;
    }



    /**
     * Find out the type (file extension) for the image to be saved.
     *
     * @return string as image extension.
     */
    protected function getTargetImageExtension()
    {
        // switch on mimetype
        if (isset($this->extension)) {
            return strtolower($this->extension);
        } else {
            return substr(image_type_to_extension($this->fileType), 1);
        }
    }



    /**
     * Save image.
     *
     * @param string  $src       as target filename.
     * @param string  $base      as base directory where to store images.
     * @param boolean $overwrite or not, default to always overwrite file.
     *
     * @return $this or false if no folder is set.
     */
    public function save($src = null, $base = null, $overwrite = true)
    {
        if (isset($src)) {
            $this->setTarget($src, $base);
        }

        if ($overwrite === false && is_file($this->cacheFileName)) {
            $this->Log("Not overwriting file since its already exists and \$overwrite if false.");
            return;
        }

        is_writable($this->saveFolder)
            or $this->raiseError('Target directory is not writable.');

        $type = $this->getTargetImageExtension();
        $this->Log("Saving image as " . $type);
        switch($type) {

            case 'jpeg':
            case 'jpg':
                $this->Log("Saving image as JPEG to cache using quality = {$this->quality}.");
                imagejpeg($this->image, $this->cacheFileName, $this->quality);

                // Use JPEG optimize if defined
                if ($this->jpegOptimizeCmd) {
                    if ($this->verbose) {
                        clearstatcache();
                        $this->log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes.");
                    }
                    $res = array();
                    $cmd = $this->jpegOptimizeCmd . " -outfile $this->cacheFileName $this->cacheFileName";
                    exec($cmd, $res);
                    $this->log($cmd);
                    $this->log($res);
                }
                break;

            case 'gif':
                $this->Log("Saving image as GIF to cache.");
                imagegif($this->image, $this->cacheFileName);
                break;

            case 'png':
            default:
                $this->Log("Saving image as PNG to cache using compression = {$this->compress}.");

                // Turn off alpha blending and set alpha flag
                imagealphablending($this->image, false);
                imagesavealpha($this->image, true);
                imagepng($this->image, $this->cacheFileName, $this->compress);

                // Use external program to filter PNG, if defined
                if ($this->pngFilterCmd) {
                    if ($this->verbose) {
                        clearstatcache();
                        $this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes.");
                    }
                    $res = array();
                    $cmd = $this->pngFilterCmd . " $this->cacheFileName";
                    exec($cmd, $res);
                    $this->Log($cmd);
                    $this->Log($res);
                }

                // Use external program to deflate PNG, if defined
                if ($this->pngDeflateCmd) {
                    if ($this->verbose) {
                        clearstatcache();
                        $this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes.");
                    }
                    $res = array();
                    $cmd = $this->pngDeflateCmd . " $this->cacheFileName";
                    exec($cmd, $res);
                    $this->Log($cmd);
                    $this->Log($res);
                }
                break;
        }

        if ($this->verbose) {
            clearstatcache();
            $this->log("Saved image to cache.");
            $this->log(" Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
            $this->log(" Number of colors in image = " . $this->ColorsTotal($this->image));
            $index = imagecolortransparent($this->image);
            $this->log(" Detected transparent color = " . ($index > 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
        }

        return $this;
    }



    /**
     * Convert image from one colorpsace/color profile to sRGB without
     * color profile.
     *
     * @param string  $src      of image.
     * @param string  $dir      as base directory where images are.
     * @param string  $cache    as base directory where to store images.
     * @param string  $iccFile  filename of colorprofile.
     * @param boolean $useCache or not, default to always use cache.
     *
     * @return string | boolean false if no conversion else the converted
     *                          filename.
     */
    public function convert2sRGBColorSpace($src, $dir, $cache, $iccFile, $useCache = true)
    {
        if ($this->verbose) {
            $this->log("# Converting image to sRGB colorspace.");
        }

        if (!class_exists("Imagick")) {
            $this->log(" Ignoring since Imagemagick is not installed.");
            return false;
        }

        // Prepare
        $this->setSaveFolder($cache)
             ->setSource($src, $dir)
             ->generateFilename(null, false, 'srgb_');

        // Check if the cached version is accurate.
        if ($useCache && is_readable($this->cacheFileName)) {
            $fileTime  = filemtime($this->pathToImage);
            $cacheTime = filemtime($this->cacheFileName);

            if ($fileTime <= $cacheTime) {
                $this->log(" Using cached version: " . $this->cacheFileName);
                return $this->cacheFileName;
            }
        }

        // Only covert if cachedir is writable
        if (is_writable($this->saveFolder)) {
            // Load file and check if conversion is needed
            $image      = new Imagick($this->pathToImage);
            $colorspace = $image->getImageColorspace();
            $this->log(" Current colorspace: " . $colorspace);

            $profiles      = $image->getImageProfiles('*', false);
            $hasICCProfile = (array_search('icc', $profiles) !== false);
            $this->log(" Has ICC color profile: " . ($hasICCProfile ? "YES" : "NO"));

            if ($colorspace != Imagick::COLORSPACE_SRGB || $hasICCProfile) {
                $this->log(" Converting to sRGB.");

                $sRGBicc = file_get_contents($iccFile);
                $image->profileImage('icc', $sRGBicc);

                $image->transformImageColorspace(Imagick::COLORSPACE_SRGB);
                $image->writeImage($this->cacheFileName);
                return $this->cacheFileName;
            }
        }

        return false;
    }



    /**
     * Create a hard link, as an alias, to the cached file.
     *
     * @param string $alias where to store the link,
     *                      filename without extension.
     *
     * @return $this
     */
    public function linkToCacheFile($alias)
    {
        if ($alias === null) {
            $this->log("Ignore creating alias.");
            return $this;
        }

        if (is_readable($alias)) {
            unlink($alias);
        }

        $res = link($this->cacheFileName, $alias);

        if ($res) {
            $this->log("Created an alias as: $alias");
        } else {
            $this->log("Failed to create the alias: $alias");
        }

        return $this;
    }



    /**
     * Add HTTP header for putputting together with image.
     *
     * @param string $type  the header type such as "Cache-Control"
     * @param string $value the value to use
     *
     * @return void
     */
    public function addHTTPHeader($type, $value)
    {
        $this->HTTPHeader[$type] = $value;
    }



    /**
     * Output image to browser using caching.
     *
     * @param string $file   to read and output, default is to
     *                       use $this->cacheFileName
     * @param string $format set to json to output file as json
     *                       object with details
     *
     * @return void
     */
    public function output($file = null, $format = null)
    {
        if (is_null($file)) {
            $file = $this->cacheFileName;
        }

        if (is_null($format)) {
            $format = $this->outputFormat;
        }

        $this->log("Output format is: $format");

        if (!$this->verbose && $format == 'json') {
            header('Content-type: application/json');
            echo $this->json($file);
            exit;
        } elseif ($format == 'ascii') {
            header('Content-type: text/plain');
            echo $this->ascii($file);
            exit;
        }

        $this->log("Outputting image: $file");

        // Get image modification time
        clearstatcache();
        $lastModified = filemtime($file);
        $gmdate = gmdate("D, d M Y H:i:s", $lastModified);

        if (!$this->verbose) {
            header('Last-Modified: ' . $gmdate . " GMT");
        }

        foreach($this->HTTPHeader as $key => $val) {
            header("$key: $val");
        }

        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified) {

            if ($this->verbose) {
                $this->log("304 not modified");
                $this->verboseOutput();
                exit;
            }

            header("HTTP/1.0 304 Not Modified");

        } else {

            // Get details on image
            $info = getimagesize($file);
            !empty($info) or $this->raiseError("The file doesn't seem to be an image.");
            $mime = $info['mime'];
            $size = filesize($file);

            if ($this->verbose) {
                $this->log("Last-Modified: " . $gmdate . " GMT");
                $this->log("Content-type: " . $mime);
                $this->log("Content-length: " . $size);
                $this->verboseOutput();

                if (is_null($this->verboseFileName)) {
                    exit;
                }
            }

            header("Content-type: $mime");
            header("Content-length: $size");
            readfile($file);
        }

        exit;
    }



    /**
     * Create a JSON object from the image details.
     *
     * @param string $file the file to output.
     *
     * @return string json-encoded representation of the image.
     */
    public function json($file = null)
    {
        $file = $file ? $file : $this->cacheFileName;

        $details = array();

        clearstatcache();

        $details['src']       = $this->imageSrc;
        $lastModified         = filemtime($this->pathToImage);
        $details['srcGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);

        $details['cache']       = basename($this->cacheFileName);
        $lastModified           = filemtime($this->cacheFileName);
        $details['cacheGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);

        $this->load($file);

        $details['filename']    = basename($file);
        $details['mimeType']    = image_type_to_mime_type($this->fileType);
        $details['width']       = $this->width;
        $details['height']      = $this->height;
        $details['aspectRatio'] = round($this->width / $this->height, 3);
        $details['size']        = filesize($file);
        $details['colors'] = $this->colorsTotal($this->image);
        $details['includedFiles'] = count(get_included_files());
        $details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ;
        $details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB";
        $details['memoryLimit'] = ini_get('memory_limit');

        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
            $details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s";
        }

        if ($details['mimeType'] == 'image/png') {
            $details['pngType'] = $this->getPngTypeAsString(null, $file);
        }

        $options = null;
        if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) {
            $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES;
        }

        return json_encode($details, $options);
    }



    /**
     * Set options for creating ascii version of image.
     *
     * @param array $options empty to use default or set options to change.
     *
     * @return void.
     */
    public function setAsciiOptions($options = array())
    {
        $this->asciiOptions = $options;
    }



    /**
     * Create an ASCII version from the image details.
     *
     * @param string $file the file to output.
     *
     * @return string ASCII representation of the image.
     */
    public function ascii($file = null)
    {
        $file = $file ? $file : $this->cacheFileName;

        $asciiArt = new CAsciiArt();
        $asciiArt->setOptions($this->asciiOptions);
        return $asciiArt->createFromFile($file);
    }



    /**
     * Log an event if verbose mode.
     *
     * @param string $message to log.
     *
     * @return this
     */
    public function log($message)
    {
        if ($this->verbose) {
            $this->log[] = $message;
        }

        return $this;
    }



    /**
     * Do verbose output to a file.
     *
     * @param string $fileName where to write the verbose output.
     *
     * @return void
     */
    public function setVerboseToFile($fileName)
    {
        $this->log("Setting verbose output to file.");
        $this->verboseFileName = $fileName;
    }



    /**
     * Do verbose output and print out the log and the actual images.
     *
     * @return void
     */
    private function verboseOutput()
    {
        $log = null;
        $this->log("As JSON: \n" . $this->json());
        $this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M");
        $this->log("Memory limit: " . ini_get('memory_limit'));

        $included = get_included_files();
        $this->log("Included files: " . count($included));

        foreach ($this->log as $val) {
            if (is_array($val)) {
                foreach ($val as $val1) {
                    $log .= htmlentities($val1) . '<br/>';
                }
            } else {
                $log .= htmlentities($val) . '<br/>';
            }
        }

        if (!is_null($this->verboseFileName)) {
            file_put_contents(
                $this->verboseFileName,
                str_replace("<br/>", "\n", $log)
            );
        } else {
            echo <<<EOD
<h1>CImage Verbose Output</h1>
<pre>{$log}</pre>
EOD;
        }
    }



    /**
     * Raise error, enables to implement a selection of error methods.
     *
     * @param string $message the error message to display.
     *
     * @return void
     * @throws Exception
     */
    private function raiseError($message)
    {
        throw new Exception($message);
    }
}



/**
 * Deal with the cache directory and cached items.
 *
 */
class CCache
{
    /**
     * Path to the cache directory.
     */
    private $path;



    /**
     * Set the path to the cache dir which must exist.
     *
     * @param string path to the cache dir.
     *
     * @throws Exception when $path is not a directory.
     *
     * @return $this
     */
    public function setDir($path)
    {
        if (!is_dir($path)) {
            throw new Exception("Cachedir is not a directory.");
        }

        $this->path = $path;

        return $this;
    }



    /**
     * Get the path to the cache subdir and try to create it if its not there.
     *
     * @param string $subdir name of subdir
     * @param array  $create default is to try to create the subdir
     *
     * @return string | boolean as real path to the subdir or
     *                          false if it does not exists
     */
    public function getPathToSubdir($subdir, $create = true)
    {
        $path = realpath($this->path . "/" . $subdir);

        if (is_dir($path)) {
            return $path;
        }

        if ($create && is_writable($this->path)) {
            $path = $this->path . "/" . $subdir;

            if (mkdir($path)) {
                return realpath($path);
            }
        }

        return false;
    }



    /**
     * Get status of the cache subdir.
     *
     * @param string $subdir name of subdir
     *
     * @return string with status
     */
    public function getStatusOfSubdir($subdir)
    {
        $path = realpath($this->path . "/" . $subdir);

        $exists = is_dir($path);
        $res  = $exists ? "exists" : "does not exist";
        
        if ($exists) {
            $res .= is_writable($path) ? ", writable" : ", not writable";
        }

        return $res;
    }



    /**
     * Remove the cache subdir.
     *
     * @param string $subdir name of subdir
     *
     * @return null | boolean true if success else false, null if no operation
     */
    public function removeSubdir($subdir)
    {
        $path = realpath($this->path . "/" . $subdir);

        if (is_dir($path)) {
            return rmdir($path);
        }

        return null;
    }
}



/**
 * Resize and crop images on the fly, store generated images in a cache.
 *
 * @author  Mikael Roos mos@dbwebb.se
 * @example http://dbwebb.se/opensource/cimage
 * @link    https://github.com/mosbth/cimage
 *
 */

$version = "v0.7.9 (2015-12-07)";

// For CRemoteImage
define("CIMAGE_USER_AGENT", "CImage/$version");



/**
 * Display error message.
 *
 * @param string $msg to display.
 * @param int $type of HTTP error to display.
 *
 * @return void
 */
function errorPage($msg, $type = 500)
{
    global $mode;

    switch ($type) {
        case 403:
            $header = "403 Forbidden";
            break;
        case 404:
            $header = "404 Not Found";
            break;
        default:
            $header = "500 Internal Server Error";
    }

    header("HTTP/1.0 $header");

    if ($mode == "development") {
        die("[img.php] $msg");
    }

    if ($mode == "strict") {
        $header = "404 Not Found";
    }

    error_log("[img.php] $msg");
    die("HTTP/1.0 $header");
}



/**
 * Custom exception handler.
 */
set_exception_handler(function ($exception) {
    errorPage(
        "<p><b>img.php: Uncaught exception:</b> <p>"
        . $exception->getMessage()
        . "</p><pre>"
        . $exception->getTraceAsString()
        . "</pre>",
        500
    );
});



/**
 * Get input from query string or return default value if not set.
 *
 * @param mixed $key     as string or array of string values to look for in $_GET.
 * @param mixed $default value to return when $key is not set in $_GET.
 *
 * @return mixed value from $_GET or default value.
 */
function get($key, $default = null)
{
    if (is_array($key)) {
        foreach ($key as $val) {
            if (isset($_GET[$val])) {
                return $_GET[$val];
            }
        }
    } elseif (isset($_GET[$key])) {
        return $_GET[$key];
    }
    return $default;
}



/**
 * Get input from query string and set to $defined if defined or else $undefined.
 *
 * @param mixed $key       as string or array of string values to look for in $_GET.
 * @param mixed $defined   value to return when $key is set in $_GET.
 * @param mixed $undefined value to return when $key is not set in $_GET.
 *
 * @return mixed value as $defined or $undefined.
 */
function getDefined($key, $defined, $undefined)
{
    return get($key) === null ? $undefined : $defined;
}



/**
 * Get value from config array or default if key is not set in config array.
 *
 * @param string $key    the key in the config array.
 * @param mixed $default value to be default if $key is not set in config.
 *
 * @return mixed value as $config[$key] or $default.
 */
function getConfig($key, $default)
{
    global $config;
    return isset($config[$key])
        ? $config[$key]
        : $default;
}



/**
 * Log when verbose mode, when used without argument it returns the result.
 *
 * @param string $msg to log.
 *
 * @return void or array.
 */
function verbose($msg = null)
{
    global $verbose, $verboseFile;
    static $log = array();

    if (!($verbose || $verboseFile)) {
        return;
    }

    if (is_null($msg)) {
        return $log;
    }

    $log[] = $msg;
}



/**
 * Get configuration options from file, if the file exists, else use $config
 * if its defined or create an empty $config.
 */
$configFile = __DIR__.'/'.basename(__FILE__, '.php').'_config.php';

if (is_file($configFile)) {
    $config = require $configFile;
} elseif (!isset($config)) {
    $config = array();
}



/**
* verbose, v - do a verbose dump of what happens
* vf - do verbose dump to file
*/
$verbose = getDefined(array('verbose', 'v'), true, false);
$verboseFile = getDefined('vf', true, false);
verbose("img.php version = $version");



/**
* status - do a verbose dump of the configuration
*/
$status = getDefined('status', true, false);



/**
 * Set mode as strict, production or development.
 * Default is production environment.
 */
$mode = getConfig('mode', 'production');

// Settings for any mode
set_time_limit(20);
ini_set('gd.jpeg_ignore_warning', 1);

if (!extension_loaded('gd')) {
    errorPage("Extension gd is not loaded.", 500);
}

// Specific settings for each mode
if ($mode == 'strict') {

    error_reporting(0);
    ini_set('display_errors', 0);
    ini_set('log_errors', 1);
    $verbose = false;
    $status = false;
    $verboseFile = false;

} elseif ($mode == 'production') {

    error_reporting(-1);
    ini_set('display_errors', 0);
    ini_set('log_errors', 1);
    $verbose = false;
    $status = false;
    $verboseFile = false;

} elseif ($mode == 'development') {

    error_reporting(-1);
    ini_set('display_errors', 1);
    ini_set('log_errors', 0);
    $verboseFile = false;

} elseif ($mode == 'test') {

    error_reporting(-1);
    ini_set('display_errors', 1);
    ini_set('log_errors', 0);

} else {
    errorPage("Unknown mode: $mode", 500);
}

verbose("mode = $mode");
verbose("error log = " . ini_get('error_log'));



/**
 * Set default timezone if not set or if its set in the config-file.
 */
$defaultTimezone = getConfig('default_timezone', null);

if ($defaultTimezone) {
    date_default_timezone_set($defaultTimezone);
} elseif (!ini_get('default_timezone')) {
    date_default_timezone_set('UTC');
}



/**
 * Check if passwords are configured, used and match.
 * Options decide themself if they require passwords to be used.
 */
$pwdConfig   = getConfig('password', false);
$pwdAlways   = getConfig('password_always', false);
$pwdType     = getConfig('password_type', 'text');
$pwd         = get(array('password', 'pwd'), null);

// Check if passwords match, if configured to use passwords
$passwordMatch = null;
if ($pwd) {
    switch ($pwdType) {
        case 'md5':
            $passwordMatch = ($pwdConfig === md5($pwd));
            break;
        case 'hash':
            $passwordMatch = password_verify($pwd, $pwdConfig);
            break;
        case 'text':
            $passwordMatch = ($pwdConfig === $pwd);
            break;
        default:
            $passwordMatch = false;
    }
}

if ($pwdAlways && $passwordMatch !== true) {
    errorPage("Password required and does not match or exists.", 403);
}

verbose("password match = $passwordMatch");



/**
 * Prevent hotlinking, leeching, of images by controlling who access them
 * from where.
 *
 */
$allowHotlinking = getConfig('allow_hotlinking', true);
$hotlinkingWhitelist = getConfig('hotlinking_whitelist', array());

$serverName  = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null;
$referer     = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null;
$refererHost = parse_url($referer, PHP_URL_HOST);

if (!$allowHotlinking) {
    if ($passwordMatch) {
        ; // Always allow when password match
        verbose("Hotlinking since passwordmatch");
    } elseif ($passwordMatch === false) {
        errorPage("Hotlinking/leeching not allowed when password missmatch.", 403);
    } elseif (!$referer) {
        errorPage("Hotlinking/leeching not allowed and referer is missing.", 403);
    } elseif (strcmp($serverName, $refererHost) == 0) {
        ; // Allow when serverName matches refererHost
        verbose("Hotlinking disallowed but serverName matches refererHost.");
    } elseif (!empty($hotlinkingWhitelist)) {
        $whitelist = new CWhitelist();
        $allowedByWhitelist = $whitelist->check($refererHost, $hotlinkingWhitelist);

        if ($allowedByWhitelist) {
            verbose("Hotlinking/leeching allowed by whitelist.");
        } else {
            errorPage("Hotlinking/leeching not allowed by whitelist. Referer: $referer.", 403);
        }

    } else {
        errorPage("Hotlinking/leeching not allowed.", 403);
    }
}

verbose("allow_hotlinking = $allowHotlinking");
verbose("referer = $referer");
verbose("referer host = $refererHost");



/**
 * Get the source files.
 */
$autoloader  = getConfig('autoloader', false);
$cimageClass = getConfig('cimage_class', false);

if ($autoloader) {
    require $autoloader;
} elseif ($cimageClass) {
    require $cimageClass;
}



/**
 * Create the class for the image.
 */
$img = new CImage();
$img->setVerbose($verbose || $verboseFile);



/**
 * Get the cachepath from config.
 */
$cachePath = getConfig('cache_path', __DIR__ . '/../cache/');
$cache = new CCache();
$cache->setDir($cachePath);




/**
 * Allow or disallow remote download of images from other servers.
 * Passwords apply if used.
 *
 */
$allowRemote = getConfig('remote_allow', false);

if ($allowRemote && $passwordMatch !== false) {
    $cacheRemote = $cache->getPathToSubdir("remote");
    
    $pattern = getConfig('remote_pattern', null);
    $img->setRemoteDownload($allowRemote, $cacheRemote, $pattern);

    $whitelist = getConfig('remote_whitelist', null);
    $img->setRemoteHostWhitelist($whitelist);
}



/**
 * shortcut, sc - extend arguments with a constant value, defined
 * in config-file.
 */
$shortcut       = get(array('shortcut', 'sc'), null);
$shortcutConfig = getConfig('shortcut', array(
    'sepia' => "&f=grayscale&f0=brightness,-10&f1=contrast,-20&f2=colorize,120,60,0,0&sharpen",
));

verbose("shortcut = $shortcut");

if (isset($shortcut)
    && isset($shortcutConfig[$shortcut])) {

    parse_str($shortcutConfig[$shortcut], $get);
    verbose("shortcut-constant = {$shortcutConfig[$shortcut]}");
    $_GET = array_merge($_GET, $get);
}



/**
 * src - the source image file.
 */
$srcImage = urldecode(get('src'))
    or errorPage('Must set src-attribute.', 404);

// Check for valid/invalid characters
$imagePath           = getConfig('image_path', __DIR__ . '/img/');
$imagePathConstraint = getConfig('image_path_constraint', true);
$validFilename       = getConfig('valid_filename', '#^[a-z0-9A-Z-/_ \.:]+$#');

// Dummy image feature
$dummyEnabled  = getConfig('dummy_enabled', true);
$dummyFilename = getConfig('dummy_filename', 'dummy');
$dummyImage = false;

preg_match($validFilename, $srcImage)
    or errorPage('Filename contains invalid characters.', 404);

if ($dummyEnabled && $srcImage === $dummyFilename) {

    // Prepare to create a dummy image and use it as the source image.
    $dummyImage = true;

} elseif ($allowRemote && $img->isRemoteSource($srcImage)) {

    // If source is a remote file, ignore local file checks.

} elseif ($imagePathConstraint) {

    // Check that the image is a file below the directory 'image_path'.
    $pathToImage = realpath($imagePath . $srcImage);
    $imageDir    = realpath($imagePath);

    is_file($pathToImage)
        or errorPage(
            'Source image is not a valid file, check the filename and that a
            matching file exists on the filesystem.',
            404
        );

    substr_compare($imageDir, $pathToImage, 0, strlen($imageDir)) == 0
        or errorPage(
            'Security constraint: Source image is not below the directory "image_path"
            as specified in the config file img_config.php.',
            404
        );
}

verbose("src = $srcImage");



/**
 * Manage size constants from config file, use constants to replace values
 * for width and height.
 */
$sizeConstant = getConfig('size_constant', function () {

    // Set sizes to map constant to value, easier to use with width or height
    $sizes = array(
        'w1' => 613,
        'w2' => 630,
    );

    // Add grid column width, useful for use as predefined size for width (or height).
    $gridColumnWidth = 30;
    $gridGutterWidth = 10;
    $gridColumns     = 24;

    for ($i = 1; $i <= $gridColumns; $i++) {
        $sizes['c' . $i] = ($gridColumnWidth + $gridGutterWidth) * $i - $gridGutterWidth;
    }

    return $sizes;
});

$sizes = call_user_func($sizeConstant);



/**
 * width, w - set target width, affecting the resulting image width, height and resize options
 */
$newWidth     = get(array('width', 'w'));
$maxWidth     = getConfig('max_width', 2000);

// Check to replace predefined size
if (isset($sizes[$newWidth])) {
    $newWidth = $sizes[$newWidth];
}

// Support width as % of original width
if ($newWidth[strlen($newWidth)-1] == '%') {
    is_numeric(substr($newWidth, 0, -1))
        or errorPage('Width % not numeric.', 404);
} else {
    is_null($newWidth)
        or ($newWidth > 10 && $newWidth <= $maxWidth)
        or errorPage('Width out of range.', 404);
}

verbose("new width = $newWidth");



/**
 * height, h - set target height, affecting the resulting image width, height and resize options
 */
$newHeight = get(array('height', 'h'));
$maxHeight = getConfig('max_height', 2000);

// Check to replace predefined size
if (isset($sizes[$newHeight])) {
    $newHeight = $sizes[$newHeight];
}

// height
if ($newHeight[strlen($newHeight)-1] == '%') {
    is_numeric(substr($newHeight, 0, -1))
        or errorPage('Height % out of range.', 404);
} else {
    is_null($newHeight)
        or ($newHeight > 10 && $newHeight <= $maxHeight)
        or errorPage('Height out of range.', 404);
}

verbose("new height = $newHeight");



/**
 * aspect-ratio, ar - affecting the resulting image width, height and resize options
 */
$aspectRatio         = get(array('aspect-ratio', 'ar'));
$aspectRatioConstant = getConfig('aspect_ratio_constant', function () {
    return array(
        '3:1'    => 3/1,
        '3:2'    => 3/2,
        '4:3'    => 4/3,
        '8:5'    => 8/5,
        '16:10'  => 16/10,
        '16:9'   => 16/9,
        'golden' => 1.618,
    );
});

// Check to replace predefined aspect ratio
$aspectRatios = call_user_func($aspectRatioConstant);
$negateAspectRatio = ($aspectRatio[0] == '!') ? true : false;
$aspectRatio = $negateAspectRatio ? substr($aspectRatio, 1) : $aspectRatio;

if (isset($aspectRatios[$aspectRatio])) {
    $aspectRatio = $aspectRatios[$aspectRatio];
}

if ($negateAspectRatio) {
    $aspectRatio = 1 / $aspectRatio;
}

is_null($aspectRatio)
    or is_numeric($aspectRatio)
    or errorPage('Aspect ratio out of range', 404);

verbose("aspect ratio = $aspectRatio");



/**
 * crop-to-fit, cf - affecting the resulting image width, height and resize options
 */
$cropToFit = getDefined(array('crop-to-fit', 'cf'), true, false);

verbose("crop to fit = $cropToFit");



/**
 * Set default background color from config file.
 */
$backgroundColor = getConfig('background_color', null);

if ($backgroundColor) {
    $img->setDefaultBackgroundColor($backgroundColor);
    verbose("Using default background_color = $backgroundColor");
}



/**
 * bgColor - Default background color to use
 */
$bgColor = get(array('bgColor', 'bg-color', 'bgc'), null);

verbose("bgColor = $bgColor");



/**
 * Do or do not resample image when resizing.
 */
$resizeStrategy = getDefined(array('no-resample'), true, false);

if ($resizeStrategy) {
    $img->setCopyResizeStrategy($img::RESIZE);
    verbose("Setting = Resize instead of resample");
}




/**
 * fill-to-fit, ff - affecting the resulting image width, height and resize options
 */
$fillToFit = get(array('fill-to-fit', 'ff'), null);

verbose("fill-to-fit = $fillToFit");

if ($fillToFit !== null) {

    if (!empty($fillToFit)) {
        $bgColor   = $fillToFit;
        verbose("fillToFit changed bgColor to = $bgColor");
    }

    $fillToFit = true;
    verbose("fill-to-fit (fixed) = $fillToFit");
}



/**
 * no-ratio, nr, stretch - affecting the resulting image width, height and resize options
 */
$keepRatio = getDefined(array('no-ratio', 'nr', 'stretch'), false, true);

verbose("keep ratio = $keepRatio");



/**
 * crop, c - affecting the resulting image width, height and resize options
 */
$crop = get(array('crop', 'c'));

verbose("crop = $crop");



/**
 * area, a - affecting the resulting image width, height and resize options
 */
$area = get(array('area', 'a'));

verbose("area = $area");



/**
 * skip-original, so - skip the original image and always process a new image
 */
$useOriginal = getDefined(array('skip-original', 'so'), false, true);
$useOriginalDefault = getConfig('skip_original', false);

if ($useOriginalDefault === true) {
    verbose("use original is default ON");
    $useOriginal = true;
}

verbose("use original = $useOriginal");



/**
 * no-cache, nc - skip the cached version and process and create a new version in cache.
 */
$useCache = getDefined(array('no-cache', 'nc'), false, true);

verbose("use cache = $useCache");



/**
 * quality, q - set level of quality for jpeg images
 */
$quality = get(array('quality', 'q'));
$qualityDefault = getConfig('jpg_quality', null);

is_null($quality)
    or ($quality > 0 and $quality <= 100)
    or errorPage('Quality out of range', 404);

if (is_null($quality) && !is_null($qualityDefault)) {
    $quality = $qualityDefault;
}

verbose("quality = $quality");



/**
 * compress, co - what strategy to use when compressing png images
 */
$compress = get(array('compress', 'co'));
$compressDefault = getConfig('png_compression', null);

is_null($compress)
    or ($compress > 0 and $compress <= 9)
    or errorPage('Compress out of range', 404);

if (is_null($compress) && !is_null($compressDefault)) {
    $compress = $compressDefault;
}

verbose("compress = $compress");



/**
 * save-as, sa - what type of image to save
 */
$saveAs = get(array('save-as', 'sa'));

verbose("save as = $saveAs");



/**
 * scale, s - Processing option, scale up or down the image prior actual resize
 */
$scale = get(array('scale', 's'));

is_null($scale)
    or ($scale >= 0 and $scale <= 400)
    or errorPage('Scale out of range', 404);

verbose("scale = $scale");



/**
 * palette, p - Processing option, create a palette version of the image
 */
$palette = getDefined(array('palette', 'p'), true, false);

verbose("palette = $palette");



/**
 * sharpen - Processing option, post filter for sharpen effect
 */
$sharpen = getDefined('sharpen', true, null);

verbose("sharpen = $sharpen");



/**
 * emboss - Processing option, post filter for emboss effect
 */
$emboss = getDefined('emboss', true, null);

verbose("emboss = $emboss");



/**
 * blur - Processing option, post filter for blur effect
 */
$blur = getDefined('blur', true, null);

verbose("blur = $blur");



/**
 * rotateBefore - Rotate the image with an angle, before processing
 */
$rotateBefore = get(array('rotateBefore', 'rotate-before', 'rb'));

is_null($rotateBefore)
    or ($rotateBefore >= -360 and $rotateBefore <= 360)
    or errorPage('RotateBefore out of range', 404);

verbose("rotateBefore = $rotateBefore");



/**
 * rotateAfter - Rotate the image with an angle, before processing
 */
$rotateAfter = get(array('rotateAfter', 'rotate-after', 'ra', 'rotate', 'r'));

is_null($rotateAfter)
    or ($rotateAfter >= -360 and $rotateAfter <= 360)
    or errorPage('RotateBefore out of range', 404);

verbose("rotateAfter = $rotateAfter");



/**
 * autoRotate - Auto rotate based on EXIF information
 */
$autoRotate = getDefined(array('autoRotate', 'auto-rotate', 'aro'), true, false);

verbose("autoRotate = $autoRotate");



/**
 * filter, f, f0-f9 - Processing option, post filter for various effects using imagefilter()
 */
$filters = array();
$filter = get(array('filter', 'f'));
if ($filter) {
    $filters[] = $filter;
}

for ($i = 0; $i < 10; $i++) {
    $filter = get(array("filter{$i}", "f{$i}"));
    if ($filter) {
        $filters[] = $filter;
    }
}

verbose("filters = " . print_r($filters, 1));



/**
* json -  output the image as a JSON object with details on the image.
* ascii - output the image as ASCII art.
 */
$outputFormat = getDefined('json', 'json', null);
$outputFormat = getDefined('ascii', 'ascii', $outputFormat);

verbose("outputformat = $outputFormat");

if ($outputFormat == 'ascii') {
    $defaultOptions = getConfig(
        'ascii-options',
        array(
            "characterSet" => 'two',
            "scale" => 14,
            "luminanceStrategy" => 3,
            "customCharacterSet" => null,
        )
    );
    $options = get('ascii');
    $options = explode(',', $options);

    if (isset($options[0]) && !empty($options[0])) {
        $defaultOptions['characterSet'] = $options[0];
    }

    if (isset($options[1]) && !empty($options[1])) {
        $defaultOptions['scale'] = $options[1];
    }

    if (isset($options[2]) && !empty($options[2])) {
        $defaultOptions['luminanceStrategy'] = $options[2];
    }

    if (count($options) > 3) {
        // Last option is custom character string
        unset($options[0]);
        unset($options[1]);
        unset($options[2]);
        $characterString = implode($options);
        $defaultOptions['customCharacterSet'] = $characterString;
    }

    $img->setAsciiOptions($defaultOptions);
}




/**
 * dpr - change to get larger image to easier support larger dpr, such as retina.
 */
$dpr = get(array('ppi', 'dpr', 'device-pixel-ratio'), 1);

verbose("dpr = $dpr");



/**
 * convolve - image convolution as in http://php.net/manual/en/function.imageconvolution.php
 */
$convolve = get('convolve', null);
$convolutionConstant = getConfig('convolution_constant', array());

// Check if the convolve is matching an existing constant
if ($convolve && isset($convolutionConstant)) {
    $img->addConvolveExpressions($convolutionConstant);
    verbose("convolve constant = " . print_r($convolutionConstant, 1));
}

verbose("convolve = " . print_r($convolve, 1));



/**
 * no-upscale, nu - Do not upscale smaller image to larger dimension.
 */
$upscale = getDefined(array('no-upscale', 'nu'), false, true);

verbose("upscale = $upscale");



/**
 * Get details for post processing
 */
$postProcessing = getConfig('postprocessing', array(
    'png_filter'        => false,
    'png_filter_cmd'    => '/usr/local/bin/optipng -q',

    'png_deflate'       => false,
    'png_deflate_cmd'   => '/usr/local/bin/pngout -q',

    'jpeg_optimize'     => false,
    'jpeg_optimize_cmd' => '/usr/local/bin/jpegtran -copy none -optimize',
));



/**
 * alias - Save resulting image to another alias name.
 * Password always apply, must be defined.
 */
$alias          = get('alias', null);
$aliasPath      = getConfig('alias_path', null);
$validAliasname = getConfig('valid_aliasname', '#^[a-z0-9A-Z-_]+$#');
$aliasTarget    = null;

if ($alias && $aliasPath && $passwordMatch) {

    $aliasTarget = $aliasPath . $alias;
    $useCache    = false;

    is_writable($aliasPath)
        or errorPage("Directory for alias is not writable.", 403);

    preg_match($validAliasname, $alias)
        or errorPage('Filename for alias contains invalid characters. Do not add extension.', 404);

} elseif ($alias) {
    errorPage('Alias is not enabled in the config file or password not matching.', 403);
}

verbose("alias = $alias");



/**
 * Add cache control HTTP header.
 */
$cacheControl = getConfig('cache_control', null);

if ($cacheControl) {
    verbose("cacheControl = $cacheControl");
    $img->addHTTPHeader("Cache-Control", $cacheControl);
}



/**
 * Prepare a dummy image and use it as source image.
 */
if ($dummyImage === true) {
    $dummyDir = $cache->getPathToSubdir("dummy");

    $img->setSaveFolder($dummyDir)
        ->setSource($dummyFilename, $dummyDir)
        ->setOptions(
            array(
                'newWidth'  => $newWidth,
                'newHeight' => $newHeight,
                'bgColor'   => $bgColor,
            )
        )
        ->setJpegQuality($quality)
        ->setPngCompression($compress)
        ->createDummyImage()
        ->generateFilename(null, false)
        ->save(null, null, false);

    $srcImage = $img->getTarget();
    $imagePath = null;

    verbose("src (updated) = $srcImage");
}



/**
 * Prepare a sRGB version of the image and use it as source image.
 */
$srgbDefault = getConfig('srgb_default', false);
$srgbColorProfile = getConfig('srgb_colorprofile', __DIR__ . '/../icc/sRGB_IEC61966-2-1_black_scaled.icc');
$srgb = getDefined('srgb', true, null);

if ($srgb || $srgbDefault) {

    $filename = $img->convert2sRGBColorSpace(
        $srcImage,
        $imagePath,
        $cache->getPathToSubdir("srgb"),
        $srgbColorProfile,
        $useCache
    );

    if ($filename) {
        $srcImage = $img->getTarget();
        $imagePath = null;
        verbose("srgb conversion and saved to cache = $srcImage");
    } else {
        verbose("srgb not op");
    }
}



/**
 * Display status
 */
if ($status) {
    $text  = "img.php version = $version\n";
    $text .= "PHP version = " . PHP_VERSION . "\n";
    $text .= "Running on: " . $_SERVER['SERVER_SOFTWARE'] . "\n";
    $text .= "Allow remote images = $allowRemote\n";

    $res = $cache->getStatusOfSubdir("");
    $text .= "Cache $res\n";

    $res = $cache->getStatusOfSubdir("remote");
    $text .= "Cache remote $res\n";

    $res = $cache->getStatusOfSubdir("dummy");
    $text .= "Cache dummy $res\n";

    $res = $cache->getStatusOfSubdir("srgb");
    $text .= "Cache srgb $res\n";

    $text .= "Alias path writable = " . is_writable($aliasPath) . "\n";

    $no = extension_loaded('exif') ? null : 'NOT';
    $text .= "Extension exif is $no loaded.<br>";

    $no = extension_loaded('curl') ? null : 'NOT';
    $text .= "Extension curl is $no loaded.<br>";

    $no = extension_loaded('imagick') ? null : 'NOT';
    $text .= "Extension imagick is $no loaded.<br>";

    $no = extension_loaded('gd') ? null : 'NOT';
    $text .= "Extension gd is $no loaded.<br>";

    if (!$no) {
        $text .= print_r(gd_info(), 1);
    }

    echo <<<EOD
<!doctype html>
<html lang=en>
<meta charset=utf-8>
<title>CImage status</title>
<pre>$text</pre>
EOD;
    exit;
}



/**
 * Log verbose details to file
 */
if ($verboseFile) {
    $img->setVerboseToFile("$cachePath/log.txt");
}



/**
 * Hook after img.php configuration and before processing with CImage
 */
$hookBeforeCImage = getConfig('hook_before_CImage', null);

if (is_callable($hookBeforeCImage)) {
    verbose("hookBeforeCImage activated");

    $allConfig = $hookBeforeCImage($img, array(
            // Options for calculate dimensions
            'newWidth'  => $newWidth,
            'newHeight' => $newHeight,
            'aspectRatio' => $aspectRatio,
            'keepRatio' => $keepRatio,
            'cropToFit' => $cropToFit,
            'fillToFit' => $fillToFit,
            'crop'      => $crop,
            'area'      => $area,
            'upscale'   => $upscale,

            // Pre-processing, before resizing is done
            'scale'        => $scale,
            'rotateBefore' => $rotateBefore,
            'autoRotate'   => $autoRotate,

            // General processing options
            'bgColor'    => $bgColor,

            // Post-processing, after resizing is done
            'palette'   => $palette,
            'filters'   => $filters,
            'sharpen'   => $sharpen,
            'emboss'    => $emboss,
            'blur'      => $blur,
            'convolve'  => $convolve,
            'rotateAfter' => $rotateAfter,

            // Output format
            'outputFormat' => $outputFormat,
            'dpr'          => $dpr,

            // Other
            'postProcessing' => $postProcessing,
    ));
    verbose(print_r($allConfig, 1));
    extract($allConfig);
}



/**
 * Display image if verbose mode
 */
if ($verbose) {
    $query = array();
    parse_str($_SERVER['QUERY_STRING'], $query);
    unset($query['verbose']);
    unset($query['v']);
    unset($query['nocache']);
    unset($query['nc']);
    unset($query['json']);
    $url1 = '?' . htmlentities(urldecode(http_build_query($query)));
    $url2 = '?' . urldecode(http_build_query($query));
    echo <<<EOD
<!doctype html>
<html lang=en>
<meta charset=utf-8>
<title>CImage verbose output</title>
<style>body{background-color: #ddd}</style>
<a href=$url1><code>$url1</code></a><br>
<img src='{$url1}' />
<pre id="json"></pre>
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
<script type="text/javascript">
window.getDetails = function (url, id) {
  $.getJSON(url, function(data) {
    element = document.getElementById(id);
    element.innerHTML = "filename: " + data.filename + "\\nmime type: " + data.mimeType + "\\ncolors: " + data.colors + "\\nsize: " + data.size + "\\nwidth: " + data.width + "\\nheigh: " + data.height + "\\naspect-ratio: " + data.aspectRatio + ( data.pngType ? "\\npng-type: " + data.pngType : '');
  });
}
</script>
<script type="text/javascript">window.getDetails("{$url2}&json", "json")</script>
EOD;
}



/**
 * Load, process and output the image
 */
$img->log("Incoming arguments: " . print_r(verbose(), 1))
    ->setSaveFolder($cachePath)
    ->useCache($useCache)
    ->setSource($srcImage, $imagePath)
    ->setOptions(
        array(
            // Options for calculate dimensions
            'newWidth'  => $newWidth,
            'newHeight' => $newHeight,
            'aspectRatio' => $aspectRatio,
            'keepRatio' => $keepRatio,
            'cropToFit' => $cropToFit,
            'fillToFit' => $fillToFit,
            'crop'      => $crop,
            'area'      => $area,
            'upscale'   => $upscale,

            // Pre-processing, before resizing is done
            'scale'        => $scale,
            'rotateBefore' => $rotateBefore,
            'autoRotate'   => $autoRotate,

            // General processing options
            'bgColor'    => $bgColor,

            // Post-processing, after resizing is done
            'palette'   => $palette,
            'filters'   => $filters,
            'sharpen'   => $sharpen,
            'emboss'    => $emboss,
            'blur'      => $blur,
            'convolve'  => $convolve,
            'rotateAfter' => $rotateAfter,

            // Output format
            'outputFormat' => $outputFormat,
            'dpr'          => $dpr,
        )
    )
    ->loadImageDetails()
    ->initDimensions()
    ->calculateNewWidthAndHeight()
    ->setSaveAsExtension($saveAs)
    ->setJpegQuality($quality)
    ->setPngCompression($compress)
    ->useOriginalIfPossible($useOriginal)
    ->generateFilename($cachePath)
    ->useCacheIfPossible($useCache)
    ->load()
    ->preResize()
    ->resize()
    ->postResize()
    ->setPostProcessingOptions($postProcessing)
    ->save()
    ->linkToCacheFile($aliasTarget)
    ->output();



