<?php

namespace Larapen\TextToImage\Libraries\Intervention\Image;

use Closure;

class Image
{
	/**
	 * The image resource identifier of current image
	 *
	 * @var resource
	 */
	public $resource;
	
	/**
	 * Type of current image
	 *
	 * @var string
	 */
	public $type;
	
	/**
	 * Width of current image
	 *
	 * @var integer
	 */
	public $width;
	
	/**
	 * Height of current image
	 *
	 * @var integer
	 */
	public $height;
	
	/**
	 * Directory path of current image
	 *
	 * @var string
	 */
	public $dirname;
	
	/**
	 * Trailing name component of current image filename
	 *
	 * @var string
	 */
	public $basename;
	
	/**
	 * File extension of current image filename
	 *
	 * @var string
	 */
	public $extension;
	
	/**
	 * Combined filename (basename and extension)
	 *
	 * @var string
	 */
	public $filename;
	
	/**
	 * MIME type of image
	 *
	 * @var string
	 */
	public $mime;
	
	/**
	 * Attributes of the original created image
	 *
	 * @var Array
	 */
	protected $original;
	
	/**
	 * Identifier for cached images
	 *
	 * @var boolean
	 */
	public $cached = false;
	
	/**
	 * Result of image after encoding
	 *
	 * @var string
	 */
	public $encoded;
	
	
	/**
	 * Create a new instance of Image class
	 *
	 * @param string $source
	 * @param integer $width
	 * @param integer $height
	 * @param mixed $bgcolor
	 */
	public function __construct($source = null, $width = null, $height = null, $bgcolor = null)
	{
		// set image properties
		if (!is_null($source)) {
			if ($this->isImageResource($source)) {
				// image properties come from gd image resource
				$this->initFromResource($source);
			} elseif ($this->isBinary($source)) {
				// image properties come from binary image string
				$this->initFromString($source);
			} else {
				// image properties come from image file
				$this->initFromPath($source);
			}
		} else {
			// new empty resource
			$this->initEmpty($width, $height, $bgcolor);
		}
	}
	
	/**
	 * Open a new image resource from image file
	 *
	 * @param mixed $source
	 * @return Image
	 */
	public static function make($source)
	{
		return new Image($source);
	}
	
	/**
	 * Create a new empty image resource
	 *
	 * @param int $width
	 * @param int $height
	 * @param mixed $bgcolor
	 * @return Image
	 */
	public static function canvas($width, $height, $bgcolor = null)
	{
		return new Image(null, $width, $height, $bgcolor);
	}
	
	/**
	 * Create a new image resource with image data from string
	 *
	 * @param string $data
	 * @return Image
	 */
	public static function raw($string)
	{
		return new Image($string);
	}
	
	/**
	 * Create new cached image and run callback
	 * (requires additional package intervention/imagecache)
	 *
	 * @param Closure $callback
	 * @param integer $lifetime
	 * @param boolean $returnObj
	 *
	 * @return Image
	 */
	public static function cache(Closure $callback = null, $lifetime = null, $returnObj = false)
	{
		if (!class_exists('\Intervention\Image\ImageCache')) {
			throw new Exception\ImageCacheNotFoundException('Please install package intervention/imagecache before
                running this function.');
		}
		
		// Create image and run callback
		$image = new \Intervention\Image\ImageCache;
		$image = is_callable($callback) ? $callback($image) : $image;
		
		return $image->get($lifetime, $returnObj);
	}
	
	/**
	 * Set properties for image resource from image file
	 *
	 * @param string $path
	 * @return void
	 */
	private function initFromPath($path)
	{
		if (!file_exists($path)) {
			throw new Exception\ImageNotFoundException("Image file ({$path}) not found");
		}
		
		// set file info
		$this->setFileInfoFromPath($path);
		
		// set image info
		$this->setImageInfoFromPath($path);
	}
	
	/**
	 * Set properties for image resource from string
	 *
	 * @param string $string
	 * @return void
	 */
	private function initFromString($string)
	{
		$this->setImageInfoFromString($string);
	}
	
	/**
	 * Set image properties from GD image resource
	 *
	 * @param resource $resource
	 */
	private function initFromResource($resource)
	{
		if (!$this->isImageResource($resource)) {
			throw new Exception\InvalidImageResourceException;
		}
		
		$this->setImageInfoFromResource($resource);
	}
	
	/**
	 * Set properties for empty image resource
	 *
	 * @param int $width
	 * @param int $height
	 * @param mixed $bgcolor
	 * @return void
	 */
	private function initEmpty($width, $height, $bgcolor = null)
	{
		$this->width = is_numeric($width) ? intval($width) : 1;
		$this->height = is_numeric($height) ? intval($height) : 1;
		
		$this->original['width'] = $this->width;
		$this->original['height'] = $this->height;
		
		// create empty image
		$this->resource = imagecreatetruecolor($this->width, $this->height);
		
		// set background color
		if (is_null($bgcolor)) {
			// fill with transparent background instead of black
			$bgcolor = imagecolorallocatealpha($this->resource, 0, 0, 0, 127);
		} else {
			$bgcolor = $this->parseColor($bgcolor);
		}
		
		imagefill($this->resource, 0, 0, $bgcolor);
		$this->original['bgcolor'] = $bgcolor;
	}
	
	/**
	 * Modify wrapper function used by resize and grab
	 *
	 * @param integer $dst_x
	 * @param integer $dst_y
	 * @param integer $src_x
	 * @param integer $src_y
	 * @param integer $dst_w
	 * @param integer $dst_h
	 * @param integer $src_w
	 * @param integer $src_h
	 * @return Image
	 */
	private function modify($dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h)
	{
		// create new image
		$image = imagecreatetruecolor($dst_w, $dst_h);
		
		// preserve transparency
		$transIndex = imagecolortransparent($this->resource);
		
		if ($transIndex != -1) {
			$rgba = imagecolorsforindex($image, $transIndex);
			$transColor = imagecolorallocatealpha($image, $rgba['red'], $rgba['green'], $rgba['blue'], 127);
			imagefill($image, 0, 0, $transColor);
			imagecolortransparent($image, $transColor);
		} else {
			imagealphablending($image, false);
			imagesavealpha($image, true);
		}
		
		// copy content from resource
		imagecopyresampled($image, $this->resource, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
		
		// set new content as recource
		$this->resource = $image;
		
		// set new dimensions
		$this->width = $dst_w;
		$this->height = $dst_h;
		
		return $this;
	}
	
	/**
	 * Open a new image resource from image file
	 *
	 * @param string $path
	 * @return Image
	 */
	public function open($path)
	{
		$this->initFromPath($path);
		
		return $this;
	}
	
	/**
	 * Resize current image based on given width/height
	 *
	 * Width and height are optional, the not given parameter is calculated
	 * based on the given. The ratio boolean decides whether the resizing
	 * should keep the image ratio. You can also pass along a boolean to
	 * prevent the image from being upsized.
	 *
	 * @param integer $width The target width for the image
	 * @param integer $height The target height for the image
	 * @param boolean $ratio Determines if the image ratio should be preserved
	 * @param boolean $upsize Determines whether the image can be upsized
	 *
	 * @return Image
	 */
	public function resize($width = null, $height = null, $ratio = false, $upsize = true)
	{
		// catch legacy call
		if (is_array($width)) {
			$dimensions = $width;
			
			return $this->legacyResize($dimensions);
		}
		
		// Evaluate passed parameters.
		$width = isset($width) ? intval($width) : null;
		$height = $max_height = isset($height) ? intval($height) : null;
		$ratio = $ratio ? true : false;
		$upsize = $upsize ? true : false;
		
		// If the ratio needs to be kept.
		if ($ratio) {
			// If both width and hight have been passed along, the width and
			// height parameters are maximum values.
			if (!is_null($width) && !is_null($height)) {
				// First, calculate the height.
				$height = intval($width / $this->width * $this->height);
				
				// If the height is too large, set it to the maximum
				// height and calculate the width.
				if ($height > $max_height) {
					$height = $max_height;
					$width = intval($height / $this->height * $this->width);
				}
			} elseif ($ratio && (!is_null($width) or !is_null($height))) {
				// If only one of width or height has been provided.
				$width = is_null($width) ? intval($height / $this->height * $this->width) : $width;
				$height = is_null($height) ? intval($width / $this->width * $this->height) : $height;
			}
		}
		
		// If the image can't be upsized, check if the given width and/or
		// height are too large.
		if (!$upsize) {
			// If the given width is larger then the image width,
			// then don't resize it.
			if (!is_null($width) && $width > $this->width) {
				$width = $this->width;
				
				// If ratio needs to be kept, height is recalculated.
				if ($ratio) {
					$height = intval($width / $this->width * $this->height);
				}
			}
			
			// If the given height is larger then the image height,
			// then don't resize it.
			if (!is_null($height) && $height > $this->height) {
				$height = $this->height;
				
				// If ratio needs to be kept, width is recalculated.
				if ($ratio) {
					$width = intval($height / $this->height * $this->width);
				}
			}
		}
		
		// If both the width and height haven't been passed along,
		// throw an exception.
		if (is_null($width) && is_null($height)) {
			throw new Exception\DimensionOutOfBoundsException('width or height needs to be defined');
		} elseif (is_null($width)) { // If only the width hasn't been set, keep the current width.
			$width = $this->width;
		} elseif (is_null($height)) { // If only the height hasn't been set, keep the current height.
			$height = $this->height;
		}
		
		// Create new image in new dimensions.
		return $this->modify(0, 0, 0, 0, $width, $height, $this->width, $this->height);
	}
	
	/**
	 * Legacy method to support old resizing calls
	 *
	 * @param array $dimensions
	 * @return Image
	 */
	public function legacyResize($dimensions = [])
	{
		$width = array_key_exists('width', $dimensions) ? intval($dimensions['width']) : null;
		$height = array_key_exists('height', $dimensions) ? intval($dimensions['height']) : null;
		
		return $this->resize($width, $height, true);
	}
	
	/**
	 * Resize image to new width, constraining proportions
	 *
	 * @param integer $width
	 * @return Image
	 */
	public function widen($width)
	{
		return $this->resize($width, null, true);
	}
	
	/**
	 * Resize image to new height, constraining proportions
	 *
	 * @param integer $height
	 * @return Image
	 */
	public function heighten($height)
	{
		return $this->resize(null, $height, true);
	}
	
	/**
	 * Resize image canvas
	 *
	 * @param int $width
	 * @param int $height
	 * @param string $anchor
	 * @param boolean $relative
	 * @param mixed $bgcolor
	 * @return Image
	 */
	public function resizeCanvas($width, $height, $anchor = null, $relative = false, $bgcolor = null)
	{
		// check of only width or height is set
		$width = is_null($width) ? $this->width : intval($width);
		$height = is_null($height) ? $this->height : intval($height);
		
		// check on relative width/height
		if ($relative) {
			$width = $this->width + $width;
			$height = $this->height + $height;
		}
		
		// check for negative width
		if ($width <= 0) {
			$width = $this->width + $width;
		}
		
		// check for negative height
		if ($height <= 0) {
			$height = $this->height + $height;
		}
		
		// create new canvas
		$image = imagecreatetruecolor($width, $height);
		
		if ($width > $this->width || $height > $this->height) {
			$bgcolor = is_null($bgcolor) ? '000000' : $bgcolor;
			imagefill($image, 0, 0, $this->parseColor($bgcolor));
		}
		
		if ($width >= $this->width) {
			$src_w = $this->width;
		} else {
			$src_w = $width;
		}
		
		if ($height >= $this->height) {
			$src_h = $this->height;
		} else {
			$src_h = $height;
		}
		
		// define anchor
		switch ($anchor) {
			case 'top-left':
			case 'left-top':
				$src_x = 0;
				$src_y = 0;
				break;
			case 'top':
			case 'top-center':
			case 'top-middle':
			case 'center-top':
			case 'middle-top':
				$src_x = ($width < $this->width) ? intval(($this->width - $width) / 2) : 0;
				$src_y = 0;
				break;
			case 'top-right':
			case 'right-top':
				$src_x = ($width < $this->width) ? intval($this->width - $width) : 0;
				$src_y = 0;
				break;
			case 'left':
			case 'left-center':
			case 'left-middle':
			case 'center-left':
			case 'middle-left':
				$src_x = 0;
				$src_y = ($height < $this->height) ? intval(($this->height - $height) / 2) : 0;
				break;
			case 'right':
			case 'right-center':
			case 'right-middle':
			case 'center-right':
			case 'middle-right':
				$src_x = ($width < $this->width) ? intval($this->width - $width) : 0;
				$src_y = ($height < $this->height) ? intval(($this->height - $height) / 2) : 0;
				break;
			case 'bottom-left':
			case 'left-bottom':
				$src_x = 0;
				$src_y = ($height < $this->height) ? intval($this->height - $height) : 0;
				break;
			case 'bottom':
			case 'bottom-center':
			case 'bottom-middle':
			case 'center-bottom':
			case 'middle-bottom':
				$src_x = ($width < $this->width) ? intval(($this->width - $width) / 2) : 0;
				$src_y = ($height < $this->height) ? intval($this->height - $height) : 0;
				break;
			case 'bottom-right':
			case 'right-bottom':
				$src_x = ($width < $this->width) ? intval($this->width - $width) : 0;
				$src_y = ($height < $this->height) ? intval($this->height - $height) : 0;
				break;
			default:
			case 'center':
			case 'middle':
			case 'center-center':
			case 'middle-middle':
				$src_x = ($width < $this->width) ? intval(($this->width - $width) / 2) : 0;
				$src_y = ($height < $this->height) ? intval(($this->height - $height) / 2) : 0;
				break;
		}
		
		// define dest. pos
		$dst_x = ($width <= $this->width) ? 0 : intval(($width - $this->width) / 2);
		$dst_y = ($height <= $this->height) ? 0 : intval(($height - $this->height) / 2);
		
		// copy content from resource
		imagecopy($image, $this->resource, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h);
		
		// set new content as recource
		$this->resource = $image;
		
		// set new dimensions
		$this->width = $width;
		$this->height = $height;
		
		return $this;
	}
	
	/**
	 * Crop the current image
	 *
	 * @param integer $width
	 * @param integer $height
	 * @param integer $pos_x
	 * @param integer $pos_y
	 *
	 * @return Image
	 */
	public function crop($width, $height, $pos_x = null, $pos_y = null)
	{
		$width = is_numeric($width) ? intval($width) : null;
		$height = is_numeric($height) ? intval($height) : null;
		$pos_x = is_numeric($pos_x) ? intval($pos_x) : null;
		$pos_y = is_numeric($pos_y) ? intval($pos_y) : null;
		
		if (is_null($pos_x) && is_null($pos_y)) {
			// center position of width/height rectangle
			$pos_x = floor(($this->width - intval($width)) / 2);
			$pos_y = floor(($this->height - intval($height)) / 2);
		}
		
		if (is_null($width) || is_null($height)) {
			throw new Exception\DimensionOutOfBoundsException('width and height of cutout needs to be defined');
		}
		
		return $this->modify(0, 0, $pos_x, $pos_y, $width, $height, $width, $height);
	}
	
	/**
	 * Cut out a detail of the image in given ratio and resize to output size
	 *
	 * @param integer $width
	 * @param integer $height
	 *
	 * @return Image
	 */
	public function grab($width = null, $height = null)
	{
		// catch legacy call
		if (is_array($width)) {
			$dimensions = $width;
			
			return $this->legacyGrab($dimensions);
		}
		
		$width = is_numeric($width) ? intval($width) : null;
		$height = is_numeric($height) ? intval($height) : null;
		
		if (!is_null($width) or !is_null($height)) {
			// if width or height are not set, define values automatically
			$width = is_null($width) ? $height : $width;
			$height = is_null($height) ? $width : $height;
		} else {
			// width or height not defined (resume with original values)
			throw new Exception\DimensionOutOfBoundsException('width or height needs to be defined');
		}
		
		// ausschnitt berechnen
		$grab_width = $this->width;
		$ratio = $grab_width / $width;
		
		if ($height * $ratio <= $this->height) {
			$grab_height = round($height * $ratio);
			$src_x = 0;
			$src_y = round(($this->height - $grab_height) / 2);
		} else {
			$grab_height = $this->height;
			$ratio = $grab_height / $height;
			$grab_width = round($width * $ratio);
			$src_x = round(($this->width - $grab_width) / 2);
			$src_y = 0;
		}
		
		return $this->modify(0, 0, $src_x, $src_y, $width, $height, $grab_width, $grab_height);
	}
	
	/**
	 * Legacy Method to support older grab calls
	 *
	 * @param array $dimensions
	 * @return Image
	 */
	public function legacyGrab($dimensions = [])
	{
		$width = array_key_exists('width', $dimensions) ? intval($dimensions['width']) : null;
		$height = array_key_exists('height', $dimensions) ? intval($dimensions['height']) : null;
		
		return $this->grab($width, $height);
	}
	
	/**
	 * Trim away image space in given color
	 *
	 * @param string $base Position of the color to trim away
	 * @param array $away Borders to trim away
	 * @return Image
	 */
	public function trim($base = null, $away = null)
	{
		// default values
		$checkTransparency = false;
		
		// define borders to trim away
		if (is_null($away)) {
			$away = ['top', 'right', 'bottom', 'left'];
		} elseif (is_string($away)) {
			$away = [$away];
		}
		
		// define base color position
		switch (strtolower($base)) {
			case 'transparent':
			case 'trans':
				$checkTransparency = true;
				$base_x = 0;
				$base_y = 0;
				break;
			case 'bottom-right':
			case 'right-bottom':
				$base_x = $this->width - 1;
				$base_y = $this->height - 1;
				break;
			default:
			case 'top-left':
			case 'left-top':
				$base_x = 0;
				$base_y = 0;
				break;
		}
		
		// pick base color
		$color = $this->pickColor($base_x, $base_y, 'array');
		
		// search for values that are not base color
		$x_values = [];
		$y_values = [];
		
		for ($y = 0; $y < $this->height; $y++) {
			for ($x = 0; $x < $this->width; $x++) {
				$checkColor = $this->pickColor($x, $y, 'array');
				
				if (($checkColor != $color && $checkTransparency == false) or ($checkColor['a'] != 0 && $checkTransparency == true)) {
					$x_values[] = $x;
					$y_values[] = $y;
				}
			}
		}
		
		// define area to crop out
		if (count($x_values)) {
			sort($x_values);
			$src_x = reset($x_values);
			$width = end($x_values) - $src_x + 1;
		}
		
		if (count($y_values)) {
			sort($y_values);
			$src_y = reset($y_values);
			$height = end($y_values) - $src_y + 1;
		}
		
		// check if top border should be trimmed away
		if (!in_array('top', $away) && !in_array('TOP', $away)) {
			$height = $height + $src_y;
			$src_y = 0;
		}
		
		// check if bottom border should be trimmed away
		if (!in_array('bottom', $away) && !in_array('BOTTOM', $away)) {
			$height = $this->height - $src_y;
		}
		
		// check if left border should be trimmed away
		if (!in_array('left', $away) && !in_array('LEFT', $away)) {
			$width = $width + $src_x;
			$src_x = 0;
		}
		
		// check if right border should be trimmed away
		if (!in_array('right', $away) && !in_array('RIGHT', $away)) {
			$width = $this->width - $src_x;
		}
		
		// modify image if all values are set
		if (isset($width) && isset($height) && isset($src_x) && isset($src_y)) {
			$this->modify(0, 0, $src_x, $src_y, $width, $height, $width, $height);
		}
		
		return $this;
	}
	
	/**
	 * Mirror image horizontally or vertically
	 *
	 * @param mixed $mode
	 * @return Image
	 */
	public function flip($mode = null)
	{
		$x = 0;
		$y = 0;
		$width = $this->width;
		$height = $this->height;
		
		switch (strtolower($mode)) {
			case 2:
			case 'v':
			case 'vert':
			case 'vertical':
				$y = $height - 1;
				$height = $height * (-1);
				break;
			default:
				$x = $width - 1;
				$width = $width * (-1);
				break;
		}
		
		return $this->modify(0, 0, $x, $y, $this->width, $this->height, $width, $height);
	}
	
	/**
	 * Insert another image on top of the current image
	 *
	 * @param mixed $source
	 * @param integer $pos_x
	 * @param integer $pos_y
	 * @param string $anchor
	 * @return Image
	 */
	public function insert($source, $pos_x = 0, $pos_y = 0, $anchor = null)
	{
		$obj = is_a($source, 'Intervention\Image\Image') ? $source : (new Image($source));
		
		// define anchor
		switch ($anchor) {
			case 'top':
			case 'top-center':
			case 'top-middle':
			case 'center-top':
			case 'middle-top':
				$pos_x = intval((($this->width - $obj->width) / 2) + $pos_x);
				$pos_y = $pos_y;
				break;
			case 'top-right':
			case 'right-top':
				$pos_x = intval($this->width - $obj->width - $pos_x);
				$pos_y = $pos_y;
				break;
			case 'left':
			case 'left-center':
			case 'left-middle':
			case 'center-left':
			case 'middle-left':
				$pos_x = $pos_x;
				$pos_y = intval((($this->height - $obj->height) / 2) + $pos_y);
				break;
			case 'right':
			case 'right-center':
			case 'right-middle':
			case 'center-right':
			case 'middle-right':
				$pos_x = intval($this->width - $obj->width - $pos_x);
				$pos_y = intval((($this->height - $obj->height) / 2) + $pos_y);
				break;
			case 'bottom-left':
			case 'left-bottom':
				$pos_x = $pos_x;
				$pos_y = intval($this->height - $obj->height - $pos_y);
				break;
			case 'bottom':
			case 'bottom-center':
			case 'bottom-middle':
			case 'center-bottom':
			case 'middle-bottom':
				$pos_x = intval((($this->width - $obj->width) / 2) + $pos_x);
				$pos_y = intval($this->height - $obj->height - $pos_y);
				break;
			case 'bottom-right':
			case 'right-bottom':
				$pos_x = intval($this->width - $obj->width - $pos_x);
				$pos_y = intval($this->height - $obj->height - $pos_y);
				break;
			case 'center':
			case 'middle':
			case 'center-center':
			case 'middle-middle':
				$pos_x = intval((($this->width - $obj->width) / 2) + $pos_x);
				$pos_y = intval((($this->height - $obj->height) / 2) + $pos_y);
				break;
			default:
			case 'top-left':
			case 'left-top':
				$pos_x = intval($pos_x);
				$pos_y = intval($pos_y);
				break;
		}
		
		imagealphablending($this->resource, true); // enable alphablending just for imagecopy
		imagecopy($this->resource, $obj->resource, $pos_x, $pos_y, 0, 0, $obj->width, $obj->height);
		
		return $this;
	}
	
	/**
	 * Set opacity of current image
	 *
	 * @param integer $transparency
	 * @return Image
	 */
	public function opacity($transparency)
	{
		if (is_numeric($transparency) && $transparency >= 0 && $transparency <= 100) {
			$transparency = intval($transparency) / 100;
		} else {
			throw new Exception\OpacityOutOfBoundsException('Opacity must be between 0 and 100');
		}
		
		// --------------------------------------------------------------------
		// http://stackoverflow.com/questions/2396415/what-does-new-self-mean-in-php
		// --------------------------------------------------------------------
		// create alpha mask
		$alpha = new self(null, $this->width, $this->height);
		$alpha->fill(sprintf('rgba(0, 0, 0, %.1f)', $transparency));
		
		// apply alpha mask
		$this->mask($alpha, true);
		
		return $this;
	}
	
	/**
	 * Apply given image as alpha mask on current image
	 *
	 * @param mixed $source
	 * @param boolean $mask_with_alpha
	 * @return Image
	 */
	public function mask($source, $mask_with_alpha = false)
	{
		// create new empty image
		$maskedImage = new Image(null, $this->width, $this->height);
		
		// create mask
		$mask = is_a($source, 'Intervention\Image\Image') ? $source : (new Image($source));
		
		// resize mask to size of current image (if necessary)
		if ($mask->width != $this->width || $mask->height != $this->height) {
			$mask->resize($this->width, $this->height);
		}
		
		// redraw old image pixel by pixel considering alpha map
		for ($x = 0; $x < $this->width; $x++) {
			for ($y = 0; $y < $this->height; $y++) {
				$color = $this->pickColor($x, $y, 'array');
				$alpha = $mask->pickColor($x, $y, 'array');
				
				if ($mask_with_alpha) {
					$alpha = $alpha['a']; // use alpha channel as mask
				} else {
					$alpha = floatval(round($alpha['r'] / 255, 2)); // use red channel as mask
				}
				
				// preserve alpha of original image...
				if ($color['a'] < $alpha) {
					$alpha = $color['a'];
				}
				
				$pixelColor = [$color['r'], $color['g'], $color['b'], $alpha];
				$maskedImage->pixel($pixelColor, $x, $y);
			}
		}
		
		// apply masked image to current instance
		$this->resource = $maskedImage->resource;
		$this->width = $maskedImage->width;
		$this->height = $maskedImage->height;
		
		return $this;
	}
	
	/**
	 * Rotate image with given angle
	 *
	 * @param float $angle
	 * @param string $color
	 * @param int $ignore_transparent
	 * @return Image
	 */
	public function rotate($angle = 0, $bgcolor = '#000000', $ignore_transparent = 0)
	{
		// rotate image
		$this->resource = imagerotate($this->resource, $angle, $this->parseColor($bgcolor), $ignore_transparent);
		
		// re-read width/height
		$this->width = imagesx($this->resource);
		$this->height = imagesy($this->resource);
		
		return $this;
	}
	
	/**
	 * Fill image with given color or image source at position x,y
	 *
	 * @param mixed $source
	 * @param integer $pos_x
	 * @param integer $pos_y
	 * @return Image
	 */
	public function fill($source, $pos_x = 0, $pos_y = 0)
	{
		if (is_a($source, 'Intervention\Image\Image')) {
			// fill with image
			imagesettile($this->resource, $source->resource);
			$source = IMG_COLOR_TILED;
		} elseif ($this->isImageResource($source)) {
			// fill with image resource
			imagesettile($this->resource, $source);
			$source = IMG_COLOR_TILED;
		} elseif (is_string($source) && $this->isBinary($source)) {
			// fill with image from binary string
			$img = new self($source);
			imagesettile($this->resource, $img->resource);
			$source = IMG_COLOR_TILED;
		} elseif (is_string($source) && file_exists(realpath($source))) {
			$img = new self($source);
			imagesettile($this->resource, $img->resource);
			$source = IMG_COLOR_TILED;
		} else {
			// fill with color
			$source = $this->parseColor($source);
		}
		
		imagefill($this->resource, $pos_x, $pos_y, $source);
		
		return $this;
	}
	
	/**
	 * Set single pixel
	 *
	 * @param string $color
	 * @param integer $pos_x
	 * @param integer $pos_y
	 * @return Image
	 */
	public function pixel($color, $pos_x = 0, $pos_y = 0)
	{
		imagesetpixel($this->resource, $pos_x, $pos_y, $this->parseColor($color));
		
		return $this;
	}
	
	/**
	 * Draw rectangle in current image starting at point 1 and ending at point 2
	 *
	 * @param string $color
	 * @param integer $x1
	 * @param integer $y1
	 * @param integer $x2
	 * @param integer $y2
	 * @param boolean $filled
	 * @return Image
	 */
	public function rectangle($color, $x1 = 0, $y1 = 0, $x2 = 10, $y2 = 10, $filled = true)
	{
		$callback = $filled ? 'imagefilledrectangle' : 'imagerectangle';
		call_user_func($callback, $this->resource, $x1, $y1, $x2, $y2, $this->parseColor($color));
		
		return $this;
	}
	
	/**
	 * Draw a line in current image starting at point 1 and ending at point 2
	 *
	 * @param string $color
	 * @param integer $x1
	 * @param integer $y1
	 * @param integer $x2
	 * @param integer $y2
	 * @return Image
	 */
	public function line($color, $x1 = 0, $y1 = 0, $x2 = 10, $y2 = 10)
	{
		imageline($this->resource, $x1, $y1, $x2, $y2, $this->parseColor($color));
		
		return $this;
	}
	
	/**
	 * Draw an ellipse centered at given coordinates.
	 *
	 * @param string $color
	 * @param integer $pos_x
	 * @param integer $pos_y
	 * @param integer $width
	 * @param integer $height
	 * @return Image
	 */
	public function ellipse($color, $pos_x = 0, $pos_y = 0, $width = 10, $height = 10, $filled = true)
	{
		$callback = $filled ? 'imagefilledellipse' : 'imageellipse';
		call_user_func($callback, $this->resource, $pos_x, $pos_y, $width, $height, $this->parseColor($color));
		
		return $this;
	}
	
	/**
	 * Draw a circle centered at given coordinates
	 *
	 * @param string $color
	 * @param integer $x
	 * @param integer $y
	 * @param integer $radius
	 * @param boolean $filled
	 * @return Image
	 */
	public function circle($color, $x = 0, $y = 0, $radius = 10, $filled = true)
	{
		return $this->ellipse($color, $x, $y, $radius * 2, $radius * 2, $filled);
	}
	
	/**
	 * Write text in current image
	 *
	 * @param string $text
	 * @param integer $pos_x
	 * @param integer $pos_y
	 * @param integer $angle
	 * @param integer $size
	 * @param string $color
	 * @param string $fontfile
	 * @return Image
	 */
	public function text($text, $pos_x = 0, $pos_y = 0, $size = 16, $color = '000000', $angle = 0, $fontfile = null)
	{
		if (is_null($fontfile)) {
			imagestring($this->resource, $size, $pos_x, $pos_y, $text, $this->parseColor($color));
		} else {
			imagealphablending($this->resource, true); // enable alphablending for imagettftext
			imagettftext($this->resource, $size, $angle, $pos_x, $pos_y, $this->parseColor($color), $fontfile, $text);
		}
		
		return $this;
	}
	
	/**
	 * Changes the brightness of the current image
	 *
	 * @param int $level [description]
	 *
	 * @return Image
	 */
	public function brightness($level)
	{
		if ($level < -100 || $level > 100) {
			throw new Exception\BrightnessOutOfBoundsException('Brightness level must be between -100 and +100');
		}
		
		imagefilter($this->resource, IMG_FILTER_BRIGHTNESS, ($level * 2.55));
		
		return $this;
	}
	
	/**
	 * Changes the contrast of the current image
	 *
	 * @param int $level
	 *
	 * @return Image
	 */
	public function contrast($level)
	{
		if ($level < -100 || $level > 100) {
			throw new Exception\ContrastOutOfBoundsException('Contrast level must be between -100 and +100');
		}
		
		imagefilter($this->resource, IMG_FILTER_CONTRAST, ($level * -1));
		
		return $this;
	}
	
	/**
	 * Pixelate current image
	 *
	 * @param integer $size
	 * @param boolean $advanced
	 * @return Image
	 */
	public function pixelate($size = 10, $advanced = true)
	{
		imagefilter($this->resource, IMG_FILTER_PIXELATE, $size, $advanced);
		
		return $this;
	}
	
	/**
	 * Turn current image into a greyscale verision
	 *
	 * @return Image
	 */
	public function grayscale()
	{
		imagefilter($this->resource, IMG_FILTER_GRAYSCALE);
		
		return $this;
	}
	
	/**
	 * Alias of greyscale
	 *
	 * @return Image
	 */
	public function greyscale()
	{
		$this->grayscale();
		
		return $this;
	}
	
	/**
	 * Invert colors of current image
	 *
	 * @return Image
	 */
	public function invert()
	{
		imagefilter($this->resource, IMG_FILTER_NEGATE);
		
		return $this;
	}
	
	/**
	 * Apply colorize filter to current image
	 *
	 * @param integer $red
	 * @param integer $green
	 * @param integer $blue
	 * @return Image
	 */
	public function colorize($red, $green, $blue)
	{
		if (($red < -100 || $red > 100) || ($green < -100 || $green > 100) || ($blue < -100 || $blue > 100)) {
			throw new Exception\ColorizeOutOfBoundsException('Colorize levels must be between -100 and +100');
		}
		
		// normalize colorize levels
		$red = round($red * 2.55);
		$green = round($green * 2.55);
		$blue = round($blue * 2.55);
		
		// apply filter
		imagefilter($this->resource, IMG_FILTER_COLORIZE, $red, $green, $blue);
		
		return $this;
	}
	
	/**
	 * Apply blur filter on the current image
	 *
	 * @param integer $amount
	 * @return Image
	 */
	public function blur($amount = 1)
	{
		for ($i = 0; $i < intval($amount); $i++) {
			imagefilter($this->resource, IMG_FILTER_GAUSSIAN_BLUR);
		}
		
		return $this;
	}
	
	/**
	 * Set a maximum number of colors for the current image
	 *
	 * @param integer $count
	 * @param mixed $matte
	 * @return Image
	 */
	public function limitColors($count = null, $matte = null)
	{
		// create empty canvas
		$resource = imagecreatetruecolor($this->width, $this->height);
		
		// define matte
		$matte = is_null($matte) ? imagecolorallocatealpha($resource, 0, 0, 0, 127) : $this->parseColor($matte);
		
		// fill with matte and copy original image
		imagefill($resource, 0, 0, $matte);
		
		// set transparency
		imagecolortransparent($resource, $matte);
		
		// copy original image
		imagecopy($resource, $this->resource, 0, 0, 0, 0, $this->width, $this->height);
		
		if (is_numeric($count) && $count <= 256) {
			// decrease colors
			imagetruecolortopalette($resource, true, intval($count));
		}
		
		// set new resource
		$this->resource = $resource;
		
		return $this;
	}
	
	/**
	 * Determine whether an Image should be interlaced
	 *
	 * @param boolean $interlace
	 * @return Image
	 */
	public function interlace($interlace = true)
	{
		imageinterlace($this->resource, $interlace);
		
		return $this;
	}
	
	/**
	 * Applies gamma correction
	 *
	 * @param float $input
	 * @param float $output
	 * @return Image
	 */
	public function gamma($input, $output)
	{
		imagegammacorrect($this->resource, $input, $output);
		
		return $this;
	}
	
	/**
	 * Reset to original image resource
	 *
	 * @return void
	 */
	public function reset()
	{
		if (is_null($this->dirname) && is_null($this->basename)) {
			$this->initEmpty($this->original['width'], $this->original['height'], $this->original['bgcolor']);
		} else {
			$this->initFromPath($this->dirname . '/' . $this->basename);
		}
		
		return $this;
	}
	
	/**
	 * Encode image in different formats
	 *
	 * @param string $format
	 * @param integer $quality
	 * @return string
	 */
	public function encode($format = null, $quality = 90)
	{
		$format = is_null($format) ? $this->type : $format;
		
		if ($quality < 0 || $quality > 100) {
			throw new Exception\ImageQualityException('Quality of image must range from 0 to 100.');
		}
		
		ob_start();
		
		switch (strtolower($format)) {
			case 'data-url':
				echo sprintf('data:%s;base64,%s', $this->mime, base64_encode($this->encode($this->type, $quality)));
				break;
			
			case 'gif':
			case IMAGETYPE_GIF:
				imagegif($this->resource);
				$this->type = IMAGETYPE_GIF;
				$this->mime = image_type_to_mime_type(IMAGETYPE_GIF);
				break;
			
			case 'png':
			case IMAGETYPE_PNG:
				$quality = round($quality / 11.11111111111); // transform quality to png setting
				imagealphablending($this->resource, false);
				imagesavealpha($this->resource, true);
				imagepng($this->resource, null, $quality);
				$this->type = IMAGETYPE_PNG;
				$this->mime = image_type_to_mime_type(IMAGETYPE_PNG);
				break;
			
			default:
			case 'jpg':
			case 'jpeg':
			case IMAGETYPE_JPEG:
				imagejpeg($this->resource, null, $quality);
				$this->type = IMAGETYPE_JPEG;
				$this->mime = image_type_to_mime_type(IMAGETYPE_JPEG);
				break;
		}
		
		$this->encoded = ob_get_contents();
		
		ob_end_clean();
		
		return $this->encoded;
	}
	
	/**
	 * Picks and formats color at position
	 *
	 * @param int $x
	 * @param int $y
	 * @param string $format
	 * @return mixed
	 */
	public function pickColor($x, $y, $format = null)
	{
		// pick color at postion
		$color = imagecolorat($this->resource, $x, $y);
		
		// format color
		switch (strtolower($format)) {
			case 'rgb':
				$color = imagecolorsforindex($this->resource, $color);
				$color = sprintf('rgb(%d, %d, %d)', $color['red'], $color['green'], $color['blue']);
				break;
			
			case 'rgba':
				$color = imagecolorsforindex($this->resource, $color);
				$color = sprintf('rgba(%d, %d, %d, %.2f)', $color['red'], $color['green'], $color['blue'], $this->alpha2rgba($color['alpha']));
				break;
			
			case 'hex':
				$color = imagecolorsforindex($this->resource, $color);
				$color = sprintf('#%02x%02x%02x', $color['red'], $color['green'], $color['blue']);
				break;
			
			case 'int':
			case 'integer':
				// in gd2 library color already is int...
				break;
			
			default:
			case 'array':
				$color = imagecolorsforindex($this->resource, $color);
				$color = [
					'r' => $color['red'],
					'g' => $color['green'],
					'b' => $color['blue'],
					'a' => $this->alpha2rgba($color['alpha']),
				];
				break;
		}
		
		return $color;
	}
	
	/**
	 * Allocate color from given string
	 *
	 * @param string $value
	 * @return int
	 */
	public function parseColor($value)
	{
		// alpha value
		$a = 0;
		
		if (is_int($value)) {
			// color is alread allocated
			$allocatedColor = $value;
		} elseif (is_array($value)) {
			// parse color array like: array(155, 155, 155)
			if (count($value) == 4) {
				// color array with alpha value
				list($r, $g, $b, $a) = $value;
				$a = $this->alpha2gd($a);
			} elseif (count($value) == 3) {
				// color array without alpha value
				list($r, $g, $b) = $value;
			}
		} elseif (is_string($value)) {
			// parse color string in hexidecimal format like #cccccc or cccccc or ccc
			$hexPattern = '/^#?([a-f0-9]{1,2})([a-f0-9]{1,2})([a-f0-9]{1,2})$/i';
			
			// parse color string in format rgb(140, 140, 140)
			$rgbPattern = '/^rgb ?\(([0-9]{1,3}), ?([0-9]{1,3}), ?([0-9]{1,3})\)$/i';
			
			// parse color string in format rgba(255, 0, 0, 0.5)
			$rgbaPattern = '/^rgba ?\(([0-9]{1,3}), ?([0-9]{1,3}), ?([0-9]{1,3}), ?([0-9.]{1,4})\)$/i';
			
			if (preg_match($hexPattern, $value, $matches)) {
				$r = strlen($matches[1]) == '1' ? hexdec('0x' . $matches[1] . $matches[1]) : hexdec('0x' . $matches[1]);
				$g = strlen($matches[2]) == '1' ? hexdec('0x' . $matches[2] . $matches[2]) : hexdec('0x' . $matches[2]);
				$b = strlen($matches[3]) == '1' ? hexdec('0x' . $matches[3] . $matches[3]) : hexdec('0x' . $matches[3]);
			} elseif (preg_match($rgbPattern, $value, $matches)) {
				$r = ($matches[1] >= 0 && $matches[1] <= 255) ? intval($matches[1]) : 0;
				$g = ($matches[2] >= 0 && $matches[2] <= 255) ? intval($matches[2]) : 0;
				$b = ($matches[3] >= 0 && $matches[3] <= 255) ? intval($matches[3]) : 0;
			} elseif (preg_match($rgbaPattern, $value, $matches)) {
				$r = ($matches[1] >= 0 && $matches[1] <= 255) ? intval($matches[1]) : 0;
				$g = ($matches[2] >= 0 && $matches[2] <= 255) ? intval($matches[2]) : 0;
				$b = ($matches[3] >= 0 && $matches[3] <= 255) ? intval($matches[3]) : 0;
				$a = $this->alpha2gd($matches[4]);
			}
		}
		
		if (isset($allocatedColor)) {
			return $allocatedColor;
		} elseif (isset($r) && isset($g) && isset($b)) {
			return imagecolorallocatealpha($this->resource, $r, $g, $b, $a);
		} else {
			throw new Exception\ImageColorException("Error parsing color [{$value}]");
		}
	}
	
	/**
	 * Save image in filesystem
	 *
	 * @param string $path
	 * @param integer $quality
	 * @return Image
	 */
	public function save($path = null, $quality = 90)
	{
		$path = is_null($path) ? ($this->dirname . '/' . $this->basename) : $path;
		$saved = @file_put_contents($path, $this->encode(pathinfo($path, PATHINFO_EXTENSION), $quality));
		
		if ($saved === false) {
			throw new Exception\ImageNotWritableException;
		}
		
		return $this;
	}
	
	/**
	 * Read Exif data from the current image
	 *
	 * Note: Windows PHP Users - in order to use this method you will need to
	 * enable the mbstring and exif extensions within the php.ini file.
	 *
	 * @param string $key
	 * @return mixed
	 */
	public function exif($key = null)
	{
		if (!function_exists('exif_read_data')) {
			throw new Exception\ExifFunctionsNotAvailableException;
		}
		
		$data = exif_read_data($this->dirname . '/' . $this->basename, 'EXIF', false);
		
		if (!is_null($key) && is_array($data)) {
			return array_key_exists($key, $data) ? $data[$key] : null;
		}
		
		return $data;
	}
	
	/**
	 * Convert rgba alpha (0-1) value to gd value (0-127)
	 *
	 * @param float $input
	 * @return int
	 */
	private function alpha2gd($input)
	{
		$range_input = range(1, 0, 1 / 127);
		$range_output = range(0, 127);
		
		foreach ($range_input as $key => $value) {
			if ($value <= $input) {
				return $range_output[$key];
			}
		}
		
		return 127;
	}
	
	/**
	 * Convert gd alpha (0-127) value to rgba alpha value (0-1)
	 *
	 * @param int $input
	 * @return float
	 */
	private function alpha2rgba($input)
	{
		$range_input = range(0, 127);
		$range_output = range(1, 0, 1 / 127);
		
		foreach ($range_input as $key => $value) {
			if ($value >= $input) {
				return round($range_output[$key], 2);
			}
		}
		
		return 1;
	}
	
	/**
	 * Checks if string contains printable characters
	 *
	 * @param mixed $input
	 * @return boolean
	 */
	private function isBinary($input)
	{
		if (is_resource($input)) {
			return false;
		}
		
		return (!ctype_print($input));
	}
	
	/**
	 * Checks if the input object is image resource
	 *
	 * @param mixed $input
	 * @return boolean
	 */
	private function isImageResource($input)
	{
		// --------------------------------------------------------------------
		// There was a logical error in the program that wasn't considered.
		// Namely, how should the program handle a valid resource handle
		// that wasn't a valid image resource handle.
		// Previously this method simply returned false if $input wasn't
		// a valid image resource handle.  But in the constructor, the next
		// conditional passed the file handle to the Image::isBinary method.
		// The Image::isBinary method only checked if the input didn't contain
		// any printable characters which for a handle is true. So the program
		// incorrectly assumed the file handle as a binary string causing errors.
		// By throwing an exception for resource handles that are not of type
		// 'gd', the program stop the futher processing of data, and the
		// developer is given a descriptive exception.
		// --------------------------------------------------------------------
		if (is_resource($input)) {
			if (get_resource_type($input) != 'gd') {
				throw new Exception\InvalidImageResourceException;
			}
			
			return true;
		}
		
		return false;
	}
	
	/**
	 * Checks if the current image has (half) transparent pixels
	 *
	 * @return boolean
	 */
	private function hasTransparency()
	{
		$step_x = min(max(floor($this->width / 50), 1), 10);
		$step_y = min(max(floor($this->height / 50), 1), 10);
		
		for ($x = 0; $x < $this->width; $x = $x + $step_x) {
			for ($y = 0; $y < $this->height; $y = $y + $step_y) {
				$color = $this->pickColor($x, $y);
				if ($color['a'] < 1) {
					return true;
				}
			}
		}
		
		return false;
	}
	
	/**
	 * Set file info from image path in filesystem
	 *
	 * @param string $path
	 */
	private function setFileInfoFromPath($path)
	{
		// set file info
		$info = pathinfo($path);
		$this->dirname = array_key_exists('dirname', $info) ? $info['dirname'] : null;
		$this->basename = array_key_exists('basename', $info) ? $info['basename'] : null;
		$this->extension = array_key_exists('extension', $info) ? $info['extension'] : null;
		$this->filename = array_key_exists('filename', $info) ? $info['filename'] : null;
	}
	
	/**
	 * Set image info from image path in filesystem
	 *
	 * @param string $path
	 */
	private function setImageInfoFromPath($path)
	{
		$info = @getimagesize($path);
		
		if ($info === false) {
			throw new Exception\InvalidImageTypeException("Wrong image type ({$this->type}) only use JPG, PNG or GIF images.");
		}
		
		$this->width = $info[0];
		$this->height = $info[1];
		$this->type = $info[2];
		$this->mime = $info['mime'];
		
		// set resource
		switch ($this->type) {
			case IMAGETYPE_PNG:
				$this->resource = imagecreatefrompng($path);
				break;
			
			case IMAGETYPE_JPEG:
				$this->resource = imagecreatefromjpeg($path);
				break;
			
			case IMAGETYPE_GIF:
				$this->resource = imagecreatefromgif($path);
				break;
			
			default:
				throw new Exception\InvalidImageTypeException("Wrong image type ({$this->type}) only use JPG, PNG or GIF images.");
				break;
		}
	}
	
	/**
	 * Set local image info from GD resource
	 *
	 * @param resource $resource
	 */
	private function setImageInfoFromResource($resource)
	{
		$this->resource = $resource;
		$this->width = imagesx($this->resource);
		$this->height = imagesy($this->resource);
		$this->original['width'] = $this->width;
		$this->original['height'] = $this->height;
	}
	
	/**
	 * Set local image information from image string
	 *
	 * @param string $string
	 */
	private function setImageInfoFromString($string)
	{
		// Without the '@' passing in an invalid binary-safe string will
		// cause PHP to raise a warning. (Which is fine in production since
		// you should have display errors off. right?)
		// So supress the warning, and then check if there was an error.
		$resource = @imagecreatefromstring($string);
		
		if ($resource === false) {
			throw new Exception\InvalidImageDataStringException;
		}
		
		$this->resource = $resource;
		$this->width = imagesx($this->resource);
		$this->height = imagesy($this->resource);
		$this->original['width'] = $this->width;
		$this->original['height'] = $this->height;
	}
	
	/**
	 * Send direct output with proper header
	 *
	 * @param string $type
	 * @param integer $quality
	 * @return string
	 */
	public function response($type = null, $quality = 90)
	{
		// encode image in desired type (default: current type)
		$this->encode($type, $quality);
		
		// If this is a Laravel application, prepare response object
		if (function_exists('app') && is_a($app = app(), 'Illuminate\Foundation\Application')) {
			$response = \Response::make($this->encoded);
			$response->header('Content-Type', $this->mime);
			$response->header('Content-Encoding', 'none'); // hotfix
			
			return $response;
		}
		
		// If this is not Laravel, set header directly
		header('Content-Type: ' . $this->mime);
		
		return $this->encoded;
	}
	
	/**
	 * Returns image stream
	 *
	 * @return string
	 */
	public function __toString()
	{
		return $this->encode();
	}
}
