Resizing images before upload

An article, posted almost 8 years ago filed in form, canvas, image, data, data-url, jpeg & png.

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.

Enjoyed this? Follow me on Mastodon or add the RSS, euh ATOM feed to your feed reader.

Op de hoogte blijven?

Maandelijks maak ik een selectie artikelen en zorg ik voor wat extra context bij de meer technische stukken. Schrijf je hieronder in:

Mailfrequentie = 1x per maand. Je privacy wordt serieus genomen: de mailinglijst bestaat alleen op onze servers.