Resizing images before upload

An article, posted almost 7 years ago filed in form, canvas, image, data, 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:

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.