Add support for animated images to ImageResource.
This specifically enables using @sprite with animated .gif files.

Patch by: bobv
Review by: jgw

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5841 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/resources/client/ImageResource.java b/user/src/com/google/gwt/resources/client/ImageResource.java
index 75c2572..4a30dd7 100644
--- a/user/src/com/google/gwt/resources/client/ImageResource.java
+++ b/user/src/com/google/gwt/resources/client/ImageResource.java
@@ -35,6 +35,14 @@
   @Target(ElementType.METHOD)
   public @interface ImageOptions {
     /**
+     * If <code>true</code>, the image will be flipped about the y-axis when
+     * {@link com.google.gwt.i18n.client.LocaleInfo#isRTL()} returns
+     * <code>true</code>. This is intended to be used by graphics that are
+     * sensitive to layout direction, such as arrows and disclosure indicators.
+     */
+    boolean flipRtl() default false;
+
+    /**
      * This option affects the image bundling optimization to allow the image to
      * be used with the {@link CssResource} {@code @sprite} rule where
      * repetition of the image is desired.
@@ -42,14 +50,6 @@
      * @see "CssResource documentation"
      */
     RepeatStyle repeatStyle() default RepeatStyle.None;
-
-    /**
-     * If <code>true</code>, the image will be flipped along the y-axis when
-     * {@link com.google.gwt.i18n.client.LocaleInfo#isRTL()} returns
-     * <code>true</code>. This is intended to be used by graphics that are
-     * sensitive to layout direction, such as arrows and disclosure indicators.
-     */
-    boolean flipRtl() default false;
   }
 
   /**
@@ -103,4 +103,9 @@
    * Returns the width of the image.
    */
   int getWidth();
+
+  /**
+   * Return <code>true</code> if the image contains multiple frames.
+   */
+  boolean isAnimated();
 }
diff --git a/user/src/com/google/gwt/resources/client/impl/ImageResourcePrototype.java b/user/src/com/google/gwt/resources/client/impl/ImageResourcePrototype.java
index 11bcd64..6bf0136 100644
--- a/user/src/com/google/gwt/resources/client/impl/ImageResourcePrototype.java
+++ b/user/src/com/google/gwt/resources/client/impl/ImageResourcePrototype.java
@@ -23,6 +23,7 @@
  */
 public class ImageResourcePrototype implements ImageResource {
 
+  private final boolean animated;
   private final String name;
   private final String url;
   private final int left;
@@ -30,14 +31,18 @@
   private final int width;
   private final int height;
 
+  /**
+   * Only called by generated code.
+   */
   public ImageResourcePrototype(String name, String url, int left, int top,
-      int width, int height) {
+      int width, int height, boolean animated) {
     this.name = name;
     this.left = left;
     this.top = top;
     this.height = height;
     this.width = width;
     this.url = url;
+    this.animated = animated;
   }
 
   /**
@@ -78,4 +83,8 @@
   public int getWidth() {
     return width;
   }
+
+  public boolean isAnimated() {
+    return animated;
+  }
 }
diff --git a/user/src/com/google/gwt/resources/rg/ImageBundleBuilder.java b/user/src/com/google/gwt/resources/rg/ImageBundleBuilder.java
index 941fc68..7abe8e0 100644
--- a/user/src/com/google/gwt/resources/rg/ImageBundleBuilder.java
+++ b/user/src/com/google/gwt/resources/rg/ImageBundleBuilder.java
@@ -31,11 +31,14 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
 import javax.imageio.ImageIO;
+import javax.imageio.ImageReader;
+import javax.imageio.stream.MemoryCacheImageInputStream;
 
 /**
  * Accumulates state for the bundled image.
@@ -213,6 +216,8 @@
 
     BufferedImage getImage();
 
+    BufferedImage[] getImages();
+
     int getLeft();
 
     String getName();
@@ -287,7 +292,7 @@
 
     private boolean hasBeenPositioned;
     private final int height, width;
-    private final BufferedImage image;
+    private final BufferedImage[] images;
     private int left, top;
     private final String name;
     private final AffineTransform transform = new AffineTransform();
@@ -299,7 +304,7 @@
       this.name = other.getName();
       this.height = other.getHeight();
       this.width = other.getWidth();
-      this.image = other.getImage();
+      this.images = other.getImages();
       this.left = other.getLeft();
       this.top = other.getTop();
       setTransform(other.getTransform());
@@ -307,17 +312,28 @@
 
     public ImageRect(String name, BufferedImage image) {
       this.name = name;
-      this.image = image;
+      this.images = new BufferedImage[] {image};
       this.width = image.getWidth();
       this.height = image.getHeight();
     }
 
+    public ImageRect(String name, BufferedImage[] images) {
+      this.name = name;
+      this.images = images;
+      this.width = images[0].getWidth();
+      this.height = images[0].getHeight();
+    }
+
     public int getHeight() {
       return height;
     }
 
     public BufferedImage getImage() {
-      return image;
+      return images[0];
+    }
+
+    public BufferedImage[] getImages() {
+      return images;
     }
 
     public int getLeft() {
@@ -344,6 +360,10 @@
       return hasBeenPositioned;
     }
 
+    public boolean isAnimated() {
+      return images.length > 1;
+    }
+
     public void setPosition(int left, int top) {
       hasBeenPositioned = true;
       this.left = left;
@@ -603,10 +623,40 @@
     logger = logger.branch(TreeLogger.TRACE,
         "Adding image '" + imageName + "'", null);
 
-    BufferedImage image;
+    BufferedImage image = null;
     // Load the image
     try {
-      image = ImageIO.read(imageUrl);
+      String path = imageUrl.getPath();
+      String suffix = path.substring(path.lastIndexOf('.') + 1);
+
+      /*
+       * ImageIO uses an SPI pattern API. We don't care about the particulars of
+       * the implementation, so just choose the first ImageReader.
+       */
+      Iterator<ImageReader> it = ImageIO.getImageReadersBySuffix(suffix);
+      if (it.hasNext()) {
+        ImageReader reader = it.next();
+
+        reader.setInput(new MemoryCacheImageInputStream(imageUrl.openStream()));
+
+        int numImages = reader.getNumImages(true);
+        if (numImages == 0) {
+          // Fall through
+
+        } else if (numImages == 1) {
+          image = reader.read(0);
+
+        } else {
+          // Read all contained images
+          BufferedImage[] images = new BufferedImage[numImages];
+          for (int i = 0; i < numImages; i++) {
+            images[i] = reader.read(i);
+          }
+
+          ImageRect rect = new ImageRect(imageName, images);
+          throw new UnsuitableForStripException(rect);
+        }
+      }
     } catch (IllegalArgumentException iex) {
       if (imageName.toLowerCase().endsWith("png")
           && iex.getMessage() != null
@@ -625,7 +675,7 @@
         throw iex;
       }
     } catch (IOException e) {
-      logger.log(TreeLogger.ERROR, "Unable to read image resource", null);
+      logger.log(TreeLogger.ERROR, "Unable to read image resource", e);
       throw new UnableToCompleteException();
     }
 
diff --git a/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java b/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java
index e9c38a5..b0df671 100644
--- a/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java
+++ b/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java
@@ -86,7 +86,7 @@
           + urlExpressions[1] + " : " + urlExpressions[0] + ",");
     }
     sw.println(rect.getLeft() + ", " + rect.getTop() + ", " + rect.getWidth()
-        + ", " + rect.getHeight());
+        + ", " + rect.getHeight() + ", " + rect.isAnimated());
 
     sw.outdent();
     sw.print(")");
@@ -224,9 +224,17 @@
     } catch (UnsuitableForStripException e) {
       // Add the image to the output as a separate resource
       rect = e.getImageRect();
-      byte[] imageBytes = ImageBundleBuilder.toPng(logger, rect);
-      String urlExpression = context.deploy(rect.getName() + ".png",
-          "image/png", imageBytes, false);
+
+      String urlExpression;
+      if (rect.isAnimated()) {
+        // Can't re-encode animated images, so we emit it as-is
+        urlExpression = context.deploy(resource, false);
+      } else {
+        // Re-encode the image as a PNG to strip random header data
+        byte[] imageBytes = ImageBundleBuilder.toPng(logger, rect);
+        urlExpression = context.deploy(rect.getName() + ".png", "image/png",
+            imageBytes, false);
+      }
       urlsByExternalImageRect.put(rect, new String[] {urlExpression, null});
     }
 
diff --git a/user/test/com/google/gwt/resources/client/ImageResourceTest.java b/user/test/com/google/gwt/resources/client/ImageResourceTest.java
index f16ba77..f209e68 100644
--- a/user/test/com/google/gwt/resources/client/ImageResourceTest.java
+++ b/user/test/com/google/gwt/resources/client/ImageResourceTest.java
@@ -31,6 +31,9 @@
  */
 public class ImageResourceTest extends GWTTestCase {
   static interface Resources extends ClientBundle {
+    @Source("animated.gif")
+    ImageResource animated();
+
     @Source("16x16.png")
     ImageResource i16x16();
 
@@ -74,6 +77,21 @@
     return "com.google.gwt.resources.Resources";
   }
 
+  public void testAnimated() {
+    Resources r = GWT.create(Resources.class);
+
+    ImageResource a = r.animated();
+
+    assertTrue(a.isAnimated());
+    assertEquals(16, a.getWidth());
+    assertEquals(16, a.getHeight());
+    assertEquals(0, a.getLeft());
+    assertEquals(0, a.getTop());
+
+    // Make sure the animated image is encoded separately
+    assertFalse(a.getURL().equals(r.i16x16().getURL()));
+  }
+
   public void testDedup() {
     Resources r = GWT.create(Resources.class);
 
diff --git a/user/test/com/google/gwt/resources/client/animated.gif b/user/test/com/google/gwt/resources/client/animated.gif
new file mode 100644
index 0000000..a0e7007
--- /dev/null
+++ b/user/test/com/google/gwt/resources/client/animated.gif
Binary files differ