<?php namespace ProcessWire;

/**
 * ProcessWire ImageSizerGD
 *
 * Code for IPTC, auto rotation and sharpening by Horst Nogajski.
 * http://nogajski.de/
 *
 * Other user contributions as noted.
 *
 * Copyright (C) 2016-2019 by Horst Nogajski and Ryan Cramer
 * This file licensed under Mozilla Public License v2.0 http://mozilla.org/MPL/2.0/
 *
 * https://processwire.com
 * 
 * @method bool imSaveReady($im, $filename)
 *
 */
class ImageSizerEngineGD extends ImageSizerEngine {

	public static function getModuleInfo() {
		return array(
			'title' => 'GD Image Sizer',
			'version' => 1,
			'summary' => "Uses PHP’s built-in GD library to resize images.",
			'author' => 'Horst Nogajski',
		);
	}

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

	/**
	 * @var int ?
	 *
	 */
	protected $imageDepth;

	/**
	 * @var bool
	 * 
	 */
	protected $gammaLinearized;

	/**
	 * Webp support available?
	 * 
	 * @var bool|null
	 * 
	 */
	static protected $webpSupport = null;

	/**
	 * Get formats GD and resize
	 * 
	 * @return array
	 * 
	 */
	protected function validSourceImageFormats() {
		return array('JPG', 'JPEG', 'PNG', 'GIF');
	}

	/**
	 * Get an array of image file extensions this ImageSizerModule can create
	 *
	 * @return array of uppercase file extensions, i.e. ['PNG', 'JPG']
	 *
	 */
	protected function validTargetImageFormats() {
		$formats = $this->validSourceImageFormats();
		if($this->supported('webp')) $formats[] = 'WEBP';
		return $formats;
	}

	/**
	 * Get library version string
	 *
	 * @return string Returns version string or blank string if not applicable/available
	 * @since 3.0.138
	 *
	 */
	public function getLibraryVersion() {
		$gd = gd_info();
		return isset($gd['GD Version']) ? $gd['GD Version'] : '';
	}

	/**
	 * Return whether or not GD can proceed - Is the current image(sub)format supported?
	 *
	 * @param string $action
	 * @return bool
	 *
	 */
	public function supported($action = 'imageformat') {
		// first we check parts that are mandatory for all $actions
		if(!function_exists('gd_info')) return false;
		// and if it passes the mandatory requirements, we check particularly aspects here
		
		switch($action) {

			case 'imageformat':
				// compare current imagefile infos fetched from ImageInspector
				$requested = $this->getImageInfo(false);
				switch($requested) {
					case 'gif-anim':
					case 'gif-trans-anim':
						// Animated GIF images are not supported, but GD renders the first image of the animation
						#return false;
					default:
						return true;
				}
				break;
			
			case 'webp':
				if(self::$webpSupport === null) {
					// only call it once
					$gd  = gd_info();
					self::$webpSupport = isset($gd['WebP Support']) ? $gd['WebP Support'] : false;
				}
				return self::$webpSupport;
				break;
			
			case 'install':
				/*
				$gd  = gd_info();
				$jpg = isset($gd['JPEG Support']) ? $gd['JPEG Support'] : false;
				$png = isset($gd['PNG Support']) ? $gd['PNG Support'] : false;
				$gif = isset($gd['GIF Read Support']) && isset($gd['GIF Create Support']) ? $gd['GIF Create Support'] : false;
				$freetype = isset($gd['FreeType Support']) ? $gd['FreeType Support'] : false;
				$webp = isset($gd['WebP Support']) ? $gd['WebP Support'] : false;
				$this->config->gdReady = true;
				*/
				return true;

			default:
				return false;
		}
	}

	/**
	 * Process the image resize
	 * 
	 * @param string $srcFilename Source file
	 * @param string $dstFilename Destination file
	 * @param int $fullWidth Current width
	 * @param int $fullHeight Current height
	 * @param int $finalWidth Requested final width
	 * @param int $finalHeight Requested final height
	 * @return bool
	 * @throws WireException
	 * 
	 */
	protected function processResize($srcFilename, $dstFilename, $fullWidth, $fullHeight, $finalWidth, $finalHeight) {
		
		$this->modified = false;
		$isModified = false;
		if(isset($this->info['bits'])) $this->imageDepth = $this->info['bits'];
		$this->imageFormat = strtoupper(str_replace('image/', '', $this->info['mime']));

		if(!in_array($this->imageFormat, $this->validSourceImageFormats())) {
			throw new WireException(sprintf($this->_("loaded file '%s' is not in the list of valid images"), basename($dstFilename)));
		}

		$image = null;
		$orientations = null; // @horst
		$needRotation = $this->autoRotation !== true ? false : ($this->checkOrientation($orientations) &&
		(!empty($orientations[0]) || !empty($orientations[1])) ? true : false);

		// check if we can load the sourceimage into ram
		if(self::checkMemoryForImage(array($this->info['width'], $this->info['height'], $this->info['channels'])) === false) {
			throw new WireException(basename($srcFilename) . " - not enough memory to load");
		}

		switch($this->imageType) { // @teppo
			case \IMAGETYPE_GIF:
				$image = @imagecreatefromgif($srcFilename);
				break;
			case \IMAGETYPE_PNG:
				$image = @imagecreatefrompng($srcFilename);
				break;
			case \IMAGETYPE_JPEG:
				$image = @imagecreatefromjpeg($srcFilename);
				break;
		}

		if(!$image) return false;

		if($this->imageType != \IMAGETYPE_PNG || !$this->hasAlphaChannel()) {
			// @horst: linearize gamma to 1.0 - we do not use gamma correction with pngs containing alphachannel, because GD-lib doesn't respect transparency here (is buggy)
			$this->gammaCorrection($image, true);
		}

		if($this->rotate || $needRotation) { // @horst
			$degrees = $this->rotate ? $this->rotate : $orientations[0];
			$image = $this->imRotate($image, $degrees);
			$isModified = true; 
			if(abs($degrees) == 90 || abs($degrees) == 270) {
				// we have to swap width & height now!
				$tmp = array($this->getWidth(), $this->getHeight());
				$this->setImageInfo($tmp[1], $tmp[0]);
			}
		}

		if($this->flip || $needRotation) {
			$vertical = null;
			if($this->flip) {
				$vertical = $this->flip == 'v';
			} else if($orientations[1] > 0) {
				$vertical = $orientations[1] == 2;
			}
			if(!is_null($vertical)) {
				$image = $this->imFlip($image, $vertical);
				$isModified = true;
			}
		}
		
		$zoom = $this->getFocusZoomPercent();
		if($zoom > 1) {
			// we need to configure a cropExtra call to respect the zoom factor
			$this->cropExtra = $this->getFocusZoomCropDimensions($zoom, $fullWidth, $fullHeight, $finalWidth, $finalHeight);
			$this->cropping = false;
		}

		// if there is requested to crop _before_ resize, we do it here @horst
		if(is_array($this->cropExtra)) {
			// check if we can load a second copy from sourceimage into ram
			if(self::checkMemoryForImage(array($this->info['width'], $this->info['height'], 3)) === false) {
				throw new WireException(basename($srcFilename) . " - not enough memory to load a copy for cropExtra");
			}

			$imageTemp = imagecreatetruecolor(imagesx($image), imagesy($image));  // create an intermediate memory image
			$this->prepareImageLayer($imageTemp, $image);
			imagecopy($imageTemp, $image, 0, 0, 0, 0, imagesx($image), imagesy($image)); // copy our initial image into the intermediate one
			imagedestroy($image); // release the initial image

			// get crop values and create a new initial image
			list($x, $y, $w, $h) = $this->cropExtra;

			// check if we can load a cropped version into ram
			if(self::checkMemoryForImage(array($w, $h, 3)) === false) {
				throw new WireException(basename($srcFilename) . " - not enough memory to load a cropped version for cropExtra");
			}

			$image = imagecreatetruecolor($w, $h);
			$this->prepareImageLayer($image, $imageTemp);
			imagecopy($image, $imageTemp, 0, 0, $x, $y, $w, $h);
			unset($x, $y, $w, $h);
			$isModified = true;

			// now release the intermediate image and update settings
			imagedestroy($imageTemp);
			$imageTemp = null;
			$this->setImageInfo(imagesx($image), imagesy($image));
			// $this->cropping = false; // ?? set this to prevent overhead with the following manipulation ??
		}

		// here we check for cropping, upscaling, sharpening
		// we get all dimensions at first, before any image operation !
		$bgX = $bgY = 0;
		$bgWidth = $fullWidth;
		$bgHeight = $fullHeight;
		$resizeMethod = $this->getResizeMethod($bgWidth, $bgHeight, $finalWidth, $finalHeight, $bgX, $bgY);
		$thumb = null;

		// now lets check what operations are necessary:
		if(0 == $resizeMethod) {

			// this is the case if the original size is requested or a greater size but upscaling is set to false

			// current version is already the desired result, we only may have to compress JPEGs but leave GIF and PNG as is:
			
			if(!$isModified && !$this->webpOnly && !$this->webpAdd && ($this->imageType == \IMAGETYPE_PNG || $this->imageType == \IMAGETYPE_GIF)) {
				$result = @copy($srcFilename, $dstFilename);
				if(isset($image) && is_resource($image)) @imagedestroy($image); // clean up
				if(isset($image)) $image = null;
				return $result; // early return !
			}

			// process JPEGs
			if(self::checkMemoryForImage(array(imagesx($image), imagesy($image), 3)) === false) {
				throw new WireException(basename($srcFilename) . " - not enough memory to copy the final image");
			}
			$this->sharpening = 'none'; // we set sharpening to none, as the image only gets compressed, but not resized
			$thumb = imagecreatetruecolor(imagesx($image), imagesy($image));          // create the final memory image
			$this->prepareImageLayer($thumb, $image);
			imagecopy($thumb, $image, 0, 0, 0, 0, imagesx($image), imagesy($image));  // copy our intermediate image into the final one

		} else if(2 == $resizeMethod) { // 2 = resize with aspect ratio

			// this is the case if we scale up or down _without_ cropping

			if(self::checkMemoryForImage(array($finalWidth, $finalHeight, 3)) === false) {
				throw new WireException(basename($srcFilename) . " - not enough memory to resize to the final image");
			}

			$thumb = imagecreatetruecolor($finalWidth, $finalHeight);
			$this->prepareImageLayer($thumb, $image);
			imagecopyresampled($thumb, $image, 0, 0, 0, 0, $finalWidth, $finalHeight, $this->image['width'], $this->image['height']);

		} else if(4 == $resizeMethod) { // 4 = resize and crop with aspect ratio, - or crop without resizing ($upscaling == false)

			// we have to scale up or down and to _crop_

			if(self::checkMemoryForImage(array($bgWidth, $bgHeight, 3)) === false) {
				throw new WireException(basename($srcFilename) . " - not enough memory to resize to the intermediate image");
			}
			
			$sourceX = 0;
			$sourceY = 0;
			$sourceWidth = $this->image['width'];
			$sourceHeight = $this->image['height'];
		
			$thumb2 = imagecreatetruecolor($bgWidth, $bgHeight);
			$this->prepareImageLayer($thumb2, $image);
			imagecopyresampled(
				$thumb2, // destination image
				$image, // source image
				0, // destination X 
				0, // destination Y
				$sourceX, // source X
				$sourceY, // source Y
				$bgWidth, // destination width
				$bgHeight, // destination height
				$sourceWidth, // source width
				$sourceHeight // source height
			);

			if(self::checkMemoryForImage(array($finalWidth, $finalHeight, 3)) === false) {
				throw new WireException(basename($srcFilename) . " - not enough memory to crop to the final image");
			}

			$thumb = imagecreatetruecolor($finalWidth, $finalHeight);
			$this->prepareImageLayer($thumb, $image);
			imagecopyresampled(
				$thumb, // destination image
				$thumb2,  // source image
				0, // destination X
				0, // destination Y
				$bgX, // source X
				$bgY, // source Y
				$finalWidth, // destination width
				$finalHeight, // destination height
				$finalWidth, // source width
				$finalHeight // source height
			);
			imagedestroy($thumb2);
		}

		// early release of obsolete GD image object(s) to free memory before processing sharpening
		if(isset($image) && is_resource($image)) @imagedestroy($image); // @horst
		if(isset($thumb2) && is_resource($thumb2)) @imagedestroy($thumb2);
		if(isset($image)) $image = null;
		if(isset($thumb2)) $thumb2 = null;

		// optionally apply sharpening to the final thumb
		if($this->sharpening && $this->sharpening != 'none') { // @horst
			if(\IMAGETYPE_PNG != $this->imageType || !$this->hasAlphaChannel()) {
				$w = imagesx($thumb);
				$h = imagesy($thumb);
				if($this->useUSM) {
					// calculate if there is enough memory available to apply the USM algorithm, if enabled
					if(true === ($this->useUSM = self::checkMemoryForImage(array($w, $h, 3), array($w, $h, 3)))) {
						// is needed for the USM sharpening function to calculate the best sharpening params
						$this->usmValue = $this->calculateUSMfactor($finalWidth, $finalHeight);
						$thumb = $this->imSharpen($thumb, $this->sharpening);
					}
				}
				if(!$this->useUSM) {
					if(false !== self::checkMemoryForImage(array($w, $h, 3))) {
						$thumb = $this->imSharpen($thumb, $this->sharpening);
					}
				}
			}
		}

		// write to file(s)
		if(file_exists($dstFilename)) $this->wire()->files->unlink($dstFilename);
		
		$result = null; // null=not yet known
		
		switch($this->imageType) {
			
			case \IMAGETYPE_GIF:
				// correct gamma from linearized 1.0 back to 2.0
				$this->gammaCorrection($thumb, false);
				// save the final GIF image file
				if($this->imSaveReady($thumb, $srcFilename)) $result = imagegif($thumb, $dstFilename);
				break;
				
			case \IMAGETYPE_PNG:
				// optionally correct gamma from linearized 1.0 back to 2.0
				if(!$this->hasAlphaChannel()) $this->gammaCorrection($thumb, false);
				// save the final PNG image file and always use highest compression level (9) per @horst
				if($this->imSaveReady($thumb, $srcFilename)) $result = imagepng($thumb, $dstFilename, 9);
				break;

			case \IMAGETYPE_JPEG:
				// correct gamma from linearized 1.0 back to 2.0
				$this->gammaCorrection($thumb, false);
				if($this->imSaveReady($thumb, $srcFilename)) {
					// optionally apply interlace bit to the final image. this will result in progressive JPEGs
					if($this->interlace) {
						if(0 == imageinterlace($thumb, 1)) {
							// log that setting the interlace bit has failed ?
							// ...
						}
					}
					// save the final JPEG image file
					$result = imagejpeg($thumb, $dstFilename, $this->quality);
				}
				break;
				
			default:
				$result = false;
		}
		
		// release the last GD image object
		if(isset($thumb) && is_resource($thumb)) @imagedestroy($thumb);
		if(isset($thumb)) $thumb = null;
		if($result === null) $result = $this->webpResult; // if webpOnly option used

		return $result;
	}

	/**
	 * Called before saving of image, returns true if save should proceed, false if not
	 * 
	 * Also Creates a webp file when settings indicate it should. 
	 * 
	 * @param resource $im
	 * @param string $filename Source filename
	 * @return bool
	 * 
	 */
	protected function ___imSaveReady($im, $filename) {
		if($this->webpOnly || $this->webpAdd) {
			$this->webpResult = $this->imSaveWebP($im, $filename, $this->webpQuality);
		}
		return $this->webpOnly ? false : true; 
	}
	
	/**
	 * Create WebP image (@horst)
	 * Is requested by image options: ["webpAdd" => true] OR ["webpOnly" => true]
	 *
	 * @param resource $im
	 * @param string $filename
	 * @param int $quality
	 *
	 * @return boolean true | false
	 * 
	 */
	protected function imSaveWebP($im, $filename, $quality = 90) {
		if(!function_exists('imagewebp')) return false;
		$path_parts = pathinfo($filename);
		$webpFilename = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.webp';
		if(file_exists($webpFilename)) $this->wire()->files->unlink($webpFilename);
		return imagewebp($im, $webpFilename, $quality);
	}
	
	/**
	 * Rotate image (@horst)
	 *
	 * @param resource $im
	 * @param int $degree
	 *
	 * @return resource
	 *
	 */
	protected function imRotate($im, $degree) {
		$degree = (is_float($degree) || is_int($degree)) && $degree > -361 && $degree < 361 ? $degree : false;
		if($degree === false) return $im;
		if(in_array($degree, array(-360, 0, 360))) return $im;
		$angle = 360 - $degree; // because imagerotate() expects counterclockwise angle rather than degrees
		return @imagerotate($im, $angle, imagecolorallocate($im, 0, 0, 0));
	}

	/**
	 * Flip image (@horst)
	 *
	 * @param resource $im
	 * @param bool $vertical (default = false)
	 *
	 * @return resource
	 *
	 */
	protected function imFlip($im, $vertical = false) {
		$sx = imagesx($im);
		$sy = imagesy($im);
		$im2 = @imagecreatetruecolor($sx, $sy);
		if($vertical === true) {
			@imagecopyresampled($im2, $im, 0, 0, 0, ($sy - 1), $sx, $sy, $sx, 0 - $sy);
		} else {
			@imagecopyresampled($im2, $im, 0, 0, ($sx - 1), 0, $sx, $sy, 0 - $sx, $sy);
		}
		return $im2;
	}

	/**
	 * Sharpen image (@horst)
	 *
	 * @param resource $im
	 * @param string $mode May be: none | soft | medium | strong
	 *
	 * @return resource|bool
	 *
	 */
	protected function imSharpen($im, $mode) {

		// due to a bug in PHP's bundled GD-Lib with the function imageconvolution in some PHP versions
		// we have to bypass this for those who have to run on this PHP versions
		// see: https://bugs.php.net/bug.php?id=66714
		// and here under GD: http://php.net/ChangeLog-5.php#5.5.11
		$buggyPHP = (version_compare(phpversion(), '5.5.8', '>') && version_compare(phpversion(), '5.5.11', '<')) ? true : false;
		if($buggyPHP && !$this->useUSM
			&& self::checkMemoryForImage(array(imagesx($im), imagesy($im), 3), array(imagesx($im), imagesy($im), 3)) !== true
		) {
			// we have not enough memory available for USM and cannot use the other algorithm because of the buggy PHP version
			return $im;
		}

		// USM method is used for buggy PHP versions
		// for regular versions it can be omitted per: useUSM = false passes as pageimage option
		// or set in the site/config.php under $config->imageSizerOptions: 'useUSM' => false | true
		if($buggyPHP || $this->useUSM) {

			switch($mode) {

				case 'none':
					return $im;
					break;

				case 'strong':
					$amount = 160;
					$radius = 1.0;
					$threshold = 7;
					break;

				case 'medium':
					$amount = 130;
					$radius = 0.75;
					$threshold = 7;
					break;

				case 'soft':

				default:
					$amount = 100;
					$radius = 0.5;
					$threshold = 7;
			}

			// calculate the final amount according to the usmValue
			$this->usmValue = $this->usmValue < 0 ? 0 : ($this->usmValue > 100 ? 100 : $this->usmValue);
			if(0 == $this->usmValue) return $im;
			$amount = intval($amount / 100 * $this->usmValue);

			// apply unsharp mask filter
			return $this->unsharpMask($im, $amount, $radius, $threshold);
		}

		// if we do not use USM, we use our default sharpening method,
		// entirely based on GDs imageconvolution
		switch($mode) {

			case 'none':
				return $im;
				break;

			case 'strong':
				$sharpenMatrix = array(
					array(-1.2, -1, -1.2),
					array(-1, 16, -1),
					array(-1.2, -1, -1.2)
				);
				break;

			case 'medium':
				$sharpenMatrix = array(
					array(-1.1, -1, -1.1),
					array(-1, 20, -1),
					array(-1.1, -1, -1.1)
				);
				break;

			case 'soft':

			default:
				$sharpenMatrix = array(
					array(-1, -1, -1),
					array(-1, 24, -1),
					array(-1, -1, -1)
				);
		}

		// calculate the sharpen divisor
		$divisor = array_sum(array_map('array_sum', $sharpenMatrix));
		$offset = 0;

		// TODO 4 -c errorhandling: Throw WireException?
		if(!imageconvolution($im, $sharpenMatrix, $divisor, $offset)) return false;

		return $im;
	}


	/**
	 * apply GammaCorrection to an image (@horst)
	 *
	 * with mode = true it linearizes an image to 1
	 * with mode = false it set it back to the originating gamma value
	 *
	 * @param resource $image
	 * @param bool $mode
	 *
	 */
	protected function gammaCorrection(&$image, $mode) {
		if(-1 == $this->defaultGamma || !is_bool($mode)) return;
		if($mode) {
			// linearizes to 1.0
			if(imagegammacorrect($image, $this->defaultGamma, 1.0)) $this->gammaLinearized = true;
		} else {
			if(!isset($this->gammaLinearized) || !$this->gammaLinearized) return;
			// switch back to original Gamma
			if(imagegammacorrect($image, 1.0, $this->defaultGamma)) unset($this->gammaLinearized);
		}
	}

	/**
	 * Unsharp Mask for PHP - version 2.1.1
	 *
	 * Unsharp mask algorithm by Torstein Hønsi 2003-07.
	 * thoensi_at_netcom_dot_no.
	 * Please leave this notice.
	 *
	 * http://vikjavev.no/computing/ump.php
	 * 
	 * @param resource $img
	 * @param int $amount
	 * @param int $radius
	 * @param int $threshold
	 * @return resource
	 *
	 */
	protected function unsharpMask($img, $amount, $radius, $threshold) {
		// Attempt to calibrate the parameters to Photoshop:
		if($amount > 500) $amount = 500;
		$amount = $amount * 0.016;
		if($radius > 50) $radius = 50;
		$radius = $radius * 2;
		if($threshold > 255) $threshold = 255;

		$radius = abs(round($radius));     // Only integers make sense.
		if($radius == 0) {
			return $img;
		}
		$w = imagesx($img);
		$h = imagesy($img);
		$imgCanvas = imagecreatetruecolor($w, $h);
		$imgBlur = imagecreatetruecolor($w, $h);

		// due to a bug in PHP's bundled GD-Lib with the function imageconvolution in some PHP versions
		// we have to bypass this for those who have to run on this PHP versions
		// see: https://bugs.php.net/bug.php?id=66714
		// and here under GD: http://php.net/ChangeLog-5.php#5.5.11
		$buggyPHP = (version_compare(phpversion(), '5.5.8', '>') && version_compare(phpversion(), '5.5.11', '<')) ? true : false;

		// Gaussian blur matrix:
		//
		//    1    2    1
		//    2    4    2
		//    1    2    1
		//
		//////////////////////////////////////////////////
		if(function_exists('imageconvolution') && !$buggyPHP) {
			$matrix = array(
				array(1, 2, 1),
				array(2, 4, 2),
				array(1, 2, 1)
			);
			imagecopy($imgBlur, $img, 0, 0, 0, 0, $w, $h);
			imageconvolution($imgBlur, $matrix, 16, 0);
		} else {
			// Move copies of the image around one pixel at the time and merge them with weight
			// according to the matrix. The same matrix is simply repeated for higher radii.
			for($i = 0; $i < $radius; $i++) {
				imagecopy($imgBlur, $img, 0, 0, 1, 0, $w - 1, $h); // left
				imagecopymerge($imgBlur, $img, 1, 0, 0, 0, $w, $h, 50); // right
				imagecopymerge($imgBlur, $img, 0, 0, 0, 0, $w, $h, 50); // center
				imagecopy($imgCanvas, $imgBlur, 0, 0, 0, 0, $w, $h);

				imagecopymerge($imgBlur, $imgCanvas, 0, 0, 0, 1, $w, $h - 1, 33.33333); // up
				imagecopymerge($imgBlur, $imgCanvas, 0, 1, 0, 0, $w, $h, 25); // down
			}
		}

		if($threshold > 0) {
			// Calculate the difference between the blurred pixels and the original
			// and set the pixels
			for($x = 0; $x < $w - 1; $x++) { // each row
				for($y = 0; $y < $h; $y++) { // each pixel

					$rgbOrig = imagecolorat($img, $x, $y);
					$rOrig = (($rgbOrig >> 16) & 0xFF);
					$gOrig = (($rgbOrig >> 8) & 0xFF);
					$bOrig = ($rgbOrig & 0xFF);

					$rgbBlur = imagecolorat($imgBlur, $x, $y);

					$rBlur = (($rgbBlur >> 16) & 0xFF);
					$gBlur = (($rgbBlur >> 8) & 0xFF);
					$bBlur = ($rgbBlur & 0xFF);

					// When the masked pixels differ less from the original
					// than the threshold specifies, they are set to their original value.
					$rNew = (abs($rOrig - $rBlur) >= $threshold)
						? max(0, min(255, ($amount * ($rOrig - $rBlur)) + $rOrig))
						: $rOrig;
					$gNew = (abs($gOrig - $gBlur) >= $threshold)
						? max(0, min(255, ($amount * ($gOrig - $gBlur)) + $gOrig))
						: $gOrig;
					$bNew = (abs($bOrig - $bBlur) >= $threshold)
						? max(0, min(255, ($amount * ($bOrig - $bBlur)) + $bOrig))
						: $bOrig;

					if(($rOrig != $rNew) || ($gOrig != $gNew) || ($bOrig != $bNew)) {
						$pixCol = imagecolorallocate($img, $rNew, $gNew, $bNew);
						imagesetpixel($img, $x, $y, $pixCol);
					}
				}
			}
		} else {
			for($x = 0; $x < $w; $x++) { // each row
				for($y = 0; $y < $h; $y++) { // each pixel
					$rgbOrig = imagecolorat($img, $x, $y);
					$rOrig = (($rgbOrig >> 16) & 0xFF);
					$gOrig = (($rgbOrig >> 8) & 0xFF);
					$bOrig = ($rgbOrig & 0xFF);

					$rgbBlur = imagecolorat($imgBlur, $x, $y);

					$rBlur = (($rgbBlur >> 16) & 0xFF);
					$gBlur = (($rgbBlur >> 8) & 0xFF);
					$bBlur = ($rgbBlur & 0xFF);

					$rNew = ($amount * ($rOrig - $rBlur)) + $rOrig;
					if($rNew > 255) {
						$rNew = 255;
					} else if($rNew < 0) {
						$rNew = 0;
					}
					$gNew = ($amount * ($gOrig - $gBlur)) + $gOrig;
					if($gNew > 255) {
						$gNew = 255;
					} else if($gNew < 0) {
						$gNew = 0;
					}
					$bNew = ($amount * ($bOrig - $bBlur)) + $bOrig;
					if($bNew > 255) {
						$bNew = 255;
					} else if($bNew < 0) {
						$bNew = 0;
					}
					$rgbNew = ($rNew << 16) + ($gNew << 8) + $bNew;
					imagesetpixel($img, $x, $y, $rgbNew);
				}
			}
		}
		imagedestroy($imgCanvas);
		imagedestroy($imgBlur);

		return $img;
	}


	/**
	 * Calculate USM factor
	 *
	 * Return an integer value indicating how much an image should be sharpened
	 * according to resizing scalevalue and absolute target dimensions
	 *
	 * @param mixed $targetWidth width of the targetimage
	 * @param mixed $targetHeight height of the targetimage
	 * @param mixed $origWidth
	 * @param mixed $origHeight
	 *
	 * @return int
	 *
	 */
	protected function calculateUSMfactor($targetWidth, $targetHeight, $origWidth = null, $origHeight = null) {

		if(null === $origWidth) $origWidth = $this->getWidth();
		if(null === $origHeight) $origHeight = $this->getHeight();
		
		$w = ceil($targetWidth / $origWidth * 100);
		$h = ceil($targetHeight / $origHeight * 100);
		
		$resizingScalevalue = null;
		$target = null;
		$res = null;

		// select the resizing scalevalue with check for crop images
		if($w == $h || ($w - 1) == $h || ($w + 1) == $h) {  // equal, no crop
			$resizingScalevalue = $w;
			$target = $targetWidth;
		} else { // crop
			if(($w < $h && $w < 100) || ($w > $h && $h >= 100)) {
				$resizingScalevalue = $w;
				$target = $targetWidth;
			} elseif(($w < $h && $w >= 100) || ($w > $h && $h < 100)) {
				$resizingScalevalue = $h;
				$target = $targetHeight;
			}
		}

		// adjusting with respect to the scalefactor
		$resizingScalevalue = ($resizingScalevalue * -1) + 100;
		$resizingScalevalue = $resizingScalevalue < 0 ? $resizingScalevalue * -1 : $resizingScalevalue;

		if($resizingScalevalue > 0 && $resizingScalevalue < 10) $resizingScalevalue += 15;
			else if($resizingScalevalue > 9 && $resizingScalevalue < 25) $resizingScalevalue += 20;
			else if($resizingScalevalue > 24 && $resizingScalevalue < 40) $resizingScalevalue += 35;
			else if($resizingScalevalue > 39 && $resizingScalevalue < 55) $resizingScalevalue += 20;
			else if($resizingScalevalue > 54 && $resizingScalevalue < 70) $resizingScalevalue += 5;
			else if($resizingScalevalue > 69 && $resizingScalevalue < 80) $resizingScalevalue -= 10;

		// adjusting with respect to absolute dimensions
		if($target < 50) $res = intval($resizingScalevalue / 18 * 3);
			else if($target < 100) $res = intval($resizingScalevalue / 18 * 4);
			else if($target < 200) $res = intval($resizingScalevalue / 18 * 6);
			else if($target < 300) $res = intval($resizingScalevalue / 18 * 8);
			else if($target < 400) $res = intval($resizingScalevalue / 18 * 10);
			else if($target < 500) $res = intval($resizingScalevalue / 18 * 12);
			else if($target < 600) $res = intval($resizingScalevalue / 18 * 15);
			else if($target > 599) $res = $resizingScalevalue;

		$res = $res < 0 ? $res * -1 : $res; // avoid negative numbers

		return $res;
	}


	/**
	 * Prepares a new created GD image resource according to the IMAGETYPE
	 *
	 * Intended for use by the resize() method
	 *
	 * @param resource $im, destination resource needs to be prepared
	 * @param resource $image, with GIF we need to read from source resource
	 *
	 */
	protected function prepareImageLayer(&$im, &$image) {

		if($this->imageType == IMAGETYPE_PNG) {
			// @adamkiss PNG transparency
			imagealphablending($im, false);
			imagesavealpha($im, true);

		} else if($this->imageType == IMAGETYPE_GIF) {
			// @mrx GIF transparency
			$transparentIndex = imagecolortransparent($image);
			if($transparentIndex >= 0 && $transparentIndex < imagecolorstotal($image)) {
				$transparentColor = imagecolorsforindex($image, $transparentIndex);
				if(!empty($transparentColor)) {
					$transparentNew = imagecolorallocate($im, $transparentColor['red'], $transparentColor['green'], $transparentColor['blue']);
					$transparentNewIndex = imagecolortransparent($im, $transparentNew);
					imagefill($im, 0, 0, $transparentNewIndex);
				}
			}

		} else {
			$bgcolor = imagecolorallocate($im, 0, 0, 0);
			imagefilledrectangle($im, 0, 0, imagesx($im), imagesy($im), $bgcolor);
			imagealphablending($im, false);
		}
	}


	/**
	 * calculation if there is enough memory available at runtime for loading and resizing an given imagefile
	 *
	 * @param array $sourceDimensions - array with three values: width, height, number of channels
	 * @param array|bool $targetDimensions - optional - mixed: bool true | false or array with three values:
	 *  width, height, number of channels
	 * @param int|float Multiply needed memory by this factor
	 *
	 * @return bool|null if a calculation was possible (true|false), or null if the calculation could not be done
	 *
	 */
	static public function checkMemoryForImage($sourceDimensions, $targetDimensions = false, $factor = 1) {

		// with this static we only once need to read from php.ini and calculate phpMaxMem,
		// regardless how often this function is called in a request
		static $phpMaxMem = null;

		if(null === $phpMaxMem) {
			$sMem = trim(strtoupper(ini_get('memory_limit')), ' B'); // trim B just in case it has Mb rather than M
			switch(substr($sMem, -1)) {
				case 'M':
					$phpMaxMem = ((int) $sMem) * 1048576;
					break;
				case 'K':
					$phpMaxMem = ((int) $sMem) * 1024;
					break;
				case 'G':
					$phpMaxMem = ((int) $sMem) * 1073741824;
					break;
				default:
					$phpMaxMem = (int) $sMem;
			}
		}

		if($phpMaxMem <= 0) {
			// we couldn't read the MaxMemorySetting or there isn't one set,
			// so in both cases we do not know if there is enough or not
			return null;
		}

		// calculate $sourceDimensions
		if(!isset($sourceDimensions[0]) || !isset($sourceDimensions[1]) || !isset($sourceDimensions[2]) || 
			!is_int($sourceDimensions[0]) || !is_int($sourceDimensions[1]) || !is_int($sourceDimensions[2])) {
			return null;
		}

		//            width             *        height        *       channels
		$imgMem = ($sourceDimensions[0] * $sourceDimensions[1] * $sourceDimensions[2]);

		if(true === $targetDimensions) {
			// we have to add ram for a copy of the sourceimage
			$imgMem += $imgMem;

		} else if(is_array($targetDimensions)) {
			// we have to add ram for a targetimage
			if(!isset($targetDimensions[0]) || !isset($targetDimensions[1]) || !isset($targetDimensions[2]) || 
				!is_int($targetDimensions[0]) || !is_int($targetDimensions[1]) || !is_int($targetDimensions[2])) {
				return null;
			}

			$imgMem += ($targetDimensions[0] * $targetDimensions[1] * $targetDimensions[2]);
		}

		// read current allocated memory
		$curMem = memory_get_usage(true);  // memory_get_usage() is always available with PHP since 5.2.1

		// check if there is enough RAM loading the image(s), plus 3 MB for GD to use for calculations/transforms
		$extraMem = 3 * 1048576;
		$availableMem = $phpMaxMem - $curMem;
		$neededMem = ($imgMem + $extraMem) * $factor;
		
		return $availableMem >= $neededMem; 
	}

	/**
	 * Additional functionality on top of existing checkMemoryForImage function for the flip/rotate actions
	 * 
	 * @param string $filename Filename to check. Default is whatever was set to this ImageSizer. 
	 * @param bool $double Need enough for both src and dst files loaded at same time? (default=true)
	 * @param int|float $factor Tweak factor (multiply needed memory by this factor), i.e. 2 for rotate actions. (default=1)
	 * @param string $action Name of action (if something other than "action")
	 * @param bool $throwIfNot Throw WireException if not enough memory? (default=false)
	 * @return bool
	 * @throws WireException
	 * 
	 */
	protected function hasEnoughMemory($filename = '', $double = true, $factor = 1, $action = 'action', $throwIfNot = false) {
		$error = '';
		if(empty($filename)) $filename = $this->filename;
		if($filename) {
			if($filename != $this->filename || empty($this->info['width'])) {
				$this->prepare($filename); // to populate $this->info
			}
		} else {
			$error = 'No filename to check memory for'; 
		}
		if(!$error) {
			$hasEnough = self::checkMemoryForImage(array(
				$this->info['width'],
				$this->info['height'],
				$this->info['channels']
			), $double, $factor);
			if($hasEnough === false) {
				$error = sprintf($this->_('Not enough memory for “%1$s” on image file: %2$s'), $action, basename($filename));
			}
		}
		if($error) {
			if($throwIfNot) {
				throw new WireException($error);
			} else {
				$this->error($error);
				return false;
			}
		}
		return true; 
	}

	/**
	 * Process a rotate or flip action
	 *
	 * @param string $srcFilename
	 * @param string $dstFilename
	 * @param string $action One of 'rotate' or 'flip'
	 * @param int|string $value If rotate, specify int of degrees. If flip, specify one of 'vertical', 'horizontal' or 'both'.
	 * @return bool
	 * @throws WireException
	 *
	 */
	private function processAction($srcFilename, $dstFilename, $action, $value) {

		$action = strtolower($action);
		$ext = strtolower(pathinfo($srcFilename, PATHINFO_EXTENSION));
		$useTransparency = true;
		$memFactor = 1;
		$img = null;
		
		if(empty($dstFilename)) $dstFilename = $srcFilename;
		
		if($action == 'rotate') $memFactor *= 2;
		if(!$this->hasEnoughMemory($srcFilename, true, $memFactor, $action, false)) return false;
		
		if($ext == 'jpg' || $ext == 'jpeg') {
			$img = imagecreatefromjpeg($srcFilename);
			$useTransparency = false;
		} else if($ext == 'png') {
			$img = imagecreatefrompng($srcFilename);
		} else if($ext == 'gif') {
			$img = imagecreatefromgif($srcFilename);
		}

		if(!$img) {
			$this->error("imagecreatefrom$ext failed", Notice::debug);
			return false;
		}

		if($useTransparency) {
			imagealphablending($img, true);
			imagesavealpha($img, true);
		}

		$success = true;
		$method = '_processAction' . ucfirst($action);
		$imgNew = $this->$method($img, $value);

		if($imgNew === false) {
			// action fail
			$success = false;
			$this->error($this->className() . ".$method(img, $value) returned fail", Notice::debug);
		} else if($imgNew !== $img) {
			// a new img object was created
			imagedestroy($img);
			$img = $imgNew;
			if($useTransparency) {
				imagealphablending($img, true);
				imagesavealpha($img, true);
			}
		} else {
			// existing img object was updated
			$img = $imgNew;
		}

		if($success) {
			if($ext == 'png') {
				$success = imagepng($img, $dstFilename, 9);
			} else if($ext == 'gif') {
				$success = imagegif($img, $dstFilename);
			} else {
				$success = imagejpeg($img, $dstFilename, $this->quality);
			}
			if(!$success) $this->error("image{$ext}() failed", Notice::debug);
		}

		imagedestroy($img);

		return $success;
	}

	/**
	 * Process flip action (internal)
	 * 
	 * @param resource $img
	 * @param string $flipType vertical, horizontal or both
	 * @return bool|resource
	 * 
	 */
	private function _processActionFlip(&$img, $flipType) {
		if(!function_exists('imageflip')) {
			$this->error("Image flip requires PHP 5.5 or newer");
			return false;
		}
		if(!in_array($flipType, array('vertical', 'horizontal', 'both'))) {
			$this->error("Image flip type must be one of: 'vertical', 'horizontal', 'both'");
			return false;
		}
		$constantName = 'IMG_FLIP_' . strtoupper($flipType);
		$flipType = constant($constantName);
		if($flipType === null) {
			$this->error("Unknown constant for image flip: $constantName");
			return false;
		}
		$success = imageflip($img, $flipType);
		return $success ? $img : false;
	}

	/**
	 * Process rotate action (internal)
	 * 
	 * @param resource $img
	 * @param $degrees
	 * @return bool|resource
	 * 
	 */
	private function _processActionRotate(&$img, $degrees) {
		$degrees = (int) $degrees;
		$angle = 360 - $degrees; // imagerotate is anti-clockwise
		$imgNew = imagerotate($img, $angle, 0);
		return $imgNew ? $imgNew : false;
	}
	
	private function _processActionGreyscale(&$img, $unused) {
		if($unused) {}
		imagefilter($img, IMG_FILTER_GRAYSCALE);
		return $img;
	}
	
	private function _processActionSepia(&$img, $sepia = 55) {
		imagefilter($img, IMG_FILTER_GRAYSCALE);
		imagefilter($img, IMG_FILTER_BRIGHTNESS, -30);
		imagefilter($img, IMG_FILTER_COLORIZE, 90, (int) $sepia, 30);
		return $img;
	}

	/**
	 * Process rotate of an image
	 *
	 * @param string $srcFilename
	 * @param string $dstFilename
	 * @param int $degrees Clockwise degrees, i.e. 90, 180, 270, -90, -180, -270
	 * @return bool
	 *
	 */
	protected function processRotate($srcFilename, $dstFilename, $degrees) {
		return $this->processAction($srcFilename, $dstFilename, 'rotate', $degrees);
	}

	/**
	 * Process vertical or horizontal flip of an image
	 *
	 * @param string $srcFilename
	 * @param string $dstFilename
	 * @param string $flipType Specify vertical, horizontal, or both
	 * @return bool
	 *
	 */
	protected function processFlip($srcFilename, $dstFilename, $flipType) {
		return $this->processAction($srcFilename, $dstFilename, 'flip', $flipType);
	}
	
	/**
	 * Convert image to greyscale
	 *
	 * @param string $dstFilename If different from source file
	 * @return bool
	 *
	 */
	public function convertToGreyscale($dstFilename = '') {
		return $this->processAction($this->filename, $dstFilename, 'greyscale', null);
	}

	/**
	 * Convert image to sepia
	 *
	 * @param string $dstFilename If different from source file
	 * @param float|int $sepia Sepia value
	 * @return bool
	 *
	 */
	public function convertToSepia($dstFilename = '', $sepia = 55) {
		return $this->processAction($this->filename, $dstFilename, 'sepia', $sepia);
	}


}
