Upload binary data in play

Back

Reading Binary Data in a Play Controller

Today I was reading some code from the Guardian and ended up looking into Drew Noakes metadata extractor library. Since I've written blog posts about images before and how to manipulate them I figure'd it might be fun to dive into reading Exif data. I'm not going to talk about how to extract the data, but rather how you can use Drew Noake's library on the backend without having to send the entire image file across the net.

While you can parse the exif data with javascript, writing a library for that that works with images of all kinds of different formats isn't something I want to do. Instead, I'd rather send the data to the back and use Drew's metadata extractor. So first I need to figure out how to get the part of the file that has exif data. According to the page 11 of the specification:

The size of APP1 including all these elements shall not exceed the 64 Kbytes specified in the JPEG standard.

Of course, according to the javascript post:

The Exif specification states that all of the data should exist in the first 64kb, but IPTC sometimes goes beyond that, especially when formatted as XMP.

So we'll be safe and use the first 128kb of the image data. Using the FileReader object in the browser we can do this easily.

var input = document.getElementById('somefileinput');
var readerForExif = new FileReader();
readerForExif.readAsArrayBuffer(input.files[0]);
readerForExif.result.slice(0, 1024 * 128);

Assuming you wait after calling readAsArrayBuffer, the you'll get back an ArrayBuffer that you can manipulate. Putting this together and using the onload field of our reader we can make a simple block of code that posts binary data to a server from the front end javascript:

var readerForExif = new FileReader();
readerForExif.onload = function (e) {
    var first128Kb = e.target.result.slice(0,1024 * 128);
    var view = new Uint8Array(first128Kb);
    var xhr = new XMLHttpRequest;
    xhr.open("POST", "/exif", true);
    xhr.send(view);
};
readerForExif.readAsArrayBuffer(input.files[0]);

Easy right? In a production implementation you might want to specify a content type header of application/octet-stream*, but for this example the above code is enough to get you sending data to your backend. Once the data is posted we need to parse it.

Play has a bunch of BodyParsers, and unsurprisingly, for something as raw as a byte stream, we'll be using the raw parser! To invoke the parser we simple start our controller action like so:

import play.api._
import play.api.mvc._

class MyController extends Controller {
    def myFunc = Action(parse.raw) { implicit request => 
        val rawParser = request.body
        val maybeBytes = rawParser.asBytes() //Tada! Option[Array[Bytes]]!
        ...
    }
}

We still need one tweak to make this work with our front end code though. According to the documentation the maximum body size our parser will parse is 100KB unless we specify play.http.parser.maxMemoryBuffer in application.conf. However, I found that this property didn't effect the raw parser, probably because it's hard coded. They've fixed this in the newer play versions, but I got around this by specifying the maximum content length size directly in the action:

def myFunc = Action(parse.raw(1024 * 124)) { implicit request => 

So all together the method to read binary data becomes extremely easy:

import play.api.libs.json._
def readExifFromBinary = Action(parse.raw(1024 * 128)) { implicit request =>
    val raw = request.body
    val bytes = raw.asBytes().getOrElse(Array[Byte]())
    val exif : Map[String,String] = readExifFrom(bytes)
    Ok( Json.toJson(exif) )
}

The only thing left to do is to create readExifFrom(b: Array[Bytes]). If you peak at the javadocs for ImageMetadataReader you'll notice that there is an overloaded version of readMetadata that takes a BufferedInputStream. It's trivial to convert an array of bytes to a BufferedInputStream by using ByteArrayInputStream

val inputStream = new java.io.ByteArrayInputStream(bytes)
val bufferedInputStream = new java.io.BufferedInputStream(byteArrayInputStream)

And then we can create an instance of the Metadata class:

val metadata = ImageMetadataReader.readMetadata(bufferedInputStream)

and then follow the general idea the guardian uses to create a simple String to String Map.

import scala.collection.JavaConversions._

metadata.getDirectories().toList.flatMap { dir =>
    dir.getTags()
        .filter(_.hasTagName()).toList
        .map { tag =>
            tag.getTagName() -> Option(tag.getDescription).fold("")(identity)
        }
}.toMap

The only interesting thing of note here is that we use Option to make sure we don't accidently get a null value for our descriptions. Once you have these building blocks in place, we have an extremely simple exif reading method that is both efficient and leverages all the power of the server side** while taking advantage of the newer javascript File API to cut down how much data we have to send to the back end to get information about our file.

*Among other things, you may want to make it cross browser too depending on your use case.
**Not to mention type safety and extensive java libraries to deal with different image format types

Other Posts

comments powered by Disqus