| /* |
| * 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.resources.rg; |
| |
| import com.google.gwt.core.ext.TreeLogger; |
| import com.google.gwt.core.ext.UnableToCompleteException; |
| import com.google.gwt.dev.util.log.PrintWriterTreeLogger; |
| import com.google.gwt.dev.util.log.speedtracer.CompilerEventType; |
| import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger; |
| |
| import org.w3c.dom.Node; |
| |
| import java.awt.Graphics2D; |
| import java.awt.RenderingHints; |
| import java.awt.geom.AffineTransform; |
| import java.awt.image.BufferedImage; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.PrintWriter; |
| import java.net.MalformedURLException; |
| 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.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| |
| import javax.imageio.ImageIO; |
| import javax.imageio.ImageReader; |
| import javax.imageio.metadata.IIOMetadata; |
| import javax.imageio.metadata.IIOMetadataFormatImpl; |
| import javax.imageio.stream.MemoryCacheImageInputStream; |
| |
| /** |
| * Accumulates state for the bundled image. |
| */ |
| class ImageBundleBuilder { |
| /** |
| * Abstracts the process of arranging a number of images into a composite |
| * image. |
| */ |
| interface Arranger { |
| /** |
| * Determine the total area required to store a composite image. |
| */ |
| Size arrangeImages(Collection<ImageRect> rects); |
| } |
| |
| /** |
| * 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 class BestFitArranger implements Arranger { |
| 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. |
| return (c != 0) ? c : b.getName().compareTo(a.getName()); |
| } |
| }; |
| |
| 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. |
| return (c != 0) ? c : b.getName().compareTo(a.getName()); |
| } |
| }; |
| |
| 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<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<ImageRect> rectsOrderedByWidth = new ArrayList<ImageRect>( |
| rects); |
| Collections.sort(rectsOrderedByWidth, decreasingWidthComparator); |
| |
| // Place the first, tallest image as the first column. |
| final ImageRect 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<ImageRect> rectsInColumn = new ArrayList<ImageRect>(); |
| for (int j = i; j < n; 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() |
| && (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 void arrangeColumn(List<ImageRect> rectsInColumn, |
| List<ImageRect> remainingRectsOrderedByWidth) { |
| final ImageRect 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 ImageRect 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 ImageRect 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(); |
| } |
| } |
| } |
| |
| /** |
| * 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<ImageRect> rects) { |
| int height = 1; |
| int width = 0; |
| |
| for (ImageRect rect : rects) { |
| rect.setPosition(width, 0); |
| width += rect.getWidth(); |
| height = lcm(height, rect.getHeight()); |
| } |
| |
| List<ImageRect> toAdd = new ArrayList<ImageRect>(); |
| for (ImageRect rect : rects) { |
| int y = rect.getHeight(); |
| while (y < height) { |
| ImageRect newRect = new ImageRect(rect); |
| newRect.setPosition(rect.getLeft(), y); |
| y += rect.getHeight(); |
| toAdd.add(newRect); |
| } |
| } |
| rects.addAll(toAdd); |
| |
| return new Size(width, height); |
| } |
| } |
| |
| /** |
| * Does not rearrange the rectangles, but simply computes the size of the |
| * canvas needed to hold the images in their current positions. |
| */ |
| static class IdentityArranger implements Arranger { |
| public Size arrangeImages(Collection<ImageRect> rects) { |
| int height = 0; |
| int width = 0; |
| |
| for (ImageRect rect : rects) { |
| height = Math.max(height, rect.getTop() + rect.getHeight()); |
| width = Math.max(width, rect.getLeft() + rect.getWidth()); |
| } |
| |
| return new Size(width, height); |
| } |
| } |
| |
| /** |
| * The rectangle at which the original image is placed into the composite |
| * image. |
| */ |
| static class ImageRect { |
| |
| private boolean hasBeenPositioned, lossy; |
| private int height, width; |
| private final int intrinsicHeight, intrinsicWidth; |
| private final BufferedImage[] images; |
| private int left, top; |
| private final String name; |
| private final AffineTransform transform = new AffineTransform(); |
| |
| /** |
| * Copy constructor. |
| */ |
| public ImageRect(ImageRect other) { |
| this.name = other.getName(); |
| 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... images) { |
| this.name = name; |
| this.images = images; |
| this.intrinsicWidth = images[0].getWidth(); |
| this.intrinsicHeight = images[0].getHeight(); |
| this.height = this.width = -1; |
| } |
| |
| public int getHeight() { |
| return height > 0 ? height : intrinsicHeight; |
| } |
| |
| public BufferedImage getImage() { |
| return images[0]; |
| } |
| |
| public BufferedImage[] getImages() { |
| return images; |
| } |
| |
| public int getLeft() { |
| return left; |
| } |
| |
| public String getName() { |
| return name; |
| } |
| |
| public int getTop() { |
| return top; |
| } |
| |
| public AffineTransform getTransform() { |
| return new AffineTransform(transform); |
| } |
| |
| public int getWidth() { |
| return width > 0 ? width : intrinsicWidth; |
| } |
| |
| public boolean hasBeenPositioned() { |
| return hasBeenPositioned; |
| } |
| |
| public boolean isAnimated() { |
| return images.length > 1; |
| } |
| |
| public boolean isLossy() { |
| 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; |
| } |
| |
| public void setPosition(int left, int top) { |
| hasBeenPositioned = true; |
| this.left = left; |
| this.top = top; |
| } |
| |
| public void setTransform(AffineTransform transform) { |
| 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); |
| return toReturn; |
| } |
| |
| private boolean checkTransform(AffineTransform tx) { |
| double[] in = {0, 0, intrinsicWidth, intrinsicHeight}; |
| double[] out = {0, 0, 0, 0}; |
| |
| tx.transform(in, 0, out, 0, 2); |
| |
| // 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; |
| } |
| } |
| |
| /** |
| * Used to return the size of the resulting image from the method |
| * {@link ImageBundleBuilder#arrangeImages()}. |
| */ |
| static class Size { |
| private final int width, height; |
| |
| Size(int width, int height) { |
| this.width = width; |
| this.height = height; |
| } |
| } |
| |
| /** |
| * Performs a simple vertical arrangement of rectangles. Images will be tiled |
| * horizontally to fill the full width of the image. |
| */ |
| static class VerticalArranger implements Arranger { |
| public Size arrangeImages(Collection<ImageRect> rects) { |
| int height = 0; |
| int width = 1; |
| |
| for (ImageRect rect : rects) { |
| rect.setPosition(0, height); |
| width = lcm(width, rect.getWidth()); |
| height += rect.getHeight(); |
| } |
| |
| List<ImageRect> toAdd = new ArrayList<ImageRect>(); |
| for (ImageRect rect : rects) { |
| int x = rect.getWidth(); |
| while (x < width) { |
| ImageRect newRect = new ImageRect(rect); |
| newRect.setPosition(x, rect.getTop()); |
| x += rect.getWidth(); |
| toAdd.add(newRect); |
| } |
| } |
| rects.addAll(toAdd); |
| |
| return new Size(width, height); |
| } |
| } |
| |
| /* |
| * 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. |
| */ |
| static final String BUNDLE_FILE_TYPE = "png"; |
| static final String BUNDLE_MIME_TYPE = "image/png"; |
| private static final int IMAGE_MAX_SIZE = Integer.getInteger( |
| "gwt.imageResource.maxBundleSize", 256); |
| |
| public static void main(String[] args) { |
| final TreeLogger logger = new PrintWriterTreeLogger(new PrintWriter( |
| System.out)); |
| if (args.length < 2) { |
| logger.log(TreeLogger.ERROR, ImageBundleBuilder.class.getSimpleName() |
| + " <output file> <input file> ..."); |
| System.exit(-1); |
| } |
| |
| ImageBundleBuilder builder = new ImageBundleBuilder(); |
| boolean fail = false; |
| for (int i = 1, j = args.length; i < j; i++) { |
| TreeLogger loopLogger = logger.branch(TreeLogger.DEBUG, |
| "Processing argument " + args[i]); |
| File file = new File(args[i]); |
| |
| Exception ex = null; |
| try { |
| builder.assimilate(loopLogger, args[i], file.toURI().toURL()); |
| } catch (MalformedURLException e) { |
| ex = e; |
| } catch (UnableToCompleteException e) { |
| ex = e; |
| } catch (UnsuitableForStripException e) { |
| ex = e; |
| } |
| if (ex != null) { |
| loopLogger.log(TreeLogger.ERROR, "Unable to assimilate image", ex); |
| fail = true; |
| } |
| } |
| |
| if (fail) { |
| System.exit(-1); |
| } |
| |
| final String outFile = args[0]; |
| try { |
| BufferedImage bundledImage = builder.drawBundledImage(new BestFitArranger()); |
| byte[] bytes = createImageBytes(logger, bundledImage); |
| |
| FileOutputStream out = new FileOutputStream(outFile); |
| out.write(bytes); |
| out.close(); |
| } catch (IOException e) { |
| logger.log(TreeLogger.ERROR, "Unable to write output file", e); |
| System.exit(-2); |
| } catch (UnableToCompleteException e) { |
| logger.log(TreeLogger.ERROR, "Unable to draw output image", e); |
| System.exit(-2); |
| } |
| |
| System.exit(0); |
| } |
| |
| public static byte[] toPng(TreeLogger logger, ImageRect rect) |
| throws UnableToCompleteException { |
| // Create the bundled image. |
| BufferedImage bundledImage = new BufferedImage(rect.getWidth(), |
| rect.getHeight(), 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(); |
| |
| setBetterRenderingQuality(g2d); |
| |
| g2d.drawImage(rect.getImage(), rect.transform(), null); |
| g2d.dispose(); |
| |
| byte[] imageBytes = createImageBytes(logger, bundledImage); |
| return imageBytes; |
| } |
| |
| /** |
| * Write the bundled image into a byte array, so that we can compute its |
| * strong name. |
| */ |
| private static byte[] createImageBytes(TreeLogger logger, |
| BufferedImage bundledImage) throws UnableToCompleteException { |
| 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(); |
| } |
| return imageBytes; |
| } |
| |
| /** |
| * Compute the greatest common denominator of two numbers. |
| */ |
| private static int gcd(int a, int b) { |
| while (b != 0) { |
| int t = b; |
| b = a % b; |
| a = t; |
| } |
| return a; |
| } |
| |
| /** |
| * Compute the least common multiple of two numbers. This is used by |
| * {@link HorizontalArranger} and {@link VerticalArranger} to determine how |
| * large the composite image should be to allow every image to line up when |
| * repeated. |
| */ |
| private static int lcm(int a, int b) { |
| return b / gcd(a, b) * a; |
| } |
| |
| private static void setBetterRenderingQuality(Graphics2D g2d) { |
| g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, |
| RenderingHints.VALUE_INTERPOLATION_BICUBIC); |
| g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, |
| RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); |
| } |
| |
| private final Map<String, ImageRect> imageNameToImageRectMap = new HashMap<String, ImageRect>(); |
| |
| public ImageBundleBuilder() { |
| } |
| |
| /** |
| * Copy constructor. |
| */ |
| public ImageBundleBuilder(ImageBundleBuilder other) { |
| for (Map.Entry<String, ImageRect> entry : other.imageNameToImageRectMap.entrySet()) { |
| imageNameToImageRectMap.put(entry.getKey(), new ImageRect( |
| entry.getValue())); |
| } |
| } |
| |
| /** |
| * 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 |
| * @param resource the URL from which the image data wil be loaded |
| * @throws UnableToCompleteException if the image with name |
| * <code>imageName</code> cannot be added to the master composite |
| * image |
| */ |
| public ImageRect assimilate(TreeLogger logger, String imageName, URL resource) |
| throws UnableToCompleteException, UnsuitableForStripException { |
| |
| /* |
| * 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, resource); |
| |
| imageNameToImageRectMap.put(imageName, rect); |
| } |
| return rect; |
| } |
| |
| public int getImageCount() { |
| return imageNameToImageRectMap.size(); |
| } |
| |
| public ImageRect getMapping(String imageName) { |
| return imageNameToImageRectMap.get(imageName); |
| } |
| |
| /** |
| * Remove an image from the builder. |
| */ |
| public ImageRect removeMapping(String imageName) { |
| return imageNameToImageRectMap.remove(imageName); |
| } |
| |
| /** |
| * Render the composited image into an array of bytes. |
| */ |
| public byte[] render(TreeLogger logger, Arranger arranger) |
| throws UnableToCompleteException { |
| if (imageNameToImageRectMap.isEmpty()) { |
| return null; |
| } |
| |
| // Create the bundled image from all of the constituent images. |
| BufferedImage bundledImage = drawBundledImage(arranger); |
| |
| byte[] imageBytes = createImageBytes(logger, bundledImage); |
| |
| return imageBytes; |
| } |
| |
| private ImageRect addImage(TreeLogger logger, String imageName, URL imageUrl) |
| throws UnableToCompleteException, UnsuitableForStripException { |
| |
| logger = logger.branch(TreeLogger.TRACE, |
| "Adding image '" + imageName + "'", null); |
| |
| BufferedImage image = null; |
| // Be safe by default and assume that the incoming image is lossy |
| boolean lossy = true; |
| // Load the image |
| try (InputStream is = imageUrl.openStream(); |
| MemoryCacheImageInputStream imageInputStream = new MemoryCacheImageInputStream(is)) { |
| /* |
| * ImageIO uses an SPI pattern API. We don't care about the particulars of |
| * the implementation, so just choose the first ImageReader. |
| */ |
| Iterator<ImageReader> it = ImageIO.getImageReaders(imageInputStream); |
| readers : while (it.hasNext()) { |
| ImageReader reader = it.next(); |
| reader.setInput(imageInputStream); |
| |
| int numImages = reader.getNumImages(true); |
| if (numImages == 0) { |
| // Fall through |
| |
| } else if (numImages == 1) { |
| try { |
| image = reader.read(0); |
| IIOMetadata metadata = reader.getImageMetadata(0); |
| if (metadata != null |
| && metadata.isStandardMetadataFormatSupported()) { |
| // http://java.sun.com/j2se/1.5.0/docs/api/javax/imageio/metadata/doc-files/standard_metadata.html |
| Node data = metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); |
| metadata : for (int i = 0, j = data.getChildNodes().getLength(); i < j; i++) { |
| Node child = data.getChildNodes().item(i); |
| if (child.getLocalName().equalsIgnoreCase("compression")) { |
| for (int k = 0, l = child.getChildNodes().getLength(); k < l; k++) { |
| Node child2 = child.getChildNodes().item(k); |
| if (child2.getLocalName().equalsIgnoreCase("lossless")) { |
| Node value = child2.getAttributes().getNamedItem("value"); |
| if (value == null) { |
| // The default is true, according to the DTD |
| lossy = false; |
| } else { |
| lossy = !Boolean.parseBoolean(value.getNodeValue()); |
| } |
| break metadata; |
| } |
| } |
| } |
| } |
| } |
| } catch (Exception e) { |
| // Hope we have another reader that can handle the image |
| continue readers; |
| } |
| |
| } else { |
| // Read all contained images |
| BufferedImage[] images = new BufferedImage[numImages]; |
| |
| try { |
| for (int i = 0; i < numImages; i++) { |
| images[i] = reader.read(i); |
| } |
| } catch (Exception e) { |
| // Hope we have another reader that can handle the image |
| continue readers; |
| } |
| |
| ImageRect rect = new ImageRect(imageName, images); |
| throw new UnsuitableForStripException(rect); |
| } |
| } |
| } catch (IllegalArgumentException iex) { |
| if (imageName.toLowerCase(Locale.ROOT).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; |
| } |
| } catch (IOException e) { |
| logger.log(TreeLogger.ERROR, "Unable to read image resource", e); |
| throw new UnableToCompleteException(); |
| } |
| |
| if (image == null) { |
| logger.log(TreeLogger.ERROR, "Unrecognized image file format", null); |
| throw new UnableToCompleteException(); |
| } |
| |
| ImageRect toReturn = new ImageRect(imageName, image); |
| toReturn.setLossy(lossy); |
| |
| // Don't composite the image if it's lossy or if it is too big |
| if (lossy || toReturn.getHeight() > IMAGE_MAX_SIZE |
| || toReturn.getWidth() > IMAGE_MAX_SIZE) { |
| throw new UnsuitableForStripException(toReturn); |
| } |
| |
| return toReturn; |
| } |
| |
| /** |
| * 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(Arranger arranger) { |
| |
| /* |
| * 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 = new LinkedList<ImageRect>( |
| imageNameToImageRectMap.values()); |
| |
| // Arrange images and determine the size of the resulting bundle. |
| Size size = arranger.arrangeImages(imageRects); |
| |
| // Create the bundled image. |
| BufferedImage bundledImage = new BufferedImage(size.width, size.height, |
| BufferedImage.TYPE_INT_ARGB_PRE); |
| |
| SpeedTracerLogger.Event graphicsEvent = SpeedTracerLogger.start(CompilerEventType.GRAPHICS_INIT, |
| "java.awt.headless", System.getProperty("java.awt.headless")); |
| Graphics2D g2d = bundledImage.createGraphics(); |
| |
| setBetterRenderingQuality(g2d); |
| |
| graphicsEvent.end(); |
| |
| for (ImageRect imageRect : imageRects) { |
| g2d.drawImage(imageRect.getImage(), imageRect.transform(), null); |
| } |
| g2d.dispose(); |
| |
| return bundledImage; |
| } |
| } |