Since Trayn is all about individualizing the workouts of your athletes it is in the very nature of the application that you have to work with a list of people. When doing so, it is always nice to have pictures of the people next to their names. Because preparing and uploading pictures for all athletes of your team can be a tedious and time consuming process, we tried hard to make this as quick and easy as possible.

In the end we want a resized version of the original image cropped to a square that is stored on the server. The traditional approach would be to upload the full image, display it to the user to allow her to specify the cropping area, and finally process it to the specified dimension. This seems to be a waste of bandwidth and time of our users to transfer the original image back and forth. So our refined approach goes as follows: After the user selects a picture from his hard drive, we make use of the FileReader API and the canvas element to show the local file in a dialog where the user can select the cropping area interactively. When this is done, and the user clicks the “Upload” button, we slice the image from the canvas accordingly and create a Blob object from it, which can then be uploaded to the server using the FormData interface in an AJAX request.

This procedure works in MSIE starting with version 10 and all version of Mozilla Firefox and Google Chrome that have not been around back when the dinosaurs still roamed mother earth. Since only a quite small image – in our case 360px by 360px – is transferred to the server, everything seems to happen almost instantaneously.

Let’s take a closer look at the steps involved in our approach. We have put together a minimal version of our profile picture uploader in this JSFiddle; the snippets below were shortened for clarity. Basically there are two events we listen to: The first one is when the value of the file input changes. We take the value from the input and use the FileReader API to load the picture from disk and return it as a DataURI:

var readFile = function(file, options) {
    var dfd = new $.Deferred();
    
    // define FileReader object
    var reader = new FileReader();
    var that = this;

    // init reader onload event handlers
    reader.onload = function(e) {
        var image = $('<img/>')
            .load(function() {
                //when image fully loaded
                var newimageurl = getCanvasImage(this, options);
                dfd.resolve(newimageurl, this);
            })
           .attr('src', e.target.result);
    };
    reader.onerror = function(e) {
        dfd.reject("Couldn't read file " + file.name);
    }
    // begin reader read operation
    reader.readAsDataURL(file);
    return dfd.promise();
}

The method getCanvasImage puts the contents of the picture on a canvas element, resizes it to the specified dimensions, and finally returns the scaled image as DataURI:

var getCanvasImage = function(image, options) {
    // define canvas
    var canvas = document.createElement("canvas");
    canvas.height = options.height;
    canvas.width = options.width;

    var ctx = canvas.getContext("2d");
    // fit image to canvas
    ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height);
    // convert canvas to jpeg url
    return canvas.toDataURL("image/jpeg");
}

This DataURI is then used as source of an img-tag and the user can select the desired crop area. This is done through the excellent Jcrop jQuery plugin.

The second event we have to take care of is the user hitting the “Upload” button. Again we read the file from disk but this time we use the coordinates specified with the help of Jcrop to crop the image. See the actual code of the getCanvasImage method in the JSFiddle for details.

With the image at the right size we utilize the FormData API to upload it via AJAX. But first the DataURI image has to be converted to an Blob. This is done through this scary piece of code:

var dataURItoBlob = function(dataURI) {
    // convert base64 to raw binary data held in a string
    // doesn't handle URLEncoded DataURIs
    var byteString;
    if (dataURI.split(',')[0].indexOf("base64") >= 0) {
        byteString = atob(dataURI.split(',')[1]);
    } else {
        byteString = unescape(dataURI.split(',')[1]);
    }

    // separate out the mime component
    var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]

    // write the bytes of the string to an ArrayBuffer
    var ab = new ArrayBuffer(byteString.length);
    var ia = new Uint8Array(ab);
    for (var i = 0; i < byteString.length; i++) {
        ia[i] = byteString.charCodeAt(i);
    }
    return new Blob([ab], {
        type: mimeString
    });
}

The actual upload might then look something like this:

var data = new FormData();
data.append('file', dataURItoBlob(imgDataURI));

$.ajax({
    url: "/upload",
    data: data,
    cache: false,
    contentType: false,
    processData: false,
    type: 'POST'
});

The pieces of code shown here were simplified for easy reading. Please see the JSFiddle for a complete example.

Credits: The code for reading and converting the image files is based on the blog post HTML5 Drag and Drop File Upload with Canvas.