Sunday, January 8, 2012

GWT Image Bundles

Often times, websites contain a number of small, icon-sized images. These images don't take long to download individually, but collectively, they can slow down the load time of a webpage because one HTTP request has to be made for every image. Plus, the HTTP protocol states that browsers can only make two simultaneous requests per domain at a time. This means that no matter how fast your Internet connection is, only two images can be downloaded at a time.

CSS image sprites help to get around this bottleneck. An image sprite is a single image that has multiple images inside of it. CSS styling is used to "cut out" an individual image from the image sprite and display it on the page. By consolidating all the images into a single image, the browser only has to make one HTTP request, as opposed to making multiple HTTP requests for each image. This decreases the load time of the page.

An image sprite from Amazon.

GWT gives you the ability to combine your application's icons into an image sprite automatically. In this tutorial, I'm going to show you how this is done by adding some images to my Twitter Search application. I will be adding:

  1. An icon for the "Search" button.
  2. An icon for the "Privacy Policy" button.
  3. An icon to display along with error messages.
The TwitterSearch application with the new images.

Creating an image bundle interface

First, create an interface that extends the ClientBundle interface. This is called an image bundle and it's what we'll use to access the individual images from our image sprite.

package com.acme.twittersearch.client;

import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.ImageResource;

public interface Images extends ClientBundle {
  @Source("search.png")
  ImageResource search();

  @Source("error.png")
  ImageResource error();

  @Source("document.png")
  ImageResource privacyPolicy();

  @Source("loading.gif")
  ImageResource loading();
}

The image bundle contains one method per image. Each method must return a ImageResource object and can be given any name. Each method is also annotated with the path to the image file. This path is relative to the location of the image bundle interface. Since my image bundle is in the com.acme.twittersearch.client package, this means that all my images are there too (the images cannot be in the war directory, they have to be inside the classpath, alongside the source code). The GWT Eclipse plugin will tell you if the path to an image is incorrect by throwing a Java compilation error.

I also add the loading animation image to my image bundle. Normally, animated GIFs cannot be added to image sprites, but GWT still allows you to add them to image bundles. They won't be added to the image sprite that GWT generates, but programmatically, they are treated as if they were part of the image sprite.

Adding the images to the application

Now that I've created my image bundle, I can add the images to my application. First, I'll create an instance of the image bundle via deferred binding:

public class TwitterSearch implements EntryPoint {
  private final Images images = GWT.create(Images.class);
  [...]
}

Instances of ImageResource can be passed into the constructor of the Image class, so that's what I'll do for the error and loading images.

public class TwitterSearch implements EntryPoint {
  [...]
  private Image errorImage;
  private Image loadingImage;
  [...]

  private void createWidgets() {
    [...]
    errorImage = new Image(images.error());
    loadingImage = new Image(images.loading());
    [...]
  }
}

The "search" and "privacy policy" images are a little more complicated because of the way the Button widget works. After creating an Image object for each icon, I generate raw HTML code, which will be passed into the Button constructor (note: calling the toString() method on any widget object will return its HTML code).

public class TwitterSearch implements EntryPoint {
  [...]
  private Button privacyPolicyButton;
  private Button searchButton;
  [...]

  private void createWidgets() {
    [...]

    Image icon = new Image(images.search());
    SafeHtmlBuilder builder = new SafeHtmlBuilder();
    builder.appendHtmlConstant(icon.toString());
    builder.appendEscaped(" Search");
    searchButton = new Button(builder.toSafeHtml());
    searchButton.addClickHandler(new ClickHandler() {
      [...]
    });

    icon = new Image(images.privacyPolicy());
    builder = new SafeHtmlBuilder();
    builder.appendHtmlConstant(icon.toString());
    builder.appendEscaped(" Privacy Policy");
    privacyPolicyButton = new Button(builder.toSafeHtml());
    privacyPolicyButton.addClickHandler(new ClickHandler() {
      [...]
    });

    [...]
  }
}

And that's really all there is to it! GWT will create the actual image sprite for you. To see what the image sprite looks like, compile the GWT application and then navigate to the war/twittersearch folder. The image sprite will be somewhere inside this folder (it will have a crazy name like "F0B5712E038983254B46645D77476DA7.cache.png").

GWT automatically creates an image sprite from the images in our image bundle.

More information on GWT image bundles can be found in the GWT Developer Guide.

The complete source code from this tutorial can be downloaded here.

CORRECTION (1/12/2011): Even though GWT generates an image sprite from an image bundle when compiled, it actually only uses the image sprite for IE 6 and possibly IE 7 (thanks Boris). What GWT really does is encode the images as data URIs. A data URI puts the actual image data directly inside the HTML page, instead of referencing the image via a URL like normal (I wrote a brief blog post on data URIs a while back and also wrote a data URI generation tool). I'm not sure why GWT does this, but I'm sure it has its reasons. You should avoid using data URIs for large images--since the image data is embedded directly inside the page, the browser can't cache it, so the image basically has to be downloaded every time the page is loaded. --Mike

12 comments:

Boris Jockov said...

GWT does create an image sprite, but it uses it only for IE7 and possibly IE6. Chrome & FF are treated with data URI.

Michael Angstadt said...

Looks like you're right. I tried deploying the app to Tomcat and running it there to test to see if maybe it only used data URIs in dev mode, but it uses data URIs when deployed too. Thanks for the correction.

Anonymous said...

If you like, you can force GWT to use sprites instead of data URIs in all browsers by setting a property in your module file:

<set-property name='ClientBundle.enableInlining' value='false' />

Michael Angstadt said...

Thanks for the tip! Do you know if this brings any performance gain?

L5 said...

I can't find anyone addressing how to use an ImageResource dynamically. That is, defined properly using ClientBundle but then accessed through other classes using a method like getIcon(String nameOfIcon) where you don't know which icon you'll need until you are building your object that needs it, and want to call for it by name. (I know it's been a while since this blog post, but I'm flailing madly, here)

Michael Angstadt said...

Hi L5,

Have you tried creating a new instance of the "Image" class using the "Image(String)" constructor? This constructor allows the developer to pass in a URL to the image. In order to figure out what the URL of an image on the classpath looks like, you could create a "ClientBundle" class as I describe in my blog post, and then call the "ImageResource.getSafeUri()" method.

L5 said...

Thanks, Michael. For now, where I know the image will be one of a list (enum), I'm using a switch and hard-coding. But when we need more than can be handled this way, I'll try what you say. The image is needed for clickable buttons (Sencha's lib GXT) which apparently require actual ImageResource objects/classes? (I'm very new to this) -- if I can figure out how to implement your idea, I'll post back sometime.

Michael Angstadt said...

Ok, let me know!

Zied Hamdi OneView said...

Still nothin about dynamically reading images As requested L5? it's wiered it's possible with i18n strings and not with images

Zied Hamdi OneView said...
This comment has been removed by the author.
Farruh Atabaev said...

Ok Micheal Thanks...

Thomas Wrobel said...

A bit late but I'll post this if people find it useful;



In your gwt.xml will force gwt to use image strips. VERY useful if your using them for animations, as DataURLs are too slow for that.

You might also need this;
"-Dgwt.imageResource.maxBundleSize=1000" in GWT compile options, VM arguments. This will override the normal size limit