Monday, February 21, 2011

Crop and Upload Image - Client Side

I want to be able to extend my moving icon program to allow a user to upload their own icon. However, I want to give the user the ability to crop the image so they just upload the portion that they want. Here is how I do that.

Technologies Used
I use the HTML 5 file capabilities to upload the image to the browser. I use the Jcrop plugin to jQuery to actually specify the crop region. I also leaned heavily on another demo I found on the web (http://www.pandamatak.com/people/anand/blog/2010/11/clientside_image_crop_and_then.html) to figure out how to do it, though I have tweaked it some.

HTML
First you need a place to drag the image too.
<div id='cropWidget' height='400' width='400'>  
  <div id='surface'></div>  
  <canvas id='canv1' height='400' width='400'></canvas>  
</div>
The surface is where an image can be dragged from your file system and dropped to. canv1 is the canvas object that will handle image manipulation.

Next we need a place to show the cropped portion of the image.
<canvas id='canv2' height='200' width='200'></canvas>  
The only other portion is a control to put the content, two text controls which will show the size of the cropped image, and a button to upload the image.
<input id="content64" name="content64" type="hidden" />
<div class='field'>  
  <label for="width">Width</label>  
  <br /> 
  <input id="width" name="width" readOnly="true" size="5" type="text" />  
</div>  
<div class='field'>  
  <label for="height">Height</label>  
  <br /> 
  <input id="height" name="height" readOnly="true" size="5" type="text" />  
</div>  
<div class='actions'>  
  <input disabled="disabled" id="imageSubmit" name="commit" 
         onClick="uploadSelection();" type="submit" value="Create Image" />  
</div>
There are a couple of things to note here. First the "readOnly" attribute on the text inputs. The user won't be entering the width and height, they will be calculated by JavaScript. Also, the imageSubmit button is disabled. It does not become enabled until an image has been uploaded. And then on click, the JavaScript function uploadSelection will be called.

CSS
The surface element and the canv1 element need to be superimposed on top of each other for this to look right. This CSS accomplishes this, plus adds a little bit of styling.
<style>  
  #cropWidget {position:relative; width 400px; height:400px; padding:5px;} 
  #surface {position:absolute; top:0px; left: 0px; background:blue;  
            width:400px; height:400px; z-index:100; opacity: 0.50;} 
  #canv1 {position:absolute; top:0px; left: 0px; border:1px solid blue; 
          width:400px; height:400px;} 
  #canv2 {position:relative; border:1px solid yellow; width:200px; height:200px;} 
</style>

JavaScript
Here is all of the javascript. I will explain the parts below.
  1 var cropWidget = document.getElementById('cropWidget');
  2 var surface = document.getElementById('surface');
  3 var canv1 = document.getElementById('canv1');
  4 var canv2 = document.getElementById('canv2');
  5 var imageSubmit = document.getElementById('imageSubmit');
  6 var ctx1 = canv1.getContext('2d');
  7 var ctx2 = canv2.getContext('2d');
  8  
  9 function resize(comp, width, height) { 
 10   comp.width = width; 
 11   comp.height = height; 
 12   comp.style.width = img.width + 'px'; 
 13   comp.style.height = img.height + 'px';
 14 } 
 15  
 16 function displayImage(img) { 
 17   resize(canv1, img.width, img.height); 
 18   resize(surface, img.width, img.height); 
 19   resize(cropWidget, img); 
 20   while(surface.childNodes[0]) surface.removeChild(surface.childNodes[0]);
 21   surface.appendChild(img); 
 22   ctx1.drawImage(img, 0, 0, img.width, img.height);
 23   jQuery("#" + img.id).Jcrop({ aspectRatio: 1, onSelect: cropImage });
 24 } 
 25  
 26 function loadImg(imgFile) { 
 27   if (!imgFile.type.match(/image.*/)) return;
 28   var img = document.createElement("img"); 
 29   img.id = "pic"; 
 30   img.file = imgFile; 
 31  
 32   var reader = new FileReader();
 33   reader.onload = function(e) { 
 34     img.onload = function() { displayImage(img); }; 
 35     img.src = e.target.result; 
 36     resize(canv2, 200, 200);
 37   }; 
 38   reader.readAsDataURL(imgFile); 
 39 } 
 40  
 41 function cropImage(c) { 
 42   var w = c.x2-c.x; 
 43   var h = c.y2-c.y; 
 44   resize(canv2, w, h); 
 45   ctx2.drawImage(canv1, c.x, c.y, w, h, 0, 0, w, h);
 46   $("#width").val(w); 
 47   $("#height").val(h); 
 48   imageSubmit.disabled = false; 
 49 } 
 50  
 51 function uploadSelection() { 
 52   var imgData = document.getElementById('canv2').toDataURL("image/png"); 
 53   $("#content64").val(imgData); 
 54   alert("This would've upload your image to a server, if there was one."); 
 55   return false;
 56 } 
 57  
 58 surface.addEventListener("dragover", function(e) {e.preventDefault();}, true); 
 59 surface.addEventListener("drop", function(e) {e.preventDefault();  
 60                                            loadImg(e.dataTransfer.files[0]);},
 61                          true); 
 62 ctx1.fillText("Drop image here", 60, 100); 
 63 ctx2.fillText("Preview", 60, 100);
Lines 1-5 just create variables for the various HTML objects, and 6-7 get the drawing contexts for the canvases. resize (lines 9-14) is a helper method for changing the size of an HTML component. Lines 12-13 resize the display of the component, while lines 10-11 resize its internal structures, if the component is a canvas. displayImage (lines 16-24) first resizes the HTML components to fit the image. Then any previous image is removed from the surface. Then the image is added to the surface and drawn to the canvas. Finally jcrop is called to enable cropping of this image, calling the function cropImage when a region is selected. cropImage (lines 41-49) resizes canv2 and then draws the portion of the image onto this canvas. Then it stores the width and height into the HTML width and height components and enables the submit button.

Jumping to the end, lines 62-63 put some text onto the canvases. Lines 58-61 enable the drag and drop functionality, setting loadImg as the function that is called when a file is dropped. loadImg (26-39) first ensures that it is actually an image being dropped. Then it creates an HTML <img> tag to contain the image. Line 32 creates an HTML5 FileReader and line 38 reads the file that was dropped. The function defined on lines 33-37 is called when the file has been read. Line 35 sets the source of the <img> tag that was created a few lines above. Line 34 calls the displayImage function once the image has been loaded fully by the browser, and line 38 resizes the crop canvas to a fixed size of 200x200, which also has the effect of erasing any previous image drawn on it.

That is basically the extent of the drag and drop functionality. The only remaining method is uploadSelection (lines 51-56) which is called when the "Create Image" button is clicked. This function gets the base64 encoding of the cropped image and copies it to the HTML element content64 where, if this were actually connected to a server, it would be posted as part of the form back to the server.

Demo
Here is what all of this looks like.

Image Cropper

Drag and drop image here:

Cropped image is here:

16 comments:

ghthor said...

Awesome tutorial man. Your code saved me a lot of work.

Michael Haddox-Schatz said...

I am glad that you found it useful.

george said...

Hi,

Great example!. I'm trying to get working jCrop + HTML5 (image canvas) with no success.

Do you know how to do it?. I succeed only with your way (using File Capabilities) but I don't want to use File Capabilities :/

Thanks

Michael Haddox-Schatz said...

george, just to understand, you want the client to use jCrop on an image, but you don't want the user to choose the image from their file system? I assume the image is coming from a URL then, presumably your web site. Is this correct?

george said...

Exactly, I just want to apply jCrop to an image generated using HTML5 with Canvas.

Something like this: https://gist.github.com/401510fdb360a69355c1

I've tried lot of times with no success :/

Michael Haddox-Schatz said...

I am guessing your issue is that javascript is executing before the image has been downloaded. What if you wrap lines 4-8 in an onload and then set the src afterwards?
i.e. something like:
img.onload = function() {
document.getElementById('surface').appendChild(img);
ctx = document.getElementById('canv1').getContext('2d');
ctx.drawImage(x, 0, 0,400,400);
jQuery('#pic').Jcrop({ aspectRatio: 1});
}
img.src = "<url here>"

Michael Haddox-Schatz said...

Also, based on your code snippet, shouldn't the draw image line be:

ctx.drawImage(img, 0, 0, 400, 400);

i.e. variable 'img', rather than 'x'?

george said...

Yes, just a fast copy & paste.

This is a simple example: https://gist.github.com/d598ff731242b618fc0d

Canvas image and img are being rendered and jCrop is only applied to 'img' and not Canvas as I would want.

Not working yet :/

george said...

btw, i'm wasting your time, i would like to transfer a few bucks to you. paypal?

Michael Haddox-Schatz said...

What doesn't work for you? I was able to get it to work with some slight tweaking. Basically, I think you can take the code from this post and just tweak it so that instead of having a loadImg function, that code executes on page load, and of course, get rid of the file reader.

Michael Haddox-Schatz said...

In particular, I added a <canvas id='canv2' height='200' width='200'></canvas> element to the HTML that you posted, and modified the javascript to look like:
var img = new Image();
function resize(comp, width, height) {
comp.width = width;
comp.height = height;
comp.style.width = width + 'px';
comp.style.height = height + 'px';
}

$(function() {

var canv2 = document.getElementById('canv2');
var ctx2 = canv2.getContext('2d');

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var surface = document.getElementById('surface');

function cropImage(c) {
var w = c.x2-c.x;
var h = c.y2-c.y;
resize(canv2, w, h);
ctx2.drawImage(canvas, c.x, c.y, w, h, 0, 0, w, h);
}

img.id = "pic";
img.onload = function() {
document.getElementById('surface').appendChild(img);
resize(canvas, img.width, img.height);
resize(surface, img.width, img.height);
ctx.drawImage(img, 0, 0, img.width, img.height);
jQuery('#pic').Jcrop({ aspectRatio: 1
, onSelect: cropImage
});
}
img.src = "<URL>";

george said...

Thanks Michael,

I got your example working :) but is not what I wanted.

I tried to create the visual effect that jCrop does using only Canvas and in only one canvas element.

I don't think is possible without doing some pixel processing. I will go that way.

Thanks for all your help again!

Michael Haddox-Schatz said...

I guess I don't completely understand what you want. Without the second canvas, the jCrop select still showed up for me. What do you want to have happen upon select? Do you want it to replace the image in the first canvas? (either shrinking or like a zoom?) Or do you want some other behavior?

Author said...

Thanks for the post..

html5 image crop tool

Anonymous said...

Thanks for the awesome solution. The little thing I want to ask is, what if there is a file type input field. input type="file" like this, can i use the same logic like this?

Anonymous said...

I'm looking for a tutorial like this, thanks. Can you also show how to do this on multiple images?