An Image Auto-Cropper in JavaScript

The JavaScript program below allows the user to specify an input file, a background color, and a difference threshold, and will automatically crop the image to remove any pixels around its border that match the background color within the specified difference threshold. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

UPDATE 2071/06/22 – The code has been updated to reuse the same code for scanning pixels along both the x and y axes, rather than simply using two different sets of nested loops with identical contents. For efficiency’s sake, the code also now scans each column or row of pixels from both sides, stopping when the first non-croppable pixel is found, rather than scanning the whole line of pixels including the non-croppable ones. A long comment describing this new scanning process in relative detail has also been added.


<html>
<body>

<!-- ui -->

<div id="divUI">

	<label><b>Image Autocropper</b></label>
	<p>Load an image file, click on it to set a background color, set a difference threshold, and click the Crop button.</p>
	
	<div>
		<label>Image to Crop:</label>
		<input type="file" onchange="inputImageToCrop_Changed(this);"></input>
		<div id="divDisplayImageToCrop">[none]</div>
	</div>

	<div>
		</div>
			<label>Background Color RGB:</label>
			<input id="inputColorToCrop" value="255,255,255"></input>
		</div>
		<div>
			<label>Color Difference Threshold:</label>
			<input id="inputColorDifferenceThreshold" value="0"></input>
		</div>
		<button onclick="buttonImageAutocrop_Clicked();">Crop</button>
	</div>

	<div>
		<label>Image Cropped:</label>
		<div id="divDisplayImageCropped">[none]</div>
	</div>

</div>

<!-- ui ends-->

<script type="text/javascript">

// ui events

function buttonImageAutocrop_Clicked()
{
	// todo - validation

	var divDisplayImageToCrop = 
		document.getElementById("divDisplayImageToCrop");
	var inputColorToCrop = 
		document.getElementById("inputColorToCrop");
	var inputColorDifferenceThreshold = 
		document.getElementById("inputColorDifferenceThreshold");

	var imageToCropAsCanvas = 
		divDisplayImageToCrop.getElementsByTagName("canvas")[0];

	var colorToCrop = inputColorToCrop.value;
	var colorDifferenceThreshold = inputColorDifferenceThreshold.value;

	var imageCroppedAsCanvas = new ImageAutocropper().cropCanvas
	(
		imageToCropAsCanvas,
		colorToCrop,
		colorDifferenceThreshold
	);
	imageCroppedAsCanvas.style = "border:1px solid";

	var divDisplayImageCropped = 
		document.getElementById("divDisplayImageCropped");
	divDisplayImageCropped.innerHTML = "";
	divDisplayImageCropped.appendChild(imageCroppedAsCanvas);
}

function inputImageToCrop_Changed(input)
{
	var file = input.files[0];
	var fileReader = new FileReader();
	fileReader.onload = function(eventFileLoaded)
	{
		var imageAsDataURL = eventFileLoaded.target.result;
		var imageAsDOMElement = document.createElement("img");
		imageAsDOMElement.onload = function(eventImageLoaded)
		{
			var imageAsCanvas = document.createElement("canvas");
			imageAsCanvas.style = "border:1px solid";
			imageAsCanvas.width = imageAsDOMElement.width;
			imageAsCanvas.height = imageAsDOMElement.height;

			var graphics = imageAsCanvas.getContext("2d");
			graphics.drawImage(imageAsDOMElement, 0, 0);

			imageAsCanvas.onmousedown = function(mouseEvent)
			{
				var x = mouseEvent.x;
				var y = mouseEvent.y;

				var pixelRGBA = graphics.getImageData
				(
					x, y, 1, 1
				).data;

				var pixelAsString = 
					+ pixelRGBA[0] + ","
					+ pixelRGBA[1] + ","
					+ pixelRGBA[2]

				var inputColorToCrop = 
					document.getElementById("inputColorToCrop");

				inputColorToCrop.value = pixelAsString;
			}

			var divDisplayImageToCrop = document.getElementById
			(
				"divDisplayImageToCrop"
			);
			divDisplayImageToCrop.innerHTML = "";
			divDisplayImageToCrop.appendChild
			(
				imageAsCanvas
			);
		}
		imageAsDOMElement.src = imageAsDataURL;
	}
	fileReader.readAsDataURL(file);
}

// classes

function Bounds(min, max)
{
	this.min = min;
	this.max = max;
}
{
	Bounds.prototype.size = function()
	{
		return this.max.clone().subtract(this.min);
	}
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	Coords.prototype.clone = function()
	{
		return new Coords(this.x, this.y);
	}

	Coords.prototype.dimension = function(dimensionIndex, valueToSet)
	{
		var returnValue;

		if (valueToSet == null)		
		{
			returnValue = (dimensionIndex == 0 ? this.x : this.y);
		}
		else 
		{
			if (dimensionIndex == 0)
			{
				this.x = valueToSet;
			}
			else
			{
				this.y = valueToSet;
			}
		}

		return returnValue;
	}

	Coords.prototype.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;
		return this;
	}
}

function ImageAutocropper()
{
	// do nothing
}
{
	ImageAutocropper.prototype.cropCanvas = function
	(
		imageToCropAsCanvas,
		colorToCrop,
		colorDifferenceThreshold
	)
	{			
		var cropBounds = this.cropCanvas_FindBounds
		(
			imageToCropAsCanvas,
			colorToCrop,
			colorDifferenceThreshold
		);

		var imageCroppedAsCanvas = document.createElement("canvas");

		var imageCroppedSize = cropBounds.size();

		imageCroppedAsCanvas.width = imageCroppedSize.x;
		imageCroppedAsCanvas.height = imageCroppedSize.y;

		var graphics2 = imageCroppedAsCanvas.getContext("2d");
		graphics2.drawImage
		(
			imageToCropAsCanvas, 
			// source rectangle
			cropBounds.min.x, cropBounds.min.y,
			imageCroppedSize.x, imageCroppedSize.y,
			// target rectangle
			0, 0, 
			imageCroppedSize.x, imageCroppedSize.y
		);

		return imageCroppedAsCanvas
	}

	ImageAutocropper.prototype.cropCanvas_FindBounds = function
	(
		imageToCropAsCanvas,
		colorToCrop,
		colorDifferenceThreshold
	)
	{
		var colorToCropRGB = colorToCrop.split(",");

		var graphics = imageToCropAsCanvas.getContext("2d");

		var imageToCropSize = new Coords
		(
			imageToCropAsCanvas.width,
			imageToCropAsCanvas.height
		);

		var cropBounds = new Bounds
		(
			imageToCropSize.clone(),
			new Coords(0, 0)
		);

		// We start with the upper-left pixel,
		// and go down until we find a "non-croppable" pixel outside the threshold.
		// When that happens, we expand the top bound if necessary,
		// then jump to the bottom pixel in the column,
		// and proceed upward until, again, a pixel outside the threshold is found.
		// This time we expand the bottom bound if necessary.
		// Then we go to the top of the next column of pixels,
		// and repeat the top-down and bottom-up loops for each column,
		// expanding the bounds as necessary
		// so that they include all known non-croppable pixels.
		// Then we switch axes from top-to-bottom to left-to-right 
		// and repeat to set the left and right bounds.

		var pixelPos = new Coords();

		var numberOfAxes = 2;

		for (var a0 = 0; a0 < numberOfAxes; a0++)
		{
			var a1 = 1 - a0;

			var sizeAlongAxis0 = imageToCropSize.dimension(a0);
			var sizeAlongAxis1 = imageToCropSize.dimension(a1);

			for (var i = 0; i < sizeAlongAxis0; i++)
			{
				pixelPos.dimension(a0, i);

				for (var d = 0; d < 2; d++)
				{
					var jStart = (d == 0 ? 0 : sizeAlongAxis1 - 1);
					var jEnd = (d == 0 ? sizeAlongAxis1 : -1);
					var jStep = (d == 0 ? 1 : -1);

					for (var j = jStart; j != jEnd; j += jStep)
					{
	 					pixelPos.dimension(a1, j);
	
						var isPixelWithinThreshold = 
							this.cropCanvas_FindBounds_IsPixelWithinThreshold
							(
								graphics, 
								colorToCropRGB, 
								colorDifferenceThreshold, 
								pixelPos
							);

						if (isPixelWithinThreshold == false)
						{
							this.cropCanvas_FindBounds_BoundsExpand
							(
								cropBounds, pixelPos
							);

							break;
						}

					} // end for j

				} // end for d
		
			} // end for i

		} // end for a

		return cropBounds;
	}

	ImageAutocropper.prototype.cropCanvas_FindBounds_IsPixelWithinThreshold = function
	(
		graphics, colorToCropRGB, colorDifferenceThreshold, pixelPos
	)
	{
		var isPixelWithinThreshold = false;

		var pixelRGB = graphics.getImageData
		(
			pixelPos.x, pixelPos.y, 1, 1
		).data;

		var pixelDifference = 0;
		var numberOfColorComponents = 3; // rgb

		for (var c = 0; c < numberOfColorComponents; c++)
		{
			var componentDifference = Math.abs
			(
				pixelRGB[c] - colorToCropRGB[c]
			);
			pixelDifference += componentDifference;
		}

		var isPixelWithinThreshold =  
			(pixelDifference <= colorDifferenceThreshold);

		return isPixelWithinThreshold;
	}

	ImageAutocropper.prototype.cropCanvas_FindBounds_BoundsExpand = function
	(
		cropBounds, pixelPos
	)
	{
		if (pixelPos.x < cropBounds.min.x)
		{
			cropBounds.min.x = pixelPos.x;
		}

		if (pixelPos.x > cropBounds.max.x)
		{
			cropBounds.max.x = pixelPos.x;
		}

		if (pixelPos.y < cropBounds.min.y)
		{
			cropBounds.min.y = pixelPos.y;
		}

		if (pixelPos.y > cropBounds.max.y)
		{
			cropBounds.max.y = pixelPos.y;
		}

		return cropBounds;
	}

}

</script>

</body>
</html>

Advertisements
This entry was posted in Uncategorized and tagged , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s