Introduction
Most of the web applications allow users to upload media, especially images - random photographs, avatars etc. Sometimes we might want to provide our users with the feature of cropping their avatars. This is a legacy requirement which has been well translated into pragmatic code. We're not going to re-invent the wheel here. We're going to take a novel/different approach to this requirement. The question is, why do we need to do this? Well, as the title of the article suggests, we're going to crop images client side i.e. in the browser before sending out the image file to the server. This way we're actually able to forgo quite a bit of server side implementation, thereby (comparatively) reducing load on the server.
Image manipulation
There has been considerable work done in the field of image manipulation - cropping, filtering etc. As far as cropping/clipping is concerned, what we require is to let the user select a particular area on the image (that's the area on the image that we need to preserve) and then chop off all remaining areas. Usually this has been achieved by sending the file along with the area to be cropped to the server and then the server does the honors for us.
Traditional Server side implementation
Let's have a look at a sample server side implementation using PHP:
Our use-case stands as: we've uploaded our file onto the server, stored on the file system and then we refer the file and crop it.
<?php
/* crop method takes the fileName on the filesystem along with
* the starting coordinates from where to begin cropping and
* the crop area defined by its width and height
*/
function crop($fileName, $startX, $startY, $width, $height){
$fileContents = file_get_contents($fileName);
if($fileContents){
/*create a blank image with dimensions as that of the cropped area
*this is going to be our final image
*/
$dest = imagecreatetruecolor($width, $height);
$destX = 0;
$destY = 0;
/** create an image identifier from file titled $fileName */
$src = imagecreatefromjpeg($fileName);
/** copy the portion of the $src which needs to be cropped into the $dest */
imagecopyresampled ($dest, $src, $destX, $destY, $startX, $startY, $width, $height, $width, $height);
/** save the file */
imagejpeg($dest, "thumbnail_".$fileName);
}
}
?>
code walkthrough
First we create a blank image whose dimensions are same as that of the selected area by the user. Then we create an image resource which references the image file uploaded to the server. Refer to the image below for visualizing the following cropping process:
We've the top-left coordinates of the selected area on the image along with the extent of the selection. All we need to do is to copy all pixels within the selection area onto our blank image. We'll begin pasting these pixels onto the blank image from its top-left corner i.e. (0, 0). That's exactly what imagecopyresampled method does for us. Note that in case our source image's width/height didn't match that of the destination's width/height, then appropriate shrinking/stretching of the selected area will be done. Once that is done, we're ready to save the new cropped file. Note that this is a very basic implementation of the requirement.
Client Side implementation
The same process needs to done on the client side as well. But as of now, we didn't have enough resources/APIs available which could make such situations conducive to image cropping on the client side. But with the HTML5 <canvas> element, this is definitely possible now. We'll use a canvas element to perform all low level image manipulations. Before we begin, we need to get ourselves acquainted with a few important canvas APIs.
For selecting an area on the image we'll be using a very useful jQuery plugin called imgAreaSelect. We'll also be referring to the HTML5 FileReader API as well. If you're not familiar with the HTML5 FileReader API, please refer to this article. Okay let's begin with designing our HTML.
HTML
<input type='file' name='avatar' /><img src='' width='400px' id='preview' />
JavaScript
$("input[type='file']").bind('change', function(evt){
var file = this.files[0];
var fileReader = new FileReader();
fileReader.onload = function(event){
var originalImage = event.target.result;
$('#preview')
.attr('src', originalImage)
.imgAreaSelect({
aspectRatio: '1:1', //square area selection
handles: true,
onSelectEnd: function(img, selection){
/*selection is an object containing the width, height of selection along with the top-left and
*bottom-right coordinates of the selected rectangle
*For e.g. selection = {
* x1: 12, //top-left X
* y1: 10, // top-left Y
* x2: 145, //bottom-right X
* y2: 178, //bottom-right Y
* width: 133, //selection width
* height: 168 // selection height
* }
var image = new Image();
image.src = originalImage;
image.onload = function(){
var imgObj = this;
var canvas = document.createElement('canvas');
$(canvas)
.attr('width', imgObj.width)
.attr('height', imgObj.height);
var ctx = canvas.getContext('2d');
ctx.drawImage(imageObj, 0, 0, imgObj.width, imgObj.height);
var imageData = ctx.getImageData(
selection.x1,
selection.y1,
selection.width,
selection.height
);
$(canvas)
.attr('width', selection.width)
.attr('height', selection.height);
ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, selection.width, selection.height);
ctx.putImageData(imageData, 0, 0);
var dataUrl = canvas.toDataURL();
/* the dataUrl variable could be set as 'src' attribute of an image and could be sent
* to the server where it could be stored.
*/
}
}
});
}
fileReader.readAsDataURL(file);
});
code walkthrough
We first select a file following which we read it using the FileReader API. Note that the file object is a blob and that it's being accessed/read via the readAsDataURL method which converts the blob into a base64 encoded string. Then we create an Image resource and load the file contents onto the resource. When the image resource is loaded, we allow the user to select an area to crop. Upon the completion of the user's selection we capture the area of selection and load the Image resource onto a canvas element using the drawImage method. From, the canvas we cut out the area whose coordinates match that of the area selected on the original image. Finally we reset our canvas and put back the cropped area (retrieved previously) onto it. The toDataURL method converts the contents rendered on the canvas into a base64 encoded string representing the cropped image. By default, this method generates data in the PNG format which however could be altered. Once we have the data URL representation of the image, this could be sent to the server which then could be stored as a blob or could be written onto a file and then saved in the file system.
Conclusion
This article doesn't preach that image manipulation should be shifted client side. Server side libraries are more exhaustive and provide plethora of features for e.g. ImageMagick PHP extension. However certain features as cropping, applying filters etc. could definitely be shifted downtown and the server could be made more available. We need to bet more on HTML5, at the same time gracefully degrading to other technologies in case of unfavorable environments.
Browser Support
We used the following HTML5 goodies in this article. They are supported in most of the browsers as of now. Further information could be found here:
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.