com.google.gwt.resources.rg.ImageBundleBuilder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of vaadin-client Show documentation
Show all versions of vaadin-client Show documentation
Vaadin is a web application framework for Rich Internet Applications (RIA).
Vaadin enables easy development and maintenance of fast and
secure rich web
applications with a stunning look and feel and a wide browser support.
It features a server-side architecture with the majority of the logic
running
on the server. Ajax technology is used at the browser-side to ensure a
rich
and interactive user experience.
/*
* 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.core.shared.impl.StringCase;
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.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.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 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 decreasingHeightComparator = new Comparator() {
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 decreasingWidthComparator = new Comparator() {
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 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 rectsOrderedByHeight = new ArrayList(
rects);
Collections.sort(rectsOrderedByHeight, decreasingHeightComparator);
// Create a list of ImageRects ordered by decreasing width used for
// packing
// individual columns.
final ArrayList rectsOrderedByWidth = new ArrayList(
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 rectsInColumn = new ArrayList();
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 rectsInColumn,
List 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 rects) {
int height = 1;
int width = 0;
for (ImageRect rect : rects) {
rect.setPosition(width, 0);
width += rect.getWidth();
height = lcm(height, rect.getHeight());
}
List toAdd = new ArrayList();
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 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 rects) {
int height = 0;
int width = 1;
for (ImageRect rect : rects) {
rect.setPosition(0, height);
width = lcm(width, rect.getWidth());
height += rect.getHeight();
}
List toAdd = new ArrayList();
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()
+ "