What's new? | Help | Directory | Sign in
Google
gwt-rolodex
A gwt widget for the animated display of image sets
  
  
  
  
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
/*
* 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.yesmail.gwt.rolodex.rebind;

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 javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.util.*;

public class RolodexImageBundleBuilder {

/**
* The rectangle at which the original image is placed into the composite
* image.
*/
public static class ImageRect {

public int height, mirroredHeight;

public int heightOffset;

public BufferedImage image, mirroredImage;

public int expandedLeft, leftCollapsedLeft, rightCollapsedLeft;

public int collapsedWidth;

public int width;

public ImageRect(BufferedImage image) {
this.image = image;
if (image == null) return;
this.width = image.getWidth();
this.height = image.getHeight();
}
}

protected static final AlphaComposite alphaComposite =
AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f);

protected static final int COLUMN_BUFFER_SIZE = 3;

protected static final double COLLAPSE_SCALE = 0.333333;

protected static final int imageType = BufferedImage.TYPE_INT_ARGB_PRE;

protected static final double WET_FLOOR_SCALE_FACTOR = 1.5;

private static final boolean FORCE_DEFAULT_STRATEGY = false;

private static HashMap<RenderingHints.Key, Object> renderConfig =
new HashMap<RenderingHints.Key, Object>();

static {
// This is currently optimized for quality
renderConfig
.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
renderConfig.put(RenderingHints.KEY_COLOR_RENDERING,
RenderingHints.VALUE_COLOR_RENDER_QUALITY);
renderConfig.put(RenderingHints.KEY_ALPHA_INTERPOLATION,
RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);

renderConfig.put(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
renderConfig
.put(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DEFAULT);
renderConfig.put(RenderingHints.KEY_FRACTIONALMETRICS,
RenderingHints.VALUE_FRACTIONALMETRICS_DEFAULT);
renderConfig.put(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
renderConfig.put(RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_DEFAULT);
renderConfig.put(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
}

protected static RenderingHints renderingHints =
new RenderingHints(renderConfig);

/*
* 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";

private final Map<String, ImageRect> imageNameToImageRectMap =
new HashMap<String, ImageRect>();

private int maxHeight;


/**
* 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 com.google.gwt.core.ext.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 int getMaxHeight() {
return maxHeight;
}

public String writeBundledImage(
TreeLogger logger, GeneratorContext context, int floorColor)
throws UnableToCompleteException {

// Create the bundled image from all of the constituent images.
BufferedImage bundledImage = drawBundledImage(logger, floorColor);

// Write the bundled image into a byte array, so that we can compute
// its strong name.
byte[] imageBytes;

try {
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
ImageIO.write(bundledImage, BUNDLE_FILE_TYPE, byteOutputStream);
imageBytes = byteOutputStream.toByteArray();
} catch (IOException e) {
logger.log(TreeLogger.ERROR,
"Unable to generate file name for image bundle file", null);
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;
}

protected BufferedImage prepareBundledImage(
Collection<ImageRect> orderedImageRects) {
// 1st pass
// Determine how big the composited image should be by taking the
// sum of the widths and the max of the heights.
int nextLeft = 0;
int maxHeight = 0;
for (ImageRect imageRect : orderedImageRects) {
imageRect.collapsedWidth = (int) (imageRect.width *
(1 - COLLAPSE_SCALE)) + COLUMN_BUFFER_SIZE + 2;
imageRect.leftCollapsedLeft = nextLeft;
imageRect.rightCollapsedLeft = nextLeft + imageRect.collapsedWidth;
imageRect.expandedLeft = nextLeft + (imageRect.collapsedWidth * 2);
nextLeft += imageRect.width + (imageRect.collapsedWidth * 2);
if (imageRect.height > maxHeight) {
maxHeight = imageRect.height;
}
}

int maxBaseHeight = maxHeight;

// 2nd pass to calculate the drawing offset
for (ImageRect imageRect : orderedImageRects) {
imageRect.heightOffset = maxBaseHeight - imageRect.height;
}

// account for additional wetfloor height
maxHeight = (int) (maxHeight * WET_FLOOR_SCALE_FACTOR);

// 2nd pass
// Create the bundled image.
BufferedImage bundledImage = new BufferedImage(nextLeft, maxHeight,
imageType);
this.maxHeight = maxHeight;
return bundledImage;
}

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(image);
} catch (IOException e) {
logger.log(TreeLogger.ERROR, "Unable to read image resource", null);
throw new UnableToCompleteException();
}
}

protected static void clearGraphics(Graphics2D g2d, BufferedImage image) {
Composite composite = g2d.getComposite();
g2d.setComposite(alphaComposite);
g2d.fillRect(0, 0, image.getWidth(), image.getHeight());
g2d.setComposite(composite);
}


/*
* This method creates the bundled image through the composition of the other
* images.
*
* This method could be implemented in a variety of ways. For example, one
* could use a knapsack algorithm to draw these images in an optimal amount of
* space.
*
* In this particular implementation, we iterate through the image rectangles
* in ascending order of associated filename, and draw the rectangles from
* left to right in a single row.
*
* 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. For example, at the time of this writing, the image names are added
* to imageNameToImageRectMap based on the alphabetical ordering of their
* associated methods. This behavior is the result of the oracle returning the
* list of a type's methods in alphabetical order. However, this behavior is
* undocumented, and should not be relied on. If this behavior were to change,
* it would inadvertently affect the generation of bundled images.
*/
private BufferedImage drawBundledImage(TreeLogger logger, int floorColor) {
Color backgroundColor = new Color(floorColor);

// Impose an ordering on the image rectangles, so that we construct
// the bundled image in a deterministic way.
SortedMap<String, ImageRect> sortedImageNameToImageRectMap =
new TreeMap<String, ImageRect>();
sortedImageNameToImageRectMap.putAll(imageNameToImageRectMap);
Collection<ImageRect> orderedImageRects =
sortedImageNameToImageRectMap.values();

BufferedImage bundledImage = prepareBundledImage(orderedImageRects);
Graphics2D g2d = bundledImage.createGraphics();
g2d.setRenderingHints(renderingHints);

// clear it with alpha
clearGraphics(g2d, bundledImage);

ImageEffectsStrategy imageEffectsStrategy = getImageEffectsStrategy(logger);

for (ImageRect imageRect : orderedImageRects) {
imageRect.mirroredImage = imageEffectsStrategy.drawWetFloorImage(imageRect, backgroundColor);
imageRect.mirroredHeight = imageRect.mirroredImage.getHeight();
imageEffectsStrategy.drawCollapsedLeftImage(imageRect, g2d);
imageEffectsStrategy.drawCollapsedRightImage(imageRect, g2d);
g2d.drawImage(imageRect.mirroredImage, imageRect.expandedLeft,
imageRect.heightOffset, null);
}

g2d.dispose();

return bundledImage;
}

protected ImageEffectsStrategy getImageEffectsStrategy(TreeLogger logger) {
Class jaiClass = null;
try {
jaiClass = Class.forName("javax.media.jai.GraphicsJAI");
} catch (ClassNotFoundException e) {
}
if (jaiClass == null | FORCE_DEFAULT_STRATEGY) {
return new DefaultImageEffectsStrategyImpl();
} else {
try {
Class jaiStrategyClass = Class.forName("com.yesmail.gwt.rolodex.rebind.JAIImageEffectsStrategyImpl");
ImageEffectsStrategy strategy = (ImageEffectsStrategy)jaiStrategyClass.getConstructor().newInstance();
return strategy;
} catch (Exception e) {
logger.log(TreeLogger.WARN, "Unable to load JAIImageEffectsStrategyImpl, using DefaultImageEffectsStrategyImpl", e);
return new DefaultImageEffectsStrategyImpl();
}
}
}

private void putMapping(String imageName, ImageRect rect) {
imageNameToImageRectMap.put(imageName, rect);
}
}
Show details Hide details

Change log

r6 by chris.f.jones on Mar 13, 2008   Diff
Fixed license headers
Go to: 
Project members, sign in to write a code review

Older revisions

r3 by chris.f.jones on Mar 12, 2008   Diff
http://code.google.com/p/gwt-
rolodex/issues/detail?id=1:
  Resolved with a strategy impl for
JAI that will do a nice looking
perspective transform with AA'ed
...
r2 by chris.f.jones on Feb 21, 2008   Diff
Round 1 for the gwt-rolodex widget.
This uses a cheesy algorithm for doing
the image transforms, so it leaves
behind a lot of jaggies but it looks
ok for most cases, next step is to
...
All revisions of this file

File info

Size: 14382 bytes, 386 lines