The What?
Let’s say you want to dynamically display an image on your site, and there’s data associated with this image that you also want to acquire.Here are 2 scenarios:
- Your site uses Latex equation images and you want to use CSS to vertically align the image with the surrounding the text (Detail).
- You are Google and you want to display Instant Preview image of a site along with some metadata.
The Neat Trick
We want to load both the image & metadata in only 1 HTTP request and here's how.- Request to load your image using a simple <img /> tag or javascript.
- Make the HTTP Response for the image request add the metadata you are looking for to the HTTP Set-Cookie header.
- Once the image is loaded on the client’s browser, use javascript to access the browser's cookies to retrieve the metadata returned by the image request.
- You can now delete the cookie for cleanup.
Example HTTP Response:
HTTP/1.0 200 OK Content-Length: 451 Content-Type: image/jpeg Expires: Fri, 30 Oct 2020 13:19:41 GMT Set-Cookie: metadata=MYDATA; Domain=.sprklab.com; Path=/ [BINARY IMAGE INFORMATION]
How Google can make Instant Preview faster
Google seems to have encountered the same problem. They are trying to load their Instant Preview image with some metadata in 1 HTTP Request. But they took a different approach, instead of having the server return the image binary, the server returns a JSON data that includes the actual image encoded in base64. Then they use Data URI Scheme to load the image.But this solution has 3 downsides:
- Base64 encoding causes the image data to be 33% larger
- Loading image from JSON string is slower than loading an image binary
- Not all browsers support Data URI Scheme, including IE7. (IE8 limits to 32kb)
Demonstration
Server side: (Django)
from django.conf import settings
from django.http import HttpResponse
from StringIO import StringIO
import random
import Image, ImageFont, ImageDraw
def getRandomNumberImage(request):
number = str(random.randint(1, 9999))
# Pick a random font to use
fontslocation = settings.MEDIA_ROOT + "notes/captcha/"
fonts = os.listdir(fontslocation)
font = ImageFont.truetype(fontslocation + random.choice(fonts), 25)
# Draw the Image
image = Image.new("RGBA", (80, 60), (255,255,255, 0))
draw = ImageDraw.Draw(image)
draw.text((0, 0), number, font=font, fill="#000000")
data = StringIO()
image.save(data, format="PNG")
data.seek(0)
image_data = data.read()
# Create our Response
response = HttpResponse(image_data, mimetype="image/png")
# Append the metadata as a cookie
key = request.GET["key"]
response["Set-Cookie"] = "%s=%s; Domain=.sprklab.com; Path=/" % (key, number)
return response
Client side:
function get_cookie(c_name) {
if (document.cookie.length <= 0)
return "";
c_start = document.cookie.indexOf(c_name + "=");
if (c_start == -1)
return "";
c_start = c_start + c_name.length + 1;
c_end = document.cookie.indexOf(";", c_start);
if (c_end == -1)
c_end = document.cookie.length;
return unescape(document.cookie.substring(c_start, c_end));
}
function delete_cookie(name)
{
document.cookie = name + '=; Domain=.sprklab.com; Path=/; expires=Monday, 19-Aug-1996 05:00:00 GMT';
}
// Loads an image given a image source.
// on_success is a callback with the parameters (ImageElement, metadata)
function load_metaimage(src, on_success)
{
var img = new Image();
// Give the image a random cookie name to avoid name conflict when loading multiple images at once
var key = "mykey_" + Math.ceil(Math.random() * 99999);
img.src = src + "?key=" + key;
img.onload = function() {
// When the image is loaded, the cookie should include our meta data
var metadata = get_cookie(key);
delete_cookie(key);
on_success(img, metadata);
};
}
// Demo usage
function loadRandomNumber() {
load_metaimage("/ext/mathcaptcha_cookie", function(img, metadata) {
$("#image_container").empty().append(img);
$("#metadata_container").html(metadata);
$("#demo_container").show();
});
}