Steven Berridge

Web Developer

Facebook Style Image Cropper - Part 2

In Part 1 of this tutorial we covered setting up the JavaScript to let a user choose how they want an image to be cropped when resizing it to a certain size.

Part 2 will cover the server side part which will take the users selection, resize and crop their image then display it back to the user.

Example

The HTML Form

Below is the form we'll be using the send our image to the server to be cropped.

<form method="post" action="crop.php" enctype="multipart/form-data>
	<div id="container">
		<img src="http://placehold.it/300x200" alt="">
	</div>
	<input type="file" id="file" name="image" data-container="container" data-dimensions="300x200">
	<script>
		(function() {
			new ImageCropper(document.getElementById("file"));
		})();
	</script>
	<input type="submit" value="crop">
</form>

Note that we've given the form an enctype of "multipart/form-data", this is required in order to send files to the server.

The PHP Class

To keep all the code for this project contained we'll create a class that'll handle most of the work. Below is the skeleton for our ImageCropper PHP class.

class ImageCropper {

	private $image;
	private $dimensions;
	private $coordinates;
	private $originalDimensions;
	private $ratio;
	private $ext;
	private $mime;

	public function __construct($path) {
		
	}

	private function createImage($path,$mime) {
		
	}

	public function resize($width, $height) {
		
	}

	public function crop($x,$y) {
		
	}

	private function renderImage() {
		
	}

	public function save($path) {
		
	}

	public function render() {
		
	}

	public function download($filename) {
		
	}

}

The class is made up of 7 properties and 8 methods, these will be defined and built as we go through the tutorial. To begin with we'll start with the __construct function.

__construct()

The construct method is automatically called whenever a new instance of a class is created giving you space to perform any tasks needed to prepare the class. We'll be using the function to gather together some information about the uploaded image.

public function __construct($path) {
	$info = getimagesize($path);
	if($info === false) {
		throw new Exception('Provided file is not an image');
	}
	$this->originalDimensions = array($info[0],$info[1]);
	$this->ratio = $info[0]/$info[1];
	$this->mime = $info['mime'];
	$mime = explode('/',$this->mime);
	$ext = $mime[1];
	$this->ext = $ext;
	$this->image = $this->createImage($path,$info['mime']);
}

The method accepts a single argument called $path which holds the path to the image, by using that with with getimagesize() function we can, firstly, determine whether or not the path passed into the function leads to an actual image file and secondly get some information about the image, such as it's dimensions and mime type.

If we weren't given an actual image then $info will contain false, if that is the case then we throw an exception which we will later be able to catch to display an appropriate message to the user.

If we do have an image then we can begin defining some of our classes properties. The first 2 items in the $info variable are the dimensions of our image, the width and height respectively, we take those and place them in an array then store that in the originalDimensions property. We then work out the aspect ratio of the image by dividing the width by the height, storing that in the ratio property. Along with the width and height of the image, the getimagesize() function also, oddly, returns the mime type of the image which we can use later to work out what type of image we're working with, we store that in the mime property.

Using the mime type of the image we can figure out what image format we are working with, to do that we first need to use the explode() function to split the mime string into an array containing two parts, the type and the subtype of the file, with the type being "image" and the subtype being the type of image, such as png or gif. We store the type of image we're working with in the $ext variable and then set the ext property of our class so we can reference it in a later function. The last thing this function does is call the next method in the class, createImage, which will use the GD library to create the image, setting the image property in the process.

createImage()

The createImage function, when complete, will take a file path and use that to generate an image using the GD library which can later be manipulated in various ways using PHP.

private function createImage($path) {
	switch($this->ext) {
		case 'png':
			return imagecreatefrompng($path);
		case 'jpg':
		case 'jpeg':
			return imagecreatefromjpeg($path);
		case 'gif':
			return imagecreatefromgif($path);
		default:
			throw new Exception('Invalid image filetype used');
	}
}

The GD library, awkwardly, has a whole bunch of different functions for creating images, the one we need depends on which image format we're using. Using a switch statement we can compare the $ext variable against possible file types until we get a match, when we do we use the appropriate function to create the image, for example if our image is a png then we use imagecreatefrompng($path);, if none of the file types match then we throw an exception showing that the file given wasn't a valid image file. Once the correct file type is found the created GD image is returned to the __construct function, setting the image property of the class.

resize()

The resize method is where we start getting into some basic image manipulation. This function will take a width and height and resize the image to roughly those dimensions.

public function resize($width, $height) {
	$this->dimensions = array((int)$width,(int)$height);
	$newWidth = $this->dimensions[0];
	$newHeight = round($newWidth/$this->ratio);
	if($newHeight < $this->dimensions[1]) {
		$newHeight = $this->dimensions[1];
		$newWidth = round($newHeight*$this->ratio);
	}
	$img = imagecreatetruecolor($newWidth, $newHeight);
	imagecopyresampled(
		$img, 
		$this->image, 
		0, 
		0, 
		0, 
		0, 
		$newWidth, 
		$newHeight, 
		$this->originalDimensions[0], 
		$this->originalDimensions[1]
	);
	$this->image = $img;
}

The first thing we do is take the width and height variables passed into the function and place them in an array, we use (int) to convert the width and height to integers incase they were passed in as strings, then we assign that array to the dimensions property of the class. Next we need to figure out the new width and height of the image, to do that we first assume that the image will be the width that we're resizing to, we then calculate how high the image would be if that was the width using the aspect ratio of the image. Next we check if the newly calculated height is less than the height we're resizing to, if it is then we know that the image will be too small that way around so we do the opposite, setting the new height to be the height we're resizing to and calculating the width using the aspect ratio.

The imagecreatetruecolor function can be used to create a blank image of a specified width and height which can then be "drawn" on using other image functions to generate the picture. For example here we create a blank image that's the width and height that we're resizing our image to, we then copy the a resized version of the supplied image onto this blank image using the imagecopyresampled function.

The imagecopyresampled function is a fairly complicated function, it takes 10 parameters (listed below) which define how the image is copied.

Destination Image
The image we're copying to, in this case the image we created with imagecreatetruecolor.
Source Image
The image we're copying, which in this case is the image we created in the __construct function from the one uploaded by the user.
Destination X
The position from the left where we're placing the copied section on the destination image. We want to place it on the far left so we're setting this to 0.
Destination Y
The position from the top where we're placing the copied section on the destination image. We want to place it at the top so we're setting it to 0.
Source X
The position from the left where we want to copy from on the source image. We want to copy everything so we set this to 0.
Source Y
The position from the left where we want to copy from on the source image. We want to copy everything so we set this to 0.
Destination Width
How wide the copied region will be on the destination image. If this is a different size to the source width then the region will be resized. We set this to our newly calculated width, stored in $newWidth which will resize the image to the correct width.
Destination Height
How high the copied region will be on the destination image. If this is a different size to the source height then the region will be resized. We set this to our newly calculated height, stored in $newHeight which will resize the image to the correct height.
Source Width
How much of the source image we want to copy across the X axis. For example if 0 is given for the source X parameter and 100 is given for the source width parameter then everything within the range of 0px to 100px from the left will be copied. Since we want to copy everything we set this to the original images width, which is stored in the originalDimensions array.
Source Height
How much of the source image we want to copy across the Y axis. For example if 0 is given for the source Y parameter and 100 is given for the source height parameter then everything within the range of 0px to 100px from the top will be copied. Since we want to copy everything we set this to the original images height, which is stored in the originalDimensions array.

Once the image has been resized we overwite the image property with the resized version.

crop()

The crop function will take the roughly resized image created by the resize function and crop it to the correct dimensions.

public function crop($x,$y) {
	$img = imagecreatetruecolor($this->dimensions[0], $this->dimensions[1]);
	imagecopyresampled(
		$img, 
		$this->image, 
		0, 
		0, 
		abs($x), 
		abs($y), 
		$this->dimensions[0], 
		$this->dimensions[1], 
		$this->dimensions[0], 
		$this->dimensions[1]
	);
	$this->image = $img;
}

This function is fairly similar to the resize function, first of all we make an image using the imagecreatetruecolor function, the dimensions we pass into this function are those we want the image to be when fully cropped, which we stored in the dimensions property.

Next we once again use the imagecopyresampled function to crop the image to it's final size. The parameters passed into this function are explained below.

Destination Image
We're copying the cropped section of the image onto the blank image we created using imagecreatetruecolor, which is stored in the $img variable.
Source Image
The image we're cropping from is the resized version of the image supplied by the user, which is stored in the image property of the class.
Destination X
We want the cropped section to be placed on the far left so we set this to 0.
Destination Y
We want the cropped section to be placed at the top so we set this to 0.
Source X
We set the source X to be the position from the left the user chose when cropping the image, we use the abs function to make sure the number used is positive.
Source Y
We set the source Y to be the position from the top the user chose when cropping the image, we use the abs function to make sure the number used is positive.
Destination Width & Source Width
Both the destination and the source width are set to the same value, which is the width we're cropping the image to stored in $this->dimensions[0].
Destination Height & Source Height
Both the destination and the source height are set to the same value, which is the height we're cropping the image to stored in $this->dimensions[1].

At this point the image has been fully resized and cropped to the users specifications, the last thing to do in this function is to overwrite the classes image property with the cropped version.

All that's left to do now is add ways to present this image back to the user.

renderImage()

The renderImage function will, similarly to the createImage function, work out the correct GD image function to use based on the images extension, then use that to output the images raw data to the browser.

private function renderImage($path=null) {
	$function = null;
	switch($this->ext) {
		case 'png':
			$function = "imagepng";
			break;
		case 'jpg':
		case 'jpeg':
			$function = "imagejpeg";
			break;
		case 'gif':
			$function = "imagegif";
			break;
		default:
			throw new Exception('Invalid image filetype used');
	}
	$function($this->image,$path);
}

As you can see, a switch statement is used to choose the correct function. That function is then executed using the image stored in the image property. An optional file path can be given which will save that image to a location on the server.

render()

The render function will take the image we've cropped and display it to the user in their browser.

public function render() {
	header('Content-Type: '.$this->mime);
	$this->renderImage();
}

In order for the browser to display the image we need to tell it what kind of content we want it to show, to do this we need to set the content type header to the mime type of the image. This tells the browser that the information it is showing is supposed to be an image and it will display it accordingly.

Once the content type header has been set we can use the renderImage function which will call the appropriate GD image function outputting the raw image data, the browser will then take that data and use it to display the image.

download()

This function will download the image to the users PC.

public function download($filename) {
	header('Content-Disposition: attachment; filename='.$filename.'.'.$this->ext);
	$this->renderImage();
}

The download function takes a single parameter which is the filename we want to use when saving the image to the users PC. The header function is then used to tell the browser that we want it to save the data to the users PC, using content disposition, and which filename we want it to use. We then use the renderImage function again to output the image data so the browser can save it.

save()

The save function will simply save the cropped image to the server.

public function save($path) {
	$this->renderImage($path.'.'.$this->ext);
}

This is by far the simplest function in the class, it takes a file path and saves the image in that location using the renderImage function.

With the function in place the class is now complete.

Using the class

Now that the class is complete we just need to add some code that takes the data from the form and uses the class to crop the image.

$image = $_FILES['image'];
list($width,$height) = json_decode($_POST['dimensions']);
list($x,$y) = json_decode($_POST['coords']);

$crop = new ImageCropper($image['tmp_name']);
$crop->resize($width,$height);
$crop->crop($x,$y);
$crop->save('/var/www/mysite/public_html/uploads/cropped');

$crop->render();
/* OR */
$crop->download('cropped');

So here we're putting the ImageCropper class to use. We first take the uploaded image out of the $_FILES global array, then using the list function we create variables for the width and height from the dimensions JSON array posted to the server. Using the same method we also create variables for the x and y positions set by the user, which are posted to the server in the coords JSON array.

Next we create a new instance of the ImageCropper class, passing it the temporary file path of the uploaded image, this triggers the __construct function.

Using the classes resize function the image is roughly resized to the correct dimensions before it is cropped down to the correct size using the crop function.

Lastly we have some examples of the actions we can take on the finalised image; saving it, rendering it in the browser or saving it to the users computer. Since the render and download functions both modify the page headers, only one of the methods can be used at a time.

Full Example

class ImageCropper {
	private $image;
	private $dimensions;
	private $coordinates;
	private $originalDimensions;
	private $ratio;
	private $ext;
	private $mime;
	public function __construct($path) {
		$info = getimagesize($path);
		if($info === false) {
			throw new Exception('Provided file is not an image');
		}
		$this->originalDimensions = array($info[0],$info[1]);
		$this->ratio = $info[0]/$info[1];
		$this->mime = $info['mime'];
		$mime = explode('/',$this->mime);
		$ext = $mime[1];
		$this->ext = $ext;
		$this->image = $this->createImage($path);
	}
	private function createImage($path) {
		
		switch($this->ext) {
			case 'png':
				return imagecreatefrompng($path);
			case 'jpg':
			case 'jpeg':
				return imagecreatefromjpeg($path);
			case 'gif':
				return imagecreatefromgif($path);
			default:
				throw new Exception('Invalid image filetype used');
		}
	}
	public function resize($width, $height) {
		$this->dimensions = array((int)$width,(int)$height);
		$newWidth = $this->dimensions[0];
		$newHeight = round($newWidth/$this->ratio);
		if($newHeight < $this->dimensions[1]) {
			$newHeight = $this->dimensions[1];
			$newWidth = round($newHeight*$this->ratio);
		}
		$img = imagecreatetruecolor($newWidth, $newHeight);
		imagecopyresampled(
			$img, 
			$this->image, 
			0, 
			0, 
			0, 
			0, 
			$newWidth, 
			$newHeight, 
			$this->originalDimensions[0], 
			$this->originalDimensions[1]
		);
		$this->image = $img;
	}
	public function crop($x,$y) {
		$img = imagecreatetruecolor($this->dimensions[0], $this->dimensions[1]);
		imagecopyresampled(
			$img, 
			$this->image, 
			0, 
			0, 
			abs($x), 
			abs($y), 
			$this->dimensions[0], 
			$this->dimensions[1], 
			$this->dimensions[0], 
			$this->dimensions[1]
		);
		$this->image = $img;
	}
	private function renderImage($path=null) {
		$function = null;
		switch($this->ext) {
			case 'png':
				$function = "imagepng";
				break;
			case 'jpg':
			case 'jpeg':
				$function = "imagejpeg";
				break;
			case 'gif':
				$function = "imagegif";
				break;
			default:
				throw new Exception('Invalid image filetype used');
		}
		$function($this->image,$path);
	}
	public function save($path) {
		$this->renderImage($path.'.'.$this->ext);
	}
	public function render() {
		header('Content-Type: '.$this->mime);
		$this->renderImage();
	}
	public function download($filename) {
		header('Content-Disposition: attachment; filename='.$filename.'.'.$this->ext);
		$this->renderImage();
	}
}

$image = $_FILES['image'];
list($width,$height) = json_decode($_POST['dimensions']);
list($x,$y) = json_decode($_POST['coords']);

$crop = new ImageCropper($image['tmp_name']);
$crop->resize($width,$height);
$crop->crop($x,$y);
$crop->save('/var/www/mysite/public_html/uploads/cropped');

$crop->render();
/* OR */
$crop->download('cropped');