David C. Yorke
Teknon Systems, LLC
dyorke@teknonsys.com

Retina Images in Google Web Toolkit (GWT)

To forgo the lengthy explanations and implementation details, skip to the download and installation instructions.

This project is also available on GitHub.

What is Google Web Toolkit (GWT)?

From the GWT website:

Google Web Toolkit (GWT) is a development toolkit for building and optimizing complex browser-based applications. Its goal is to enable productive development of high-performance web applications without the developer having to be an expert in browser quirks, XMLHttpRequest, and JavaScript. GWT is used by many products at Google, including Google Wave and the new version of AdWords. It's open source, completely free, and used by thousands of developers around the world.

In short, GWT enables developers to use the Java programming language to create desktop-class applications targeted to modern web browsers.

Why use retina images?

This topic has been covered by many, including Marco Arment. The gist of the argument is this: web pages with standard resolution images look horrible on retina displays. While the number of retina displays in circulation may be relatively few, their use with GWT apps may be disproportionately high. The iPad with its fast JavaScript engine and large screen is especially suited while older, non-compliant browsers (such as IE) are generally not supported by GWT apps.

To demonstrate the difference retina images can make, take a look at the two images below using a retina display such as the 3rd generation iPad or the new MacBook Pro with Retina Display. The Rubik's cube was shot with an iPhone 4S and scaled down to web size with Photoshop.

When viewed with a retina display, GWT automatically uses the retina version of the image on the left rather than the standard image. When a non-retina display is used, both the left and right image are are served up as the standard image. Unlike the method used by Apple, only the appropriate image is sent to the browser.

The lack of clarity with standard images is especially apparent when the image contains text, such as the Rubik's Cube label above. Below are screenshots of Mobile Safari browsing Daring Fireball. The standard image was made on an iPhone 3GS while the retina image was made on an iPhone 4S. Because they were taken from different devices, the two images are not exactly the same. Again, these images look fine on a standard-resolution screen, but downright ugly on a retina display.

Objectives

The GWT module described here was created to do the following:

  1. Download retina images only on retina devices
  2. Do not download standard images when retina images are downloaded
  3. Automatically detect retina images uses the @2x naming convention
  4. Set the size of images and sprites of retina images to be half the actual size
  5. Use the background-size CSS property to scale down retina images

Implementation

Implementation is fairly straightforward. The full module (including source code) is available for download below.

First, we create a GWT property, display.type, to create a new permutation for retina displays using deferred binding. The module uses the window.devicePixelRatio JavaScript property as described by Peter-Paul Koch and linked by John Gruber. Although Koch recommends watching for other ratios (such as 1.5), the module only looks for a value of 2 for simplicity.


  <define-property name="display.type" values="retina,normal"/>

  <property-provider name="display.type"><![CDATA[
    if($wnd.devicePixelRatio==2)
      return "retina";
    return "normal";
  ]]></property-provider>
      

Because the browsers that implement retina display all seem to be derived from WebKit, and because the primary target is Safari, we specifically exclude non-WebKit browsers, thus avoiding extra compiler permutations.


  <set-property name="display.type" value="normal">
    <none>
      <when-property-is name="user.agent" value="safari"/>
    </none>
  </set-property>
      

Objectives 1 & 2: Send only retina images to retina displays and only to retina displays

This could not be implemented in the module directly, instead it is implemented with deferred binding at the time of application development. See Using the module for details.

Objective 3: Automatically detect retina images uses the @2x naming convention

The module provides a subinterface to ImageResource called RetinaImageResource. The subinterface overrides the DefaultExtensions annotation to add (and give priority to) @2x images:


  @DefaultExtensions(value =
    {"@2x.png","@2x.jpg","@2x.gif","@2x.bmp",".png",".jpg",".gif",".bmp"})
      

If no @2x image is found, the compiler continues to search for regular images.

Objective 4: Set the size of images and sprites of retina images to be half the actual size

To accomplish this, the module provides an alternative to ImageResourceGenerator assigned to the new RetinaImageResource


  @ResourceGeneratorType(RetinaImageResourceGenerator.class)
      

The createAssignment() method is used to check for @2x in the file name and reduce the height and width if found. This method outputs java source code for the concrete implementation of the RetinaImageResource. The entire method follows.


  @Override
  public String createAssignment(TreeLogger logger, ResourceContext context,
      JMethod method) throws UnableToCompleteException {
    String name = method.getName();
    
    URL[] resources=ResourceGeneratorUtil.findResources(logger,context,method);
    assert resources.length==1 : "Should be exactly one resource";
    boolean retina=resources[0].toString().indexOf("@2x.")!=-1;

    SourceWriter sw = new StringSourceWriter();
    sw.println("new " + RetinaImageResourcePrototype.class.getName() + "(");
    sw.indent();
    sw.println('"' + name + "\",");

    ImageResourceDeclaration image = new ImageResourceDeclaration(method);
    DisplayedImage bundle = getImage(image);
    ImageRect rect = bundle.getImageRect(image);
    assert rect != null : "No ImageRect ever computed for " + name;

    String[] urlExpressions = new String[] {bundle.getNormalContentsFieldName(),
      bundle.getRtlContentsFieldName()};
    assert urlExpressions[0] != null : "No primary URL expression for " + name;

    if (urlExpressions[1] == null) {
      sw.println(UriUtils.class.getName() + ".fromTrustedString(" + 
        urlExpressions[0] + "),");
    } else {
      sw.println(UriUtils.class.getName() + ".fromTrustedString("
          + "com.google.gwt.i18n.client.LocaleInfo.getCurrentLocale().isRTL() ?"
          + urlExpressions[1] + " : " + urlExpressions[0] + "),");
    }
    
    int width=retina?rect.getWidth()/2:rect.getWidth();
    int height=retina?rect.getHeight()/2:rect.getHeight();
    sw.println(rect.getLeft() + ", " + rect.getTop() + ", " + width + ", "
        + height + ", " + rect.isAnimated() + ", " + rect.isLossy() + ", "
        + retina);

    sw.outdent();
    sw.print(")");

    return sw.toString();
  }
      

Objective 5: Use the background-size CSS property to scale down retina images

The module adds a new implementation of ClippedImageImpl to add the background-size property.


  public @Override void adjust(Element img,SafeUri url,int left,int top,
    int width,int height)
    {
    super.adjust(img,url,left,top,width,height);
    img.getStyle().setProperty("backgroundSize",width+"px "+height+"px");
    }
      

The module also overrides the getSafeHtml() method accordingly.


  @Override
  public SafeHtml getSafeHtml(SafeUri url,int left,int top,int width,int height)
    {
    String style = "width: " + width + "px; height: " + height 
        + "px; background: url(" + url.asString() + ") " + "no-repeat "
        + (-left + "px ") + (-top + "px;") + "background-size: "
        + width + "px " + height + "px;";

    return getTemplate().image(clearImage,
      SafeStylesUtils.fromTrustedString(style));
    }
      

These modifications are only necessary with retina displays, so deferred binding is used:


  <replace-with
    class="com.google.gwt.user.client.ui.impl.ClippedImageImplRetina">
    <when-type-is class="com.google.gwt.user.client.ui.impl.ClippedImageImpl"/>
    <when-property-is name="display.type" value="retina"/>
  </replace-with>
      

Using the module

The module can be downloaded here: RetinaImages.jar with source available on GitHub. To use it, first inherit the module in your .gwt.xml file:


  <inherits name="com.teknonsys.RetinaImages"/>
      

Images that may have a retina version should use RetinaImageResource instead of ImageResource in your resource bundles. Because we only want the retina images in the retina permutation, we need to use deferred binding.

In order to use the deferred binding, we effectively have to have two bundles, one with retina images and one without. Note that the retina bundle must extend the normal bundle. Here's a sample bundle:


  public interface DemoClientBundle extends ClientBundle
    {
    ImageResource screenshot();
    ImageResource cube();

    public interface Retina extends DemoClientBundle
      {
      RetinaImageResource screenshot();
      RetinaImageResource cube();
      }
    }
      

We then need a separate factory for each bundle. One for the normal images:


  public class ClientBundleFactory
    {
    public DemoClientBundle create()
      {return GWT.create(DemoClientBundle.class);}
    }
      

And one for retina images:


  public class RetinaClientBundleFactory extends ClientBundleFactory
    {
    public @Override DemoClientBundle create()
      {return GWT.create(DemoClientBundle.Retina.class);}
    }
      

We can now tell the compiler to load the retina bundle only on retina displays:


  <replace-with class="com.teknonsys.client.RetinaClientBundleFactory">
    <when-type-is class="com.teknonsys.client.ClientBundleFactory"/>
    <when-property-is name="display.type" value="retina"/>
  </replace-with>
      

Notice the display.type property. This is set automatically by the module according to the window.devicePixelRatio JavaScript property.

Any images that may have retina versions can now be added to the Retina bundle. Retina enhancements will still only be used if an @2x image is found.