/**
 * <p>Title: imagecropinfo.js</p>
 * <p>Description: Image-crop-info javascript library.</p>
 * <p>Copyright: Copyright (c) 2007</p>
 * <p>Company: Kreber Graphics, Inc.</p>
 * @author Charlie Reading
 */

// Includes
var imagecropinfo_js = 1;
var includes = ((typeof browser_js == 'undefined') ? " browser.js\n" : "")
		+ ((typeof imagetools_js == 'undefined') ? " imageTools.js\n" : "")
		+ ((typeof Prototype == 'undefined') ? " prototype.js\n" : "");
if (includes != "")
{
	alert("imagecropinfo.js: you must first include:\n" + includes);
}

/*
 * USAGE
 * 
 * This is a general-purpose image-cropping tool.
 * 
 * Set up static distribution via ImageCropInfo.dfltPortDistributeXX/landDistributeXX -- this allows the distribution of "waste" to be different between portrait/landscape images.
 *   This is useful because portrait images are often people and do better cropped weighted to leave more of the top of the image (try to avoid head-chopping).
 *   
 * If you have a totally new image (no previous crop-info), use one of the image-size-based constructors.  This will take a first shot at cropping according to
 * 		the specified distribution..
 * 
 * If you have existing crop information, use the imageCenterX/imageCenterY/imageScale constructor, then use one of the adjust() calls to specify the
 * 		actual image info.  This prevents your existing information from being stomped on.
 * 
 * If at any time you wish to reset the crop information to an initial condition (like when using an image-size-based constructor), use one of the seed() calls.
 */

var ImageCropInfo = Class.create();

// crop states
ImageCropInfo.kCropState_OK						= 0;		// crop is ok
ImageCropInfo.kCropState_Warning			= 1;		// crop is below warning resolution
ImageCropInfo.kCropState_Minimum			= 2;		// crop is below minimum resolution

// default target resolutions
ImageCropInfo.dfltTargetMinResolution = 0;		// no min by default
ImageCropInfo.dfltTargetWarnResolution = 0;	// no warn by default

// default "waste" distributions
ImageCropInfo.dfltPortDistributeX = 0.5;			// centered by default
ImageCropInfo.dfltPortDistributeY = 0.5;			// centered by default
ImageCropInfo.dfltLandDistributeX = 0.5;			// centered by default
ImageCropInfo.dfltLandDistributeY = 0.5;			// centered by default

// out-of-bounds threshold
ImageCropInfo.kOutOfBoundsThreshold = 0.01;	// bounds violations must be greater than this (1%)

// (INTERNAL) seeding from-s
ImageCropInfo._FromInitialize	 				= 0;		// initialize()
ImageCropInfo._FromSeed								= 1;		// seed()
ImageCropInfo._FromAdjust							= 2;		// adjust()

// has geometry.js?
ImageCropInfo.hasGeometry = (typeof Geometry != 'undefined');

ImageCropInfo.prototype =
{
	iciOptions:						null,

	imageCenterX:					null,
	imageCenterY:					null,
	imageScale:						null,		// points -> image-pixels scaler
	
	imageWidthPx:					0,
	imageHeightPx:					0,
	
	targetWidthPt:					0,
	targetHeightPt:				0,
	targetMinResolution:		ImageCropInfo.dfltTargetMinResolution,
	targetWarnResolution:	ImageCropInfo.dfltTargetWarnResolution,
	
	portDistributeX:				ImageCropInfo.dfltPortDistributeX,
	portDistributeY:				ImageCropInfo.dfltPortDistributeY,
	landDistributeX:				ImageCropInfo.dfltLandDistributeX,
	landDistributeY:				ImageCropInfo.dfltLandDistributeY,

	targetAspect:					0,
	cropLeftPx:						0,
	cropTopPx:							0,
	cropWidthPx:						0,
	cropHeightPx:					0,
	cropBottomPx:					0,
	isCropAdjusted:				false,
	effectiveResolution:		0,
	
	hasCropInfo:						false,
	hasImageInfo:					false,
	hasCropRect:						false,
	
	/**
	 * Constructor
	 *
	 * @param options - options
	 * 			imageCenterX - image-center-x (0.0->1.0/left->right)
	 *			imageCenterY - image-center-y (0.0->1.0/top->bottom)
	 *			imageScale - image-scale (image-size / target-size)
	 *		OR
	 * 			imageWidthPx - image width (in pixels)
	 * 			imageHeightPx - image height (in pixels)
	 * 			targetWidthPt - target width (in points)
	 * 			targetHeightPt - target height (in points)
	 * 			targetMinResolution - target min resolution (in dpi)
	 * 			targetWarnResolution - target warn resolution (in dpi)
	 * 			portDistributeX - portrait image extra pixels distribution factor (0.0->1.0/left->right) for X axis
	 * 			portDistributeY - portrait image extra pixels distribution factor (0.0->1.0/top->bottom) for Y axis
	 * 			landDistributeX - landscape image extra pixels distribution factor (0.0->1.0/left->right) for X axis
	 * 			landDistributeY - landscape image extra pixels distribution factor (0.0->1.0/top->bottom) for Y axis
	 */
	initialize: function(options)
	{
		if (ImageCropInfo.DEBUG) this._debug("ImageCropInfo.initialize(): options="+this._inspectOptions(options));
		
		this._stashOptions(options, ImageCropInfo._FromInitialize);
		
		if (this.hasImageInfo)
		{
			this._adjust(true);
		}
	},
	
	
	//
	// Seeding
	//
	
	/**
	 * Seed ImageCropInfo (as if newly constructed)
	 *
	 * @param options - options
	 * 			imageCenterX - image-center-x (0.0->1.0/left->right)
	 *			imageCenterY - image-center-y (0.0->1.0/top->bottom)
	 *			imageScale - image-scale (image-size / target-size)
	 *		OR
	 * 			imageWidthPx - image width (in pixels)
	 * 			imageHeightPx - image height (in pixels)
	 * 			targetWidthPt - target width (in points)
	 * 			targetHeightPt - target height (in points)
	 * 			targetMinResolution - target min resolution (in dpi)
	 * 			targetWarnResolution - target warn resolution (in dpi)
	 * 			portDistributeX - portrait image extra pixels distribution factor (0.0->1.0/left->right) for X axis
	 * 			portDistributeY - portrait image extra pixels distribution factor (0.0->1.0/top->bottom) for Y axis
	 * 			landDistributeX - landscape image extra pixels distribution factor (0.0->1.0/left->right) for X axis
	 * 			landDistributeY - landscape image extra pixels distribution factor (0.0->1.0/top->bottom) for Y axis
	 */
	seed: function(options)
	{
		if (ImageCropInfo.DEBUG) this._debug("ImageCropInfo.seed(): options="+this._inspectOptions(options));

		this._stashOptions(options, ImageCropInfo._FromSeed);
		
		if (this.hasImageInfo)
		{
			this._adjust(true);
		}
	},


	//
	// Adjusting
	//
	
	/**
	 * Adjust ImageCropInfo (only update crop-info if necessary)
	 *
	 * @param options - options
	 * 		imageWidthPx - image width (in pixels)
	 * 		imageHeightPx - image height (in pixels)
	 * 		targetWidthPt - target width (in points)
	 * 		targetHeightPt - target height (in points)
	 * 		targetMinResolution - target min resolution (in dpi)
	 * 		targetWarnResolution - target warn resolution (in dpi)
	 * 		portDistributeX - portrait image extra pixels distribution factor (0.0->1.0/left->right) for X axis
	 * 		portDistributeY - portrait image extra pixels distribution factor (0.0->1.0/top->bottom) for Y axis
	 * 		landDistributeX - landscape image extra pixels distribution factor (0.0->1.0/left->right) for X axis
	 * 		landDistributeY - landscape image extra pixels distribution factor (0.0->1.0/top->bottom) for Y axis
	 * @return true if crop had to be adjusted, false otherwise
	 */
	adjust: function(options)
	{
		if (ImageCropInfo.DEBUG) this._debug("ImageCropInfo.adjust(): options="+this._inspectOptions(options));
		if (! this.hasCropInfo) { this._debug("ImageCropInfo.adjust(): must have pre-existing crop-info.", true); return; }

		this._stashOptions(options, ImageCropInfo._FromAdjust);
		options = options || {};
		
		// adjust, forcing reset
		return this._adjust(false);
	},

	/**
	 * Adjust from crop-rectangle.
	 *
	 * @param options (will prefer cropRectPx)
	 *		cropRectPx - crop-rectangle (in pixels)
	 *		cropLeftPx - crop-left (in pixels)
	 *		cropTopPx - crop-top (in pixels)
	 *		cropWidthPx - crop-width (in pixels)
	 *		cropHeightPx - crop-height (in pixels)
	 * @return true if crop adjusted, false otherwise
	 */
	adjustFromCropRectangle: function(options)
	{
		if (ImageCropInfo.DEBUG) this._debug("ImageCropInfo.adjustFromCropRectangle(): options="+this._inspectOptions(options));
		if (! this.hasImageInfo) { this._debug("ImageCropInfo.adjustFromCropRectangle(): must have pre-existing image-info.", true); return; }
		
		if (! Object.isUndefined(options.cropRectPx))
		{
			this.cropLeftPx = options.cropRectPx.left;
			this.cropTopPx = options.cropRectPx.top;
			this.cropWidthPx = options.cropRectPx.width;
			this.cropHeightPx = options.cropRectPx.height;
		}
		else if ((! Object.isUndefined(options.cropLeftPx)) && (! Object.isUndefined(options.cropTopPx)) && (! Object.isUndefined(options.cropWidthPx)) && (! Object.isUndefined(options.cropHeightPx)))
		{
			this.cropLeftPx = options.cropLeftPx;
			this.cropTopPx = options.cropTopPx;
			this.cropWidthPx = options.cropWidthPx;
			this.cropHeightPx = options.cropHeightPx;
		}
		else
		{
			this._debug("ImageCropInfo.adjustFromCropRectangle(): options must contain cropRectPx or cropLeftPx/cropTopPx/cropWidthPx/cropHeightPx");
			return;
		}

		var centerXPx = this.cropLeftPx + Math.round(this.cropWidthPx / 2);
		var centerYPx = this.cropTopPx + Math.round(this.cropHeightPx / 2);

		var nowImageCenterX = (centerXPx / this.imageWidthPx);
		var nowImageCenterY = (centerYPx / this.imageHeightPx);
 		var nowImageScale = this.cropWidthPx / this.targetWidthPt;
		
		this.isCropAdjusted |= ((nowImageCenterX != this.imageCenterX)
				|| (nowImageCenterY != this.imageCenterY)
				|| (nowImageScale != this.imageScale));
		this.imageCenterX = nowImageCenterX;
 		this.imageCenterY = nowImageCenterY;
 		this.imageScale = nowImageScale;
 		
		// we now have everything (if we didn't before)
		this.hasCropInfo = true;
		this.hasCropRect = true;
//debug("adjustFromCropRectangle(): hasCropInfo="+this.hasCropInfo+", hasImageInfo="+this.hasImageInfo+", hasCropRect="+this.hasCropRect);

		this._calcPostCrop();

 		return this.isCropAdjusted;
	},
	
	
	//
	// Converters
	//
	
	/**
	 * Convert to image-dimension.
	 *
	 * @return image-dimension (in pixels) { width, height }
	 */
	toImageDim: function()
	{
		if (ImageCropInfo.DEBUG) this._debug("ImageCropInfo.toImageDim()");
		if (! this.hasImageInfo) { this._debug("ImageCropInfo.toImageDim(): must have pre-existing image-info.", true); return; }
		
		return ((ImageCropInfo.hasGeometry) ? Geometry.dimension(this.imageWidthPx, this.imageHeightPx) : { width:this.imageWidthPx, height:this.imageHeightPx });
	},
	
	/**
	 * Convert to target-dimension.
	 *
	 * @return target-dimension (in points) { width, height }
	 */
	toTargetDim: function()
	{
		if (ImageCropInfo.DEBUG) this._debug("ImageCropInfo.toTargetDim()");
		if (! this.hasImageInfo) { this._debug("ImageCropInfo.toTargetDim(): must have pre-existing image-info.", true); return; }
		
		return ((ImageCropInfo.hasGeometry) ? Geometry.dimension(this.targetWidthPt, this.targetHeightPt) : { width:this.targetWidthPt, height:this.targetHeightPt });
	},
	
	/**
	 * Convert to a crop-dimension (pixels).
	 * 
	 * @return Dimension representing crop (in pixels), null if not seeded/adjusted
	 */
	toCropDim: function()
	{
		if (ImageCropInfo.DEBUG) this._debug("ImageCropInfo.toCropDim()");
		if (! this.hasCropRect) { this._debug("ImageCropInfo.toCropDim(): must have pre-existing crop-rect.", true); return; }
		
		return ((ImageCropInfo.hasGeometry) ? Geometry.dimension(this.cropWidthPx, this.cropHeightPx) : { width:this.cropWidthPx, height:this.cropHeightPx });
	},
	
	/**
	 * Convert to crop-rectangle (pixels).
	 *
	 * @return crop-rectangle (in pixels) { left, top, width, height, bottom }
	 */
	toCropRect: function()
	{
		if (ImageCropInfo.DEBUG) this._debug("ImageCropInfo.toCropRect()");
		if (! this.hasCropRect) { this._debug("ImageCropInfo.toCropRect(): must have pre-existing crop-rect.", true); return; }
		
		return ((ImageCropInfo.hasGeometry)
				? Geometry.rectangle(this.cropLeftPx, this.cropTopPx, this.cropWidthPx, this.cropHeightPx)
				: { left:this.cropLeftPx, top:this.cropTopPx, width:this.cropWidthPx, height:this.cropHeightPx, bottom:this.cropBottomPx });
	},
	
	/**
	 * Convert to crop-rectangle (points).
	 *
	 * @return crop-rectangle (in points) { left, top, width, height, bottom }
	 */
	toCropRectPt: function()
	{
		if (ImageCropInfo.DEBUG) this._debug("ImageCropInfo.toCropRectPt()");
		if (! this.hasCropRect) { this._debug("ImageCropInfo.toCropRectPt(): must have pre-existing crop-rect.", true); return; }
		
		var cropLeftPt = this.cropLeftPx / this.imageScale;
		var cropTopPt = this.cropTopPx / this.imageScale;
		var cropWidthPt = this.cropWidthPx / this.imageScale;
		var cropHeightPt = this.cropHeightPx / this.imageScale;
		var cropBottomPt = this.cropBottomPx / this.imageScale;
		return ((ImageCropInfo.hasGeometry)
				? Geometry.rectangle(cropLeftPt, cropTopPt, cropWidthPt, cropHeightPt)
				: { left: cropLeftPt, top: cropTopPt, width: cropWidthPt, height: cropHeightPt, bottom: cropBottomPt });
	},
	
	/**
	 * Convert to "crop-state".
	 *
	 * @param options (will prefer cropDimPx) -- if none, will just use effective-resolution
	 *		cropDimPx - crop-dimension (in pixels)
	 *		cropWidthPx - crop-width (in pixels)
	 *		cropHeightPx - crop-height (in pixels)
	 * @return crop-state (kCropState_x value)
	 */
	toCropState: function(options)
	{
		if (ImageCropInfo.DEBUG) this._debug("ImageCropInfo.toCropState(): options="+this._inspectOptions(options));
		if (! this.hasImageInfo) { this._debug("ImageCropInfo.toCropState(): must have pre-existing image-info.", true); return; }
		
		var effective = this.calcEffectiveResolution(options);
		if ((this.targetMinResolution > 0) && (effective < this.targetMinResolution))
		{
			return ImageCropInfo.kCropState_Minimum;
		}
		else if ((this.targetWarnResolution > 0) && (effective < this.targetWarnResolution))
		{
			return ImageCropInfo.kCropState_Warning;
		}
		
		if (! this.hasCropRect) { this._debug("ImageCropInfo.toCropState(): must have pre-existing crop-rect.", true); return; }
		
		return ImageCropInfo.kCropState_OK;
	},
	
	
	//
	// Resolution
	//
	
	/**
	 * Test if effective-resolution of crop-dimension is sufficient for specified-resolution.
	 *
	 * @param resolution - resolution to test against.
	 * @param options (will prefer cropDimPx) -- if none, will just use effective-resolution
	 *		cropDimPx - crop-dimension (in pixels)
	 *		cropWidthPx - crop-width (in pixels)
	 *		cropHeightPx - crop-height (in pixels)
	 * @return true if cropped-image is of sufficient resolution, false otherwise.
	 */
	isResolutionSufficient: function(resolution, options)
	{
		if (ImageCropInfo.DEBUG) this._debug("ImageCropInfo.isResolutionSufficient(): "+Object.inspect($H({ resolution: resolution, options: this._inspectOptions(options) })));
		if (! this.hasCropRect) { this._debug("ImageCropInfo.isResolutionSufficient(): must have pre-existing crop-rect.", true); return; }
		
		options = options || {};
		
		var effective = this.calcEffectiveResolution(options);
		return (effective >= resolution);
	},
	
	/**
	 * Calculate effective resolution for the specified crop-dimension
	 *
	 * @param options (will prefer cropDimPx) -- if none, will just use effective-resolution
	 *		cropDimPx - crop-dimension (in pixels)
	 *		cropWidthPx - crop-width (in pixels)
	 *		cropHeightPx - crop-height (in pixels)
	 * @return effective resolution for the specified crop-dimension.
	 */
	calcEffectiveResolution: function(options)
	{
		if (ImageCropInfo.DEBUG) this._debug("ImageCropInfo.calcEffectiveResolution(): options="+this._inspectOptions(options));
		if (! this.hasCropRect) { this._debug("ImageCropInfo.calcEffectiveResolution(): must have pre-existing crop-rect.", true); return; }
		
		options = options || {};
		
		if (options.cropDimPx)
		{
			return Math.max((72.0 * options.cropDimPx.width) / this.targetWidthPt, (72.0 * options.cropDimPx.height) / this.targetHeightPt);
		}
		else if ((options.cropWidthPx) && (options.cropHeightPx))
		{
			return Math.max((72.0 * options.cropWidthPx) / this.targetWidthPt, (72.0 * options.cropHeightPx) / this.targetHeightPt);
		}
		
		return Math.max((72.0 * this.cropWidthPx) / this.targetWidthPt, (72.0 * this.cropHeightPx) / this.targetHeightPt);
	},
	
	
	//
	// Internals
	//
	
	/**
	 * (INTERNAL) Stash options
	 *
	 * @param options - options
	 * 		imageCenterX - image-center-x (0.0->1.0/left->right)
	 *		imageCenterY - image-center-y (0.0->1.0/top->bottom)
	 *		imageScale - image-scale (image-size / target-size)
	 * 		imageWidthPx - image width (in pixels)
	 * 		imageHeightPx - image height (in pixels)
	 * 		targetWidthPt - target width (in points)
	 * 		targetHeightPt - target height (in points)
	 * 		targetMinResolution - target min resolution (in dpi)
	 * 		targetWarnResolution - target warn resolution (in dpi)
	 * 		portDistributeX - portrait image extra pixels distribution factor (0.0-1.0) for X axis
	 * 		portDistributeY - portrait image extra pixels distribution factor (0.0-1.0) for Y axis
	 * 		landDistributeX - landscape image extra pixels distribution factor (0.0-1.0) for X axis
	 * 		landDistributeY - landscape image extra pixels distribution factor (0.0-1.0) for Y axis
	 * @param from - _FromX value
	 */
	_stashOptions: function(options, from)
	{
		options = options || {};
		//if (ImageCropInfo.DEBUG) this._debug("ImageCropInfo._stashOptions(): "+Object.inspect($H({ options: this._inspectOptions(options), from: from })));
		
		// fix options
		if (typeof options.imageCenterX == "string") options.imageCenterX = parseFloat(options.imageCenterX);
		if (typeof options.imageCenterY == "string") options.imageCenterY = parseFloat(options.imageCenterY);
		if (typeof options.imageScale == "string") options.imageScale = parseFloat(options.imageScale);
		
		this.iciOptions = options;
		
		// crop-info/image-info mutually exclusive (and no crop-info from adjust)
		var optCropInfo = (((options.imageCenterX || null) != null)
				&& ((options.imageCenterY || null) != null)
				&& ((options.imageScale || null) != null));
		var optImageInfo = (((options.imageWidthPx || null) != null)
				&& ((options.imageHeightPx || null) != null)
				&& ((options.targetWidthPt || null) != null)
				&& ((options.targetHeightPt || null) != null));
		if (((optCropInfo) && (optImageInfo)) || ((optCropInfo) && (from == ImageCropInfo._FromAdjust)))
		{
			this._debug("ImageCropInfo._stashOptions(): crop-info/image-info mutually exclusive (and no crop-info from adjust)");
			return;
		}

		// if initialize/seed, pre-clear "has" flags
		if ((from == ImageCropInfo._FromInitialize) || (from == ImageCropInfo._FromSeed))
		{
			this.hasCropInfo = false;
			this.hasImageInfo = false;
		}
		
		// no matter what, we have no crop-rect
		this.hasCropRect = false;
//debug("_stashOptions(1): hasCropInfo="+this.hasCropInfo+", hasImageInfo="+this.hasImageInfo+", hasCropRect="+this.hasCropRect);
		
		// crop info can only come from initialize/seed
		if (optCropInfo)
		{
			this.imageCenterX = options.imageCenterX;
			this.imageCenterY = options.imageCenterY;
			this.imageScale = options.imageScale;
			this.hasCropInfo = true;
		}
		
		// image info can come from anywhere
		if (optImageInfo)
		{
			this.imageWidthPx = options.imageWidthPx;
			this.imageHeightPx = options.imageHeightPx;
			this.targetWidthPt = options.targetWidthPt;
			this.targetHeightPt = options.targetHeightPt;
			this.targetMinResolution = (((options.targetMinResolution || null) != null) ? options.targetMinResolution : this.targetMinResolution);
			this.targetWarnResolution = (((options.targetWarnResolution || null) != null) ? options.targetWarnResolution : this.targetWarnResolution);
			this.hasImageInfo = true;
		}
		
		// distribute
		var dfltToThis = (from == ImageCropInfo._FromAdjust);
		this.portDistributeX = (((options.portDistributeX || null) != null) ? options.portDistributeX : ((dfltToThis) ? this.portDistributeX : ImageCropInfo.dfltPortDistributeX));
		this.portDistributeY = (((options.portDistributeY || null) != null) ? options.portDistributeY : ((dfltToThis) ? this.portDistributeY : ImageCropInfo.dfltPortDistributeY));
		this.landDistributeX = (((options.landDistributeX || null) != null) ? options.landDistributeX : ((dfltToThis) ? this.landDistributeX : ImageCropInfo.dfltLandDistributeX));
		this.landDistributeY = (((options.landDistributeY || null) != null) ? options.landDistributeY : ((dfltToThis) ? this.landDistributeY : ImageCropInfo.dfltLandDistributeY));
//debug("_stashOptions(2): hasCropInfo="+this.hasCropInfo+", hasImageInfo="+this.hasImageInfo+", hasCropRect="+this.hasCropRect);
	},


	/**
	 * (INTERNAL) Adjust crop of image (possibly forcing it).
	 *
	 * @param forceReset - true to force reset (as if uninitialized), false to adjust
	 * @return true if crop had to be adjusted, false otherwise
	 */
	_adjust: function(forceReset)
	{
		//if (ImageCropInfo.DEBUG) this._debug("ImageCropInfo._adjust()"+Object.inspect($H({ forceReset: forceReset })));
		this.isCropAdjusted = false;
		
		var isImagePortrait = (this.imageWidthPx <= this.imageHeightPx);
		var distributeX = ((isImagePortrait) ? this.portDistributeX : this.landDistributeX);
		var distributeY = ((isImagePortrait) ? this.portDistributeY : this.landDistributeY);
		
		var dDoubleNull = ((typeof kDouble_null != 'undefined') ? kDouble_null : Number.MAX_VALUE);
		var isScaled = ((! forceReset) && (this.imageScale != null) && (this.imageScale != dDoubleNull));
		var actualImageCenterX = ((isScaled) ? this.imageCenterX : 0.5);
		var actualImageCenterY = ((isScaled) ? this.imageCenterY : 0.5);
		var actualImageScale = ((isScaled) ? this.imageScale : 1.0);

		var dCenterXPx = (actualImageCenterX * this.imageWidthPx);
		var dCenterYPx = (actualImageCenterY * this.imageHeightPx);

		// these _will_ be set by one or more of the following
		var dCropLeftPx = 0;
		var dCropTopPx = 0;
		var dCropWidthPx = 0;
		var dCropHeightPx = 0;

		var isOutOfBounds = false;
		
		// fit horizontally
		if ((isScaled) && (! isOutOfBounds))
		{
			dCropWidthPx = (actualImageScale * this.targetWidthPt);
			var halfWidth = dCropWidthPx / 2.0;
			dCropLeftPx = (dCenterXPx - halfWidth);
			var difference = (this.imageWidthPx - dCropWidthPx) / dCropWidthPx;
			if (dCropWidthPx > this.imageWidthPx)
			{
				if (difference > ImageCropInfo.kOutOfBoundsThreshold)
				{
					isOutOfBounds = true;
				}
				else
				{
					// refit and move to center
					dCropWidthPx = this.imageWidthPx;
					dCropLeftPx = 0;
					this.imageCenterX = 0.5;
					this.imageScale = this.imageWidthPx / this.targetWidthPt;
					this.isCropAdjusted = true;
				}
			}
			else if (dCropLeftPx < 0)
			{
				// move to right
				dCropLeftPx = 0;
				dCenterXPx = dCropLeftPx + (dCropWidthPx / 2);
				this.imageCenterX = dCenterXPx / this.imageWidthPx;
				this.isCropAdjusted = true;
			}
			else if ((dCropLeftPx+dCropWidthPx) > this.imageWidthPx)
			{
				// move to left
				dCropLeftPx = this.imageWidthPx - dCropWidthPx;
				dCenterXPx = dCropLeftPx + (dCropWidthPx / 2);
				this.imageCenterX = dCenterXPx / this.imageWidthPx;
				this.isCropAdjusted = true;
			}
		}

		// fit vertically
		if ((isScaled) && (! isOutOfBounds))
		{
			dCropHeightPx = (actualImageScale * this.targetHeightPt);
			dCropTopPx = (dCenterYPx - (dCropHeightPx / 2));
			var difference = (this.imageHeightPx - dCropHeightPx) / dCropHeightPx;
			if (dCropHeightPx > this.imageHeightPx)
			{
				if (difference > ImageCropInfo.kOutOfBoundsThreshold)
				{
					isOutOfBounds = true;
				}
				else
				{
					// refit and move to center
					dCropHeightPx = this.imageHeightPx;
					dCropTopPx = 0;
					this.imageCenterY = 0.5;
					this.imageScale = this.imageHeightPx / this.targetHeightPt;
					this.isCropAdjusted = true;
				}
			}
			else if (dCropTopPx < 0)
			{
				// move down
				dCropTopPx = 0;
				dCenterYPx = dCropTopPx + (dCropHeightPx / 2);
				this.imageCenterY = dCenterYPx / this.imageHeightPx;
				this.isCropAdjusted = true;
			}
			else if ((dCropTopPx+dCropHeightPx) > this.imageHeightPx)
			{
				// move up
				dCropTopPx = this.imageHeightPx - dCropHeightPx;
				dCenterYPx = dCropTopPx + (dCropHeightPx / 2);
				this.imageCenterY = dCenterYPx / this.imageHeightPx;
				this.isCropAdjusted = true;
			}
		}

		// We check again (for non-scaled), 'cause above scaling may cause recentering
		if ((! isScaled) || (isOutOfBounds))
		{
			// yes, kFitTo_Inner -- we're fitting the _target_ to the _image_
			var dimPx = fitImage(this.targetWidthPt, this.targetHeightPt, this.imageWidthPx, this.imageHeightPx, kFitTo_Inner);
			var wasFitToWidth = ((dimPx.width) == this.imageWidthPx);
			
			this.imageScale = ((wasFitToWidth) ? this.imageWidthPx / this.targetWidthPt : this.imageHeightPx / this.targetHeightPt);
			this.imageCenterX = 0.5;
			this.imageCenterY = 0.5;
			var dWastePx;
			if (wasFitToWidth)		// fit to width (adjust y)
			{
				dWastePx = this.imageHeightPx - dimPx.height;
				this.imageCenterY = (((this.imageHeightPx - dWastePx)/2) + (dWastePx*distributeY)) / this.imageHeightPx;
				dCenterYPx = (this.imageCenterY * this.imageHeightPx);
			}
			else									// fit to height (adjust x)
			{
				dWastePx = this.imageWidthPx - dimPx.width;
				this.imageCenterX = (((this.imageWidthPx - dWastePx)/2) + (dWastePx*distributeX)) / this.imageWidthPx;
				dCenterXPx = (this.imageCenterX * this.imageWidthPx);
			}

			dCropWidthPx = (this.imageScale * this.targetWidthPt);
			dCropHeightPx = (this.imageScale * this.targetHeightPt);
			dCropLeftPx = dCenterXPx - (dCropWidthPx / 2);
			dCropTopPx = dCenterYPx - (dCropHeightPx / 2);

			this.isCropAdjusted = true;
		}

		// from Rectangle.setRect()
		var x0 = Math.floor(dCropLeftPx);
		var y0 = Math.floor(dCropTopPx);
		var x1 = Math.ceil(dCropLeftPx+dCropWidthPx);
		var y1 = Math.ceil(dCropTopPx+dCropHeightPx);
		// setBounds(x0, y0, x1-x0, y1-y0);
		
		this.cropLeftPx = x0;
		this.cropTopPx = y0;
		this.cropWidthPx = x1-x0;
		this.cropHeightPx = y1-y0;

		// we now have everything (if we didn't before)
		this.hasCropInfo = true;
		this.hasCropRect = true;
//debug("_adjust(): hasCropInfo="+this.hasCropInfo+", hasImageInfo="+this.hasImageInfo+", hasCropRect="+this.hasCropRect);
		
		this._calcPostCrop();
		 
		return this.isCropAdjusted;
	},
	
	_calcPostCrop: function()
	{
		//if (ImageCropInfo.DEBUG) this._debug("ImageCropInfo._calcPostCrop()");
		this.cropBottomPx = this.imageHeightPx - this.cropTopPx - this.cropHeightPx;
		this.effectiveResolution = this.calcEffectiveResolution();
	},
	
	_inspectOptions: function(options)
	{
		options = options || {};
		
		var collected = {};
		if (options.imageCenterX) collected.imageCenterX = options.imageCenterX;
		if (options.imageCenterY) collected.imageCenterY = options.imageCenterY;
		if (options.imageScale) collected.imageScale = options.imageScale;
		if (options.imageWidthPx) collected.imageWidthPx = options.imageWidthPx;
		if (options.imageHeightPx) collected.imageHeightPx = options.imageHeightPx;
		if (options.targetWidthPt) collected.targetWidthPt = options.targetWidthPt;
		if (options.targetHeightPt) collected.targetHeightPt = options.targetHeightPt;
		if (options.targetMinResolution) collected.targetMinResolution = options.targetMinResolution;
		if (options.targetWarnResolution) collected.targetWarnResolution = options.targetWarnResolution;
		if (options.portDistributeX) collected.portDistributeX = options.portDistributeX;
		if (options.portDistributeY) collected.portDistributeY = options.portDistributeY;
		if (options.landDistributeX) collected.landDistributeX = options.landDistributeX;
		if (options.landDistributeY) collected.landDistributeY = options.landDistributeY;
		
		return Object.inspect($H(collected));
	},
	
	_debug: function(msg, force)
	{
		if ((! ImageCropInfo.DEBUG) && (! force))
			return;
		
		try
		{
			debug(msg);
		}
		catch(ignore)
		{
		}
	}
};

ImageCropInfo.DEBUG = false;
