March 15, 2012

Generating data URIs in Go language

I wrote FotoBlogr to make blogging easier for a friend.  The main goal was to make it easy to add Flickr photos to Blogger blogs.  As time went by, I added support for adding recipe microformats to blog posts, since that’s what she primarily posts.

The app is fairly simple, it’s just one HTML file; but most of the UI is constructed at client side using JavaScript.  As soon as the page loads, it’d send out a number of AJAX requests to fetch the list of blogs, list of Flickr photos, any saved draft post, etc.  Adding features always implied adding more images (i.e. icons) to the interface.

At one point, rendering the UI needed more than 40 HTTP requests, all to the same domain.  The app that used to load instantly started taking a second or two to fully load.  I wanted to bring back the old speed while retaining all existing features.

I have seen data URIs generated by frameworks like Google Web Toolkit.  When you replace an <img src='/img/my-icon.png'> with something <img src='data:image/png;base64;image-data-inline'>, it cuts off one HTTP request from the load time[1].  I decided this is what I’d do, and this post is to document how I did it.

FotoBlogr is written using Go language and runs on Google App Engine.  One possible implementation is I can run my HTML files through some tool that will replace all (or selected) images with inline image data.  It could work, but I wanted something easier.  This is when I found out how powerful the Go language is.

Generating data URIs for images is literally one line of code:
func dataUrl(data []byte, contentType string) string {
  return fmt.Sprintf("data:%s;base64,%s",
      contentType, base64.StdEncoding.EncodeToString(data))
}
Now I have to connect this code with my HTML.  Enter Go’s template package.  Go’s templates can call functions[2].  If I can make this function callable from my templates, it’s job done.  I’ll first show you how this is used and then the implementation.

Instead of instantiating the template by parsing the file directly, now I need to call a helper function.  I.e., I had to replace calls like
  tmpl := template.Must(template.ParseFile("tmpl/home.html"))
with
  tmpl := gaesupport.NewTemplate("tmpl/home.html")
(gaesupport is the name of the package where I have some App Engine support code.)

To request inlining of images, I replaced HTML like
  <img src='/img/plus.png' width='16' height='16'>
with
  <img src='{{inline "img/plus.png" "image/png"}}' width='16' height='16'>
That’s all a caller has to.  It’s hardly any extra work, so I am happy with this solution.  Implementation of gaesupport.NewTemplate function is straightforward too.  It instantiates a new template.Template object, adds the inline function to its context so templates can call it, and then parses the template file as usual.  Here’s the full code:
package mgae

import (
  "encoding/base64"
  "fmt"
  "io/ioutil"
  "os"
  "template"
)

// Encodes data into a 'data:' URI specified at
// https://developer.mozilla.org/en/data_URIs.
func dataUrl(data []byte, contentType string) string {
  return fmt.Sprintf("data:%s;base64,%s",
      contentType, base64.StdEncoding.EncodeToString(data))
}

func inline(fileName, contentType string) (uri string, err os.Error) {
  data, err := ioutil.ReadFile(fileName)
  if err != nil {
    return "", err
  }
  return dataUrl(data, contentType), nil
}

func NewTemplate(fileName string) *template.Template {
  funcs := template.FuncMap(map[string]interface{} {
    "inline": inline,
  })

  tmpl := template.New(fileName)
  tmpl.Funcs(funcs)
  template.Must(tmpl.ParseFile(fileName))
  return tmpl
}
[1] On the other hand, it bloats up your bootstrap HTML and makes it impossible for clients to cache your image.  FotoBlogr is tiny; even with all image inline, the bootstrap is only about 70kB, so this would work for me.
[2] I’ll be honest and admit that I first thought “wow, that’s like PHP!” when I first heard it.  But it’s actually cleaner than that.  Templates can only call functions that you explicitly export.

No comments:

Post a Comment