Resize an Image Using Canvas, Drag and Drop and the File API

By  on  

It's a massive honor to have Tom "Trenkavision" Trenka write a guest post for this blog. Tom was one of the original contributors of the Dojo Toolkit and my mentor at SitePen. I've seen his genius first hand and he's always the first one to foresee issues with a potential solution. He also thinks outside the box, coming up with unconventional but reliable solutions to edge case problems. This is a perfect example.

Recently I was asked to create a user interface that allows someone to upload an image to a server (among other things) so that it could be used in the various web sites my company provides to its clients. Normally this would be an easy task—create a form with a File input, let someone navigate to the image in question on their computer, and upload it using multipart/form-data as the enctype in the form tag. Simple enough, right? In fact, there's a simple enough example right on this site.

But what if you had pre-prepare that image in some way? In other words, what if you had to resize that image first? What if you needed that image to be a particular file type, like a PNG or a JPG? Canvas to the rescue!

What is the Canvas?

The Canvas is a DOM element, added in HTML5, that allows a user to draw graphics directly in a page, usually through JavaScript. It is different from specifications such as SVG or VML in that it is a raster API as opposed to a vector API; think of it as the difference between drawing something using Adobe Illustrator (vector graphics) and working with something using Adobe Photoshop (raster).

Among the things a canvas can do is read and render images, and allow you to manipulate that image data using JavaScript. There are many articles out there that show you some of the basics of image manipulation—the majority them focusing on various image filtering techniques—but we just need to be able to resize our image to a certain specification, and a canvas can do that no problem.

Say our requirements are to ensure that an image is no taller than, say, 100 pixels no matter what the original height was. Here's the basic code to do this:

var MAX_HEIGHT = 100;
function render(src){
	var image = new Image();
	image.onload = function(){
		var canvas = document.getElementById("myCanvas");
		if(image.height > MAX_HEIGHT) {
			image.width *= MAX_HEIGHT / image.height;
			image.height = MAX_HEIGHT;
		}
		var ctx = canvas.getContext("2d");
		ctx.clearRect(0, 0, canvas.width, canvas.height);
		canvas.width = image.width;
		canvas.height = image.height;
		ctx.drawImage(image, 0, 0, image.width, image.height);
	};
	image.src = src;
}

Here's what that example does:

  1. Create a JavaScript Image object.
  2. Attach a handler to the onload event of that Image.
  3. Check to see what the dimensions of the loaded image is, and if the original image's height is greater than our maximum allowed, change those dimensions.
  4. Clear anything that is in our canvas element.
  5. Set the canvas dimensions to the dimensions of the Image, and
  6. Draw the image to the canvas.

From there, you can use the toDataURL method of the Canvas API to get a Base 64-encoded version of the image to do with what you will.

But Wait...How Do We Get that Image in the First Place?

Well Padawan, I'm glad you asked. You can't use the File Input for that; the only information you can get from that element is the path to the file someone chose. You could use that path information to try to load that image, but that technique is unreliable across browsers. So instead, we'll use the HTML5 File API to read a file off someone's disk, and use that as the source.

What is the File API?

The new File API is a way of reading and listing files on a user's local disk without violating any kind of security sandbox—so that a malicious website can't, say, write a virus to a user's disk. The object we're going is use is the FileReader, which will allow a developer to read (in various ways) the contents of a file.

Assuming we know the path to the image in question, using FileReader to load the contents and render it using the code above is pretty easy to do:

function loadImage(src){
	//	Prevent any non-image file type from being read.
	if(!src.type.match(/image.*/)){
		console.log("The dropped file is not an image: ", src.type);
		return;
	}

	//	Create our FileReader and run the results through the render function.
	var reader = new FileReader();
	reader.onload = function(e){
		render(e.target.result);
	};
	reader.readAsDataURL(src);
}

What we are doing here is creating a FileReader object, adding a handler to the onload method to do something with the results, and then reading the file contents. Pretty simple, right?

But How Do You Get that File?

Silly rabbit, be patient! Of course that is our next step. There's a number of ways to do that; for instance, you could have a simple text input to make someone enter a path to an object, but obviously most people are not developers, and would not have a real clue as to how to do that properly. To make it easy on our users, we'll use the Drag and Drop API...

Using the Drag and Drop API

The Drag and Drop API is very simple—it consists of a set of DOM events carried by most DOM elements, to which you attach handler functions. We want to let a user take a file from somewhere on their disk, drag it onto an element, and do something with it. Here's our setup:

var target = document.getElementById("drop-target");
target.addEventListener("dragover", function(e){e.preventDefault();}, true);
target.addEventListener("drop", function(e){
	e.preventDefault(); 
	loadImage(e.dataTransfer.files[0]);
}, true);

This is pretty simple:

  1. We designate an element as our drop target,
  2. We prevent anything from happening when something is dragged over it...
  3. ...and when someone drops something on our target, we prevent any default action and send the first file in the event's dataTransfer object to our loadImage function.

Now there are other things we can do, such as add some kind of preview of the Image. But most of this seems useless without being able to save the resized image. For that, we'll use Ajax to do an HTTP POST of the image data. The next example uses the Dojo Toolkit's Request module, but you can use any typical Ajax technique you'd like (we're assuming DTK 1.9.x for this example):

//	Remember that DTK 1.7+ is AMD!
require(["dojo/request"], function(request){
    request.post("image-handler.php", {
        data: {
            imageName: "myImage.png",
            imageData: encodeURIComponent(document.getElementById("canvas").toDataURL("image/png"))
        }
    }).then(function(text){
        console.log("The server returned: ", text);
    });
});

We use the toDataURL of the Canvas API to get our Base64-encoded version of our image; note the encodeURIComponent method wrapping it. Some Ajax APIs will do this for you, but it is better to be safe than sorry.

That's it! That's all you need to create an intuitive user interface that allows you to control the size of an image and post it to a server without needing complex multi-part handlers on your server!

Dr. Tom Trenka

About Dr. Tom Trenka

Tom Trenka is a software developer, expert musician and many other things based in Saint Paul, Minnesota. Former lead of the DojoX project for the Dojo Toolkit and one of the original contributors, he's a Senior Software Developer at Ai Media Group, where he writes complex data analysis applications.

Recent Features

  • By
    Create Namespaced Classes with MooTools

    MooTools has always gotten a bit of grief for not inherently using and standardizing namespaced-based JavaScript classes like the Dojo Toolkit does.  Many developers create their classes as globals which is generally frowned up.  I mostly disagree with that stance, but each to their own.  In any event...

  • By
    Write Better JavaScript with Promises

    You've probably heard the talk around the water cooler about how promises are the future. All of the cool kids are using them, but you don't see what makes them so special. Can't you just use a callback? What's the big deal? In this article, we'll...

Incredible Demos

Discussion

  1. MaxArt

    I wonder if it may work to send the raw image data:

    var imageContent = atob(document.getElementById("canvas").toDataURL("image/png").substring(22));
    
  2. Sean

    This is pretty neat, the only hassle is that your looking at IE10+ for the DataTransfer object i think

  3. This is great post,but how can I save the Image to disk on server? I has already decode the imageData to a byte array.

  4. Thanks very much!
    The imageData string starts with “data%3Aimage%2Fpng%3Bbase64%2C”, I get the substring from index 30,and use the Base64 Decode to get the byteArray,save to file. That’s OK.
    Execuse me!

  5. Greg

    I’m confused as to why canvas is required at all. Surely it would be more efficient to just place the image into an tag and render it smaller, without processing it at all?

    • jerjako

      With a large file. base64 on image source can crash the browser. But not with Canvas.

    • MaxArt

      You put it on a canvas so the browser can create the image data out of the *resized* image instead of the fully sized image you’d get from the client.

  6. Greg,

    You can certainly do that, if your goal is to just display an image. In my use case, I’m actually sending the resized image data back to a server and writing LESS/CSS stylesheets on the fly with the image embedded in it as a data URL in a background-image rule, which is where this post really came from. I’m also parsing those stylesheets and grabbing the data URL on the fly to display previous saved images.

    Hope that helps where I’m coming from!

  7. This would be a great feature for cms that use dated methods for adding images, might be a step forward in some future builds of magento etc.

  8. Absolutely brilliant as usual.

  9. Deepak Kamat

    What if my requirement is max width 100 px ? what would be the formula to calculate the height and width of the image then ?

    PS: I am too lazy to do that on my own :P

  10. Sammy Davis, jr

    Thank you for this clear, well written article.

    I was looking around for a way to get a local file into a canvas without the d&d, and I found this: http://jsfiddle.net/fWLJ9/ It has pretty good browser support, although Safari needs to be 6+. Was wondering if anyone can foresee any other problems with it.

    (It is linked from here: http://stackoverflow.com/questions/10209227/open-local-image-in-canvas?lq=1)

  11. jedi

    Please can you put in the post the content of image-handler.php file?
    To understand what and how you do it with php. Thanks. ;)

  12. Wonder

    Great article, but I get a “Cross-origin image load denied by Cross-Origin Resource Sharing policy” error on Safari. Has anyone got any idea how to get around this?

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!