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());
}
}