Steven Berridge

Web Developer

Facebook Style Image Cropper - Part 1

Part 2

In this tutorial we'll be going over how to create an image resizing and cropping system using JavaScript and PHP similar to the one found on Facebook. Part 1 will cover setting up the JavaScript to allow a user to select the area of an image they want to crop.

This tutorial will touch on the following concepts:

Example

The HTML

The initial HTML for this project will be fairly simple, all we need is a file input and a div to contain our "cropping" area and default image.

<div id='resize-container'>
	<img src='default-image.png'/>
</div>

<input type='file' name='image' id='resize-input' data-dimensions='400x250' data-container='resize-container'>

Using HTML data tags on the input we can define the dimensions we want to crop our images to and the element we want to use as our cropping area.

The CSS

.resize-container { 
	overflow: hidden;  
	position: relative; 
}
.resize-container img { 
	position: absolute; 
	cursor: move; 
}

The CSS should be fairly self explanatory, we're setting the resize container to position: relative; and the image inside the resize container to position: absolute; so we can control the position of the image when dragging the crop selection.

The JavaScript Object

Now that we have our HTML and CSS in place we can start creating the JavaScript class that will handle creating the additional inputs, elements and events to allow users to select the area they want to crop their images to.

The base frame for the JavaScript class is below.

function ImageCropper(input) {
	
	function startMove(e) {
		
	}

	function move(e) {
		
	}

	function endMove(e) {
		
	}

	function setUpMovement() {
		
	}

	function renderImage(src) {
		
	}

	function imageSelected(e) {
	
	}
}

The class has 5 functions, the purpose of each function is explained below.

imageSelected
This function will be called when a file is chosen using the file input. The function will handle validating that the file is an image and will load the image using the FileReader API.
renderImage
The render image function will take an image source provided by the FileReader API in the imageSelected function, create the image element, resize the image to the fit within the specified dimensions while keeping the images original proportions and place it within the "cropping" container.
setUpMovement
Prepares all the events and variables needed to handle moving the image when selecting how the image is to be cropped.
startMove
Event handler called when the image is clicked on.
move
Event handler called when the mouse is moved while clicking on the image, handling the dragging of the image.
endMove
Event handler called when the mouse is lifted from the image or moved off of the image.

Adding The Variables

Before we start filling the functions we should initialise all the variables we'll need. These will be added towards the bottom of the class after all the functions.

var input = input,
	mouseDown,
	startPos,
	image,
	curPos,
	coordsInput = document.createElement('input'),
	dimensions = input.getAttribute("data-dimensions").split("x"),
	dimensionsInput = document.createElement('input'),
	heightDif,
	widthDif,
	resizeContainer;
input
The file input element passed into the class
mouseDown
This will be a boolean telling us whether or not the user is currently dragging the image.
startPos
An array containing the position of the mouse when the user starts dragging the image.
image
The image element
curPos
An array containing the current position of the image.
coordsInput
The input created to hold the coordinates of where to crop the image.
dimensions
The dimensions the image is being resized to, taken from the file inputs data attribute. We're using the split method here to convert the dimensions into an array containing the width and height of the image. dimensions = [400,250];
dimensionsInput
The input created to hold the dimensions of the image.
heightDif
The difference between the height of the image and the height we're resizing it to.
widthDif
The difference between the width of the image and the width we're resizing it to.
resizeContainer
The div element containing the image for cropping.

Setting up the inputs

In the previous section we created a couple of inputs, one to hold the coordinates of our cropping area and another to hold the dimensions we're resizing our image to. In this section we will be sorting these inputs out by setting there attributes and adding them to the page. We'll also be adding an event listener to the main file input listening for the user selecting a file.

dimensionsInput.name = "dimensions";
dimensionsInput.type = "hidden";
input.parentElement.insertBefore(dimensionsInput,null);
dimensionsInput.value = JSON.stringify(dimensions);
coordsInput.type = "hidden";
coordsInput.name = "coords";
input.parentElement.insertBefore(coordsInput,null);

input.addEventListener('change',imageSelected);

In this snippet of code we're setting the attributes of the two elements and inserting them into the HTML using the insertBefore method of the file inputs parent element. When setting the dimensionsInput value we're using the JSON.stringify() method to convert the dimensions array into a string which can later be converted back into an array in the PHP script. Finally we're adding an event listener to the file input listening for the "change" event, this will be triggered when the user selects an image and will call the 'imageSelected' function.

imageSelected

The first function we'll fill out is the imageSelected function which we've assigned as the event handler for when the user chooses a file.

function imageSelected(e) {
	var file = input.files[0],
	type = file.type.split('/');
	if(type[0] !== 'image') {
		alert('You need to choose an image');
		input.value = '';
		return false;
	}
	var reader = new FileReader();
	reader.onloadend = function(e) {
		renderImage(e.target.result);
	}
	reader.readAsDataURL(file);
}

This function first of all gets the file from the inputs files property then accesses the mime type of the uploaded file, this is split on '/' in order to get the first part of the mime type which is "image" for any image files. If the uploaded file isn't an image we display an alert error and exit the function by returning false.

Next, using the FileReader API, we load the image as a data url which is a base64 encoded version of the image that we can use as an image source. Using the onloadend property of the file reader we wait until the image has fully loaded then pass the result into the renderImage function.

renderImage

The renderImage function takes the image source provided by the imageSelected function and displays it on the page.

function renderImage(src) {
	
	if(typeof resizeContainer == 'undefined') {
		if (input.getAttribute('data-container')) {
			resizeContainer = document.getElementById(input.getAttribute('data-container'));
			if(resizeContainer.className.indexOf("resize-container") == -1) {
				resizeContainer.className += " resize-container";
			}
		} else {
			resizeContainer = document.createElement('div');
			input.parentElement.insertBefore(resizeContainer, input);
			resizeContainer.className = "resize-container";
		}
		resizeContainer.style.width = dimensions[0]+"px";
		resizeContainer.style.height = dimensions[1]+"px";
	}		

	while (resizeContainer.children.length) {
		resizeContainer.removeChild(resizeContainer.children[0]);
	}

	image = document.createElement('img');
	image.style.left = "0px";
	image.style.top = "0px";
	resizeContainer.appendChild(image);

	image.onload = function() {
		var width = image.width,
		height = image.height,
		asp = width/height,
		nHeight = Math.round(dimensions[0]/asp);
		if(nHeight < dimensions[1]) {
			image.style.height = (dimensions[1])+"px";
			image.style.width = (Math.round(dimensions[1]*asp))+"px";
		} else {
			image.style.width = (dimensions[0])+"px";
			image.style.height = (nHeight)+"px";
		}
		image.width = image.style.width.replace("px","");
		image.height = image.style.height.replace("px","");
		setUpMovement();
	}

	image.src = src;
};

The first thing this function does is set up the containing element for the image. If a resizeContainer hasn't already been defined then we check if one has been specified on the input in the data-container attribute, if one has been specified then we use document.getElementById() to store a reference to that element in the resizeContainer variable, we then check if the 'resize-container' class has been set on the element, if it hasn't then we add it. If data-container hasn't been used then we create a new div, insert it into the HTML before the file input then set it's class to 'resize-container'. Once an element has been found to use as the container we set it's height and width using the dimensions array, and finally we loop through all the children in the container removing them, making room for our image.

Next we create our image element, this will be absolutely positioned within the container so we set the left and top values to 0px locking it in the upper left of the container. We then use appendChild to add the image element to the container element.

The next step is to calculate the values we need to give the height and width of the image based on the dimensions we're resizing it to. Firstly we need to find the original height and width of the image, to do that we need to wait for the image to load. This can be done by using the "onload" property of the image element which is triggered when the image has fully loaded, we create this function before setting the source of the image element so we can be sure that the function triggers when the image has loaded. Once the image has loaded we take the width and height then calculate the aspect ratio of the image, asp = width/height, with that we then calculate how high the image would be if the width was set to the width we're resizing the image to nHeight = Math.round(dimensions[0]/asp);, a check is then made to make sure that this new height is not smaller than the height we're resizing the image to, if it is smaller then we set the height of the image to the height we're resizing to then calculate and set the image width using the aspect ratio, image.style.width = (Math.round(dimensions[1]*asp))+"px";

The last thing this function does is set the height and width properties of the image element so we can easily reference them later then calls the setUpMovement() function.

setUpMovement

The purpose of the setUpMovement function will be to set up all the event handlers and variables required to control moving the image.

function setUpMovement() {
	curPos = [0,0];
	mouseDown = false;
	widthDif = image.width - dimensions[0];
	heightDif = image.height - dimensions[1];
	image.addEventListener('mousedown',startMove);
	image.addEventListener("mousemove",move);
	image.addEventListener('mouseup',endMove);
	image.addEventListener('mouseout',endMove);
};

To control moving the position of the image we first need to set the values of some of our variables. The first, curPos, is set to an array holding the coordinates of the position of top left corner of our image, to begin with this is set to '0,0', or 0px on the x axis and 0px on the y axis.

The mouseDown variable is a simple boolean letting the code know if the user is currently clicking on the image.

The widthDif and heightDif variables are set to the difference between the actual size of the image and the size we're resizing it to.

Next we add our event listeners to the image, we need 4 in total to handle the 4 mouse events; clicking the mouse down, moving the mouse around, releasing the mouse and moving the mouse off of the image. These events are handled by the final 3 functions.

startMove

The startMove function is called whenever the user clicks on the image. The purpose of this function is to determine where the user clicked on the image and to inform the code that the user is currently holding their mouse on the image.

function startMove(e) {
	e.offsetY = (e.offsetY ? e.offsetY : e.layerY);
	e.offsetX = (e.offsetX ? e.offsetX : e.layerX);
	mouseDown = true;
	startPos = [e.offsetX,e.offsetY];
	e.preventDefault();
}

The position of the click on the image is worked out using either the offsetY and offsetX or the layerY and layerX properties of the event object, we need to check if offsetY and offsetX are available as not all browsers have it available. We store the position of the click in the startPos array for later use. By setting the mouseDown variable to true the rest of the code now knows that the user is holding their mouse down on the image.

Now that we can detect when a user clicks on the image and keep a record of where they make that click we can move on to the next function, move.

move

The move function will use the current mouse position and the original position of the mouse to calculate how far the mouse has moved and reposition the image accordingly.

function move(e) {
	if(mouseDown) {
		e.offsetY = (e.offsetY ? e.offsetY : e.layerY);
		e.offsetX = (e.offsetX ? e.offsetX : e.layerX);
		
		var newY = curPos[1] + (e.offsetY - startPos[1]);
		var newX = curPos[0] + (e.offsetX - startPos[0]);			

		if(newY < -heightDif) {
			newY = -heightDif;
		} else if(newY > 0) {
			newY = 0;
		}

		if(newX < -widthDif) {
			newX = -widthDif;
		} else if(newX > 0) {
			newX = 0;
		}

		image.style.top = newY+"px";
		image.style.left = newX+"px";
		curPos = [newX,newY];
		
		e.preventDefault();
	}
}

The first thing the function does is check that the user is holding their mouse down, this function is triggered whenever the user moves their mouse over the image so we need to make sure that they are also holding down their mouse.

Next we calculate where the images x and y positions would be based on the current position of the image, the mouses current position on the image and the position the mouse was in when we first clicked the image. These new x and y positions are stored in the newX and newY variables.

The next if statement ensures that the Y value that we've calculated is valid, if it is less than the negative of the value of the heightDif variable then we set the newY variable to that value, if it is greater than 0 then we set it to 0. We need to do this to make sure that the user can't drag the image outside the boundary of our resize container. We then do the same thing for the newX variable, making sure it falls into the range of between 0 and the negative of the widthDif variable.

Now that we have valid values for our x and y coordinates we can update the images position, we do this by changing the top and left styles. Finally we update the curPos variable with the new positions.

endMove

The final function in our ImageCropper class is endMove. This function is triggered whenever the user either stops dragging the image or moves their mouse off of the image. The purpose of this function is to inform the code that we're no longer dragging the image and to update the value of the coordinates input.

function endMove(e) {
	mouseDown = false;
	coordsInput.value = JSON.stringify(curPos);
	e.preventDefault();
}

This is probably the simplest of the functions, it sets the mouseDown variable to false, stopping the image from moving when you move our mouse over it, and uses the JSON.stringify() function to convert the curPos array into a string while setting the value of the coordsInput field to the result.

With that function in place our JavaScript is now complete. Below is a full example of setting up a file input for resizing an image.

function ImageCropper(input) {
	
	function startMove(e) {
		e.offsetY = (e.offsetY ? e.offsetY : e.layerY);
		e.offsetX = (e.offsetX ? e.offsetX : e.layerX);
		mouseDown = true;
		startPos = [e.offsetX,e.offsetY];
		e.preventDefault();
	}
	function move(e) {
		if(mouseDown) {
			e.offsetY = (e.offsetY ? e.offsetY : e.layerY);
			e.offsetX = (e.offsetX ? e.offsetX : e.layerX);
			
			var newY = curPos[1] + (e.offsetY - startPos[1]);
			var newX = curPos[0] + (e.offsetX - startPos[0]);			

			if(newY < -heightDif) {
				newY = -heightDif;
			} else if(newY > 0) {
				newY = 0;
			}

			if(newX < -widthDif) {
				newX = -widthDif;
			} else if(newX > 0) {
				newX = 0;
			}

			image.style.top = newY+"px";
			image.style.left = newX+"px";
			curPos = [newX,newY];
			
			e.preventDefault();
		}
	}
	function endMove(e) {
		mouseDown = false;
		coordsInput.value = JSON.stringify(curPos);
		e.preventDefault();
	}

	function setUpMovement() {
		curPos = [0,0];
		mouseDown = false;
		widthDif = image.width - dimensions[0];
		heightDif = image.height - dimensions[1];
		image.addEventListener('mousedown',startMove);
		image.addEventListener("mousemove",move);
		image.addEventListener('mouseup',endMove);
		image.addEventListener('mouseout',endMove);
	};

	function renderImage(src) {
		
		if(typeof resizeContainer == 'undefined') {
			if (input.getAttribute('data-container')) {
				resizeContainer = document.getElementById(input.getAttribute('data-container'));
				if(resizeContainer.className.indexOf("resize-container") == -1) {
					resizeContainer.className += " resize-container";
				}
			} else {
				resizeContainer = document.getElementsByClassName('resize-container');
				if (resizeContainer.length) {
					resizeContainer = resizeContainer[0];
				} else {
					resizeContainer = document.createElement('div');
					input.parentElement.insertBefore(resizeContainer, input);
					resizeContainer.className = "resize-container";
				}
			}
			resizeContainer.style.width = dimensions[0]+"px";
			resizeContainer.style.height = dimensions[1]+"px";
		}		

		while (resizeContainer.children.length) {
			resizeContainer.removeChild(resizeContainer.children[0]);
		}

		image = document.createElement('img');
		image.style.left = "0px";
		image.style.top = "0px";
		resizeContainer.appendChild(image);

		image.onload = function() {
			var width = image.width,
			height = image.height,
			asp = width/height,
			nHeight = Math.round(dimensions[0]/asp);
			
			if(nHeight < dimensions[1]) {
				image.style.height = (dimensions[1])+"px";
				image.style.width = (Math.round(dimensions[1]*asp))+"px";
			} else {
				image.style.width = (dimensions[0])+"px";
				image.style.height = (nHeight)+"px";
			}
			image.width = image.style.width.replace("px","");
			image.height = image.style.height.replace("px","");
			setUpMovement();
		}

		image.src = src;
	};

	function imageSelected(e) {
		var file = input.files[0],
		type = file.type.split('/');
		if(type[0] !== 'image') {
			alert('You need to choose an image');
			input.value = '';
			return;
		}
		var reader = new FileReader();
		reader.onloadend = function(e) {
			renderImage(e.target.result);
		}
		reader.readAsDataURL(file);
	}
	
	if(!window.FileReader) return false;

	var input = input,
		mouseDown,
		startPos,
		image,
		curPos,
		coordsInput = document.createElement('input'),
		dimensions = input.getAttribute("data-dimensions").split("x"),
		dimensionsInput = document.createElement('input'),
		heightDif,
		widthDif,
		resizeContainer;
	
	dimensionsInput.name = "dimensions";
	dimensionsInput.type = "hidden";
	input.parentElement.insertBefore(dimensionsInput,null);
	dimensionsInput.value = JSON.stringify(dimensions);
	coordsInput.type = "hidden";
	coordsInput.name = "coords";
	input.parentElement.insertBefore(coordsInput,null);

	input.addEventListener('change',imageSelected);
}
<div id='resize-container'>
	<img src='default-image.png'/>
</div>

<input type='file' name='image' id='resize-input' data-dimensions='400x250' data-container='resize-container'>
new ImageCropper(document.getElementById('resize-input'));

That's part one of the tutorial complete, coming soon will be part two which will cover uploading the file to the server and resizing it using PHP.