001: /*
002: * Copyright 2007 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.user.rebind.ui;
017:
018: import com.google.gwt.core.ext.GeneratorContext;
019: import com.google.gwt.core.ext.TreeLogger;
020: import com.google.gwt.core.ext.UnableToCompleteException;
021: import com.google.gwt.dev.util.Util;
022:
023: import java.awt.image.BufferedImage;
024: import java.awt.Graphics2D;
025: import java.io.IOException;
026: import java.io.OutputStream;
027: import java.io.ByteArrayOutputStream;
028: import java.net.URL;
029: import java.util.HashMap;
030: import java.util.Map;
031: import java.util.Collection;
032: import java.util.SortedMap;
033: import java.util.TreeMap;
034:
035: import javax.imageio.ImageIO;
036:
037: /**
038: * Accumulates state for the bundled image.
039: */
040: class ImageBundleBuilder {
041:
042: /**
043: * The rectangle at which the original image is placed into the composite
044: * image.
045: */
046: public static class ImageRect {
047:
048: public final int height;
049: public final BufferedImage image;
050: public int left;
051: public final int width;
052:
053: public ImageRect(BufferedImage image) {
054: this .image = image;
055: this .width = image.getWidth();
056: this .height = image.getHeight();
057: }
058: }
059:
060: /*
061: * Only PNG is supported right now. In the future, we may be able to infer the
062: * best output type, and get rid of this constant.
063: */
064: private static final String BUNDLE_FILE_TYPE = "png";
065:
066: private final Map<String, ImageRect> imageNameToImageRectMap = new HashMap<String, ImageRect>();
067:
068: /**
069: * Assimilates the image associated with a particular image method into the
070: * master composite. If the method names an image that has already been
071: * assimilated, the existing image rectangle is reused.
072: *
073: * @param logger a hierarchical logger which logs to the hosted console
074: * @param imageName the name of an image that can be found on the classpath
075: * @throws UnableToCompleteException if the image with name
076: * <code>imageName</code> cannot be added to the master composite
077: * image
078: */
079: public void assimilate(TreeLogger logger, String imageName)
080: throws UnableToCompleteException {
081:
082: /*
083: * Decide whether or not we need to add to the composite image. Either way,
084: * we associated it with the rectangle of the specified image as it exists
085: * within the composite image. Note that the coordinates of the rectangle
086: * aren't computed until the composite is written.
087: */
088: ImageRect rect = getMapping(imageName);
089: if (rect == null) {
090: // Assimilate the image into the composite.
091: rect = addImage(logger, imageName);
092:
093: // Map the URL to its image so that even if the same URL is used more than
094: // once, we only include the referenced image once in the bundled image.
095: putMapping(imageName, rect);
096: }
097: }
098:
099: public ImageRect getMapping(String imageName) {
100: return imageNameToImageRectMap.get(imageName);
101: }
102:
103: public String writeBundledImage(TreeLogger logger,
104: GeneratorContext context) throws UnableToCompleteException {
105:
106: // Create the bundled image from all of the constituent images.
107: BufferedImage bundledImage = drawBundledImage();
108:
109: // Write the bundled image into a byte array, so that we can compute
110: // its strong name.
111: byte[] imageBytes;
112:
113: try {
114: ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
115: ImageIO.write(bundledImage, BUNDLE_FILE_TYPE,
116: byteOutputStream);
117: imageBytes = byteOutputStream.toByteArray();
118: } catch (IOException e) {
119: logger
120: .log(
121: TreeLogger.ERROR,
122: "Unable to generate file name for image bundle file",
123: null);
124: throw new UnableToCompleteException();
125: }
126:
127: // Compute the file name. The strong name is generated from the bytes of
128: // the bundled image. The '.cache' part indicates that it can be
129: // permanently cached.
130: String bundleFileName = Util.computeStrongName(imageBytes)
131: + ".cache." + BUNDLE_FILE_TYPE;
132:
133: // Try and write the file to disk. If a file with bundleFileName already
134: // exists, then the file will not be written.
135: OutputStream outStream = context.tryCreateResource(logger,
136: bundleFileName);
137:
138: if (outStream != null) {
139: try {
140: // Write the image bytes from the byte array to the pending stream.
141: outStream.write(imageBytes);
142:
143: // Commit the stream.
144: context.commitResource(logger, outStream);
145:
146: } catch (IOException e) {
147: logger.log(TreeLogger.ERROR, "Failed while writing", e);
148: throw new UnableToCompleteException();
149: }
150: } else {
151: logger
152: .log(
153: TreeLogger.TRACE,
154: "Generated image bundle file already exists; no need to rewrite it.",
155: null);
156: }
157:
158: return bundleFileName;
159: }
160:
161: private ImageRect addImage(TreeLogger logger, String imageName)
162: throws UnableToCompleteException {
163:
164: logger = logger.branch(TreeLogger.TRACE, "Adding image '"
165: + imageName + "'", null);
166:
167: // Fetch the image.
168: try {
169: // Could turn this lookup logic into an externally-supplied policy for
170: // increased generality.
171: URL imageUrl = getClass().getClassLoader().getResource(
172: imageName);
173: if (imageUrl == null) {
174: // This should never happen, because this check is done right after
175: // the image name is retrieved from the metadata or the method name.
176: // If there is a failure in obtaining the resource, it will happen
177: // before this point.
178: logger.log(TreeLogger.ERROR,
179: "Resource not found on classpath (is the name specified as "
180: + "Class.getResource() would expect?)",
181: null);
182: throw new UnableToCompleteException();
183: }
184:
185: BufferedImage image;
186: // Load the image
187: try {
188: image = ImageIO.read(imageUrl);
189: } catch (IllegalArgumentException iex) {
190: if (imageName.toLowerCase().endsWith("png")
191: && iex.getMessage() != null
192: && iex.getStackTrace()[0]
193: .getClassName()
194: .equals(
195: "javax.imageio.ImageTypeSpecifier$Indexed")) {
196: logger
197: .log(
198: TreeLogger.ERROR,
199: "Unable to read image. The image may not be in valid PNG format. "
200: + "This problem may also be due to a bug in versions of the "
201: + "JRE prior to 1.6. See "
202: + "http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5098176 "
203: + "for more information. If this bug is the cause of the "
204: + "error, try resaving the image using a different image "
205: + "program, or upgrade to a newer JRE.",
206: null);
207: throw new UnableToCompleteException();
208: } else {
209: throw iex;
210: }
211: }
212:
213: if (image == null) {
214: logger.log(TreeLogger.ERROR,
215: "Unrecognized image file format", null);
216: throw new UnableToCompleteException();
217: }
218:
219: return new ImageRect(image);
220:
221: } catch (IOException e) {
222: logger.log(TreeLogger.ERROR,
223: "Unable to read image resource", null);
224: throw new UnableToCompleteException();
225: }
226: }
227:
228: /*
229: * This method creates the bundled image through the composition of the other
230: * images.
231: *
232: * This method could be implemented in a variety of ways. For example, one
233: * could use a knapsack algorithm to draw these images in an optimal amount of
234: * space.
235: *
236: * In this particular implementation, we iterate through the image rectangles
237: * in ascending order of associated filename, and draw the rectangles from
238: * left to right in a single row.
239: *
240: * The most important aspect of drawing the bundled image is that it be drawn
241: * in a deterministic way. The drawing of the image should not rely on
242: * implementation details of the Generator system which may be subject to
243: * change. For example, at the time of this writing, the image names are added
244: * to imageNameToImageRectMap based on the alphabetical ordering of their
245: * associated methods. This behavior is the result of the oracle returning the
246: * list of a type's methods in alphabetical order. However, this behavior is
247: * undocumented, and should not be relied on. If this behavior were to change,
248: * it would inadvertently affect the generation of bundled images.
249: */
250: private BufferedImage drawBundledImage() {
251:
252: // Impose an ordering on the image rectangles, so that we construct
253: // the bundled image in a deterministic way.
254: SortedMap<String, ImageRect> sortedImageNameToImageRectMap = new TreeMap<String, ImageRect>();
255: sortedImageNameToImageRectMap.putAll(imageNameToImageRectMap);
256: Collection<ImageRect> orderedImageRects = sortedImageNameToImageRectMap
257: .values();
258:
259: // Determine how big the composited image should be by taking the
260: // sum of the widths and the max of the heights.
261: int nextLeft = 0;
262: int maxHeight = 0;
263: for (ImageRect imageRect : orderedImageRects) {
264: imageRect.left = nextLeft;
265: nextLeft += imageRect.width;
266: if (imageRect.height > maxHeight) {
267: maxHeight = imageRect.height;
268: }
269: }
270:
271: // Create the bundled image.
272: BufferedImage bundledImage = new BufferedImage(nextLeft,
273: maxHeight, BufferedImage.TYPE_INT_ARGB_PRE);
274: Graphics2D g2d = bundledImage.createGraphics();
275:
276: for (ImageRect imageRect : orderedImageRects) {
277:
278: // We do not need to pass in an ImageObserver, because we are working
279: // with BufferedImages. ImageObservers only need to be used when
280: // the image to be drawn is being loaded asynchronously. See
281: // http://java.sun.com/docs/books/tutorial/2d/images/drawimage.html
282: // for more information.
283: g2d.drawImage(imageRect.image, imageRect.left, 0, null);
284: }
285: g2d.dispose();
286:
287: return bundledImage;
288: }
289:
290: private void putMapping(String imageName, ImageRect rect) {
291: imageNameToImageRectMap.put(imageName, rect);
292: }
293: }
|