Re-apply patch with a bug fix.
Rework ImageResourceGenerator to address multiple issues.
	  
http://code.google.com/p/google-web-toolkit/issues/detail?id=4418
http://gwt-code-reviews.appspot.com/335802/show
Patch by: bobv
Review by: jgw



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@8231 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 b918d7e..31aefde 100644
--- a/user/src/com/google/gwt/resources/client/ImageResource.java
+++ b/user/src/com/google/gwt/resources/client/ImageResource.java
@@ -48,6 +48,14 @@
     boolean flipRtl() default false;
 
     /**
+     * Set to a positive value to override the image's intrinsic height. The
+     * image bundling code will scale the image to the desired height. If only
+     * one of <code>width</code> or <code>height</code> are set, the aspect
+     * ratio of the image will be maintained.
+     */
+    int height() default -1;
+
+    /**
      * 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.
@@ -55,6 +63,14 @@
      * @see "CssResource documentation"
      */
     RepeatStyle repeatStyle() default RepeatStyle.None;
+
+    /**
+     * Set to a positive value to override the image's intrinsic width. The
+     * image bundling code will scale the image to the desired width. If only
+     * one of <code>width</code> or <code>height</code> are set, the aspect
+     * ratio of the image will be maintained.
+     */
+    int width() default -1;
   }
 
   /**
diff --git a/user/src/com/google/gwt/resources/rg/ImageBundleBuilder.java b/user/src/com/google/gwt/resources/rg/ImageBundleBuilder.java
index 4e6dda0..48b33c7 100644
--- a/user/src/com/google/gwt/resources/rg/ImageBundleBuilder.java
+++ b/user/src/com/google/gwt/resources/rg/ImageBundleBuilder.java
@@ -17,7 +17,6 @@
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
-import com.google.gwt.dev.util.Util;
 import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
 
 import org.w3c.dom.Node;
@@ -60,7 +59,7 @@
     /**
      * Determine the total area required to store a composite image.
      */
-    Size arrangeImages(Collection<HasRect> rects);
+    Size arrangeImages(Collection<ImageRect> rects);
   }
 
   /**
@@ -74,8 +73,8 @@
    * widths of rectangles differing in the column.
    */
   static class BestFitArranger implements Arranger {
-    private static final Comparator<HasRect> decreasingHeightComparator = new Comparator<HasRect>() {
-      public int compare(HasRect a, HasRect b) {
+    private static final Comparator<ImageRect> decreasingHeightComparator = new Comparator<ImageRect>() {
+      public int compare(ImageRect a, ImageRect b) {
         final int c = b.getHeight() - a.getHeight();
         // If we encounter equal heights, use the name to keep things
         // deterministic.
@@ -83,8 +82,8 @@
       }
     };
 
-    private static final Comparator<HasRect> decreasingWidthComparator = new Comparator<HasRect>() {
-      public int compare(HasRect a, HasRect b) {
+    private static final Comparator<ImageRect> decreasingWidthComparator = new Comparator<ImageRect>() {
+      public int compare(ImageRect a, ImageRect b) {
         final int c = b.getWidth() - a.getWidth();
         // If we encounter equal heights, use the name to keep things
         // deterministic.
@@ -92,26 +91,26 @@
       }
     };
 
-    public Size arrangeImages(Collection<HasRect> rects) {
+    public Size arrangeImages(Collection<ImageRect> rects) {
       if (rects.size() == 0) {
         return new Size(0, 0);
       }
 
       // Create a list of ImageRects ordered by decreasing height used for
       // constructing columns.
-      final ArrayList<HasRect> rectsOrderedByHeight = new ArrayList<HasRect>(
+      final ArrayList<ImageRect> rectsOrderedByHeight = new ArrayList<ImageRect>(
           rects);
       Collections.sort(rectsOrderedByHeight, decreasingHeightComparator);
 
       // Create a list of ImageRects ordered by decreasing width used for
       // packing
       // individual columns.
-      final ArrayList<HasRect> rectsOrderedByWidth = new ArrayList<HasRect>(
+      final ArrayList<ImageRect> rectsOrderedByWidth = new ArrayList<ImageRect>(
           rects);
       Collections.sort(rectsOrderedByWidth, decreasingWidthComparator);
 
       // Place the first, tallest image as the first column.
-      final HasRect first = rectsOrderedByHeight.get(0);
+      final ImageRect first = rectsOrderedByHeight.get(0);
       first.setPosition(0, 0);
 
       // Setup state for laying things cumulatively.
@@ -127,9 +126,9 @@
         int colW = 0;
         int curY = 0;
 
-        final ArrayList<HasRect> rectsInColumn = new ArrayList<HasRect>();
+        final ArrayList<ImageRect> rectsInColumn = new ArrayList<ImageRect>();
         for (int j = i; j < n; j++) {
-          final HasRect current = rectsOrderedByHeight.get(j);
+          final ImageRect current = rectsOrderedByHeight.get(j);
           // Look for rects that have not been positioned with a small enough
           // height to go in this column.
           if (!current.hasBeenPositioned()
@@ -179,9 +178,9 @@
      * @param remainingRectsOrderedByWidth the sub list of ImageRects that may
      *          not have been positioned yet
      */
-    private void arrangeColumn(List<HasRect> rectsInColumn,
-        List<HasRect> remainingRectsOrderedByWidth) {
-      final HasRect first = rectsInColumn.get(0);
+    private void arrangeColumn(List<ImageRect> rectsInColumn,
+        List<ImageRect> remainingRectsOrderedByWidth) {
+      final ImageRect first = rectsInColumn.get(0);
 
       final int columnWidth = first.getWidth();
       int curY = first.getHeight();
@@ -189,7 +188,7 @@
       // Skip this first ImageRect because it is guaranteed to consume the full
       // width of the column.
       for (int i = 1, m = rectsInColumn.size(); i < m; i++) {
-        final HasRect r = rectsInColumn.get(i);
+        final ImageRect r = rectsInColumn.get(i);
         // The ImageRect was previously positioned horizontally, now set the top
         // field.
         r.setPosition(r.getLeft(), curY);
@@ -199,7 +198,7 @@
         // and
         // narrow enough to fit in the column.
         for (int j = 0, n = remainingRectsOrderedByWidth.size(); j < n; j++) {
-          final HasRect current = remainingRectsOrderedByWidth.get(j);
+          final ImageRect current = remainingRectsOrderedByWidth.get(j);
           if (!current.hasBeenPositioned()
               && (curX + current.getWidth()) <= columnWidth
               && (current.getHeight() <= r.getHeight())) {
@@ -216,50 +215,22 @@
   }
 
   /**
-   * A mockable interface to test the image arrangement algorithms.
-   */
-  interface HasRect {
-
-    int getHeight();
-
-    BufferedImage getImage();
-
-    BufferedImage[] getImages();
-
-    int getLeft();
-
-    String getName();
-
-    int getTop();
-
-    AffineTransform getTransform();
-
-    int getWidth();
-
-    boolean hasBeenPositioned();
-
-    void setPosition(int left, int top);
-
-    AffineTransform transform();
-  }
-
-  /**
    * Performs a simple horizontal arrangement of rectangles. Images will be
    * tiled vertically to fill to fill the full height of the image.
    */
   static class HorizontalArranger implements Arranger {
-    public Size arrangeImages(Collection<HasRect> rects) {
+    public Size arrangeImages(Collection<ImageRect> rects) {
       int height = 1;
       int width = 0;
 
-      for (HasRect rect : rects) {
+      for (ImageRect rect : rects) {
         rect.setPosition(width, 0);
         width += rect.getWidth();
         height = lcm(height, rect.getHeight());
       }
 
-      List<HasRect> toAdd = new ArrayList<HasRect>();
-      for (HasRect rect : rects) {
+      List<ImageRect> toAdd = new ArrayList<ImageRect>();
+      for (ImageRect rect : rects) {
         int y = rect.getHeight();
         while (y < height) {
           ImageRect newRect = new ImageRect(rect);
@@ -279,11 +250,11 @@
    * canvas needed to hold the images in their current positions.
    */
   static class IdentityArranger implements Arranger {
-    public Size arrangeImages(Collection<HasRect> rects) {
+    public Size arrangeImages(Collection<ImageRect> rects) {
       int height = 0;
       int width = 0;
 
-      for (HasRect rect : rects) {
+      for (ImageRect rect : rects) {
         height = Math.max(height, rect.getTop() + rect.getHeight());
         width = Math.max(width, rect.getLeft() + rect.getWidth());
       }
@@ -296,10 +267,11 @@
    * The rectangle at which the original image is placed into the composite
    * image.
    */
-  static class ImageRect implements HasRect {
+  static class ImageRect {
 
     private boolean hasBeenPositioned, lossy;
-    private final int height, width;
+    private int height, width;
+    private final int intrinsicHeight, intrinsicWidth;
     private final BufferedImage[] images;
     private int left, top;
     private final String name;
@@ -308,32 +280,28 @@
     /**
      * Copy constructor.
      */
-    public ImageRect(HasRect other) {
+    public ImageRect(ImageRect other) {
       this.name = other.getName();
-      this.height = other.getHeight();
-      this.width = other.getWidth();
+      this.height = other.height;
+      this.width = other.width;
       this.images = other.getImages();
       this.left = other.getLeft();
       this.top = other.getTop();
+      this.intrinsicHeight = other.intrinsicHeight;
+      this.intrinsicWidth = other.intrinsicWidth;
       setTransform(other.getTransform());
     }
 
-    public ImageRect(String name, BufferedImage image) {
-      this.name = name;
-      this.images = new BufferedImage[] {image};
-      this.width = image.getWidth();
-      this.height = image.getHeight();
-    }
-
-    public ImageRect(String name, BufferedImage[] images) {
+    public ImageRect(String name, BufferedImage... images) {
       this.name = name;
       this.images = images;
-      this.width = images[0].getWidth();
-      this.height = images[0].getHeight();
+      this.intrinsicWidth = images[0].getWidth();
+      this.intrinsicHeight = images[0].getHeight();
+      this.height = this.width = -1;
     }
 
     public int getHeight() {
-      return height;
+      return height > 0 ? height : intrinsicHeight;
     }
 
     public BufferedImage getImage() {
@@ -361,7 +329,7 @@
     }
 
     public int getWidth() {
-      return width;
+      return width > 0 ? width : intrinsicWidth;
     }
 
     public boolean hasBeenPositioned() {
@@ -376,6 +344,14 @@
       return lossy;
     }
 
+    public void setHeight(int height) {
+      this.height = height;
+      if (width <= 0) {
+        width = (int) Math.round((double) height / intrinsicHeight
+            * intrinsicWidth);
+      }
+    }
+
     public void setLossy(boolean lossy) {
       this.lossy = lossy;
     }
@@ -390,9 +366,28 @@
       this.transform.setTransform(transform);
     }
 
+    public void setWidth(int width) {
+      this.width = width;
+      if (height <= 0) {
+        height = (int) Math.round((double) width / intrinsicWidth
+            * intrinsicHeight);
+      }
+    }
+
     public AffineTransform transform() {
       AffineTransform toReturn = new AffineTransform();
+
+      // Translate
       toReturn.translate(left, top);
+
+      // Scale
+      assert !(height > 0 ^ width > 0);
+      if (height > 0) {
+        toReturn.scale((double) height / intrinsicHeight, (double) width
+            / intrinsicWidth);
+      }
+
+      // Use the base concatenation
       toReturn.concatenate(transform);
 
       assert checkTransform(toReturn);
@@ -400,18 +395,23 @@
     }
 
     private boolean checkTransform(AffineTransform tx) {
-      double[] in = {0, 0, width, height};
+      double[] in = {0, 0, intrinsicWidth, intrinsicHeight};
       double[] out = {0, 0, 0, 0};
 
       tx.transform(in, 0, out, 0, 2);
 
-      assert width == Math.abs(out[0] - out[2]);
-      assert height == Math.abs(out[1] - out[3]);
+      // Sanity check on bounds
       assert out[0] >= 0;
       assert out[1] >= 0;
       assert out[2] >= 0;
       assert out[3] >= 0;
 
+      // Check scaling
+      assert getWidth() == Math.round(Math.abs(out[0] - out[2])) : "Width "
+          + getWidth() + " != " + Math.round(Math.abs(out[0] - out[2]));
+      assert getHeight() == Math.round(Math.abs(out[1] - out[3])) : "Height "
+          + getHeight() + "!=" + Math.round(Math.abs(out[1] - out[3]));
+
       return true;
     }
   }
@@ -434,18 +434,18 @@
    * horizontally to fill the full width of the image.
    */
   static class VerticalArranger implements Arranger {
-    public Size arrangeImages(Collection<HasRect> rects) {
+    public Size arrangeImages(Collection<ImageRect> rects) {
       int height = 0;
       int width = 1;
 
-      for (HasRect rect : rects) {
+      for (ImageRect rect : rects) {
         rect.setPosition(0, height);
         width = lcm(width, rect.getWidth());
         height += rect.getHeight();
       }
 
-      List<HasRect> toAdd = new ArrayList<HasRect>();
-      for (HasRect rect : rects) {
+      List<ImageRect> toAdd = new ArrayList<ImageRect>();
+      for (ImageRect rect : rects) {
         int x = rect.getWidth();
         while (x < width) {
           ImageRect newRect = new ImageRect(rect);
@@ -487,7 +487,7 @@
 
       Exception ex = null;
       try {
-        builder.assimilate(loopLogger, args[i], file.toURL());
+        builder.assimilate(loopLogger, args[i], file.toURI().toURL());
       } catch (MalformedURLException e) {
         ex = e;
       } catch (UnableToCompleteException e) {
@@ -524,7 +524,7 @@
     System.exit(0);
   }
 
-  public static byte[] toPng(TreeLogger logger, HasRect rect)
+  public static byte[] toPng(TreeLogger logger, ImageRect rect)
       throws UnableToCompleteException {
     // Create the bundled image.
     BufferedImage bundledImage = new BufferedImage(rect.getWidth(),
@@ -582,11 +582,6 @@
 
   private final Map<String, ImageRect> imageNameToImageRectMap = new HashMap<String, ImageRect>();
 
-  /**
-   * This map is used to de-duplicate images in generated image strips.
-   */
-  private final Map<String, String> strongNametoCanonicalImageNameMap = new HashMap<String, String>();
-
   public ImageBundleBuilder() {
   }
 
@@ -624,19 +619,10 @@
     ImageRect rect = getMapping(imageName);
 
     if (rect == null) {
-      String strongName = Util.computeStrongName(Util.readURLAsBytes(resource));
-      if (strongNametoCanonicalImageNameMap.containsKey(strongName)) {
-        String previousImageName = strongNametoCanonicalImageNameMap.get(strongName);
-        rect = getMapping(previousImageName);
-        assert rect != null;
-        imageNameToImageRectMap.put(imageName, rect);
-      } else {
-        // Assimilate the image into the composite.
-        rect = addImage(logger, imageName, resource);
+      // Assimilate the image into the composite.
+      rect = addImage(logger, imageName, resource);
 
-        imageNameToImageRectMap.put(imageName, rect);
-        strongNametoCanonicalImageNameMap.put(strongName, imageName);
-      }
+      imageNameToImageRectMap.put(imageName, rect);
     }
     return rect;
   }
@@ -653,7 +639,6 @@
    * Remove an image from the builder.
    */
   public ImageRect removeMapping(String imageName) {
-    strongNametoCanonicalImageNameMap.values().remove(imageName);
     return imageNameToImageRectMap.remove(imageName);
   }
 
@@ -780,8 +765,8 @@
     toReturn.setLossy(lossy);
 
     // Don't composite the image if it's lossy or if it is too big
-    if (lossy || toReturn.height > IMAGE_MAX_SIZE
-        || toReturn.width > IMAGE_MAX_SIZE) {
+    if (lossy || toReturn.getHeight() > IMAGE_MAX_SIZE
+        || toReturn.getWidth() > IMAGE_MAX_SIZE) {
       throw new UnsuitableForStripException(toReturn);
     }
 
@@ -807,7 +792,7 @@
      * position the ImageRects in a deterministic fashion, even though we might
      * paint them in a non-deterministic order.
      */
-    Collection<HasRect> imageRects = new LinkedList<HasRect>(
+    Collection<ImageRect> imageRects = new LinkedList<ImageRect>(
         imageNameToImageRectMap.values());
 
     // Arrange images and determine the size of the resulting bundle.
@@ -818,7 +803,7 @@
         BufferedImage.TYPE_INT_ARGB_PRE);
     Graphics2D g2d = bundledImage.createGraphics();
 
-    for (HasRect imageRect : imageRects) {
+    for (ImageRect imageRect : imageRects) {
       g2d.drawImage(imageRect.getImage(), imageRect.transform(), null);
     }
     g2d.dispose();
diff --git a/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java b/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java
index e207676..db9085c 100644
--- a/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java
+++ b/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java
@@ -15,11 +15,15 @@
  */
 package com.google.gwt.resources.rg;
 
+import com.google.gwt.core.ext.BadPropertyValueException;
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
 import com.google.gwt.core.ext.typeinfo.JMethod;
-import com.google.gwt.core.ext.typeinfo.JType;
+import com.google.gwt.dev.util.StringKey;
 import com.google.gwt.dev.util.Util;
+import com.google.gwt.dev.util.collect.Maps;
+import com.google.gwt.dev.util.collect.Sets;
 import com.google.gwt.resources.client.ImageResource.ImageOptions;
 import com.google.gwt.resources.client.ImageResource.RepeatStyle;
 import com.google.gwt.resources.client.impl.ImageResourcePrototype;
@@ -37,10 +41,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.net.URL;
-import java.util.EnumMap;
 import java.util.HashMap;
-import java.util.HashSet;
-import java.util.IdentityHashMap;
 import java.util.Map;
 import java.util.Set;
 
@@ -49,72 +50,406 @@
  */
 public final class ImageResourceGenerator extends AbstractResourceGenerator {
   /**
+   * Represents a file that contains multiple image regions.
+   */
+  static class BundledImage extends DisplayedImage {
+    private final ImageBundleBuilder builder;
+    private boolean dirty = false;
+    private Map<LocalizedImage, ImageRect> images;
+    private Set<LocalizedImage> rtlImages = Sets.create();
+    private Map<ImageResourceDeclaration, LocalizedImage> localizedByImageResource;
+    private String normalContentsUrlExpression;
+    private String rtlContentsUrlExpression;
+
+    public BundledImage() {
+      builder = new ImageBundleBuilder();
+      images = Maps.create();
+      localizedByImageResource = Maps.create();
+    }
+
+    public LocalizedImage addImage(TreeLogger logger, ResourceContext context,
+        ImageResourceDeclaration image) throws UnableToCompleteException,
+        CannotBundleImageException {
+
+      LocalizedImage localized = LocalizedImage.create(logger, context, image);
+      localizedByImageResource = Maps.put(localizedByImageResource, image,
+          localized);
+      if (images.containsKey(localized)) {
+        return localized;
+      }
+
+      dirty = true;
+      ImageRect rect = null;
+      try {
+        rect = builder.assimilate(logger, image.get(), localized.getUrl());
+        if (context.supportsDataUrls()) {
+          // Treat the image as though it were external
+          builder.removeMapping(image.get());
+          throw new CannotBundleImageException(localized, rect);
+        }
+        images = Maps.put(images, localized, rect);
+      } catch (UnsuitableForStripException e) {
+        rect = e.getImageRect();
+        throw new CannotBundleImageException(localized, rect);
+      } finally {
+        assert rect != null : "No ImageRect";
+        rect.setHeight(image.getScaleHeight());
+        rect.setWidth(image.getScaleWidth());
+      }
+      return localized;
+    }
+
+    public ImageBundleBuilder getImageBundleBuilder() {
+      return builder;
+    }
+
+    @Override
+    public ImageRect getImageRect(ImageResourceDeclaration image) {
+      return images.get(localizedByImageResource.get(image));
+    }
+
+    public void render(TreeLogger logger, ResourceContext context,
+        ClientBundleFields fields, RepeatStyle repeatStyle)
+        throws UnableToCompleteException {
+      if (builder.getImageCount() == 0) {
+        // No data
+        return;
+      }
+
+      if (dirty) {
+        Arranger arranger;
+        switch (repeatStyle) {
+          case None:
+            arranger = new ImageBundleBuilder.BestFitArranger();
+            break;
+          case Horizontal:
+            arranger = new ImageBundleBuilder.VerticalArranger();
+            break;
+          case Vertical:
+            arranger = new ImageBundleBuilder.HorizontalArranger();
+            break;
+          case Both:
+            arranger = new ImageBundleBuilder.IdentityArranger();
+            break;
+          default:
+            logger.log(TreeLogger.ERROR, "Unknown RepeatStyle " + repeatStyle);
+            throw new UnableToCompleteException();
+        }
+        URL normalContents = renderToTempFile(logger, builder, arranger);
+        normalContentsUrlExpression = context.deploy(normalContents, false);
+
+        if (!rtlImages.isEmpty()) {
+          for (LocalizedImage rtlImage : rtlImages) {
+            // Create a transformation to mirror about the Y-axis and translate
+            AffineTransform tx = new AffineTransform();
+            ImageRect imageRect = images.get(rtlImage);
+            tx.setTransform(-1, 0, 0, 1, imageRect.getWidth(), 0);
+            imageRect.setTransform(tx);
+          }
+          URL rtlContents = renderToTempFile(logger, builder,
+              new ImageBundleBuilder.IdentityArranger());
+          assert rtlContents != null;
+          rtlContentsUrlExpression = context.deploy(rtlContents, false);
+        }
+
+        dirty = false;
+        logger.log(TreeLogger.DEBUG, "Composited " + builder.getImageCount()
+            + " images");
+      }
+
+      JClassType stringType = context.getGeneratorContext().getTypeOracle().findType(
+          String.class.getCanonicalName());
+
+      // Create the field that holds the normal contents
+      assert normalContentsUrlExpression != null;
+      normalContentsFieldName = fields.define(stringType, "bundledImage_"
+          + repeatStyle.name(), normalContentsUrlExpression, true, true);
+
+      // Optionally create the field that holds the RTL contents
+      if (rtlContentsUrlExpression != null) {
+        rtlContentsFieldName = fields.define(stringType, "bundledImage_"
+            + repeatStyle.name() + "_rtl", rtlContentsUrlExpression, true, true);
+      }
+    }
+
+    @Override
+    public void setRtlImage(LocalizedImage image) {
+      rtlImages = Sets.add(rtlImages, image);
+    }
+  }
+
+  /**
+   * This key is used to determine which DisplayedImage a given set of image
+   * bytes should be associated with.
+   */
+  static class BundleKey extends StringKey {
+    private static String key(ResourceContext context,
+        ImageResourceDeclaration image) {
+      if (image.getRepeatStyle() == RepeatStyle.Both) {
+        return "Unbundled: " + image.get();
+      }
+      return "Arranged: " + image.getRepeatStyle().toString();
+    }
+
+    private final RepeatStyle repeatStyle;
+
+    public BundleKey(ImageResourceDeclaration image) {
+      super("External: " + image.get());
+      this.repeatStyle = image.getRepeatStyle();
+    }
+
+    public BundleKey(ResourceContext context, ImageResourceDeclaration image) {
+      super(key(context, image));
+      this.repeatStyle = image.getRepeatStyle();
+    }
+
+    public RepeatStyle getRepeatStyle() {
+      return repeatStyle;
+    }
+
+    public boolean isExternal() {
+      return get().startsWith("External: ");
+    }
+  }
+
+  /**
    * This is shared that can be shared across permutations for a given
-   * ClientBundle type.
+   * ClientBundle .
    */
   static class CachedState {
-    /**
-     * Associates an ImageRect with the ImageBundleBuilder that will emit its
-     * bytes.
-     */
-    public final Map<ImageRect, ImageBundleBuilder> buildersByImageRect = new IdentityHashMap<ImageRect, ImageBundleBuilder>();
-
-    /**
-     * Associates a layout constraint with an ImageBundleBuilder that can
-     * satisfy that constraint.
-     */
-    public final Map<RepeatStyle, ImageBundleBuilder> buildersByRepeatStyle = new EnumMap<RepeatStyle, ImageBundleBuilder>(
-        RepeatStyle.class);
-
-    /**
-     * Associates a method name with the ImageRect that contains the data for
-     * that method.
-     */
-    public final Map<String, ImageRect> imageRectsByName = new HashMap<String, ImageRect>();
-
-    /**
-     * Records that ImageRects that also need to provide an RTL-flipped version.
-     */
-    public final Set<ImageRect> rtlImages = new HashSet<ImageRect>();
-
-    public final Map<ImageBundleBuilder, URL[]> urlsByBuilder = new IdentityHashMap<ImageBundleBuilder, URL[]>();
-
-    /**
-     * Maps an ImageRect to two URLs that contain the normal and flipped
-     * contents.
-     */
-    public final Map<ImageRect, URL[]> urlsByExternalImageRect = new IdentityHashMap<ImageRect, URL[]>();
+    public final Map<BundleKey, BundledImage> bundledImages = new HashMap<BundleKey, BundledImage>();
+    public final Map<BundleKey, ExternalImage> externalImages = new HashMap<BundleKey, ExternalImage>();
   }
 
   /**
-   * This data is specific to a particular permutation.
+   * Associates an ImageRect and a LocalizedImage.
    */
-  static class LocalState {
-    /**
-     * Maps resource URLs to field names within the generated ClientBundle type.
-     * These fields will be statically initialized to an expression that can be
-     * used to access the contents of the resource URL.
-     * 
-     * @see ImageResourceGenerator#maybeDeploy
-     */
-    public final Map<URL, String> fieldNamesByUrl = new HashMap<URL, String>();
+  static class CannotBundleImageException extends Exception {
+    private final ImageRect imageRect;
+    private final LocalizedImage localized;
 
-    /**
-     * Maps an ImageRect to a pair of Java expressions. The first can be used to
-     * access the normal version of the resource, while the second, optional,
-     * field is used to access an RTL-flipped version.
-     */
-    public final Map<ImageRect, String[]> urlExpressionsByImageRect = new HashMap<ImageRect, String[]>();
+    public CannotBundleImageException(LocalizedImage localized,
+        ImageRect imageRect) {
+      this.localized = localized;
+      this.imageRect = imageRect;
+    }
+
+    public ImageRect getImageRect() {
+      return imageRect;
+    }
+
+    public LocalizedImage getLocalizedImage() {
+      return localized;
+    }
   }
 
   /**
-   * This is set to <code>true</code> by {@link #init} if {@link #shared} was
-   * initialized from cached data.
+   * Represents a file that contains image data.
+   */
+  abstract static class DisplayedImage {
+    protected String normalContentsFieldName;
+    protected String rtlContentsFieldName;
+
+    public abstract ImageRect getImageRect(ImageResourceDeclaration image);
+
+    /**
+     * Only valid after calling {@link #render}.
+     */
+    public String getNormalContentsFieldName() {
+      return normalContentsFieldName;
+    }
+
+    /**
+     * Only valid after calling {@link #render}, may be <code>null</code> if
+     * there is no RTL version of the image.
+     */
+    public String getRtlContentsFieldName() {
+      return rtlContentsFieldName;
+    }
+
+    public abstract void setRtlImage(LocalizedImage image);
+
+    abstract void render(TreeLogger logger, ResourceContext context,
+        ClientBundleFields fields, RepeatStyle repeatStyle)
+        throws UnableToCompleteException;
+  }
+
+  /**
+   * Represents a file that contains exactly one image.
+   */
+  static class ExternalImage extends DisplayedImage {
+    private final ImageResourceDeclaration image;
+    private boolean isRtl;
+    private final LocalizedImage localized;
+    private final ImageRect rect;
+
+    /**
+     * Create an unbundled image.
+     */
+    public ExternalImage(ImageResourceDeclaration image,
+        LocalizedImage localized, ImageRect rect) {
+      this.image = image;
+      this.localized = localized;
+      this.rect = rect;
+    }
+
+    public ImageRect getImageRect(ImageResourceDeclaration image) {
+      return this.image.equals(image) ? rect : null;
+    }
+
+    public void render(TreeLogger logger, ResourceContext context,
+        ClientBundleFields fields, RepeatStyle repeatStyle)
+        throws UnableToCompleteException {
+      JClassType stringType = context.getGeneratorContext().getTypeOracle().findType(
+          String.class.getCanonicalName());
+
+      String contentsExpression = context.deploy(localized.getUrl(), false);
+      normalContentsFieldName = fields.define(stringType, "externalImage",
+          contentsExpression, true, true);
+
+      if (isRtl) {
+        // Create a transformation to mirror about the Y-axis and translate
+        AffineTransform tx = new AffineTransform();
+        tx.setTransform(-1, 0, 0, 1, rect.getWidth(), 0);
+        rect.setTransform(tx);
+
+        byte[] rtlData = ImageBundleBuilder.toPng(logger, rect);
+        String rtlContentsUrlExpression = context.deploy(image.getName()
+            + "_rtl.png", "image/png", rtlData, false);
+        rtlContentsFieldName = fields.define(stringType, "externalImage_rtl",
+            rtlContentsUrlExpression, true, true);
+      }
+    }
+
+    public void setRtlImage(LocalizedImage image) {
+      if (this.image.equals(image)) {
+        isRtl = true;
+      }
+    }
+  }
+
+  /**
+   * This represent how the user described the image in the original Java
+   * source. Its identity is based on the ImageResource JMethod.
+   */
+  static class ImageResourceDeclaration extends StringKey {
+    private static String key(JMethod method) {
+      return method.getEnclosingType().getQualifiedSourceName() + "."
+          + method.getName();
+    }
+
+    private final String name;
+    private final JMethod method;
+    private final ImageOptions options;
+
+    public ImageResourceDeclaration(JMethod method) {
+      super(key(method));
+      this.name = method.getName();
+      this.method = method;
+      this.options = method.getAnnotation(ImageOptions.class);
+    }
+
+    public JMethod getMethod() {
+      return method;
+    }
+
+    public String getName() {
+      return name;
+    }
+
+    public RepeatStyle getRepeatStyle() {
+      return options == null ? RepeatStyle.None : options.repeatStyle();
+    }
+
+    public int getScaleHeight() {
+      return options == null ? -1 : options.height();
+    }
+
+    public int getScaleWidth() {
+      return options == null ? -1 : options.width();
+    }
+
+    public boolean isFlipRtl() {
+      return options == null ? false : options.flipRtl();
+    }
+  }
+
+  /**
+   * This represents the particular collections of bits associated with a
+   * localized resource that a permutation will use. Its identity is based on
+   * the content hash of the resolved data and any transformations that will be
+   * applied to the data.
+   */
+  static class LocalizedImage extends StringKey {
+    public static LocalizedImage create(TreeLogger logger,
+        ResourceContext context, ImageResourceDeclaration image)
+        throws UnableToCompleteException {
+
+      URL[] resources = ResourceGeneratorUtil.findResources(logger, context,
+          image.getMethod());
+
+      if (resources.length != 1) {
+        logger.log(TreeLogger.ERROR, "Exactly one image may be specified", null);
+        throw new UnableToCompleteException();
+      }
+
+      URL resource = resources[0];
+
+      LocalizedImage toReturn = new LocalizedImage(image, resource);
+      return toReturn;
+    }
+
+    private static String key(ImageResourceDeclaration image, URL url) {
+      return Util.computeStrongName(Util.readURLAsBytes(url)) + ":"
+          + image.getScaleHeight() + ":" + image.getScaleWidth();
+    }
+
+    private final ImageResourceDeclaration image;
+    private final URL url;
+
+    public LocalizedImage(LocalizedImage other, URL alternateUrl) {
+      this(other.image, alternateUrl);
+    }
+
+    private LocalizedImage(ImageResourceDeclaration image, URL url) {
+      super(key(image, url));
+      this.image = image;
+      this.url = url;
+    }
+
+    public URL getUrl() {
+      return url;
+    }
+  }
+
+  /**
+   * Re-encode an image as a PNG to strip random header data.
+   */
+  private static URL renderToTempFile(TreeLogger logger,
+      ImageBundleBuilder builder, Arranger arranger)
+      throws UnableToCompleteException {
+    try {
+      byte[] imageBytes = builder.render(logger, arranger);
+      if (imageBytes == null) {
+        return null;
+      }
+
+      File file = File.createTempFile(
+          ImageResourceGenerator.class.getSimpleName(), ".png");
+      file.deleteOnExit();
+      Util.writeBytesToFile(logger, file, imageBytes);
+      return file.toURI().toURL();
+    } catch (IOException ex) {
+      logger.log(TreeLogger.ERROR, "Unable to write re-encoded PNG", ex);
+      throw new UnableToCompleteException();
+    }
+  }
+
+  /**
+   * This is used to short-circuit the {@link #prepare} method.
    */
   private boolean prepared;
   private CachedState shared;
-  private LocalState local;
-  private JType stringType;
 
   @Override
   public String createAssignment(TreeLogger logger, ResourceContext context,
@@ -126,12 +461,14 @@
     sw.indent();
     sw.println('"' + name + "\",");
 
-    ImageRect rect = shared.imageRectsByName.get(name);
+    ImageResourceDeclaration image = new ImageResourceDeclaration(method);
+    DisplayedImage bundle = getImage(context, image);
+    ImageRect rect = bundle.getImageRect(image);
     assert rect != null : "No ImageRect ever computed for " + name;
 
-    String[] urlExpressions = local.urlExpressionsByImageRect.get(rect);
-    assert urlExpressions != null : "No URL expression for " + name;
-    assert urlExpressions.length == 2;
+    String[] urlExpressions = new String[] {
+        bundle.getNormalContentsFieldName(), bundle.getRtlContentsFieldName()};
+    assert urlExpressions[0] != null : "No primary URL expression for " + name;
 
     if (urlExpressions[1] == null) {
       sw.println(urlExpressions[0] + ",");
@@ -156,41 +493,13 @@
   @Override
   public void createFields(TreeLogger logger, ResourceContext context,
       ClientBundleFields fields) throws UnableToCompleteException {
-    if (!prepared) {
-      finalizeArrangements(logger);
-    }
-
-    for (ImageRect rect : shared.imageRectsByName.values()) {
-      String[] urlExpressions;
-      {
-        URL[] contents;
-        ImageBundleBuilder builder = shared.buildersByImageRect.get(rect);
-        if (builder == null) {
-          contents = shared.urlsByExternalImageRect.get(rect);
-        } else {
-          contents = shared.urlsByBuilder.get(builder);
-        }
-        assert contents != null && contents.length == 2;
-
-        urlExpressions = new String[2];
-        urlExpressions[0] = maybeDeploy(context, fields, contents[0]);
-        urlExpressions[1] = maybeDeploy(context, fields, contents[1]);
-      }
-      local.urlExpressionsByImageRect.put(rect, urlExpressions);
-    }
-  }
-
-  @Override
-  public void finish(TreeLogger logger, ResourceContext context)
-      throws UnableToCompleteException {
-    local = null;
+    renderImageMap(logger, context, fields, shared.bundledImages);
+    renderImageMap(logger, context, fields, shared.externalImages);
   }
 
   @Override
   public void init(TreeLogger logger, ResourceContext context) {
-    // The images are bundled differently when data resources are supported
-    String key = context.getClientBundleType().getQualifiedSourceName() + ":"
-        + context.supportsDataUrls();
+    String key = createCacheKey(context);
     shared = context.getCachedData(key, CachedState.class);
     prepared = shared != null;
     if (prepared) {
@@ -199,11 +508,6 @@
       shared = new CachedState();
       context.putCachedData(key, shared);
     }
-    local = new LocalState();
-
-    stringType = context.getGeneratorContext().getTypeOracle().findType(
-        String.class.getCanonicalName());
-    assert stringType != null : "No String type";
   }
 
   /**
@@ -215,162 +519,104 @@
       ClientBundleRequirements requirements, JMethod method)
       throws UnableToCompleteException {
     if (prepared) {
+      logger.log(TreeLogger.DEBUG, "ImageResources already prepared");
       return;
     }
 
-    URL[] resources = ResourceGeneratorUtil.findResources(logger, context,
-        method);
+    ImageResourceDeclaration image = new ImageResourceDeclaration(method);
 
-    if (resources.length != 1) {
-      logger.log(TreeLogger.ERROR, "Exactly one image may be specified", null);
-      throw new UnableToCompleteException();
-    }
-
-    ImageBundleBuilder builder = getBuilder(method);
-    URL resource = resources[0];
-    String name = method.getName();
-
+    boolean cannotBundle = false;
+    DisplayedImage displayed = null;
+    LocalizedImage localizedImage;
     ImageRect rect;
     try {
-      rect = builder.assimilate(logger, name, resource);
-      if (context.supportsDataUrls()
-          || getRepeatStyle(method) == RepeatStyle.Both) {
-        // Just use the calculated meta-data
-        builder.removeMapping(name);
-        rect.setPosition(0, 0);
-        throw new UnsuitableForStripException(rect);
-      }
-      shared.buildersByImageRect.put(rect, builder);
-    } catch (UnsuitableForStripException e) {
-      // Add the image to the output as a separate resource
-      URL normalContents;
+      BundledImage bundledImage = (BundledImage) getImage(context, image);
+      localizedImage = bundledImage.addImage(logger, context, image);
+      rect = bundledImage.getImageRect(image);
+      displayed = bundledImage;
+    } catch (CannotBundleImageException e) {
+      cannotBundle = true;
+      localizedImage = e.getLocalizedImage();
       rect = e.getImageRect();
+    }
 
+    // Store the image externally
+    if (cannotBundle) {
       if (rect.isAnimated() || rect.isLossy()) {
-        // Can't re-encode animated or lossy images, so we emit it as-is
-        normalContents = resource;
+        // Don't re-encode
       } else {
-        normalContents = reencodeToTempFile(logger, rect);
-      }
-      shared.urlsByExternalImageRect.put(rect, new URL[] {normalContents, null});
-    }
+        /*
+         * Try to re-compress the image, but only use the re-compressed bytes if
+         * they actually offer a space-savings.
+         */
+        try {
+          URL contentLocation = localizedImage.getUrl();
+          int originalSize = contentLocation.openConnection().getContentLength();
 
-    shared.imageRectsByName.put(name, rect);
+          // Re-encode the data
+          URL reencodedContents = reencodeToTempFile(logger, rect);
+          int newSize = reencodedContents.openConnection().getContentLength();
 
-    if (getFlipRtl(method)) {
-      shared.rtlImages.add(rect);
-    }
-  }
-
-  private void finalizeArrangements(TreeLogger logger)
-      throws UnableToCompleteException {
-    for (Map.Entry<RepeatStyle, ImageBundleBuilder> entry : shared.buildersByRepeatStyle.entrySet()) {
-      RepeatStyle repeatStyle = entry.getKey();
-      ImageBundleBuilder builder = entry.getValue();
-      Arranger arranger;
-
-      switch (repeatStyle) {
-        case None:
-          arranger = new ImageBundleBuilder.BestFitArranger();
-          break;
-        case Horizontal:
-          arranger = new ImageBundleBuilder.VerticalArranger();
-          break;
-        case Vertical:
-          arranger = new ImageBundleBuilder.HorizontalArranger();
-          break;
-        case Both:
-          // This is taken care of when writing the external images;
-          continue;
-        default:
-          logger.log(TreeLogger.ERROR, "Unknown RepeatStyle" + repeatStyle);
-          throw new UnableToCompleteException();
-      }
-      URL normalContents = renderToTempFile(logger, builder, arranger);
-
-      shared.urlsByBuilder.put(builder, new URL[] {normalContents, null});
-    }
-
-    if (shared.rtlImages.size() > 0) {
-      Set<ImageBundleBuilder> rtlBuilders = new HashSet<ImageBundleBuilder>();
-
-      for (ImageRect rtlImage : shared.rtlImages) {
-        // Create a transformation to mirror about the Y-axis and translate
-        AffineTransform tx = new AffineTransform();
-        tx.setTransform(-1, 0, 0, 1, rtlImage.getWidth(), 0);
-        rtlImage.setTransform(tx);
-
-        if (shared.buildersByImageRect.containsKey(rtlImage)) {
-          /*
-           * This image is assigned to a builder, so we'll just remember to
-           * regenerate that builder.
-           */
-          rtlBuilders.add(shared.buildersByImageRect.get(rtlImage));
-        } else {
-          // Otherwise, emit the external version
-          URL[] contents = shared.urlsByExternalImageRect.get(rtlImage);
-          assert contents != null;
-          contents[1] = reencodeToTempFile(logger, rtlImage);
+          // But only use it if we did a better job on compression
+          if (newSize < originalSize) {
+            logger.log(TreeLogger.SPAM, "Reencoded image and saved "
+                + (originalSize - newSize) + " bytes");
+            localizedImage = new LocalizedImage(localizedImage,
+                reencodedContents);
+          }
+        } catch (IOException e2) {
+          // Non-fatal, but weird
+          logger.log(TreeLogger.WARN,
+              "Unable to determine before/after size when re-encoding image "
+                  + "data", e2);
         }
       }
-
-      for (ImageBundleBuilder builder : rtlBuilders) {
-        URL[] contents = shared.urlsByBuilder.get(builder);
-        assert contents != null && contents.length == 2;
-
-        contents[1] = renderToTempFile(logger, builder,
-            new ImageBundleBuilder.IdentityArranger());
-      }
+      ExternalImage externalImage = new ExternalImage(image, localizedImage,
+          rect);
+      shared.externalImages.put(new BundleKey(image), externalImage);
+      displayed = externalImage;
     }
-  }
 
-  private ImageBundleBuilder getBuilder(JMethod method) {
-    RepeatStyle repeatStyle = getRepeatStyle(method);
-    ImageBundleBuilder builder = shared.buildersByRepeatStyle.get(repeatStyle);
-    if (builder == null) {
-      builder = new ImageBundleBuilder();
-      shared.buildersByRepeatStyle.put(repeatStyle, builder);
-    }
-    return builder;
-  }
-
-  private boolean getFlipRtl(JMethod method) {
-    ImageOptions options = method.getAnnotation(ImageOptions.class);
-    if (options == null) {
-      return false;
-    } else {
-      return options.flipRtl();
-    }
-  }
-
-  private RepeatStyle getRepeatStyle(JMethod method) {
-    ImageOptions options = method.getAnnotation(ImageOptions.class);
-    if (options == null) {
-      return RepeatStyle.None;
-    } else {
-      return options.repeatStyle();
+    if (image.isFlipRtl()) {
+      displayed.setRtlImage(localizedImage);
     }
   }
 
   /**
-   * Create a field in the ClientBundle type that is used to intern the URL
-   * expressions that can be used to access the contents of the given resource.
-   * 
-   * @return the name of the field that was created
+   * Creates a cache key to be used with {@link ResourceContext#putCachedData}.
+   * The key is based on the ClientBundle type, support for data URLs, and the
+   * current locale.
    */
-  private String maybeDeploy(ResourceContext context,
-      ClientBundleFields fields, URL resource) throws UnableToCompleteException {
-    if (resource == null) {
-      return null;
+  private String createCacheKey(ResourceContext context) {
+    StringBuilder sb = new StringBuilder();
+    sb.append(context.getClientBundleType().getQualifiedSourceName());
+    sb.append(":").append(context.supportsDataUrls());
+    try {
+      String locale = context.getGeneratorContext().getPropertyOracle().getSelectionProperty(
+          TreeLogger.NULL, "locale").getCurrentValue();
+      sb.append(locale);
+    } catch (BadPropertyValueException e) {
+      // OK, locale isn't defined
     }
 
-    String toReturn = local.fieldNamesByUrl.get(resource);
-    if (toReturn == null) {
-      String urlExpression = context.deploy(resource, false);
-      toReturn = fields.define(stringType, "internedUrl"
-          + local.fieldNamesByUrl.size(), urlExpression, true, true);
-      local.fieldNamesByUrl.put(resource, toReturn);
+    return sb.toString();
+  }
+
+  private DisplayedImage getImage(ResourceContext context,
+      ImageResourceDeclaration image) {
+    DisplayedImage toReturn = shared.externalImages.get(new BundleKey(image));
+    if (toReturn != null) {
+      return toReturn;
     }
+
+    BundleKey key = new BundleKey(context, image);
+    toReturn = shared.bundledImages.get(key);
+    if (toReturn == null) {
+      BundledImage bundled = new BundledImage();
+      shared.bundledImages.put(key, bundled);
+      toReturn = bundled;
+    }
+
     return toReturn;
   }
 
@@ -397,25 +643,12 @@
     }
   }
 
-  /**
-   * Re-encode an image as a PNG to strip random header data.
-   */
-  private URL renderToTempFile(TreeLogger logger, ImageBundleBuilder builder,
-      Arranger arranger) throws UnableToCompleteException {
-    try {
-      byte[] imageBytes = builder.render(logger, arranger);
-      if (imageBytes == null) {
-        return null;
-      }
-
-      File file = File.createTempFile(
-          ImageResourceGenerator.class.getSimpleName(), ".png");
-      file.deleteOnExit();
-      Util.writeBytesToFile(logger, file, imageBytes);
-      return file.toURI().toURL();
-    } catch (IOException ex) {
-      logger.log(TreeLogger.ERROR, "Unable to write re-encoded PNG", ex);
-      throw new UnableToCompleteException();
+  private void renderImageMap(TreeLogger logger, ResourceContext context,
+      ClientBundleFields fields, Map<BundleKey, ? extends DisplayedImage> map)
+      throws UnableToCompleteException {
+    for (Map.Entry<BundleKey, ? extends DisplayedImage> entry : map.entrySet()) {
+      DisplayedImage bundle = entry.getValue();
+      bundle.render(logger, context, fields, entry.getKey().getRepeatStyle());
     }
   }
 }
diff --git a/user/test/com/google/gwt/resources/client/ImageResourceTest.java b/user/test/com/google/gwt/resources/client/ImageResourceTest.java
index 1b0c31c..ac3e9ef 100644
--- a/user/test/com/google/gwt/resources/client/ImageResourceTest.java
+++ b/user/test/com/google/gwt/resources/client/ImageResourceTest.java
@@ -78,6 +78,14 @@
 
     // Test default filename lookup while we're at it
     ImageResource largeLossy();
+  
+    @Source("64x64.png")
+    @ImageOptions(width = 32)
+    ImageResource scaledDown();
+    
+    @Source("64x64.png")
+    @ImageOptions(width = 128)
+    ImageResource scaledUp();
   }
 
   @Override
@@ -113,8 +121,8 @@
     assertEquals(a.getLeft(), b.getLeft());
     assertEquals(a.getLeft(), c.getLeft());
 
-    assertEquals(a.getLeft(), b.getTop());
-    assertEquals(a.getLeft(), c.getTop());
+    assertEquals(a.getTop(), b.getTop());
+    assertEquals(a.getTop(), c.getTop());
 
     delayTestFinish(10000);
     // See if the size of the image strip is what we expect
@@ -151,5 +159,11 @@
 
     assertEquals(16, r.i16x16Horizontal().getWidth());
     assertEquals(16, r.i16x16Horizontal().getHeight());
+
+    // Check scaling and aspect ratio
+    assertEquals(32, r.scaledDown().getWidth());
+    assertEquals(32, r.scaledDown().getHeight());
+    assertEquals(128, r.scaledUp().getWidth());
+    assertEquals(128, r.scaledUp().getHeight());
   }
 }