<commit_before><?php
/**
 * Class definition file for SLIR (Smart Lencioni Image Resizer)
 *
 * This file is part of SLIR (Smart Lencioni Image Resizer).
 *
 * SLIR is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * SLIR is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with SLIR.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @copyright Copyright  2009, <NAME>
 * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public
 * License version 3 (GPLv3)
 * @since 2.0
 * @package SLIR
 */
 
/* $Id$ */

/**
 * SLIR (Smart Lencioni Image Resizer)
 * Resizes images, intelligently sharpens, crops based on width:height ratios,
 * color fills transparent GIFs and PNGs, and caches variations for optimal
 * performance.
 *
 * I love to hear when my work is being used, so if you decide to use this,
 * feel encouraged to send me an email. I would appreciate it if you would
 * include a link on your site back to Shifting Pixel (either the SLIR page or
 * shiftingpixel.com), but dont worry about including a big link on each page
 * if you dont want toone will do just nicely. Feel free to contact me to
 * discuss any specifics (<EMAIL>).
 *
 * REQUIREMENTS:
 *     - PHP 5.1.0+
 *     - GD
 *
 * RECOMMENDED:
 *     - mod_rewrite
 *
 * USAGE:
 * To use, place an img tag with the src pointing to the path of SLIR (typically
 * "/slir/") followed by the parameters, followed by the path to the source
 * image to resize. All parameters follow the pattern of a one-letter code and
 * then the parameter value:
 *     - Maximum width = w
 *     - Maximum height = h
 *     - Crop ratio = c
 *     - Quality = q
 *     - Background fill color = b
 *     - Progressive = p
 *
 * EXAMPLES:
 *
 * Resizing a JPEG to a max width of 100 pixels and a max height of 100 pixels:
 * <code><img src="/slir/w100-h100/path/to/image.jpg" alt="Don't forget your alt
 * text" /></code>
 *
 * Resizing and cropping a JPEG into a square:
 * <code><img src="/slir/w100-h100-c1:1/path/to/image.jpg" alt="Don't forget
 * your alt text" /></code>
 *
 * Resizing a JPEG without interlacing (for use in Flash):
 * <code><img src="/slir/w100-p0/path/to/image.jpg" alt="Don't forget your alt
 * text" /></code>
 *
 * Matting a PNG with #990000:
 * <code><img src="/slir/b900/path/to/image.png" alt="Don't forget your alt
 * text" /></code>
 *
 * Without mod_rewrite (not recommended)
 * <code><img src="/slir/?w=100&amp;h=100&amp;c=1:1&amp;image=/path/to/image.jpg"
 * alt="Don't forget your alt text" /></code>
 *
 * @author <NAME> <<EMAIL>>
 * @date $Date$
 * @version $Revision$
 * @package SLIR
 *
 * @uses PEL
 * 
 * @todo lock files when writing?
 * @todo Prevent SLIR from calling itself
 * @todo Percentage resizing?
 * @todo Animated GIF resizing?
 * @todo Seam carving?
 * @todo Crop zoom?
 * @todo Crop offsets?
 * @todo Periodic cache clearing?
 * @todo Remote image fetching?
 * @todo Alternative support for ImageMagick?
 * @todo Prevent files in cache from being read directly?
 * @todo split directory initialization and variable checking into a separate
 * install/upgrade script with friendly error messages, an opportunity to give a
 * tip, and a button that tells me they are using it on their site if they like
 */

class SLIR
{
	/**
	 * @since 2.0
	 * @var string
	 */
	const VERSION	= '2.0b3';

	/**
	 * Path to source image
	 *
	 * @since 2.0
	 * @var string
	 */
	private $imagePath;

	/**
	 * Maximum width for resized image, in pixels
	 *
	 * @since 2.0
	 * @var integer
	 */
	private $maxWidth;

	/**
	 * Maximum height for resized image, in pixels
	 *
	 * @since 2.0
	 * @var integer
	 */
	private $maxHeight;

	/**
	 * Quality setting for resized image. Ranges from 0 (worst quality, smaller
	 * file) to 100 (best quality, biggest file). If not specified, will default
	 * to SLIR_DEFAULT_QUALITY setting in slir-config.php
	 *
	 * @since 2.0
	 * @var integer
	 */
	private $quality;

	/**
	 * Ratio of width:height to crop image to.
	 *
	 * For example, if a square shape is desired, the crop ratio should be "1:1"
	 * or if a long rectangle is desired, the crop ratio could be "4:1". Stored
	 * as an associative array with keys being 'width' and 'height'.
	 *
	 * @since 2.0
	 * @var array
	 */
	private $cropRatio;

	/**
	 * Whether JPEG should be a progressive JPEG (interlaced) or not. If not
	 * specified, will default to SLIR_DEFAULT_PROGRESSIVE_JPEG setting in
	 * slir-config.php
	 *
	 * @since 2.0
	 * @var bool
	 */
	private $progressiveJPEGs;

	/**
	 * A color, in hexadecimal format (RRGGBB), to fill in as the background
	 * color for PNG images
	 *
	 * Longhand values (e.g. "FF0000") and shorthand values (e.g. "F00") are
	 * both acceptable
	 *
	 * @since 2.0
	 * @var string
	 */
	private $backgroundFillColor;

	/**
	 * Information about the source image
	 *
	 * Generated in part by {@link http://us2.php.net/getimagesize getimagesize()}
	 *
	 * @since 2.0
	 * @var array
	 */
	private $source;

	/**
	 * Information about the rendered image
	 *
	 * @since 2.0
	 * @var string
	 */
	private $rendered;

	/**
	 * Whether or not the cache has already been initialized
	 *
	 * @since 2.0
	 * @var boolean
	 */
	private $isCacheInitialized	= FALSE;

	/**
	 * The magic starts here
	 *
	 * @since 2.0
	 */
	public function __construct()
	{
		$this->getConfig();

		// Check the cache based on the request URI
		if (SLIR_USE_REQUEST_CACHE && $this->isRequestCached())
			$this->serveRequestCachedImage();
		
		// Set up our error handler after the request cache to help keep
		// everything humming along nicely
		require 'slirexception.class.php';
		set_error_handler(array('SLIRException', 'error'));
		
		// Set all parameters for resizing
		$this->setParameters($this->getParameters());

		// See if there is anything we actually need to do
		if ($this->isSourceImageDesired())
			$this->serveSourceImage();

		// Determine rendered dimensions
		$this->getRenderProperties();

		// Check the cache based on the properties of the rendered image
		if (!$this->isRenderedCached() || !$this->serveRenderedCachedImage())
		{
			// Image is not cached in any way, so we need to render the image,
			// cache it, and serve it up to the client
			$this->render();
			$this->serveRenderedImage();
		} // if

	} // __construct()

	/**
	 * Helps control the parameters as they are set
	 *
	 * @since 2.0
	 * @param string $name
	 * @param mixed $value
	 * @todo Prevent SLIR from calling itself
	 */
	private function __set($name, $value)
	{
		switch($name)
		{
			case 'i':
			case 'image':
			case 'imagePath':
				// Images must be local files, so for convenience we strip the
				// domain if it's there
				$this->imagePath	= '/' . trim(preg_replace('/^(s?f|ht)tps?:\/\/[^\/]+/i', '', (string) urldecode($value)), '/');
				
				// Strip query string from the image path
				$this->imagePath	= preg_replace('/\?.*/', '', $this->imagePath);

				// Make sure the image path is secure
				if (!$this->isPathSecure($this->imagePath))
				{
					header('HTTP/1.1 400 Bad Request');
					throw new SLIRException('Image path may not contain ":", ".'
						. '.", "<", or ">"');
				}
				// Make sure the image file exists
				else if (!$this->imageExists())
				{
					header('HTTP/1.1 404 Not Found');
					throw new SLIRException('Image does not exist: ' . SLIR_DOCUMENT_ROOT . $this->imagePath);
				}
				// Everything seems to check out just fine, proceeding normally
				else
				{
					// Set the image info (width, height, mime type, etc.)
					$this->source	= $this->getImageInfo();

					// Make sure the file is actually an image
					if (!$this->isImage())
					{
						header('HTTP/1.1 400 Bad Request');
						throw new SLIRException('Requested file is not an '
							. 'accepted image type: ' . SLIR_DOCUMENT_ROOT
							. $this->imagePath);
					} // if
				} // if
			break;

			case 'w':
			case 'width':
			case 'maxWidth':
				$this->maxWidth		= (int) $value;
			break;

			case 'h':
			case 'height':
			case 'maxHeight':
				$this->maxHeight	= (int) $value;
			break;

			case 'q':
			case 'quality':
				$this->quality		= (int) $value;

				if ($this->quality < 0 || $this->quality > 100)
				{
					header('HTTP/1.1 400 Bad Request');
					throw new SLIRException('Quality must be between 0 and 100: '
						. $this->quality);
				}
			break;

			case 'p':
			case 'progressive':
				$this->progressiveJPEGs	= (bool) $value;
			break;

			case 'c':
			case 'cropRatio':
				$ratio				= explode(':', (string) $value);
				if (count($ratio) >= 2)
				{
					$this->cropRatio	= array(
						'width'		=> (float) $ratio[0],
						'height'	=> (float) $ratio[1],
						'ratio'		=> (float) $ratio[0] / (float) $ratio[1]
					);
				}
				else
				{
					header('HTTP/1.1 400 Bad Request');
					throw new SLIRException('Crop ratio must be in width:height'
						. ' format: ' . (string) $value);
				} // if
			break;

			case 'b';
			case 'backgroundFillColor':
				$this->backgroundFillColor	= preg_replace('/[^0-9a-fA-F]/', '',
					(string) $value);

				$bglen	= strlen($this->backgroundFillColor);
				if($bglen == 3)
				{
					$this->backgroundFillColor = $this->backgroundFillColor[0]
						.$this->backgroundFillColor[0]
						.$this->backgroundFillColor[1]
						.$this->backgroundFillColor[1]
						.$this->backgroundFillColor[2]
						.$this->backgroundFillColor[2];
				}
				else if ($bglen != 6)
				{
					header('HTTP/1.1 400 Bad Request');
					throw new SLIRException('Background fill color must be in '
						.'hexadecimal format, longhand or shorthand: '
						. $this->backgroundFillColor);
				} // if
			break;
		} // switch
	} // __set()

	/**
	 * Includes the configuration file
	 *
	 * @since 2.0
	 */
	private function getConfig()
	{
		if (file_exists('slir-config.php'))
		{
			require 'slir-config.php';
		}
		else if (file_exists('slir-config-sample.php'))
		{
			if (copy('slir-config-sample.php', 'slir-config.php'))
				require 'slir-config.php';
			else
				throw new SLIRException('Could not load configuration file. '
					. 'Please copy "slir-config-sample.php" to '
					. '"slir-config.php".');
		}
		else
		{
			throw new SLIRException('Could not find "slir-config.php" or '
				. '"slir-config-sample.php"');
		} // if
	} // getConfig()

	/**
	 * Renders specified changes to the image
	 *
	 * @since 2.0
	 */
	private function render()
	{
		// We don't want to run out of memory
		ini_set('memory_limit', SLIR_MEMORY_TO_ALLOCATE);

		// Set up a blank canvas for our rendered image (destination)
		$this->rendered['image']	= imagecreatetruecolor(
										$this->rendered['width'],
										$this->rendered['height']
									);

		// Read in the original image
		$this->source['image']		= $this->rendered['functions']['create'](SLIR_DOCUMENT_ROOT . $this->imagePath);
		
		// GIF/PNG transparency and background color
		$this->background();

		// Resample the original image into the resized canvas we set up earlier
		if ($this->source['width'] != $this->rendered['width'] || $this->source['height'] != $this->rendered['height'])
		{
			ImageCopyResampled(
				$this->rendered['image'],
				$this->source['image'],
				0,
				0,
				0,
				0,
				$this->rendered['width'],
				$this->rendered['height'],
				$this->source['width'],
				$this->source['height']
			);
		}
		else // No resizing is needed, so make a clean copy
		{
			ImageCopy(
				$this->rendered['image'],
				$this->source['image'],
				0,
				0,
				0,
				0,
				$this->source['width'],
				$this->source['height']
			);
		} // if

		
		// Cropping
		if ($this->isCroppingNeeded())
			$this->crop();
		
		// Sharpen
		if ($this->rendered['sharpen'])
			$this->sharpen();

		// Set interlacing
		if ($this->rendered['progressive'])
			imageinterlace($this->rendered['image'], 1);

	} // render()
	
	/**
	 * Crops the rendered image
	 * 
	 * @since 2.0
	 * @return boolean
	 * @todo add cropping method preference (smart or centered)
	 */
	private function crop()
	{
		// Determine crop offset
		$offset		= array(
			'top'	=> 0,
			'left'	=> 0
		);
		
		if ($this->cropRatio['ratio'] > $this->source['ratio'])
		{
			// Image is too tall so we will crop the top and bottom
			$o					= $this->offset(FALSE);
			if ($o === FALSE)
				return TRUE;
			else
				$offset['top']		= $o;
		}
		else
		{
			// Image is too wide so we will crop the left and right
			$o					= $this->offset(TRUE);
			if ($o === FALSE)
				return TRUE;
			else
				$offset['left']		= $o;
		} // if
		
		// Set up a blank canvas for our cropped image (destination)
		$cropped	= imagecreatetruecolor(
						$this->rendered['cropWidth'],
						$this->rendered['cropHeight']
						);
						
		// Copy rendered image to cropped image
		ImageCopy(
			$cropped,
			$this->rendered['image'],
			0,
			0,
			$offset['left'],
			$offset['top'],
			$this->rendered['width'],
			$this->rendered['height']
		);
		
		// Replace pre-cropped image with cropped image
		imagedestroy($this->rendered['image']);
		$this->rendered['image']	= $cropped;
		unset($cropped);
		
		return TRUE;
	} // crop()

	/**
	 * Turns on transparency for rendered image if no background fill color is
	 * specified, otherwise, fills background with specified color
	 *
	 * @since 2.0
	 */
	private function background()
	{
		if (!$this->isBackgroundFillOn())
		{
			// If this is a GIF or a PNG, we need to set up transparency
			imagealphablending($this->rendered['image'], FALSE);
			imagesavealpha($this->rendered['image'], TRUE);
		}
		else
		{
			// Fill the background with the specified color for matting purposes
			$background	= imagecolorallocate(
				$this->rendered['image'],
				hexdec($this->backgroundFillColor[0].$this->backgroundFillColor[1]),
				hexdec($this->backgroundFillColor[2].$this->backgroundFillColor[3]),
				hexdec($this->backgroundFillColor[4].$this->backgroundFillColor[5])
			);

			imagefill($this->rendered['image'], 0, 0, $background);
		} // if
	} // background()

	/**
	 * Sharpens the image based on two things:
	 *   (1) the difference between the original size and the final size
	 *   (2) the final size
	 *
	 * @since 2.0
	 */
	private function sharpen()
	{
		$sharpness	= $this->calculateSharpnessFactor(
			$this->source['width'] * $this->source['height'],
			$this->rendered['width'] * $this->rendered['height']
		);

		$sharpenMatrix	= array(
			array(-1, -2, -1),
			array(-2, $sharpness + 12, -2),
			array(-1, -2, -1)
		);

		$divisor	= $sharpness;
		$offset		= 0;

		imageconvolution(
			$this->rendered['image'],
			$sharpenMatrix,
			$divisor,
			$offset
		);
	} // sharpen()

	/**
	 * Calculates sharpness factor to be used to sharpen an image based on the
	 * area of the source image and the area of the destination image
	 *
	 * @since 2.0
	 * @author <NAME>
	 * @link http://adryrun.com
	 *
	 * @param integer $sourceArea Area of source image
	 * @param integer $destinationArea Area of destination image
	 * @return integer Sharpness factor
	 */
	private function calculateSharpnessFactor($sourceArea, $destinationArea)
	{
		$final	= sqrt($destinationArea) * (750.0 / sqrt($sourceArea));
		$a		= 52;
		$b		= -0.27810650887573124;
		$c		= .00047337278106508946;

		$result = $a + $b * $final + $c * $final * $final;

		return max(round($result), 0);
	} // calculateSharpnessFactor()

	/**
	 * @since 2.0
	 * @param string $cacheFilePath
	 * @return string Contents of the image
	 */
	private function copyIPTC($cacheFilePath)
	{
		$data	= '';

		$iptc	= $this->source['iptc'];

		// Originating program
		$iptc['2#065']	= array('Smart Lencioni Image Resizer');

		// Program version
		$iptc['2#070']	= array(SLIR::VERSION);

		foreach($iptc as $tag => $iptcData)
		{
			$tag	= substr($tag, 2);
			$data	.= $this->makeIPTCTag(2, $tag, $iptcData[0]);
		}

		// Embed the IPTC data
		return iptcembed($data, $cacheFilePath);
	} // copyIPTC()

	/**
	 * @since 2.0
	 * @author <NAME>
	 */
	function makeIPTCTag($rec, $data, $value)
	{
		$length = strlen($value);
		$retval = chr(0x1C) . chr($rec) . chr($data);

		if($length < 0x8000)
		{
			$retval .= chr($length >> 8) .  chr($length & 0xFF);
		}
		else
		{
			$retval .= chr(0x80) .
					   chr(0x04) .
					   chr(($length >> 24) & 0xFF) .
					   chr(($length >> 16) & 0xFF) .
					   chr(($length >> 8) & 0xFF) .
					   chr($length & 0xFF);
		}

		return $retval . $value;
	} // makeIPTCTag()

	/**
	 * Determines the parameters to use for resizing
	 *
	 * @since 2.0
	 */
	private function getParameters()
	{
		if (!$this->isUsingQueryString()) // Using the mod_rewrite version
			return $this->getParametersFromPath();
		else // Using the query string version
			return $_GET;
	} // getParameters()

	/**
	 * For requests that are using the mod_rewrite syntax
	 *
	 * @since 2.0
	 */
	private function getParametersFromPath()
	{
		$params	= array();

		// The parameters should be the first set of characters after the
		// SLIR path
		$request	= str_replace(SLIR_DIR, '', (string) $_SERVER['REQUEST_URI']);
		$request	= explode('/', trim($request, '/'));

		if (count($request) < 2)
		{
			header('HTTP/1.1 400 Bad Request');
			throw new SLIRException('Not enough parameters were given.', 'Available parameters:
w = Maximum width
h = Maximum height
c = Crop ratio (width:height)
q = Quality (0-100)
b = Background fill color (RRGGBB or RGB)
p = Progressive (0 or 1)

Example usage:
<img src="' . SLIR_DIR . '/w300-h300-c1:1/path/to/image.jpg" alt="Don\'t forget '
.'your alt text!" />'
			);

		} // if

		// The parameters are separated by hyphens
		$rawParams	= array_filter(explode('-', array_shift($request)));

		// The image path should be all of the remaining values in the array
		$params['i']	= implode('/', $request);

		foreach ($rawParams as $rawParam)
		{
			// The name of each parameter should be the first character of the
			// parameter string
			$name	= $rawParam[0];
			// The value of each parameter should be the remaining characters of
			// the parameter string
			$value	= substr($rawParam, 1, strlen($rawParam) - 1);

			$params[$name]	= $value;
		} // foreach

		$params	= array_filter($params);

		return $params;
	} // getParametersFromPath()

	/**
	 * Sets up parameters for image resizing
	 *
	 * @since 2.0
	 * @param array $params Associative array of parameters
	 */
	private function setParameters($params)
	{
		// Set image path first
		if (isset($params['i']) && $params['i'] != '' && $params['i'] != '/')
		{
			$this->__set('i', $params['i']);
			unset($params['i']);
		}
		else
		{
			header('HTTP/1.1 400 Bad Request');
			throw new SLIRException('Source image was not specified.');
		} // if

		// Set the rest of the parameters
		foreach($params as $name => $value)
		{
			$this->__set($name, $value);
		} // foreach

		// If either a max width or max height are not specified or larger than
		// the source image we default to the dimension of the source image so
		// they do not become constraints on our resized image.
		if (!$this->maxWidth || $this->maxWidth > $this->source['width'])
			$this->maxWidth		= $this->source['width'];

		if (!$this->maxHeight ||  $this->maxHeight > $this->source['height'])
			$this->maxHeight	= $this->source['height'];

	} // setParameters()

	/**
	 * Determines if the request is using the mod_rewrite version or the query
	 * string version
	 *
	 * @since 2.0
	 * @return bool
	 */
	private function isUsingQueryString()
	{
		if (isset($_SERVER['QUERY_STRING'])
			&& trim($_SERVER['QUERY_STRING']) != ''
			&& count(array_intersect(array('i', 'w', 'h', 'q', 'c', 'b'), array_keys($_GET)))
			)
			return TRUE;
		else
			return FALSE;
	} // isUsingQueryString()

	/**
	 * Checks to see if the image path is secure
	 *
	 * For security, directories may not contain ':' and images may not contain
	 * '..', '<', or '>'.
	 *
	 * @since 2.0
	 * @param string $path
	 * @return bool
	 */
	private function isPathSecure($path)
	{
		if (strpos(dirname($path), ':') || preg_match('/(\.\.|<|>)/', $path))
			return FALSE;
		else
			return TRUE;
	} // isImagePathSecure()

	/**
	 * Determines if the source image exists
	 *
	 * @since 2.0
	 * @return bool
	 */
	private function imageExists()
	{
		return is_file(SLIR_DOCUMENT_ROOT . $this->imagePath);
	} // imageExists()

	/**
	 * Retrieves information about the source image such as width and height
	 *
	 * @since 2.0
	 * @return array
	 */
	private function getImageInfo()
	{
		$info			= getimagesize(SLIR_DOCUMENT_ROOT . $this->imagePath, $extraInfo);

		if ($info == FALSE)
		{
			header('HTTP/1.1 400 Bad Request');
			throw new SLIRException('getimagesize failed (source file may not '
				. 'be an image): ' . SLIR_DOCUMENT_ROOT . $this->imagePath);
		}

		$info['width']	=& $info[0];
		$info['height']	=& $info[1];
		$info['ratio']	= $info['width']/$info['height'];

		// IPTC
		if(is_array($extraInfo) && isset($extraInfo['APP13']))
				$info['iptc']	= iptcparse($extraInfo['APP13']);

		return $info;
	} // getImageInfo()

	/**
	 * Checks the image info and image's mime type to see if it is an image
	 *
	 * @since 2.0
	 * @return bool
	 */
	private function isImage()
	{
		if ($this->source !== FALSE
			|| substr($this->source['mime'], 0, 6) == 'image/')
			return TRUE;
		else
			return FALSE;
	} // isImage()

	/**
	 * Checks parameters against the image's attributes and determines whether
	 * anything needs to be changed or if we simply need to serve up the source
	 * image
	 *
	 * @since 2.0
	 * @return bool
	 * @todo Add check for JPEGs and progressiveness
	 */
	private function isSourceImageDesired()
	{
		if ($this->isWidthDifferent()
			|| $this->isHeightDifferent()
			|| $this->isBackgroundFillOn()
			|| $this->isQualityOn()
			|| $this->isCroppingNeeded()
			)
			return FALSE;
		else
			return TRUE;

	} // isSourceImageDesired()

	/**
	 * @since 2.0
	 * @return bool
	 */
	private function isWidthDifferent()
	{
		if ($this->maxWidth !== NULL
			&& $this->maxWidth < $this->source['width']
			)
			return TRUE;
		else
			return FALSE;
	} // isWidthDifferent()

	/**
	 * @since 2.0
	 * @return bool
	 */
	private function isHeightDifferent()
	{
		if ($this->maxHeight !== NULL
			&& $this->maxHeight < $this->source['height']
			)
			return TRUE;
		else
			return FALSE;
	} // isHeightDifferent()

	/**
	 * @since 2.0
	 * @return bool
	 */
	private function isBackgroundFillOn()
	{
		if ($this->backgroundFillColor !== NULL
			&& ($this->isSourceGIF() || $this->isSourcePNG())
			)
			return TRUE;
		else
			return FALSE;
	} // isBackgroundFillOn()

	/**
	 * @since 2.0
	 * @return boolean
	 */
	private function isQualityOn()
	{
		if ($this->quality !== NULL)
			return TRUE;
		else
			return FALSE;
	} // isQualityOn()

	/**
	 * @since 2.0
	 * @return boolean
	 */
	private function isCroppingNeeded()
	{
		if ($this->cropRatio['width'] !== NULL
			&& $this->cropRatio['height'] !== NULL
			&& $this->cropRatio['ratio'] != $this->source['ratio']
			)
			return TRUE;
		else
			return FALSE;
	} // isCroppingNeeded()

	/**
	 * @since 2.0
	 * @parram array $imageArray
	 * @param string $type Can be 'JPEG', 'GIF', or 'PNG'
	 * @return boolean
	 */
	private function isImageOfType($imageArray, $type = 'JPEG')
	{
		$method	= "is$type";
		if (method_exists($this, $method) && isset($imageArray['mime']))
			return $this->$method($imageArray['mime']);
	} // isImageOfType()

	/**
	 * @since 2.0
	 * @param string $mimeType
	 * @return boolean
	 */
	private function isJPEG($mimeType)
	{
		if ($mimeType == 'image/jpeg')
			return TRUE;
		else
			return FALSE;
	} // isJPEG()

	/**
	 * @since 2.0
	 * @param string $mimeType
	 * @return boolean
	 */
	private function isGIF($mimeType)
	{
		if ($mimeType == 'image/gif')
			return TRUE;
		else
			return FALSE;
	} // isGIF()

	/**
	 * @since 2.0
	 * @param string $mimeType
	 * @return boolean
	 */
	private function isPNG($mimeType)
	{
		if (in_array($mimeType, array('image/png', 'image/x-png')))
			return TRUE;
		else
			return FALSE;
	} // isPNG()

	/**
	 * @since 2.0
	 * @return boolean
	 */
	private function isSourceJPEG()
	{
		return $this->isImageOfType($this->source, 'JPEG');
	} // isSourceJPEG()

	/**
	 * @since 2.0
	 * @return boolean
	 */
	private function isRenderedJPEG()
	{
		return $this->isImageOfType($this->rendered, 'JPEG');
	} // isRenderedJPEG()

	/**
	 * @since 2.0
	 * @return boolean
	 */
	private function isSourceGIF()
	{
		return $this->isImageOfType($this->source, 'GIF');
	} // isSourceGIF()

	/**
	 * @since 2.0
	 * @return boolean
	 */
	private function isRenderedGIF()
	{
		return $this->isImageOfType($this->rendered, 'GIF');
	} // isRenderedGIF()

	/**
	 * @since 2.0
	 * @return boolean
	 */
	private function isSourcePNG()
	{
		return $this->isImageOfType($this->source, 'PNG');
	} // isSourcePNG()

	/**
	 * @since 2.0
	 * @return boolean
	 */
	private function isRenderedPNG()
	{
		return $this->isImageOfType($this->rendered, 'PNG');
	} // isRenderedPNG()

	/**
	 * Computes and sets properties of the rendered image, such as the actual
	 * width, height, and quality
	 *
	 * @since 2.0
	 */
	private function getRenderProperties()
	{
		// Set default properties of the rendered image
		$this->rendered	= array(
			'width'		=> $this->source['width'],
			'height'	=> $this->source['height'],
			'quality'	=> 0,
			'mime'		=> $this->source['mime'],
			'functions'	=> array(
				'create'	=> 'ImageCreateFromJpeg',
				'output'	=> 'ImageJpeg'
			),
			'sharpen'		=> TRUE,
			'progressive'	=> $this->progressiveJPEGs,
			'background'	=> $this->backgroundFillColor
		);
		
		// Set the ratios needed for resizing. We will compare these below to
		// determine how to resize the image (based on height or based on width)
		$ratios		= array(
			'width'		=> $this->maxWidth / $this->source['width'],
			'height'	=> $this->maxHeight / $this->source['height']
		);
		
		// Values used to make comparisons below
		$compare	= array(
			'heightRatio'	=> $ratios['height'],
			'widthRatio'	=> $ratios['width']
		);
		
		// Cropping
		/*
		To determine the width and height of the rendered image, the following
		should occur.
		
		If cropping an image is required, we need to:
		 1. Compute the dimensions of the source image after cropping before
			resizing.
		 2. Compute the dimensions of the resized image before cropping. One of 
			these dimensions may be greater than maxWidth or maxHeight because
			they are based on the dimensions of the final rendered image, which
			will be cropped to fit within the specified maximum dimensions.
		 3. Compute the dimensions of the resized image after cropping. These
			must both be less than or equal to maxWidth and maxHeight.
		 4. Then when rendering, the image needs to be resized, crop offsets
			need to be computed based on the desired method (smart or centered),
			and the image needs to be cropped to the specified dimensions.
		
		If cropping an image is not required, we need to compute the dimensions
		of the image without cropping. These must both be less than or equal to
		maxWidth and maxHeight.
		*/
		if ($this->isCroppingNeeded())
		{
			// Determine the dimensions of the source image after cropping and
			// before resizing
			
			if ($this->cropRatio['ratio'] > $this->source['ratio'])
			{
				// Image is too tall so we will crop the top and bottom
				$this->source['cropHeight']	= $this->source['width'] / $this->cropRatio['ratio'];
				$this->source['cropWidth']	= $this->source['width'];
			}
			else
			{
				// Image is too wide so we will crop off the left and right sides
				$this->source['cropWidth']	= $this->source['height'] * $this->cropRatio['ratio'];
				$this->source['cropHeight']	= $this->source['height'];
			} // if
			
			$ratios['cropWidth']		= $this->maxWidth / $this->source['cropWidth'];
			$ratios['cropHeight']		= $this->maxHeight / $this->source['cropHeight'];
			
			$compare['widthRatio']		= $ratios['cropWidth'];
			$compare['heightRatio']		= $ratios['cropHeight'];
		} // if

		if (floor($compare['widthRatio'] * $this->source['height']) <= $this->maxHeight)
		{
			// Resize the image based on width
			$this->rendered['height']	= ceil($compare['widthRatio'] * $this->source['height']);
			$this->rendered['width']	= ceil($compare['widthRatio'] * $this->source['width']);
			
			// Determine dimensions after cropping
			if (isset($this->source['cropWidth']))
			{
				$this->rendered['cropHeight']	= ceil($compare['widthRatio'] * $this->source['cropHeight']);
				$this->rendered['cropWidth']	= ceil($compare['widthRatio'] * $this->source['cropWidth']);
			} // if
		}
		else if (floor($compare['heightRatio'] * $this->source['width']) <= $this->maxWidth)
		{
			// Resize the image based on height
			$this->rendered['width']	= ceil($compare['heightRatio'] * $this->source['width']);
			$this->rendered['height']	= ceil($compare['heightRatio'] * $this->source['height']);
			
			// Determine dimensions after cropping
			if (isset($this->source['cropWidth']))
			{
				$this->rendered['cropHeight']	= ceil($compare['heightRatio'] * $this->source['cropHeight']);
				$this->rendered['cropWidth']	= ceil($compare['heightRatio'] * $this->source['cropWidth']);
			} // if
		}
		else if (isset($this->source['cropWidth'])) // No resizing is needed but we still need to crop
		{
			$ratio	= ($ratios['width'] > $ratios['height']) ? $ratios['width'] : $ratios['height'];
			$this->rendered['width']		= ceil($ratio * $this->source['width']);
			$this->rendered['height']		= ceil($ratio * $this->source['height']);
			
			$this->rendered['cropWidth']	= ceil($ratio * $this->source['cropWidth']);
			$this->rendered['cropHeight']	= ceil($ratio * $this->source['cropHeight']);
		} // if

		// Determine the quality of the output image
		$this->rendered['quality']		= ($this->quality !== NULL) ? $this->quality : SLIR_DEFAULT_QUALITY;

		// Set up the appropriate image handling functions based on the original
		// image's mime type
		switch ($this->source['mime'])
		{
			case 'image/gif':
				// We will be converting GIFs to PNGs to avoid transparency
				// issues when resizing GIFs
				// This is maybe not the ideal solution, but IE6 can suck it
				$this->rendered['functions']['create']	= 'ImageCreateFromGif';
				$this->rendered['functions']['output']	= 'ImagePng';
				// We need to convert GIFs to PNGs
				$this->rendered['mime']					= 'image/png';
				$this->rendered['sharpen']				= FALSE;
				$this->rendered['progressive']			= FALSE;

				// We are converting the GIF to a PNG, and PNG needs a
				// compression level of 0 (no compression) through 9
				$this->rendered['quality']				= round(10 - ($this->rendered['quality'] / 10));
			break;

			case 'image/x-png':
			case 'image/png':
				$this->rendered['functions']['create']	= 'ImageCreateFromPng';
				$this->rendered['functions']['output']	= 'ImagePng';
				$this->rendered['mime']					= $this->source['mime'];
				$this->rendered['sharpen']				= FALSE;
				$this->rendered['progressive']			= FALSE;

				// PNG needs a compression level of 0 (no compression) through 9
				$this->rendered['quality']				= round(10 - ($this->rendered['quality'] / 10));
			break;

			default:
				$this->rendered['functions']['create']	= 'ImageCreateFromJpeg';
				$this->rendered['functions']['output']	= 'ImageJpeg';
				$this->rendered['mime']					= $this->source['mime'];
				$this->rendered['progressive']			= ($this->progressiveJPEGs !== NULL) ? $this->progressiveJPEGs : SLIR_DEFAULT_QUALITY;
				$this->rendered['background']			= NULL;
			break;
		} // switch

	} // getRenderProperties()
	
	/**
	 * Determines the optimal number of rows in from the top or left to crop
	 * the source image
	 * 
	 * @since 2.0
	 * @param boolean $fromLeft If TRUE, will calculate from the left edge. If
	 * FALSE, will calculate from the top edge
	 * @return integer|boolean
	 */
	private function offset($fromLeft = TRUE)
	{
		if ($fromLeft)
		{
			$length				= $this->rendered['cropWidth'];
			$lengthB			= $this->rendered['cropHeight'];
			$originalLength		= $this->rendered['width'];
		}
		else
		{
			$length				= $this->rendered['cropHeight'];
			$lengthB			= $this->rendered['cropWidth'];
			$originalLength		= $this->rendered['height'];
		} // if
		
		$threshold			= 0.3;
		$upperThreshold		= 1 + $threshold;
		$lowerThreshold		= 1 / (1 + $threshold);
		
		$rowsAtATime		= max(3, round(sqrt($length * $lengthB) / 50));
		$pixelRowsToCrop	= $originalLength - $length;
		
		if ($pixelRowsToCrop == 0)
			return FALSE;
		
		// $pixelStep will sacrifice accuracy for speed and memory
		$pixelStep			= max(1, round(sqrt($pixelRowsToCrop * $lengthB) / 200));

		$offset	= array(
			'near'	=> 0,
			'far'	=> 0
		);
		
		$colors	= array(
			'near'	=> array(),
			'far'	=> array()
		);
		
		$colorRatio		= 1;
		$previousWin	= NULL;
		
		// Fight the near and far rows. The stronger will remain standing.
		for($rowsCropped = 0; $rowsCropped < $pixelRowsToCrop; $rowsCropped += $rowsAtATime)
		{
			if (!isset($colors['near'][$offset['near']]))
			{
				$rows			= array();
				
				for($i = 0; $i <= $rowsAtATime; ++$i)
					$rows[]		= $this->rowInterestingness($offset['near'] + $i, $fromLeft, $pixelStep, $i);
				
				$colors['near'][$offset['near']]	= max(1, $this->rowsInterestingness($rows, $rowsAtATime, $lengthB));
			} // if
				
			if (!isset($colors['far'][$offset['far']]))
			{
				$rows		= array();
				
				for($i = 0; $i <= $rowsAtATime; ++$i)
					$rows[]	= $this->rowInterestingness($originalLength - 1 - $offset['far'] - $i, $fromLeft, $pixelStep, $i);
				
				$colors['far'][$offset['far']]	= max(1, $this->rowsInterestingness($rows, $rowsAtATime, $lengthB));
			} // if
			
			$colorRatio	= $colors['near'][$offset['near']] / $colors['far'][$offset['far']];
			
			// Compare near and far rows. Give the less interesting row the boot.
			if ($colorRatio > $upperThreshold)
			{
				$offset['far']	+=  $rowsAtATime;
				
				// Fightback. Winning side gets to go backwards through fallen rows
				// to see if they are stronger
				if ($previousWin == 'near')
					$offset['near']	-= ($offset['near'] > 0) ? $rowsAtATime : 0;
				else
					$previousWin	= 'near';
			}
			else if ($colorRatio < $lowerThreshold)
			{
				$offset['near']					+= $rowsAtATime;
				
				if ($previousWin == 'far')
					$offset['far']	-= ($offset['far'] > 0) ? $rowsAtATime : 0;
				else
					$previousWin	= 'far';
				
			}
			else
			{
				// There is no strong winner, so discard rows from the side that
				// has lost the fewest so far. Essentially this is a draw.
				
				if ($offset['near'] > $offset['far'])
					$offset['far']	+= $rowsAtATime;
				else // Discard near
					$offset['near']	+= $rowsAtATime;
					
				// No fightback for draws
				$previousWin	= NULL;
			} // if
		} // for
		
		// bounceback for potentially important details on the edge
		if ($colorRatio > (1 + ($threshold * 1.5)))
			$offset['near'] -= $rowsAtATime;
		else if ($colorRatio < (1 / (1 + ($threshold * 1.5))))
			$offset['near']	+= $rowsAtATime;
		
		return min($pixelRowsToCrop, max(0, $offset['near']));
	} // offset()
	
	/**
	 * @param array $rows
	 * @param integer $numberOfRows
	 * @param integer $numberOfPixels
	 * @return float
	 */
	private function rowsInterestingness($rows, $numberOfRows, $numberOfPixels)
	{
		$modifier				= sqrt($numberOfRows * $numberOfPixels);
		$rowsInterestingness	= 0;
		
		$previousRow			= array(
			'interestingness'		=> 0,
			'variance'				=> 0,
			'brightness'			=> 0,
			'contrast'				=> 0,
			'greatestVariance'		=> 0,
			'greatestBrightness'	=> 0,
			'greatestContrast'		=> 0
		);
		
		foreach($rows as $row)
		{
			$variance		= abs($row['variance'] - $previousRow['variance']);
			$brightness		= abs($row['brightness'] - $previousRow['brightness']);
			$contrast		= abs($row['contrast'] - $previousRow['contrast']);
			
			$rowsInterestingness	+=	(($variance + $brightness + $contrast) * $modifier)
				+ (($row['greatestVariance'] + $row['greatestBrightness']
					+ $row['greatestContrast']) * $modifier);
		} // foreach
		
		return $rowsInterestingness;
	} // rowsInterestingness()
	
	/**
	 * Calculates how interesting a row in an image is
	 * 
	 * Interestingness of a row is based on three qualities: color variance,
	 * overall brightness, and local contrast (variation in brightness)
	 * 
	 * @since 2.0
	 * @param integer $row
	 * @param boolean $fromLeft
	 * @param integer $pixelStep
	 * @param integer $offset
	 * @return array
	 */
	private function rowInterestingness($row, $fromLeft = TRUE, $pixelStep = 1, $offset = 0)
	{
		$varianceImportance		= 1;
		$brightnessImportance	= 1;
		$contrastImportance		= 1;
		
		$variance = $brightness = $contrast = $greatestVariance =
			$greatestBrightness = $greatestContrast = $previousContrast = 
			$totalVariance = $totalBrightness = $totalContrast =
			$interestingness = 0;
		
		$previousColor		= array(0,0,0);
		
		// Offset will stagger the sampled pixels from row to row in order to
		// produce a more representative sample.
		// Example:
		// . . . . . . . . .
		//  . . . . . . . . .
		// . . . . . . . . .
		$offset				= $offset % $pixelStep;
		
		if ($fromLeft)
		{
			for($totalPixels = $offset; $totalPixels < $this->rendered['height']; $totalPixels += $pixelStep)
			{
				$color				= $this->convertColorIndex(imagecolorat($this->rendered['image'], $row, $totalPixels));
				
				$variance			= $this->colorVariance($previousColor, $color) * $pixelStep;
				$brightness			= array_sum($color) * $pixelStep;
				$contrast			= abs($brightness - $previousContrast);
				
				$greatestVariance	= max($variance, $greatestVariance);
				$greatestBrightness	= max($brightness, $greatestBrightness);
				$greatestContrast	= max($contrast, $greatestContrast);
				
				$totalVariance		+= $variance;
				$totalBrightness	+= $brightness;
				$totalContrast		+= $contrast;
				
				$previousContrast	= $contrast;
				$previousColor		= $color;
			} // for
		}
		else
		{
			for($totalPixels = $offset; $totalPixels < $this->rendered['width']; $totalPixels += $pixelStep)
			{
				$color				= $this->convertColorIndex(imagecolorat($this->rendered['image'], $totalPixels, $row));
				
				$variance			= $this->colorVariance($previousColor, $color) * $pixelStep;
				$brightness			= array_sum($color) * $pixelStep;
				$contrast			= abs($brightness - $previousContrast);
				
				$greatestVariance	= max($variance, $greatestVariance);
				$greatestBrightness	= max($brightness, $greatestBrightness);
				$greatestContrast	= max($contrast, $greatestContrast);
				
				$totalVariance		+= $variance;
				$totalBrightness	+= $brightness;
				$totalContrast		+= $contrast;
				
				$previousContrast	= $contrast;
				$previousColor		= $color;
			} // for
		} // if
		
		// Weight attributes based on importance
		$totalVariance		= $totalVariance * $varianceImportance;
		$totalBrightness	= $totalBrightness * $brightnessImportance;
		$totalContrast		= $totalContrast * $contrastImportance;
		
		$greatestVariance	= $greatestVariance * $varianceImportance;
		$greatestBrightness	= $greatestBrightness * $brightnessImportance;
		$greatestContrast	= $greatestContrast * $contrastImportance;
		
		$interestingness	+= ($totalVariance + $totalBrightness + $totalContrast)
			+ (($greatestVariance + $greatestBrightness + $greatestContrast) * $totalPixels / 2);
		
		return array(
			'interestingness'		=> $interestingness,
			'variance'				=> $totalVariance,
			'brightness'			=> $totalBrightness,
			'contrast'				=> $totalContrast,
			'greatestVariance'		=> $greatestVariance,
			'greatestBrightness'	=> $greatestBrightness,
			'greatestContrast'		=> $greatestContrast
		);
	} // rowInterestingness()
	
	/**
	 * @since 2.0
	 * @param integer $int
	 * @return array
	 */
	private function convertColorIndex($int)
	{
		$a	= (255 - (($int >> 24) & 0xFF)) / 255;
		$r	= (($int >> 16) & 0xFF) * $a;
		$g	= (($int >> 8) & 0xFF) * $a;
		$b	= ($int & 0xFF) * $a;
		return array($r, $g, $b);
	} // convertColorIndex()
	
	/**
	 * @since 2.0
	 * @param array $colorArrayA
	 * @param array $colorArrayB
	 * @return float
	 */
	private function colorVariance($colorArrayA, $colorArrayB)
	{
		return abs($colorArrayA[0] - $colorArrayB[0])
			+ abs($colorArrayA[1] - $colorArrayB[1])
			+ abs($colorArrayA[2] - $colorArrayB[2]);
	} // colorVariance()
	
	/**
	 * @since 2.0
	 * @param array $rows
	 * @return integer
	 */
	private function getRowContrast($rows)
	{
		$contrast	= 0;
		$prev		= NULL;
		foreach($rows as $row)
		{
			if ($prev !== NULL)
				$contrast	+= abs($prev - $row);
			$prev = $row;
		} // foreach
		
		return $contrast;
	} // getRowContrast()
	
	/**
	 * @since 2.0
	 * @return boolean
	 */
	private function isRenderedCached()
	{<commit_msg>Improving smart cropping<commit_after><?php
/**
 * Class definition file for SLIR (Smart Lencioni Image Resizer)
 *
 * This file is part of SLIR (Smart Lencioni Image Resizer).
 *
 * SLIR is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * SLIR is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with SLIR.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @copyright Copyright  2009, <NAME>
 * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public
 * License version 3 (GPLv3)
 * @since 2.0
 * @package SLIR
 */
 
/* $Id$ */

/**
 * SLIR (Smart Lencioni Image Resizer)
 * Resizes images, intelligently sharpens, crops based on width:height ratios,
 * color fills transparent GIFs and PNGs, and caches variations for optimal
 * performance.
 *
 * I love to hear when my work is being used, so if you decide to use this,
 * feel encouraged to send me an email. I would appreciate it if you would
 * include a link on your site back to Shifting Pixel (either the SLIR page or
 * shiftingpixel.com), but dont worry about including a big link on each page
 * if you dont want toone will do just nicely. Feel free to contact me to
 * discuss any specifics (<EMAIL>).
 *
 * REQUIREMENTS:
 *     - PHP 5.1.0+
 *     - GD
 *
 * RECOMMENDED:
 *     - mod_rewrite
 *
 * USAGE:
 * To use, place an img tag with the src pointing to the path of SLIR (typically
 * "/slir/") followed by the parameters, followed by the path to the source
 * image to resize. All parameters follow the pattern of a one-letter code and
 * then the parameter value:
 *     - Maximum width = w
 *     - Maximum height = h
 *     - Crop ratio = c
 *     - Quality = q
 *     - Background fill color = b
 *     - Progressive = p
 *
 * EXAMPLES:
 *
 * Resizing a JPEG to a max width of 100 pixels and a max height of 100 pixels:
 * <code><img src="/slir/w100-h100/path/to/image.jpg" alt="Don't forget your alt
 * text" /></code>
 *
 * Resizing and cropping a JPEG into a square:
 * <code><img src="/slir/w100-h100-c1:1/path/to/image.jpg" alt="Don't forget
 * your alt text" /></code>
 *
 * Resizing a JPEG without interlacing (for use in Flash):
 * <code><img src="/slir/w100-p0/path/to/image.jpg" alt="Don't forget your alt
 * text" /></code>
 *
 * Matting a PNG with #990000:
 * <code><img src="/slir/b900/path/to/image.png" alt="Don't forget your alt
 * text" /></code>
 *
 * Without mod_rewrite (not recommended)
 * <code><img src="/slir/?w=100&amp;h=100&amp;c=1:1&amp;image=/path/to/image.jpg"
 * alt="Don't forget your alt text" /></code>
 *
 * @author <NAME> <<EMAIL>>
 * @date $Date$
 * @version $Revision$
 * @package SLIR
 *
 * @uses PEL
 * 
 * @todo lock files when writing?
 * @todo Prevent SLIR from calling itself
 * @todo Percentage resizing?
 * @todo Animated GIF resizing?
 * @todo Seam carving?
 * @todo Crop zoom?
 * @todo Crop offsets?
 * @todo Periodic cache clearing?
 * @todo Remote image fetching?
 * @todo Alternative support for ImageMagick?
 * @todo Prevent files in cache from being read directly?
 * @todo split directory initialization and variable checking into a separate
 * install/upgrade script with friendly error messages, an opportunity to give a
 * tip, and a button that tells me they are using it on their site if they like
 */

class SLIR
{
	/**
	 * @since 2.0
	 * @var string
	 */
	const VERSION	= '2.0b3';

	/**
	 * Path to source image
	 *
	 * @since 2.0
	 * @var string
	 */
	private $imagePath;

	/**
	 * Maximum width for resized image, in pixels
	 *
	 * @since 2.0
	 * @var integer
	 */
	private $maxWidth;

	/**
	 * Maximum height for resized image, in pixels
	 *
	 * @since 2.0
	 * @var integer
	 */
	private $maxHeight;

	/**
	 * Quality setting for resized image. Ranges from 0 (worst quality, smaller
	 * file) to 100 (best quality, biggest file). If not specified, will default
	 * to SLIR_DEFAULT_QUALITY setting in slir-config.php
	 *
	 * @since 2.0
	 * @var integer
	 */
	private $quality;

	/**
	 * Ratio of width:height to crop image to.
	 *
	 * For example, if a square shape is desired, the crop ratio should be "1:1"
	 * or if a long rectangle is desired, the crop ratio could be "4:1". Stored
	 * as an associative array with keys being 'width' and 'height'.
	 *
	 * @since 2.0
	 * @var array
	 */
	private $cropRatio;

	/**
	 * Whether JPEG should be a progressive JPEG (interlaced) or not. If not
	 * specified, will default to SLIR_DEFAULT_PROGRESSIVE_JPEG setting in
	 * slir-config.php
	 *
	 * @since 2.0
	 * @var bool
	 */
	private $progressiveJPEGs;

	/**
	 * A color, in hexadecimal format (RRGGBB), to fill in as the background
	 * color for PNG images
	 *
	 * Longhand values (e.g. "FF0000") and shorthand values (e.g. "F00") are
	 * both acceptable
	 *
	 * @since 2.0
	 * @var string
	 */
	private $backgroundFillColor;

	/**
	 * Information about the source image
	 *
	 * Generated in part by {@link http://us2.php.net/getimagesize getimagesize()}
	 *
	 * @since 2.0
	 * @var array
	 */
	private $source;

	/**
	 * Information about the rendered image
	 *
	 * @since 2.0
	 * @var string
	 */
	private $rendered;

	/**
	 * Whether or not the cache has already been initialized
	 *
	 * @since 2.0
	 * @var boolean
	 */
	private $isCacheInitialized	= FALSE;

	/**
	 * The magic starts here
	 *
	 * @since 2.0
	 */
	public function __construct()
	{
		$this->getConfig();

		// Check the cache based on the request URI
		if (SLIR_USE_REQUEST_CACHE && $this->isRequestCached())
			$this->serveRequestCachedImage();
		
		// Set up our error handler after the request cache to help keep
		// everything humming along nicely
		require 'slirexception.class.php';
		set_error_handler(array('SLIRException', 'error'));
		
		// Set all parameters for resizing
		$this->setParameters($this->getParameters());

		// See if there is anything we actually need to do
		if ($this->isSourceImageDesired())
			$this->serveSourceImage();

		// Determine rendered dimensions
		$this->getRenderProperties();

		// Check the cache based on the properties of the rendered image
		if (!$this->isRenderedCached() || !$this->serveRenderedCachedImage())
		{
			// Image is not cached in any way, so we need to render the image,
			// cache it, and serve it up to the client
			$this->render();
			$this->serveRenderedImage();
		} // if

	} // __construct()

	/**
	 * Helps control the parameters as they are set
	 *
	 * @since 2.0
	 * @param string $name
	 * @param mixed $value
	 * @todo Prevent SLIR from calling itself
	 */
	private function __set($name, $value)
	{
		switch($name)
		{
			case 'i':
			case 'image':
			case 'imagePath':
				// Images must be local files, so for convenience we strip the
				// domain if it's there
				$this->imagePath	= '/' . trim(preg_replace('/^(s?f|ht)tps?:\/\/[^\/]+/i', '', (string) urldecode($value)), '/');
				
				// Strip query string from the image path
				$this->imagePath	= preg_replace('/\?.*/', '', $this->imagePath);

				// Make sure the image path is secure
				if (!$this->isPathSecure($this->imagePath))
				{
					header('HTTP/1.1 400 Bad Request');
					throw new SLIRException('Image path may not contain ":", ".'
						. '.", "<", or ">"');
				}
				// Make sure the image file exists
				else if (!$this->imageExists())
				{
					header('HTTP/1.1 404 Not Found');
					throw new SLIRException('Image does not exist: ' . SLIR_DOCUMENT_ROOT . $this->imagePath);
				}
				// Everything seems to check out just fine, proceeding normally
				else
				{
					// Set the image info (width, height, mime type, etc.)
					$this->source	= $this->getImageInfo();

					// Make sure the file is actually an image
					if (!$this->isImage())
					{
						header('HTTP/1.1 400 Bad Request');
						throw new SLIRException('Requested file is not an '
							. 'accepted image type: ' . SLIR_DOCUMENT_ROOT
							. $this->imagePath);
					} // if
				} // if
			break;

			case 'w':
			case 'width':
			case 'maxWidth':
				$this->maxWidth		= (int) $value;
			break;

			case 'h':
			case 'height':
			case 'maxHeight':
				$this->maxHeight	= (int) $value;
			break;

			case 'q':
			case 'quality':
				$this->quality		= (int) $value;

				if ($this->quality < 0 || $this->quality > 100)
				{
					header('HTTP/1.1 400 Bad Request');
					throw new SLIRException('Quality must be between 0 and 100: '
						. $this->quality);
				}
			break;

			case 'p':
			case 'progressive':
				$this->progressiveJPEGs	= (bool) $value;
			break;

			case 'c':
			case 'cropRatio':
				$ratio				= explode(':', (string) $value);
				if (count($ratio) >= 2)
				{
					$this->cropRatio	= array(
						'width'		=> (float) $ratio[0],
						'height'	=> (float) $ratio[1],
						'ratio'		=> (float) $ratio[0] / (float) $ratio[1]
					);
				}
				else
				{
					header('HTTP/1.1 400 Bad Request');
					throw new SLIRException('Crop ratio must be in width:height'
						. ' format: ' . (string) $value);
				} // if
			break;

			case 'b';
			case 'backgroundFillColor':
				$this->backgroundFillColor	= preg_replace('/[^0-9a-fA-F]/', '',
					(string) $value);

				$bglen	= strlen($this->backgroundFillColor);
				if($bglen == 3)
				{
					$this->backgroundFillColor = $this->backgroundFillColor[0]
						.$this->backgroundFillColor[0]
						.$this->backgroundFillColor[1]
						.$this->backgroundFillColor[1]
						.$this->backgroundFillColor[2]
						.$this->backgroundFillColor[2];
				}
				else if ($bglen != 6)
				{
					header('HTTP/1.1 400 Bad Request');
					throw new SLIRException('Background fill color must be in '
						.'hexadecimal format, longhand or shorthand: '
						. $this->backgroundFillColor);
				} // if
			break;
		} // switch
	} // __set()

	/**
	 * Includes the configuration file
	 *
	 * @since 2.0
	 */
	private function getConfig()
	{
		if (file_exists('slir-config.php'))
		{
			require 'slir-config.php';
		}
		else if (file_exists('slir-config-sample.php'))
		{
			if (copy('slir-config-sample.php', 'slir-config.php'))
				require 'slir-config.php';
			else
				throw new SLIRException('Could not load configuration file. '
					. 'Please copy "slir-config-sample.php" to '
					. '"slir-config.php".');
		}
		else
		{
			throw new SLIRException('Could not find "slir-config.php" or '
				. '"slir-config-sample.php"');
		} // if
	} // getConfig()

	/**
	 * Renders specified changes to the image
	 *
	 * @since 2.0
	 */
	private function render()
	{
		// We don't want to run out of memory
		ini_set('memory_limit', SLIR_MEMORY_TO_ALLOCATE);

		// Set up a blank canvas for our rendered image (destination)
		$this->rendered['image']	= imagecreatetruecolor(
										$this->rendered['width'],
										$this->rendered['height']
									);

		// Read in the original image
		$this->source['image']		= $this->rendered['functions']['create'](SLIR_DOCUMENT_ROOT . $this->imagePath);
		
		// GIF/PNG transparency and background color
		$this->background();

		// Resample the original image into the resized canvas we set up earlier
		if ($this->source['width'] != $this->rendered['width'] || $this->source['height'] != $this->rendered['height'])
		{
			ImageCopyResampled(
				$this->rendered['image'],
				$this->source['image'],
				0,
				0,
				0,
				0,
				$this->rendered['width'],
				$this->rendered['height'],
				$this->source['width'],
				$this->source['height']
			);
		}
		else // No resizing is needed, so make a clean copy
		{
			ImageCopy(
				$this->rendered['image'],
				$this->source['image'],
				0,
				0,
				0,
				0,
				$this->source['width'],
				$this->source['height']
			);
		} // if

		
		// Cropping
		if ($this->isCroppingNeeded())
			$this->crop();
		
		// Sharpen
		if ($this->rendered['sharpen'])
			$this->sharpen();

		// Set interlacing
		if ($this->rendered['progressive'])
			imageinterlace($this->rendered['image'], 1);

	} // render()
	
	/**
	 * Crops the rendered image
	 * 
	 * @since 2.0
	 * @return boolean
	 * @todo add cropping method preference (smart or centered)
	 */
	private function crop()
	{
		// Determine crop offset
		$offset		= array(
			'top'	=> 0,
			'left'	=> 0
		);
		
		if ($this->cropRatio['ratio'] > $this->source['ratio'])
		{
			// Image is too tall so we will crop the top and bottom
			$o					= $this->offset(FALSE);
			if ($o === FALSE)
				return TRUE;
			else
				$offset['top']		= $o;
		}
		else
		{
			// Image is too wide so we will crop the left and right
			$o					= $this->offset(TRUE);
			if ($o === FALSE)
				return TRUE;
			else
				$offset['left']		= $o;
		} // if
		
		// Set up a blank canvas for our cropped image (destination)
		$cropped	= imagecreatetruecolor(
						$this->rendered['cropWidth'],
						$this->rendered['cropHeight']
						);
						
		// Copy rendered image to cropped image
		ImageCopy(
			$cropped,
			$this->rendered['image'],
			0,
			0,
			$offset['left'],
			$offset['top'],
			$this->rendered['width'],
			$this->rendered['height']
		);
		
		// Replace pre-cropped image with cropped image
		imagedestroy($this->rendered['image']);
		$this->rendered['image']	= $cropped;
		unset($cropped);
		
		return TRUE;
	} // crop()

	/**
	 * Turns on transparency for rendered image if no background fill color is
	 * specified, otherwise, fills background with specified color
	 *
	 * @since 2.0
	 */
	private function background()
	{
		if (!$this->isBackgroundFillOn())
		{
			// If this is a GIF or a PNG, we need to set up transparency
			imagealphablending($this->rendered['image'], FALSE);
			imagesavealpha($this->rendered['image'], TRUE);
		}
		else
		{
			// Fill the background with the specified color for matting purposes
			$background	= imagecolorallocate(
				$this->rendered['image'],
				hexdec($this->backgroundFillColor[0].$this->backgroundFillColor[1]),
				hexdec($this->backgroundFillColor[2].$this->backgroundFillColor[3]),
				hexdec($this->backgroundFillColor[4].$this->backgroundFillColor[5])
			);

			imagefill($this->rendered['image'], 0, 0, $background);
		} // if
	} // background()

	/**
	 * Sharpens the image based on two things:
	 *   (1) the difference between the original size and the final size
	 *   (2) the final size
	 *
	 * @since 2.0
	 */
	private function sharpen()
	{
		$sharpness	= $this->calculateSharpnessFactor(
			$this->source['width'] * $this->source['height'],
			$this->rendered['width'] * $this->rendered['height']
		);

		$sharpenMatrix	= array(
			array(-1, -2, -1),
			array(-2, $sharpness + 12, -2),
			array(-1, -2, -1)
		);

		$divisor	= $sharpness;
		$offset		= 0;

		imageconvolution(
			$this->rendered['image'],
			$sharpenMatrix,
			$divisor,
			$offset
		);
	} // sharpen()

	/**
	 * Calculates sharpness factor to be used to sharpen an image based on the
	 * area of the source image and the area of the destination image
	 *
	 * @since 2.0
	 * @author <NAME>
	 * @link http://adryrun.com
	 *
	 * @param integer $sourceArea Area of source image
	 * @param integer $destinationArea Area of destination image
	 * @return integer Sharpness factor
	 */
	private function calculateSharpnessFactor($sourceArea, $destinationArea)
	{
		$final	= sqrt($destinationArea) * (750.0 / sqrt($sourceArea));
		$a		= 52;
		$b		= -0.27810650887573124;
		$c		= .00047337278106508946;

		$result = $a + $b * $final + $c * $final * $final;

		return max(round($result), 0);
	} // calculateSharpnessFactor()

	/**
	 * @since 2.0
	 * @param string $cacheFilePath
	 * @return string Contents of the image
	 */
	private function copyIPTC($cacheFilePath)
	{
		$data	= '';

		$iptc	= $this->source['iptc'];

		// Originating program
		$iptc['2#065']	= array('Smart Lencioni Image Resizer');

		// Program version
		$iptc['2#070']	= array(SLIR::VERSION);

		foreach($iptc as $tag => $iptcData)
		{
			$tag	= substr($tag, 2);
			$data	.= $this->makeIPTCTag(2, $tag, $iptcData[0]);
		}

		// Embed the IPTC data
		return iptcembed($data, $cacheFilePath);
	} // copyIPTC()

	/**
	 * @since 2.0
	 * @author <NAME>
	 */
	function makeIPTCTag($rec, $data, $value)
	{
		$length = strlen($value);
		$retval = chr(0x1C) . chr($rec) . chr($data);

		if($length < 0x8000)
		{
			$retval .= chr($length >> 8) .  chr($length & 0xFF);
		}
		else
		{
			$retval .= chr(0x80) .
					   chr(0x04) .
					   chr(($length >> 24) & 0xFF) .
					   chr(($length >> 16) & 0xFF) .
					   chr(($length >> 8) & 0xFF) .
					   chr($length & 0xFF);
		}

		return $retval . $value;
	} // makeIPTCTag()

	/**
	 * Determines the parameters to use for resizing
	 *
	 * @since 2.0
	 */
	private function getParameters()
	{
		if (!$this->isUsingQueryString()) // Using the mod_rewrite version
			return $this->getParametersFromPath();
		else // Using the query string version
			return $_GET;
	} // getParameters()

	/**
	 * For requests that are using the mod_rewrite syntax
	 *
	 * @since 2.0
	 */
	private function getParametersFromPath()
	{
		$params	= array();

		// The parameters should be the first set of characters after the
		// SLIR path
		$request	= str_replace(SLIR_DIR, '', (string) $_SERVER['REQUEST_URI']);
		$request	= explode('/', trim($request, '/'));

		if (count($request) < 2)
		{
			header('HTTP/1.1 400 Bad Request');
			throw new SLIRException('Not enough parameters were given.', 'Available parameters:
w = Maximum width
h = Maximum height
c = Crop ratio (width:height)
q = Quality (0-100)
b = Background fill color (RRGGBB or RGB)
p = Progressive (0 or 1)

Example usage:
<img src="' . SLIR_DIR . '/w300-h300-c1:1/path/to/image.jpg" alt="Don\'t forget '
.'your alt text!" />'
			);

		} // if

		// The parameters are separated by hyphens
		$rawParams	= array_filter(explode('-', array_shift($request)));

		// The image path should be all of the remaining values in the array
		$params['i']	= implode('/', $request);

		foreach ($rawParams as $rawParam)
		{
			// The name of each parameter should be the first character of the
			// parameter string
			$name	= $rawParam[0];
			// The value of each parameter should be the remaining characters of
			// the parameter string
			$value	= substr($rawParam, 1, strlen($rawParam) - 1);

			$params[$name]	= $value;
		} // foreach

		$params	= array_filter($params);

		return $params;
	} // getParametersFromPath()

	/**
	 * Sets up parameters for image resizing
	 *
	 * @since 2.0
	 * @param array $params Associative array of parameters
	 */
	private function setParameters($params)
	{
		// Set image path first
		if (isset($params['i']) && $params['i'] != '' && $params['i'] != '/')
		{
			$this->__set('i', $params['i']);
			unset($params['i']);
		}
		else
		{
			header('HTTP/1.1 400 Bad Request');
			throw new SLIRException('Source image was not specified.');
		} // if

		// Set the rest of the parameters
		foreach($params as $name => $value)
		{
			$this->__set($name, $value);
		} // foreach

		// If either a max width or max height are not specified or larger than
		// the source image we default to the dimension of the source image so
		// they do not become constraints on our resized image.
		if (!$this->maxWidth || $this->maxWidth > $this->source['width'])
			$this->maxWidth		= $this->source['width'];

		if (!$this->maxHeight ||  $this->maxHeight > $this->source['height'])
			$this->maxHeight	= $this->source['height'];

	} // setParameters()

	/**
	 * Determines if the request is using the mod_rewrite version or the query
	 * string version
	 *
	 * @since 2.0
	 * @return bool
	 */
	private function isUsingQueryString()
	{
		if (isset($_SERVER['QUERY_STRING'])
			&& trim($_SERVER['QUERY_STRING']) != ''
			&& count(array_intersect(array('i', 'w', 'h', 'q', 'c', 'b'), array_keys($_GET)))
			)
			return TRUE;
		else
			return FALSE;
	} // isUsingQueryString()

	/**
	 * Checks to see if the image path is secure
	 *
	 * For security, directories may not contain ':' and images may not contain
	 * '..', '<', or '>'.
	 *
	 * @since 2.0
	 * @param string $path
	 * @return bool
	 */
	private function isPathSecure($path)
	{
		if (strpos(dirname($path), ':') || preg_match('/(\.\.|<|>)/', $path))
			return FALSE;
		else
			return TRUE;
	} // isImagePathSecure()

	/**
	 * Determines if the source image exists
	 *
	 * @since 2.0
	 * @return bool
	 */
	private function imageExists()
	{
		return is_file(SLIR_DOCUMENT_ROOT . $this->imagePath);
	} // imageExists()

	/**
	 * Retrieves information about the source image such as width and height
	 *
	 * @since 2.0
	 * @return array
	 */
	private function getImageInfo()
	{
		$info			= getimagesize(SLIR_DOCUMENT_ROOT . $this->imagePath, $extraInfo);

		if ($info == FALSE)
		{
			header('HTTP/1.1 400 Bad Request');
			throw new SLIRException('getimagesize failed (source file may not '
				. 'be an image): ' . SLIR_DOCUMENT_ROOT . $this->imagePath);
		}

		$info['width']	=& $info[0];
		$info['height']	=& $info[1];
		$info['ratio']	= $info['width']/$info['height'];

		// IPTC
		if(is_array($extraInfo) && isset($extraInfo['APP13']))
				$info['iptc']	= iptcparse($extraInfo['APP13']);

		return $info;
	} // getImageInfo()

	/**
	 * Checks the image info and image's mime type to see if it is an image
	 *
	 * @since 2.0
	 * @return bool
	 */
	private function isImage()
	{
		if ($this->source !== FALSE
			|| substr($this->source['mime'], 0, 6) == 'image/')
			return TRUE;
		else
			return FALSE;
	} // isImage()

	/**
	 * Checks parameters against the image's attributes and determines whether
	 * anything needs to be changed or if we simply need to serve up the source
	 * image
	 *
	 * @since 2.0
	 * @return bool
	 * @todo Add check for JPEGs and progressiveness
	 */
	private function isSourceImageDesired()
	{
		if ($this->isWidthDifferent()
			|| $this->isHeightDifferent()
			|| $this->isBackgroundFillOn()
			|| $this->isQualityOn()
			|| $this->isCroppingNeeded()
			)
			return FALSE;
		else
			return TRUE;

	} // isSourceImageDesired()

	/**
	 * @since 2.0
	 * @return bool
	 */
	private function isWidthDifferent()
	{
		if ($this->maxWidth !== NULL
			&& $this->maxWidth < $this->source['width']
			)
			return TRUE;
		else
			return FALSE;
	} // isWidthDifferent()

	/**
	 * @since 2.0
	 * @return bool
	 */
	private function isHeightDifferent()
	{
		if ($this->maxHeight !== NULL
			&& $this->maxHeight < $this->source['height']
			)
			return TRUE;
		else
			return FALSE;
	} // isHeightDifferent()

	/**
	 * @since 2.0
	 * @return bool
	 */
	private function isBackgroundFillOn()
	{
		if ($this->backgroundFillColor !== NULL
			&& ($this->isSourceGIF() || $this->isSourcePNG())
			)
			return TRUE;
		else
			return FALSE;
	} // isBackgroundFillOn()

	/**
	 * @since 2.0
	 * @return boolean
	 */
	private function isQualityOn()
	{
		if ($this->quality !== NULL)
			return TRUE;
		else
			return FALSE;
	} // isQualityOn()

	/**
	 * @since 2.0
	 * @return boolean
	 */
	private function isCroppingNeeded()
	{
		if ($this->cropRatio['width'] !== NULL
			&& $this->cropRatio['height'] !== NULL
			&& $this->cropRatio['ratio'] != $this->source['ratio']
			)
			return TRUE;
		else
			return FALSE;
	} // isCroppingNeeded()

	/**
	 * @since 2.0
	 * @parram array $imageArray
	 * @param string $type Can be 'JPEG', 'GIF', or 'PNG'
	 * @return boolean
	 */
	private function isImageOfType($imageArray, $type = 'JPEG')
	{
		$method	= "is$type";
		if (method_exists($this, $method) && isset($imageArray['mime']))
			return $this->$method($imageArray['mime']);
	} // isImageOfType()

	/**
	 * @since 2.0
	 * @param string $mimeType
	 * @return boolean
	 */
	private function isJPEG($mimeType)
	{
		if ($mimeType == 'image/jpeg')
			return TRUE;
		else
			return FALSE;
	} // isJPEG()

	/**
	 * @since 2.0
	 * @param string $mimeType
	 * @return boolean
	 */
	private function isGIF($mimeType)
	{
		if ($mimeType == 'image/gif')
			return TRUE;
		else
			return FALSE;
	} // isGIF()

	/**
	 * @since 2.0
	 * @param string $mimeType
	 * @return boolean
	 */
	private function isPNG($mimeType)
	{
		if (in_array($mimeType, array('image/png', 'image/x-png')))
			return TRUE;
		else
			return FALSE;
	} // isPNG()

	/**
	 * @since 2.0
	 * @return boolean
	 */
	private function isSourceJPEG()
	{
		return $this->isImageOfType($this->source, 'JPEG');
	} // isSourceJPEG()

	/**
	 * @since 2.0
	 * @return boolean
	 */
	private function isRenderedJPEG()
	{
		return $this->isImageOfType($this->rendered, 'JPEG');
	} // isRenderedJPEG()

	/**
	 * @since 2.0
	 * @return boolean
	 */
	private function isSourceGIF()
	{
		return $this->isImageOfType($this->source, 'GIF');
	} // isSourceGIF()

	/**
	 * @since 2.0
	 * @return boolean
	 */
	private function isRenderedGIF()
	{
		return $this->isImageOfType($this->rendered, 'GIF');
	} // isRenderedGIF()

	/**
	 * @since 2.0
	 * @return boolean
	 */
	private function isSourcePNG()
	{
		return $this->isImageOfType($this->source, 'PNG');
	} // isSourcePNG()

	/**
	 * @since 2.0
	 * @return boolean
	 */
	private function isRenderedPNG()
	{
		return $this->isImageOfType($this->rendered, 'PNG');
	} // isRenderedPNG()

	/**
	 * Computes and sets properties of the rendered image, such as the actual
	 * width, height, and quality
	 *
	 * @since 2.0
	 */
	private function getRenderProperties()
	{
		// Set default properties of the rendered image
		$this->rendered	= array(
			'width'		=> $this->source['width'],
			'height'	=> $this->source['height'],
			'quality'	=> 0,
			'mime'		=> $this->source['mime'],
			'functions'	=> array(
				'create'	=> 'ImageCreateFromJpeg',
				'output'	=> 'ImageJpeg'
			),
			'sharpen'		=> TRUE,
			'progressive'	=> $this->progressiveJPEGs,
			'background'	=> $this->backgroundFillColor
		);
		
		// Set the ratios needed for resizing. We will compare these below to
		// determine how to resize the image (based on height or based on width)
		$ratios		= array(
			'width'		=> $this->maxWidth / $this->source['width'],
			'height'	=> $this->maxHeight / $this->source['height']
		);
		
		// Values used to make comparisons below
		$compare	= array(
			'heightRatio'	=> $ratios['height'],
			'widthRatio'	=> $ratios['width']
		);
		
		// Cropping
		/*
		To determine the width and height of the rendered image, the following
		should occur.
		
		If cropping an image is required, we need to:
		 1. Compute the dimensions of the source image after cropping before
			resizing.
		 2. Compute the dimensions of the resized image before cropping. One of 
			these dimensions may be greater than maxWidth or maxHeight because
			they are based on the dimensions of the final rendered image, which
			will be cropped to fit within the specified maximum dimensions.
		 3. Compute the dimensions of the resized image after cropping. These
			must both be less than or equal to maxWidth and maxHeight.
		 4. Then when rendering, the image needs to be resized, crop offsets
			need to be computed based on the desired method (smart or centered),
			and the image needs to be cropped to the specified dimensions.
		
		If cropping an image is not required, we need to compute the dimensions
		of the image without cropping. These must both be less than or equal to
		maxWidth and maxHeight.
		*/
		if ($this->isCroppingNeeded())
		{
			// Determine the dimensions of the source image after cropping and
			// before resizing
			
			if ($this->cropRatio['ratio'] > $this->source['ratio'])
			{
				// Image is too tall so we will crop the top and bottom
				$this->source['cropHeight']	= $this->source['width'] / $this->cropRatio['ratio'];
				$this->source['cropWidth']	= $this->source['width'];
			}
			else
			{
				// Image is too wide so we will crop off the left and right sides
				$this->source['cropWidth']	= $this->source['height'] * $this->cropRatio['ratio'];
				$this->source['cropHeight']	= $this->source['height'];
			} // if
			
			$ratios['cropWidth']		= $this->maxWidth / $this->source['cropWidth'];
			$ratios['cropHeight']		= $this->maxHeight / $this->source['cropHeight'];
			
			$compare['widthRatio']		= $ratios['cropWidth'];
			$compare['heightRatio']		= $ratios['cropHeight'];
		} // if

		if (floor($compare['widthRatio'] * $this->source['height']) <= $this->maxHeight)
		{
			// Resize the image based on width
			$this->rendered['height']	= ceil($compare['widthRatio'] * $this->source['height']);
			$this->rendered['width']	= ceil($compare['widthRatio'] * $this->source['width']);
			
			// Determine dimensions after cropping
			if (isset($this->source['cropWidth']))
			{
				$this->rendered['cropHeight']	= ceil($compare['widthRatio'] * $this->source['cropHeight']);
				$this->rendered['cropWidth']	= ceil($compare['widthRatio'] * $this->source['cropWidth']);
			} // if
		}
		else if (floor($compare['heightRatio'] * $this->source['width']) <= $this->maxWidth)
		{
			// Resize the image based on height
			$this->rendered['width']	= ceil($compare['heightRatio'] * $this->source['width']);
			$this->rendered['height']	= ceil($compare['heightRatio'] * $this->source['height']);
			
			// Determine dimensions after cropping
			if (isset($this->source['cropWidth']))
			{
				$this->rendered['cropHeight']	= ceil($compare['heightRatio'] * $this->source['cropHeight']);
				$this->rendered['cropWidth']	= ceil($compare['heightRatio'] * $this->source['cropWidth']);
			} // if
		}
		else if (isset($this->source['cropWidth'])) // No resizing is needed but we still need to crop
		{
			$ratio	= ($ratios['width'] > $ratios['height']) ? $ratios['width'] : $ratios['height'];
			$this->rendered['width']		= ceil($ratio * $this->source['width']);
			$this->rendered['height']		= ceil($ratio * $this->source['height']);
			
			$this->rendered['cropWidth']	= ceil($ratio * $this->source['cropWidth']);
			$this->rendered['cropHeight']	= ceil($ratio * $this->source['cropHeight']);
		} // if

		// Determine the quality of the output image
		$this->rendered['quality']		= ($this->quality !== NULL) ? $this->quality : SLIR_DEFAULT_QUALITY;

		// Set up the appropriate image handling functions based on the original
		// image's mime type
		switch ($this->source['mime'])
		{
			case 'image/gif':
				// We will be converting GIFs to PNGs to avoid transparency
				// issues when resizing GIFs
				// This is maybe not the ideal solution, but IE6 can suck it
				$this->rendered['functions']['create']	= 'ImageCreateFromGif';
				$this->rendered['functions']['output']	= 'ImagePng';
				// We need to convert GIFs to PNGs
				$this->rendered['mime']					= 'image/png';
				$this->rendered['sharpen']				= FALSE;
				$this->rendered['progressive']			= FALSE;

				// We are converting the GIF to a PNG, and PNG needs a
				// compression level of 0 (no compression) through 9
				$this->rendered['quality']				= round(10 - ($this->rendered['quality'] / 10));
			break;

			case 'image/x-png':
			case 'image/png':
				$this->rendered['functions']['create']	= 'ImageCreateFromPng';
				$this->rendered['functions']['output']	= 'ImagePng';
				$this->rendered['mime']					= $this->source['mime'];
				$this->rendered['sharpen']				= FALSE;
				$this->rendered['progressive']			= FALSE;

				// PNG needs a compression level of 0 (no compression) through 9
				$this->rendered['quality']				= round(10 - ($this->rendered['quality'] / 10));
			break;

			default:
				$this->rendered['functions']['create']	= 'ImageCreateFromJpeg';
				$this->rendered['functions']['output']	= 'ImageJpeg';
				$this->rendered['mime']					= $this->source['mime'];
				$this->rendered['progressive']			= ($this->progressiveJPEGs !== NULL) ? $this->progressiveJPEGs : SLIR_DEFAULT_QUALITY;
				$this->rendered['background']			= NULL;
			break;
		} // switch

	} // getRenderProperties()
	
	/**
	 * Determines the optimal number of rows in from the top or left to crop
	 * the source image
	 * 
	 * @since 2.0
	 * @param boolean $fromLeft If TRUE, will calculate from the left edge. If
	 * FALSE, will calculate from the top edge
	 * @return integer|boolean
	 */
	private function offset($fromLeft = TRUE)
	{
		if ($fromLeft)
		{
			$length				= $this->rendered['cropWidth'];
			$lengthB			= $this->rendered['cropHeight'];
			$originalLength		= $this->rendered['width'];
		}
		else
		{
			$length				= $this->rendered['cropHeight'];
			$lengthB			= $this->rendered['cropWidth'];
			$originalLength		= $this->rendered['height'];
		} // if
		
		$threshold			= 0.15;
		$upperThreshold		= 1 + $threshold;
		$lowerThreshold		= 1 / (1 + $threshold);
		
		$rowsAtATime		= max(1, round(sqrt($length * $lengthB) / 50));
		$pixelRowsToCrop	= $originalLength - $length;
		
		if ($pixelRowsToCrop == 0)
			return FALSE;
		
		// $pixelStep will sacrifice accuracy for speed and memory
		$pixelStep			= max(1, round(sqrt($pixelRowsToCrop * $lengthB) / 200));

		$offset	= array(
			'near'	=> 0,
			'far'	=> 0
		);
		
		$colors	= array(
			'near'	=> array(),
			'far'	=> array()
		);
		
		$colorRatio		= 1;
		$returningChampion	= NULL;
		
		// Fight the near and far rows. The stronger will remain standing.
		for($rowsCropped = 0; $rowsCropped < $pixelRowsToCrop; $rowsCropped += $rowsAtATime)
		{
			if (!isset($colors['near'][$offset['near']]))
			{
				$rows			= array();
				
				for($i = 0; $i <= $rowsAtATime; ++$i)
					$rows[]		= $this->rowInterestingness($offset['near'] + $i, $fromLeft, $pixelStep, $i);
				
				$colors['near'][$offset['near']]	= max(1, $this->rowsInterestingness($rows));
			} // if
				
			if (!isset($colors['far'][$offset['far']]))
			{
				$rows		= array();
				
				for($i = 0; $i <= $rowsAtATime; ++$i)
					$rows[]	= $this->rowInterestingness($originalLength - 1 - $offset['far'] - $i, $fromLeft, $pixelStep, $i);
				
				$colors['far'][$offset['far']]	= max(1, $this->rowsInterestingness($rows));
			} // if
				
			$colorRatio	= $colors['near'][$offset['near']] / $colors['far'][$offset['far']];
			
			// Compare near and far rows. Give the less interesting row the boot.
			if ($colorRatio > $upperThreshold)
			{
				$offset['far']	+=  $rowsAtATime;
				
				// Fightback. Winning side gets to go backwards through fallen rows
				// to see if they are stronger
				if ($returningChampion == 'near')
					$offset['near']	-= ($offset['near'] > 0) ? $rowsAtATime : 0;
				else
					$returningChampion	= 'near';
			}
			else if ($colorRatio < $lowerThreshold)
			{
				$offset['near']					+= $rowsAtATime;
				
				if ($returningChampion == 'far')
					$offset['far']	-= ($offset['far'] > 0) ? $rowsAtATime : 0;
				else
					$returningChampion	= 'far';
			}
			else
			{
				// There is no strong winner, so discard rows from the side that
				// has lost the fewest so far. Essentially this is a draw.
				if ($offset['near'] > $offset['far'])
					$offset['far']	+= $rowsAtATime;
				else // Discard near
					$offset['near']	+= $rowsAtATime;
					
				// No fightback for draws
				$previousWin	= NULL;
			} // if
		} // for
		
		// Bounceback for potentially important details on the edge.
		// This may possibly be better if the winning side fights a hard final
		// push battle where it stands the chance to gain ground.
		if ($colorRatio > (1 + ($threshold * 1.5)))
			$offset['near'] -= $rowsAtATime;
		else if ($colorRatio < (1 / (1 + ($threshold * 1.5))))
			$offset['near']	+= $rowsAtATime;
		
		return min($pixelRowsToCrop, max(0, $offset['near']));
	} // offset()
	
	/**
	 * @param array $rows
	 * @return float
	 */
	private function rowsInterestingness($rows)
	{
		$rowsInterestingness	= 0;
		
		$previousRow			= $rows[0];
		
		foreach($rows as $row)
		{
			$variance		= abs($row['variance'] - $previousRow['variance']);
			$saturation		= abs($row['saturation'] - $previousRow['saturation']);
			$brightness		= abs($row['brightness'] - $previousRow['brightness']);
			$contrast		= abs($row['contrast'] - $previousRow['contrast']);
			
			$rowsInterestingness	+=	$row['interestingness']
				+ $variance + $saturation + $brightness + $contrast
				+ $row['greatestVariance'] + $row['greatestSaturation']
				+ $row['greatestBrightness'] + $row['greatestContrast'];
			
			$previousRow	= $row;
		} // foreach
		
		return $rowsInterestingness;
	} // rowsInterestingness()
	
	/**
	 * Calculates how interesting a row in an image is
	 * 
	 * Interestingness of a row is based on these qualities: color variance,
	 * saturation, brightness, and local contrast (variation in brightness)
	 * 
	 * @since 2.0
	 * @param integer $row
	 * @param boolean $fromLeft
	 * @param integer $pixelStep
	 * @param integer $offset
	 * @return array
	 */
	private function rowInterestingness($row, $fromLeft = TRUE, $pixelStep = 1, $offset = 0)
	{
		$varianceImportance		= 10;
		$saturationImportance	= .15;
		$brightnessImportance	= .03;
		$contrastImportance		= .3;
		
		$variance = $saturation = $brightness = $contrast = $greatestVariance =
			$greatestSaturation = $greatestBrightness = $greatestContrast =
			$previousBrightness = $totalVariance = $totalSaturation = 
			$totalBrightness = $totalContrast = $interestingness = 0;
		
		// Offset will stagger the sampled pixels from row to row in order to
		// produce a more representative sample.
		// Example:
		// . . . . . . . . .
		//  . . . . . . . . .
		// . . . . . . . . .
		$offset				= $offset % $pixelStep;
		
		// Set up the first color to compare against
		if ($fromLeft)
			$previousColor		= $this->convertColorIndex(imagecolorat($this->rendered['image'], $row, $offset));
		else
			$previousColor		= $this->convertColorIndex(imagecolorat($this->rendered['image'], $offset, $row));
		
		// Grab all the colors in the row
		$colors				= array();
		if ($fromLeft)
			for($totalPixels = $offset; $totalPixels < $this->rendered['height']; $totalPixels += $pixelStep)
				$colors[]	= $this->convertColorIndex(imagecolorat($this->rendered['image'], $row, $totalPixels));
		else
			for($totalPixels = $offset; $totalPixels < $this->rendered['width']; $totalPixels += $pixelStep)
				$colors[]	= $this->convertColorIndex(imagecolorat($this->rendered['image'], $totalPixels, $row));
		
		// Compare all of the grabbed colors
		foreach ($colors as $color)
		{
			$variance			= $this->colorVariance($previousColor, $color) * $pixelStep;
			$saturation			= $this->saturation($color) * $pixelStep;
			$brightness			= array_sum($color) * $pixelStep;
			$contrast			= abs($brightness - $previousBrightness);
			
			$greatestVariance	= max($variance, $greatestVariance);
			$greatestSaturation	= max($saturation, $greatestSaturation);
			$greatestBrightness	= max($brightness, $greatestBrightness);
			$greatestContrast	= max($contrast, $greatestContrast);
			
			$totalVariance		+= $variance;
			$totalSaturation	+= $saturation;
			$totalBrightness	+= $brightness;
			$totalContrast		+= $contrast;
			
			$previousBrightness	= $brightness;
			$previousColor		= $color;
		} // foreach
		
		// Weight attributes based on importance
		$totalVariance		*= $varianceImportance;
		$totalSaturation	*= $saturationImportance;
		$totalBrightness	*= $brightnessImportance;
		$totalContrast		*= $contrastImportance;
		
		$greatestVariance	*= $varianceImportance;
		$greatestSaturation	*= $saturationImportance;
		$greatestBrightness	*= $brightnessImportance;
		$greatestContrast	*= $contrastImportance;
		
		// Calculate the interestingness
		$interestingness	+= ($totalVariance + $totalSaturation + $totalBrightness + $totalContrast)
			+ (($greatestVariance + $greatestSaturation + $greatestBrightness + $greatestContrast) * $totalPixels * 2);
		
		return array(
			'interestingness'		=> $interestingness,
			'variance'				=> $totalVariance,
			'saturation'			=> $totalSaturation,
			'brightness'			=> $totalBrightness,
			'contrast'				=> $totalContrast,
			'greatestVariance'		=> $greatestVariance,
			'greatestSaturation'	=> $greatestSaturation,
			'greatestBrightness'	=> $greatestBrightness,
			'greatestContrast'		=> $greatestContrast
		);
	} // rowInterestingness()
	
	/**
	 * @since 2.0
	 * @param integer $int
	 * @return array
	 */
	private function convertColorIndex($int)
	{
		$a	= (255 - (($int >> 24) & 0xFF)) / 255;
		$r	= (($int >> 16) & 0xFF) * $a;
		$g	= (($int >> 8) & 0xFF) * $a;
		$b	= ($int & 0xFF) * $a;
		return array($r, $g, $b);
	} // convertColorIndex()
	
	/**
	 * @since 2.0
	 * @param array $colorA
	 * @param array $colorB
	 * @return float
	 */
	private function colorVariance($colorA, $colorB)
	{
		return abs($colorA[0] - $colorB[0])
			+ abs($colorA[1] - $colorB[1])
			+ abs($colorA[2] - $colorB[2]);
	} // colorVariance()
	
	
	private function saturation($color)
	{		
		$min	= min($color[0], $color[1], $color[2]);
		$max	= max($color[0], $color[1], $color[2]);
		$delta	= $max - $min;

		return ($delta == 0 || $max == 0) ? 0 : ($delta / $max) * 255;
	} // saturation()
	
	/**
	 * @since 2.0
	 * @return boolean
	 */
	private function isRenderedCached()
	{
{'fraction_non_alphanumeric': 0.15986913583261106, 'fraction_numerical': 0.011706971690857819, 'mean_word_length': 3.1351842503785967, 'pattern_counts': {'":': 2, '<': 87, '<?xml version=': 0, '>': 690, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5166396', 'n_tokens_mistral': 32489, 'n_tokens_neox': 26739, 'n_words': 7444}