Get Loaded with the File API

Andrew Dodson | November 5, 2012

Why do avatar uploads restrict us on file size? You know, “Please select an image (maximum 50 KB).” And why haven't photo manipulation Web apps come to fruition, since canvas has been around for a while? The answer to both these questions comes down to performance. Until now we’ve needed a slow plug-in or a route via a server to create and modify files in the browser. But for Internet Explorer 10, Firefox and Chrome users, developers have in their arsenal the awesome File API to make these operations possible natively in the browser.

The File API is a new JavaScript API that lets you read and write binary data objects that represent files in Web applications. In a nutshell, you can read user-selected files into Web apps and download new data objects as files from Web apps. Let’s take a deeper look.

Breaking Down the File API

The File API (as defined by the W3C) is not one type but a collection of typed objects, functions and methods.

FileList

FileList is a typed object that exists in numerous places. First, as a property on a form input element whose type is "file". Second, as part of the event dispatched on a drop file event or the Clipboard event (as a result of copying and pasting).

For example, let’s say you have a form input control such as this:

<input type=file onchange="console.log(this.files.length)" multiple />

Whenever the user clicks the form input and selects a file, this action dispatches the onchange event handler, and the FileList object (located at this.files relative to the input element) has some new entries on it. In this example, the number of files the user has selected is printed to the browser console.

Although the FileList object behaves similarly to a native Array—in that you can iterate through its contents as you can with an Array—a FileList contains only immutable instances of File objects (described next).

File

The File object represents an individual file of the FileList. The File object contains a hash of read-only metadata about the file, including name, last modification date, size and type. It is also used as a reference and can be passed to the FileReader to read its content.

Blob (Binary Large Object)

The Blob interface exposes the raw binary data of a file. Remote data can be served as a blob object via XHRRequest—if xhr.responseType is set to "blob". The new blob object is an instance of the Blob API and includes native methods such as blob.slice(i,i+n), which can be used to slice a big blob object into smaller blob objects. (I'm using the phrase "Blob interface" when talking about the JavaScript object type, and "blob object" to refer to a single instance of the Blob interface.)

In addition, with the Blob interface constructor, tiny blob objects can be merged back together again with this interface.

new Blob([blob, blob,...])

You’ll find an example in the BlobBuilder demo, which loads a single MP3 file and then breaks it into different audio files (tracks) that are each applied to their own <audio> tag for playback. In the demo, the user can reconstruct the MP3 in another order by merging the tracks, and even download the new blob object as an MP3.

Note: The W3C has deprecated the BlobBuilder in favor of Blob. Both produce the same result but in different ways. Also, WebKit's BlobBuilder varies between Internet Explorer and Firefox, so it’s best to feature-detect for Blob first.

FileReader

The FileReader interface takes a File or Blob object and reads its content —File or Blob objects are just references to something stored on the local computer. FileReader can be optimized for reading files of various types—for example, Text(UTF-8), ArrayBuffer(for binary) or base64 data-uri. The following example shows how to get the text of a file using FileReader, given a blob object instance.

var reader = new FileReader();
reader.onload = function(e){
       console.log(e.target.result);
}
reader.readAsText(blob);

Similarly, you can read other content using the read method that best suits the type of file: readAsArrayBuffer (great for working with large binary files) or readAsDataURL (great if you want to quickly embed content into a DOM object, like an image or an audio file).

The FileReader includes the several event listeners: onerror, onloadstart, onabort and onprogress (which is useful for creating a progress bar for large files and for catching problems).

URI Schemes

URI Schemes are URIs representing objects in the document. A resource can be a File or a Blob, and its corresponding URL is known as an Object URL. Given a Blob or a File reference, you can create an Object URL by using createObjectURL. For example:

var objecturl =  window.URL.createObjectURL(blob)

returns a URL that references the resource object, something like "blob:http%3A//test.com/666e6730-f45c-47c1-8012-ccc706f17191".

This string can be placed anywhere a typical URI can be placed—for example, on the src of an image tag,  if the object URI is of an image that is. The object resource lasts as long as the document, so refresh the page, and it’s gone.

FileSaver

The FileSaver interface exposes methods to write blobs to the user’s Downloads directory. The implementation is rather neat and straightforward:

window.saveAs(blob, "filename")

However, none of the browsers currently have this. Only Internet Explorer 10 supports a simple alternative: navigator.msSaveOrOpenBlob as well as navigator.msSaveBlob. But as you'll see, there are shim's that can create a similar functionality, albeit without a slick user experience.

So that's all rather low-level nuts and bolts about the File API, and it’s a lot to digest as an abstract concept. But don’t worry. Coming up is a cooking class for how to turn those ingredients into something you can chew on.

Join the Party and Build an Example

I thought I’d dress up for the party (Figure 1) and create my own little photo manipulation tool at https://adodson.com/graffiti. Check it out. Select an image from your file system, scribble over the screen, and then download your masterpiece—all using the File APIs (plus a few canvas methods and pointer events).

The Graffiti File API App
Figure 1. The Graffiti File API App

Implementing the Graffiti App

Even with the garish picture, this app has a rather simple premise:

  • Select an image as a backdrop by using File + FileList and FileReader.
  • Load the image into the canvas tag and manipulate the image with the HTML5 Canvas API.
  • Download the new image using Blob (or BlobBuilder), saveAs (or saveBlob) or an anchor tag hack with Object URLs.

Step 1: Selecting a File

There is more than one way a user can select an image for this app.

  • Pick a file using form input
  • Drag and drop a file
  • Copy and paste a file from the Clipboard

Each of these approaches wires up its listeners to trigger a custom function, named readFile(), which uses an instance of FileReader to extract the file data and add the user image to the canvas. Here is the function’s code.

// readFile, loads File Objects (which are also images) into our Canvas
// @param File Object
function readFile(file){
  // Create a new FileReader Object
  var reader = new FileReader();
  // Set an onload handler because we load files into it asynchronously
  reader.onload = function(e){
    // The response contains the Data-Uri, which we can then load into the canvas
    applyDataUrlToCanvas( reader.result );
  };
  reader.reaAsDataURL(file);
}

Here, readFile takes the File reference (shown later) and creates a new instance of the FileReader object. This function reads the data as a DataURL, but it could also read the data as binary or even as an ArrayBuffer.

To wire up the function, you use a File reference as a parameter by taking one of the approaches mentioned earlier.

Pick a File Using Form Input

The FileAPI doesn't (alas) currently define a native method to trigger file selection. But the old trusted form input with type=file does the job quite well:

<input type="file" name="picture" accept="image/png, image/jpeg"/>

As ungainly as the form input element is, it does have some new additional attributes that are perfect for this app.

The accept attribute hints at which file types are acceptable. In this example, that’s PNGs and JPEGs. It’s left to the device to handle this appropriately (Windows, for instance, opens the users Pictures library by default and shows only files of these types).

The multiple tag lets a user select one or more files in one step.

Next, you need to bind event listeners to the change event of the form input so that a user’s selection automatically triggers readFile:

document.querySelector('input[name=picture]').onchange = function(e){
     readFile(e.target.files[0]);
}

This code simply takes the first user-selected file (regardless of whether the multiple attribute was used) and then calls readFile, passing in the file as the first parameter.

Input a little style

The form input box shown here is not really in keeping with my aesthetics, or probably yours. So, position it absolutely over another element of your choice with an opacity of zero (but be prepared for it to have a fixed width, which may interfere with other elements). Or a little more complicated: dispatch a custom click event on an input box positioned off the screen. Read more on this discussion here.

Drag and Drop Files

Files can be dragged in from File Explorer and use a similar event model to the form input hack. The "drop" event occurs when a user releases the image over the canvas. The event contains a property called dataTransfer, which has a child property named files. In the following code, e.dataTransfer.files is an instance of a FileList (as mentioned before, the FileList contains a list of File references), and the first File item is the parameter for readFile. This is supported in Webkit, Firefox and Internet Explorer 10. Here’s an example:

// stop FireFox from replacing the whole page with the file.
canvas.ondragover = function () { return false; };
// Add drop handler
canvas.ondrop = function (e) {
  e.preventDefault(); e = e || window.event;
  var files = e.dataTransfer.files;
  if(files){
    readFile(files[0]);
  }
};

Copy and Paste File Data

Clipboard data can be accessed when the user pastes content into the document. This data can be a mix of text and images and not a FileList with only File references, as in the form input control or in dragging files.

In the code below, the Clipboard data is traversed and entries corresponding to the type and kind properties, which are  "*/image" and "file" respectively, are filtered out. The item’s File instance is obtained using getAsFile(), which is passed to readFile.

// paste Clipboard data
// Well not all of it just the images.
document.onpaste = function(e){
  e.preventDefault();
  if(e.clipboardData&&e.clipboardData.items){
    // pasted image
    for(var i=0, items = e.clipboardData.items;i<items.length;i++){
      if( items[i].kind==='file' && items[i].type.match(/^image/) ){
        readFile(items[i].getAsFile());
        break;
      }
    }
  }
  return false;
};

That concludes the first step. I’ve shown three approaches to obtaining a File reference and loading file data into the document. I'd love to know if there are other ways, so please send comments.

Step 2: Load the Image onto the Canvas

The readFile function in step 1 hands over a generated data-url to another custom function, applyDataUrlToCanvas. This function draws a selected image on the canvas. Here’s how it works:

  • Find the width and height of the image.
  • Find the orientation of the image.
  • Draw the best fit of the image on the canvas.

Find the Width and Height

Using the DOM Image function, you can easily determine the dimensions of any image. It’s a handy technique and goes something like this:

var img =  new Image();
img.onload = function(){
  // img.width
  // img.height
}
img.src = dataURL;

Find the Orientation

Unfortunately, there's a gotcha with the width and height, and it’s a big one: the picture could have been taken in portrait mode and saved in landscape, or the image could have been taken upside down.

Some cameras, instead of saving an image in the correct orientation, provide an Orientation property within the image’s EXIF data. This can be read from the binary data of the image.

Converting the data-url to a binary string is easy:

var base64 = dataUrl.replace(/^.*?,/,'');
var binary = atob(base64);

And fortunately, there's an open source EXIF client-side library, written by Jacob Seidelin, which will return the EXIF data as an Object. Yes, awesome!

<script src="https://www.nihilogic.dk/labs/exif/exif.js"></script>
<script src="https://www.nihilogic.dk/labs/binaryajax/binaryajax.js"></script>
<script>
var exif = EXIF.readFromBinaryFile(new BinaryFile(binary));
//exif.Orientation
</script>

The Orientation property is an integer in the range 1–8 corresponding to four rotations and four flips (rather redundant).

Draw the Image to Canvas

Having discovered the Orientation value, you can rotate and draw on the canvas. If you want to see my algorithm, just dig into the source code, which you can find at https://adodson.com/graffiti/ andhttps://github.com/MrSwitch/graffiti.

Step 3: Download the Image

The last step is to download the modified image. Within the FileAPI's repertoire, you can use the Blob interface for creating files in the client. Wrap that up with the new msSaveBlob in Internet Explorer 10, or the download attribute hack (coming up) in other modern browsers, and together they enable you to download files in the client.

Try it yourself in the demo.Click on the Download button.

The demo uses the canvas.toBlob method in Internet Explorer 10 to get the file reference of the image that is currently drawn within the canvas tag. For Chrome and FireFox, the toBlob shim works great.

canvas.toBlob(function( file ){
  // Create a blob file,

  // then download with the FileSaver
}

Make a copy of a Blob Object instance

We should be able to skip this step, but because of a quirk in all the browsers, you can't use the FileSave API directly from the Blob instance returned by canvas.toBlob. You need to copy it.

The BlobBuilder interface used for creating new Blob references is supported in Internet Explorer 10, Chrome and FireFox. But this interface has already been superseded by the Blob constructor, which has limited support right now.

First, shim away vendor prefixes of the BlobBuilder:

// Shim the BlobBuilder with the vendor prefixes
window.BlobBuilder || (window.BlobBuilder = window.MSBlobBuilder||window.MozBlobBuilder||window.WebKitBlobBuilder);

Next, future-proof your code and test for the Blob constructor. Otherwise, construct a BlobBuilder to build the blob**.** (It’s best to wrap the methods in a try-catch.) Blob is buggy in the current Chrome for Android browser. Here is the code.

var blob;
if('Blob' in window){
  try{
    // The new Blob interface
    blob = new Blob([file],{ "type" : "image\/png"});
  catch(e){}
}
if(!blob){
  try{
    // The deprecated BlobBuilder interface
    var bb = new BlobBuilder();
    bb.append( file ); 
    blob = bb.getBlob("image\/png");
  }
  catch(e){}
}

Downloading the Blob with FileSaver

The FileSaver API is also a standard yet to be adopted by any of the current crop of browsers. However, in the case of Internet Explorer 10, you can use the msSaveBlob function (which is awesome), and for Chrome and FireFox, you can at least future-proof them with vendor prefixes. So, the saveAs function needs some massive shimming:

window.saveAs || (window.saveAs == window.navigator.msSaveBlob || window.webkitSaveAs || window.mozSaveAs || window.msSaveAs /** || URL Download Hack **/ );

This fallback (described in full at https://gist.github.com/3552985)shims up the FileSaver interface using the Object URL for our new image. For browsers that support the download attribute on the anchor tag, the shim defines the href as the Object URL and then dispatches the click event to force it to download or otherwise open in a new tab. Oh, what tangled webs we weave.

Finally, given the blob and a file name, the saveAs method initiates the download:

var name = 'Graffiti.png';
if(window.saveAs){
  // Move the builder object content to a blob and 
  window.saveAs(blob, name); 
}
else{
  // Fallover, open as DataURL
  window.open(canvas.toDataURL());
}

Here, if the saveAs shim is incompatible, the fallover will open a new tab with a base64 Data-URL. This works in Internet Explorer 9, but Internet Explorer 8 is limited to a DataURI's length of 32 KB.

Wrapping Up

If you haven't already played around with the File API, I strongly urge you to. The FileAPI opens up a lot of potential for making desktop-like apps for the browser. You might want a little more control over where you save files and even overwrite the existing file. But for now, security airs on the side of caution, so you’re unlikely to see features like these soon. The specs are still in flux, but what I've highlighted in this article has seen some huge investment by browser vendors and is unlikely to change much, if at all. Please don’t quote me though.

If you need to support older browsers, take a look at my dropfile.js shim for Internet Explorer, which shims the FileReader and creates a base64 Data-URI, as well as Downloadify for a Flash-based shim replacement to FileSaver.

Resources

About the Author

Andrew Dodson is a Web technology aficionado who gets rude over EcmaScript, HTML and CSS. Andrew is passionate about writing cross-platform Web apps. He co-curates BrowserExperiments.com and contributes to HTML5 Cross Browser Polyfills to help others. When Andrew is not writing beautiful code, he likes to speak at events between London and Sydney. Andrew currently lives in Sydney and works as a freelance developer.