Resizing images before upload
The resolution of photo’s increases every year. And while some of that information may be worthy of retaining, not all is. High resolution images come at a price. Not only storage, but, especially in a mobile context, also data transfer. In this post I explain how you could create an uploader that fixes this.
The old form
Traditionally your form would look something like this:
`<form enctype="multipart/form-data">
<label for="image_upload">Upload image:</label>
<input type="file" id="image_upload" name="image_upload"/>
<input type="submit" />
</form>`
If you want to be forgiving to your end users (and not requiring them to manually resize the images themselves) you could configure your server to accept files > 20MB and resize the images server side.
However, to save bandwidth you you might want to resize the images just before uploading.
Enter canvas
To manipulate pixels we need a canvas. So we need a canvas
element.
Note: Canvas support is barely an issue, but if things don’t work we’ll write to code as such that the traditional form submit will continue to work
<canvas height="640" width="640" id="image_large_canvas">
Catch the submit
We need to catch the event:
document.querySelector("form").addEventListener("submit",
function(e) {
// …
return false;
}
)
Within this function we’re going to parse the file within the image_upload
input as a data-URL, and set this as the source of an image object that is then drawn to the canvas’ 2D context:
var file = document.getElementById("image_input").files[0]
if (file.type.match('image.*')) {
var fr = new FileReader();
fr.onload = function(fr_e) {
var canvas = document.getElementById('image_large_canvas');
var context = canvas.getContext('2d');
var imageObj = new Image();
imageObj.onload = function( io_e ) {
// …
context.drawImage(io_e.target, 0, 0);
};
imageObj.src = fr_e.target.result;
context.drawImage(io_e.target, 0, 0);
}
fr.readAsDataURL(file);
}
However, as you’ll notice when uploading a large image, you’ll only see the first 640 rows and columns of this image. We need to resize it first. And while resizing is built in as the fourth and fifth parameter of the drawImage
-method, it will probably change the aspect ratio if you would simply pass the width and the height of the canvase.
Hence, we need to adjust the size accordingly to make it fit the canvas area without changing the aspect ratio.
Make the image fit
To make this work we extend the image-object’s onload()
-callback. We get the image’s width & height, the canvas’ width & height, calculate the ratios and based on these ratios we decide wether the image should be resized to the max width or height. Then we temporarily resize to canvas to fit the image exactly to make a nice JPEG-export (as a base encoded data-url again).
You can simply pass this base encoded url in a (hidden) text-area as we do here. Serverside you can decode it, or store it as is in the database. Make sure you validate the data though.
var image_height = io_e.target.naturalHeight;
var image_width = io_e.target.naturalWidth;
var canvas_height = canvas.getAttribute("height");
var canvas_width = canvas.getAttribute("width");
var image_ratio = 1.0 * image_width / image_height;
var canvas_ratio = 1.0 * canvas_width / canvas_height;
var image_within_canvas_height, image_within_canvas_width = 10;
if (canvas_ratio <= image_ratio) {
image_within_canvas_width = canvas_width;
image_within_canvas_height = canvas_width / image_ratio;
} else {
image_within_canvas_height = canvas_height;
image_within_canvas_width = canvas_height * image_ratio;
}
canvas.setAttribute("width", Math.round(image_within_canvas_width))
canvas.setAttribute("height", Math.round(image_within_canvas_height))
context.drawImage(io_e.target, 0, 0, image_within_canvas_width, image_within_canvas_height);
document.getElementById("image_large_base64_data_url").value = canvas.toDataURL('image/jpeg', 0.6);
canvas.setAttribute("width", canvas_width)
canvas.setAttribute("height", canvas_height)
context.clearRect(0, 0, canvas.width, canvas.height);
Closing notes
This code is far from perfect, and simply a demonstration of the technique. Some things to consider:
- Actually, you don’t even need a canvas element in your DOM to modify. You can simply create a canvas element in JS and perform the manipulations in there.
- You may not want to submit base encoded files, you may still want to send your form as multipart encoded. Convert your files to a Blob and then reconstruct the to be submitted
FormData
with the reconstructed blob. - Some devices, I’m looking at you, iOS device, will send all photo’s as landscape. You need to reed a rotation Exif metadata attribute (and of course, you fix this in JS these days).
- And finally, the submit action may not really be the perfect place. Consider adding a change listener to the file input. On the other hand, it could (but I guess that is only in theory), lead to incomplete data to be submitted.