/* * Copyright 2008 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.gwt.user.rebind.ui; import com.google.gwt.core.ext.GeneratorContext; 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.speedtracer.CompilerEventType; import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.imageio.ImageIO; /** * Accumulates state for the bundled image. */ class ImageBundleBuilder { /** * The rectangle at which the original image is placed into the composite * image. */ public static class ImageRect implements HasRect { private final String name; private final int height, width; private final BufferedImage image; private int left, top; private boolean hasBeenPositioned; public ImageRect(String name, BufferedImage image) { this.name = name; this.image = image; this.width = image.getWidth(); this.height = image.getHeight(); } public int getHeight() { return height; } public int getLeft() { return left; } public String getName() { return name; } public int getTop() { return top; } public int getWidth() { return width; } public boolean hasBeenPositioned() { return hasBeenPositioned; } public void setPosition(int left, int top) { hasBeenPositioned = true; this.left = left; this.top = top; } } /** * A mockable interface to test the image arrangement algorithms. */ interface HasRect { String getName(); int getHeight(); int getLeft(); int getTop(); int getWidth(); boolean hasBeenPositioned(); void setPosition(int left, int top); } /** * Used to return the size of the resulting image from the method * {@link ImageBundleBuilder#arrangeImages()}. */ private static class Size { private final int width, height; Size(int width, int height) { this.width = width; this.height = height; } } private static final Comparator<HasRect> decreasingHeightComparator = new Comparator<HasRect>() { public int compare(HasRect a, HasRect b) { final int c = b.getHeight() - a.getHeight(); // If we encounter equal heights, use the name to keep things // deterministic. return (c != 0) ? c : b.getName().compareTo(a.getName()); } }; private static final Comparator<HasRect> decreasingWidthComparator = new Comparator<HasRect>() { public int compare(HasRect a, HasRect b) { final int c = b.getWidth() - a.getWidth(); // If we encounter equal heights, use the name to keep things // deterministic. return (c != 0) ? c : b.getName().compareTo(a.getName()); } }; /* * Only PNG is supported right now. In the future, we may be able to infer the * best output type, and get rid of this constant. */ private static final String BUNDLE_FILE_TYPE = "png"; /** * Arranges the images to try to decrease the overall area of the resulting * bundle. This uses a strategy that is basically Next-Fit Decreasing Height * Decreasing Width (NFDHDW). The rectangles to be packed are sorted in * decreasing order by height. The tallest rectangle is placed at the far * left. We attempt to stack the remaining rectangles on top of one another to * construct as many columns as necessary. After finishing each column, we * also attempt to do some horizontal packing to fill up the space left due to * widths of rectangles differing in the column. */ static Size arrangeImages(Collection<? extends HasRect> 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>( 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>(rects); Collections.sort(rectsOrderedByWidth, decreasingWidthComparator); // Place the first, tallest image as the first column. final HasRect first = rectsOrderedByHeight.get(0); first.setPosition(0, 0); // Setup state for laying things cumulatively. int curX = first.getWidth(); final int colH = first.getHeight(); for (int i = 1, n = rectsOrderedByHeight.size(); i < n; i++) { // If this ImageRect has been positioned already, move on. if (rectsOrderedByHeight.get(i).hasBeenPositioned()) { continue; } int colW = 0; int curY = 0; final ArrayList<HasRect> rectsInColumn = new ArrayList<HasRect>(); for (int j = i; j < n; j++) { final HasRect 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() && (curY + current.getHeight()) <= colH) { // Set the horizontal position here, the top field will be set in // arrangeColumn after we've collected a full set of ImageRects. current.setPosition(curX, 0); colW = Math.max(colW, current.getWidth()); curY += current.getHeight(); // Keep the ImageRects in this column in decreasing order by width. final int pos = Collections.binarySearch(rectsInColumn, current, decreasingWidthComparator); assert pos < 0; rectsInColumn.add(-1 - pos, current); } } // Having selected a set of ImageRects that fill out this column vertical, // now we'll scan the remaining ImageRects to try to fit some in the // horizontal gaps. if (!rectsInColumn.isEmpty()) { arrangeColumn(rectsInColumn, rectsOrderedByWidth); } // We're done with that column, so move the horizontal accumulator by the // width of the column we just finished. curX += colW; } return new Size(curX, colH); } /** * Companion method to {@link #arrangeImages()}. This method does a best * effort horizontal packing of a column after it was packed vertically. This * is the Decreasing Width part of Next-Fit Decreasing Height Decreasing * Width. The basic strategy is to sort the remaining rectangles by decreasing * width and try to fit them to the left of each of the rectangles we've * already picked for this column. * * @param rectsInColumn the ImageRects that were already selected for this * column * @param remainingRectsOrderedByWidth the sub list of ImageRects that may not * have been positioned yet */ private static void arrangeColumn(List<HasRect> rectsInColumn, List<HasRect> remainingRectsOrderedByWidth) { final HasRect first = rectsInColumn.get(0); final int columnWidth = first.getWidth(); int curY = first.getHeight(); // 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); // The ImageRect was previously positioned horizontally, now set the top // field. r.setPosition(r.getLeft(), curY); int curX = r.getWidth(); // Search for ImageRects that are shorter than the left most ImageRect and // narrow enough to fit in the column. for (int j = 0, n = remainingRectsOrderedByWidth.size(); j < n; j++) { final HasRect current = remainingRectsOrderedByWidth.get(j); if (!current.hasBeenPositioned() && (curX + current.getWidth()) <= columnWidth && (current.getHeight() <= r.getHeight())) { current.setPosition(r.getLeft() + curX, r.getTop()); curX += current.getWidth(); } } // Update the vertical accumulator so we'll know where to place the next // ImageRect. curY += r.getHeight(); } } private final Map<String, ImageRect> imageNameToImageRectMap = new HashMap<String, ImageRect>(); /** * Assimilates the image associated with a particular image method into the * master composite. If the method names an image that has already been * assimilated, the existing image rectangle is reused. * * @param logger a hierarchical logger which logs to the hosted console * @param imageName the name of an image that can be found on the classpath * @throws UnableToCompleteException if the image with name * <code>imageName</code> cannot be added to the master composite * image */ public void assimilate(TreeLogger logger, String imageName) throws UnableToCompleteException { /* * Decide whether or not we need to add to the composite image. Either way, * we associated it with the rectangle of the specified image as it exists * within the composite image. Note that the coordinates of the rectangle * aren't computed until the composite is written. */ ImageRect rect = getMapping(imageName); if (rect == null) { // Assimilate the image into the composite. rect = addImage(logger, imageName); // Map the URL to its image so that even if the same URL is used more than // once, we only include the referenced image once in the bundled image. putMapping(imageName, rect); } } public ImageRect getMapping(String imageName) { return imageNameToImageRectMap.get(imageName); } public String writeBundledImage(TreeLogger logger, GeneratorContext context) throws UnableToCompleteException { // Create the bundled image from all of the constituent images. BufferedImage bundledImage = drawBundledImage(); // Write the bundled image into a byte array, so that we can compute // its strong name. byte[] imageBytes; try { ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); boolean writerAvailable = ImageIO.write(bundledImage, BUNDLE_FILE_TYPE, byteOutputStream); if (!writerAvailable) { logger.log(TreeLogger.ERROR, "No " + BUNDLE_FILE_TYPE + " writer available"); throw new UnableToCompleteException(); } imageBytes = byteOutputStream.toByteArray(); } catch (IOException e) { logger.log(TreeLogger.ERROR, "An error occurred while trying to write the image bundle.", e); throw new UnableToCompleteException(); } // Compute the file name. The strong name is generated from the bytes of // the bundled image. The '.cache' part indicates that it can be // permanently cached. String bundleFileName = Util.computeStrongName(imageBytes) + ".cache." + BUNDLE_FILE_TYPE; // Try and write the file to disk. If a file with bundleFileName already // exists, then the file will not be written. OutputStream outStream = context.tryCreateResource(logger, bundleFileName); if (outStream != null) { try { // Write the image bytes from the byte array to the pending stream. outStream.write(imageBytes); // Commit the stream. context.commitResource(logger, outStream); } catch (IOException e) { logger.log(TreeLogger.ERROR, "Failed while writing", e); throw new UnableToCompleteException(); } } else { logger.log(TreeLogger.TRACE, "Generated image bundle file already exists; no need to rewrite it.", null); } return bundleFileName; } private ImageRect addImage(TreeLogger logger, String imageName) throws UnableToCompleteException { logger = logger.branch(TreeLogger.TRACE, "Adding image '" + imageName + "'", null); // Fetch the image. try { // Could turn this lookup logic into an externally-supplied policy for // increased generality. URL imageUrl = getClass().getClassLoader().getResource(imageName); if (imageUrl == null) { // This should never happen, because this check is done right after // the image name is retrieved from the metadata or the method name. // If there is a failure in obtaining the resource, it will happen // before this point. logger.log(TreeLogger.ERROR, "Resource not found on classpath (is the name specified as " + "Class.getResource() would expect?)", null); throw new UnableToCompleteException(); } BufferedImage image; // Load the image try { image = ImageIO.read(imageUrl); } catch (IllegalArgumentException iex) { if (imageName.toLowerCase().endsWith("png") && iex.getMessage() != null && iex.getStackTrace()[0].getClassName().equals( "javax.imageio.ImageTypeSpecifier$Indexed")) { logger.log( TreeLogger.ERROR, "Unable to read image. The image may not be in valid PNG format. " + "This problem may also be due to a bug in versions of the " + "JRE prior to 1.6. See " + "http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5098176 " + "for more information. If this bug is the cause of the " + "error, try resaving the image using a different image " + "program, or upgrade to a newer JRE.", null); throw new UnableToCompleteException(); } else { throw iex; } } if (image == null) { logger.log(TreeLogger.ERROR, "Unrecognized image file format", null); throw new UnableToCompleteException(); } return new ImageRect(imageName, image); } catch (IOException e) { logger.log(TreeLogger.ERROR, "Unable to read image resource", null); throw new UnableToCompleteException(); } } /** * This method creates the bundled image through the composition of the other * images. * * In this particular implementation, we use NFDHDW (see * {@link #arrangeImages()}) to get an approximate optimal image packing. * * The most important aspect of drawing the bundled image is that it be drawn * in a deterministic way. The drawing of the image should not rely on * implementation details of the Generator system which may be subject to * change. */ private BufferedImage drawBundledImage() { // There is no need to impose any order here, because arrangeImages // will position the ImageRects in a deterministic fashion, even though // we might paint them in a non-deterministic order. Collection<ImageRect> imageRects = imageNameToImageRectMap.values(); // Arrange images and determine the size of the resulting bundle. final Size size = arrangeImages(imageRects); // Create the bundled image. BufferedImage bundledImage = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB_PRE); SpeedTracerLogger.Event createGraphicsEvent = SpeedTracerLogger.start(CompilerEventType.GRAPHICS_INIT, "java.awt.headless", System.getProperty("java.awt.headless")); Graphics2D g2d = bundledImage.createGraphics(); createGraphicsEvent.end(); for (ImageRect imageRect : imageRects) { // We do not need to pass in an ImageObserver, because we are working // with BufferedImages. ImageObservers only need to be used when // the image to be drawn is being loaded asynchronously. See // http://java.sun.com/docs/books/tutorial/2d/images/drawimage.html // for more information. g2d.drawImage(imageRect.image, imageRect.left, imageRect.top, null); } g2d.dispose(); return bundledImage; } private void putMapping(String imageName, ImageRect rect) { imageNameToImageRectMap.put(imageName, rect); } }