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:

32 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?

Anonymous said...

Great Blog.. Keep it up..

Compress JPG Online Free
Online Compress JPG
Compress JPG Online

seoa2 said...

Thanks for this helpful Blog.
I used a WordPress plugin that I found interesting. So I would like to recommend it.
The plugin helps me quickly create and customize images before inserting them in my blog post.
Check at: https://getpikiz.com/wordpress-plugin/

Dilliprasad said...

I'm on the fence about this, while more customization is good, I have a feeling this is a "in-progress" update, it just feels incomplete and half-way there.
We use badge layout for apps on design approvals (visual projects), so the image being displayed is important. Old layout "feels like" it had larger images,
maybe because the images were cropped more loosely so it's easier to tell which project it was at quick glance. Now the image is cropped closer, making it
harder to scan thru at quick glance. I find myself needing to click into the project more often than usual. Which makes the whole user experience less
efficient.
I have a couple suggestions that might make it work better:
1. Increase the height of the window the cover image is being displayed.
2. Let us to choose which image to be displayed as "cover" (like how Pinterest handles cover images of each board, was hoping for this for a long time)
3. Let us adjust which part of the image to show and how tight or loose the crop is (with a fixed window, let us move the image around and maybe enlarge or
shrink it to control what shows thru the window. Pinterest does a limited form of this, which is very useful in making the cover image relevant)
4. Allow Cover Image to be ordered in different hierarchy (currently every element can be ordered differently except the Cover Image, it seems to be stuck
in the 2nd spot, would like the option to set it on another spot in the layout. This one seems like an easy fix, since you guys allow that for every other
element already)

Dilliprasad said...

I'm on the fence about this, while more customization is good, I have a feeling this is a "in-progress" update, it just feels incomplete and half-way there.
We use badge layout for apps on design approvals (visual projects), so the image being displayed is important. Old layout "feels like" it had larger images,
maybe because the images were cropped more loosely so it's easier to tell which project it was at quick glance. Now the image is cropped closer, making it
harder to scan thru at quick glance. I find myself needing to click into the project more often than usual. Which makes the whole user experience less
efficient.
I have a couple suggestions that might make it work better:
1. Increase the height of the window the cover image is being displayed.
2. Let us to choose which image to be displayed as "cover" (like how Pinterest handles cover images of each board, was hoping for this for a long time)
3. Let us adjust which part of the image to show and how tight or loose the crop is (with a fixed window, let us move the image around and maybe enlarge or
shrink it to control what shows thru the window. Pinterest does a limited form of this, which is very useful in making the cover image relevant)
4. Allow Cover Image to be ordered in different hierarchy (currently every element can be ordered differently except the Cover Image, it seems to be stuck
in the 2nd spot, would like the option to set it on another spot in the layout. This one seems like an easy fix, since you guys allow that for every other
element already)

kevin antony said...

This is a nice article here with some useful tips for those who are not used-to comment that frequently. Thanks for this helpful information I agree with all points you have given to us. I will follow all of them.
best rpa training in bangalore
rpa training in bangalore | rpa course in bangalore
RPA training in bangalore
rpa training in chennai
rpa online training

Unknown said...

Great content thanks for sharing this informative blog which provided me technical information keep posting.
Python Online certification training
python Training institute in Chennai
Python training institute in Bangalore

ragul ragul said...

Very nice post here and thanks for it .I always like and such a super contents of these post.Excellent and very cool idea and great content of different kinds of the valuable information's.
AWS training in sholinganallur
AWS training in Tambaram
AWS training in Velachery

Jaweed Khan said...

Thanks For Sharing The Information The information shared Is Very Valuable Please Keep Updating Us Time just went On reading The article Python Online Training Aws Online Training Hadoop Online Training Data Science Online Training

Nithya Sri said...
This comment has been removed by the author.
killerfanthose said...

I'm on the fence about this, while more customization is good, I have a feeling this is a "in-progress" update, it just feels incomplete and half-way there.
We use badge layout for apps on design approvals (visual projects), so the image being displayed is important. Old layout "feels like" it had larger images,
maybe because the images were cropped more loosely so it's easier to tell which project it was at quick glance. Now the image is cropped closer, making it
harder to scan thru at quick glance. I find myself needing to click into the project more often than usual. Which makes the whole user experience less
efficient.
I have a couple suggestions that might make it work better:
1. Increase the height of the window the cover image is being displayed.
2. Let us to choose which image to be displayed as "cover" (like how Pinterest handles cover images of each board, was hoping for this for a long time)
3. Let us adjust which part of the image to show and how tight or loose the crop is (with a fixed window, let us move the image around and maybe enlarge or
shrink it to control what shows thru the window. Pinterest does a limited form of this, which is very useful in making the cover image relevant)
4. Allow Cover Image to be ordered in different hierarchy (currently every element can be ordered differently except the Cover Image, it seems to be stuck
in the 2nd spot, would like the option to set it on another spot in the layout. This one seems like an easy fix, since you guys allow that for every other
element already)

Keerthana said...

All blogs contains a Highly Informative content and it is very helpful to all. Thanks for all your Efforts and Keep sharing

python training in chennai | python training in annanagar | python training in omr | python training in porur | python training in tambaram | python training in velachery

rocky said...

I like that article. Is very nice content.
Python Training in Chennai

Python Training in Bangalore

Python Training in Hyderabad

Python Training in Coimbatore

Python Training

python online training

python flask training

python flask online training



manasha said...

Great post. keep sharing such a worthy information.
Google Analytics Training In Chennai
Google Analytics Online Course


Reshma said...

This post is so interactive and informative.keep update more information...
Importance of Azure
Important reason for Using Microsoft Azure

Matt Reeves said...

https://vetacorporate.blogspot.com/2016/08/professional-english-training.html?showComment=1648720503695#c1319348901047996846

rathna priya said...

ery nice Post!!! Keep sharing

Dot Net Training in Chennai
Dot Net Training Online
Dot Net Training in Bangalore