ImageUtilspublic class ImageUtils extends Object Utilities related to image processing. |
Fields Summary |
---|
private static final boolean | FAIL_ON_MISSING_THUMBNAILNormally, this test will fail when there is a missing thumbnail. However, when
you create creating a new test, it's useful to be able to turn this off such that
you can generate all the missing thumbnails in one go, rather than having to run
the test repeatedly to get to each new render assertion generating its thumbnail. | private static final int | THUMBNAIL_SIZE | private static final double | MAX_PERCENT_DIFFERENCE |
Methods Summary |
---|
public static void | assertImageSimilar(java.lang.String relativePath, java.awt.image.BufferedImage goldenImage, java.awt.image.BufferedImage image, double maxPercentDifferent)
assertEquals("Only TYPE_INT_ARGB image types are supported", TYPE_INT_ARGB, image.getType());
if (goldenImage.getType() != TYPE_INT_ARGB) {
BufferedImage temp = new BufferedImage(goldenImage.getWidth(), goldenImage.getHeight(),
TYPE_INT_ARGB);
temp.getGraphics().drawImage(goldenImage, 0, 0, null);
goldenImage = temp;
}
assertEquals(TYPE_INT_ARGB, goldenImage.getType());
int imageWidth = Math.min(goldenImage.getWidth(), image.getWidth());
int imageHeight = Math.min(goldenImage.getHeight(), image.getHeight());
// Blur the images to account for the scenarios where there are pixel
// differences
// in where a sharp edge occurs
// goldenImage = blur(goldenImage, 6);
// image = blur(image, 6);
int width = 3 * imageWidth;
@SuppressWarnings("UnnecessaryLocalVariable")
int height = imageHeight; // makes code more readable
BufferedImage deltaImage = new BufferedImage(width, height, TYPE_INT_ARGB);
Graphics g = deltaImage.getGraphics();
// Compute delta map
long delta = 0;
for (int y = 0; y < imageHeight; y++) {
for (int x = 0; x < imageWidth; x++) {
int goldenRgb = goldenImage.getRGB(x, y);
int rgb = image.getRGB(x, y);
if (goldenRgb == rgb) {
deltaImage.setRGB(imageWidth + x, y, 0x00808080);
continue;
}
// If the pixels have no opacity, don't delta colors at all
if (((goldenRgb & 0xFF000000) == 0) && (rgb & 0xFF000000) == 0) {
deltaImage.setRGB(imageWidth + x, y, 0x00808080);
continue;
}
int deltaR = ((rgb & 0xFF0000) >>> 16) - ((goldenRgb & 0xFF0000) >>> 16);
int newR = 128 + deltaR & 0xFF;
int deltaG = ((rgb & 0x00FF00) >>> 8) - ((goldenRgb & 0x00FF00) >>> 8);
int newG = 128 + deltaG & 0xFF;
int deltaB = (rgb & 0x0000FF) - (goldenRgb & 0x0000FF);
int newB = 128 + deltaB & 0xFF;
int avgAlpha = ((((goldenRgb & 0xFF000000) >>> 24)
+ ((rgb & 0xFF000000) >>> 24)) / 2) << 24;
int newRGB = avgAlpha | newR << 16 | newG << 8 | newB;
deltaImage.setRGB(imageWidth + x, y, newRGB);
delta += Math.abs(deltaR);
delta += Math.abs(deltaG);
delta += Math.abs(deltaB);
}
}
// 3 different colors, 256 color levels
long total = imageHeight * imageWidth * 3L * 256L;
float percentDifference = (float) (delta * 100 / (double) total);
String error = null;
String imageName = getName(relativePath);
if (percentDifference > maxPercentDifferent) {
error = String.format("Images differ (by %.1f%%)", percentDifference);
} else if (Math.abs(goldenImage.getWidth() - image.getWidth()) >= 2) {
error = "Widths differ too much for " + imageName + ": " +
goldenImage.getWidth() + "x" + goldenImage.getHeight() +
"vs" + image.getWidth() + "x" + image.getHeight();
} else if (Math.abs(goldenImage.getHeight() - image.getHeight()) >= 2) {
error = "Heights differ too much for " + imageName + ": " +
goldenImage.getWidth() + "x" + goldenImage.getHeight() +
"vs" + image.getWidth() + "x" + image.getHeight();
}
assertEquals(TYPE_INT_ARGB, image.getType());
if (error != null) {
// Expected on the left
// Golden on the right
g.drawImage(goldenImage, 0, 0, null);
g.drawImage(image, 2 * imageWidth, 0, null);
// Labels
if (imageWidth > 80) {
g.setColor(Color.RED);
g.drawString("Expected", 10, 20);
g.drawString("Actual", 2 * imageWidth + 10, 20);
}
File output = new File(getTempDir(), "delta-" + imageName);
if (output.exists()) {
boolean deleted = output.delete();
assertTrue(deleted);
}
ImageIO.write(deltaImage, "PNG", output);
error += " - see details in " + output.getPath() + "\n";
error = saveImageAndAppendMessage(image, error, relativePath);
System.out.println(error);
fail(error);
}
g.dispose();
| private static java.lang.String | getName(java.lang.String relativePath)
return relativePath.substring(relativePath.lastIndexOf(separatorChar) + 1);
| private static java.io.File | getTempDir()Temp directory where to write the thumbnails and deltas.
if (System.getProperty("os.name").equals("Mac OS X")) {
return new File("/tmp"); //$NON-NLS-1$
}
return new File(System.getProperty("java.io.tmpdir")); //$NON-NLS-1$
| public static void | requireSimilar(java.lang.String relativePath, java.awt.image.BufferedImage image)
int maxDimension = Math.max(image.getWidth(), image.getHeight());
double scale = THUMBNAIL_SIZE / (double)maxDimension;
BufferedImage thumbnail = scale(image, scale, scale);
InputStream is = ImageUtils.class.getResourceAsStream(relativePath);
if (is == null) {
String message = "Unable to load golden thumbnail: " + relativePath + "\n";
message = saveImageAndAppendMessage(thumbnail, message, relativePath);
if (FAIL_ON_MISSING_THUMBNAIL) {
fail(message);
} else {
System.out.println(message);
}
}
else {
BufferedImage goldenImage = ImageIO.read(is);
assertImageSimilar(relativePath, goldenImage, thumbnail, MAX_PERCENT_DIFFERENCE);
}
| private static java.lang.String | saveImageAndAppendMessage(java.awt.image.BufferedImage image, java.lang.String initialMessage, java.lang.String relativePath)Saves the generated thumbnail image and appends the info message to an initial message
File output = new File(getTempDir(), getName(relativePath));
if (output.exists()) {
boolean deleted = output.delete();
assertTrue(deleted);
}
ImageIO.write(image, "PNG", output);
initialMessage += "Thumbnail for current rendering stored at " + output.getPath();
// initialMessage += "\nRun the following command to accept the changes:\n";
// initialMessage += String.format("mv %1$s %2$s", output.getPath(),
// ImageUtils.class.getResource(relativePath).getPath());
// The above has been commented out, since the destination path returned is in out dir
// and it makes the tests pass without the code being actually checked in.
return initialMessage;
| public static java.awt.image.BufferedImage | scale(java.awt.image.BufferedImage source, double xScale, double yScale)Resize the given image
int sourceWidth = source.getWidth();
int sourceHeight = source.getHeight();
int destWidth = Math.max(1, (int) (xScale * sourceWidth));
int destHeight = Math.max(1, (int) (yScale * sourceHeight));
int imageType = source.getType();
if (imageType == BufferedImage.TYPE_CUSTOM) {
imageType = BufferedImage.TYPE_INT_ARGB;
}
if (xScale > 0.5 && yScale > 0.5) {
BufferedImage scaled =
new BufferedImage(destWidth, destHeight, imageType);
Graphics2D g2 = scaled.createGraphics();
g2.setComposite(AlphaComposite.Src);
g2.setColor(new Color(0, true));
g2.fillRect(0, 0, destWidth, destHeight);
if (xScale == 1 && yScale == 1) {
g2.drawImage(source, 0, 0, null);
} else {
setRenderingHints(g2);
g2.drawImage(source, 0, 0, destWidth, destHeight, 0, 0, sourceWidth, sourceHeight,
null);
}
g2.dispose();
return scaled;
} else {
// When creating a thumbnail, using the above code doesn't work very well;
// you get some visible artifacts, especially for text. Instead use the
// technique of repeatedly scaling the image into half; this will cause
// proper averaging of neighboring pixels, and will typically (for the kinds
// of screen sizes used by this utility method in the layout editor) take
// about 3-4 iterations to get the result since we are logarithmically reducing
// the size. Besides, each successive pass in operating on much fewer pixels
// (a reduction of 4 in each pass).
//
// However, we may not be resizing to a size that can be reached exactly by
// successively diving in half. Therefore, once we're within a factor of 2 of
// the final size, we can do a resize to the exact target size.
// However, we can get even better results if we perform this final resize
// up front. Let's say we're going from width 1000 to a destination width of 85.
// The first approach would cause a resize from 1000 to 500 to 250 to 125, and
// then a resize from 125 to 85. That last resize can distort/blur a lot.
// Instead, we can start with the destination width, 85, and double it
// successfully until we're close to the initial size: 85, then 170,
// then 340, and finally 680. (The next one, 1360, is larger than 1000).
// So, now we *start* the thumbnail operation by resizing from width 1000 to
// width 680, which will preserve a lot of visual details such as text.
// Then we can successively resize the image in half, 680 to 340 to 170 to 85.
// We end up with the expected final size, but we've been doing an exact
// divide-in-half resizing operation at the end so there is less distortion.
int iterations = 0; // Number of halving operations to perform after the initial resize
int nearestWidth = destWidth; // Width closest to source width that = 2^x, x is integer
int nearestHeight = destHeight;
while (nearestWidth < sourceWidth / 2) {
nearestWidth *= 2;
nearestHeight *= 2;
iterations++;
}
BufferedImage scaled = new BufferedImage(nearestWidth, nearestHeight, imageType);
Graphics2D g2 = scaled.createGraphics();
setRenderingHints(g2);
g2.drawImage(source, 0, 0, nearestWidth, nearestHeight, 0, 0, sourceWidth, sourceHeight,
null);
g2.dispose();
sourceWidth = nearestWidth;
sourceHeight = nearestHeight;
source = scaled;
for (int iteration = iterations - 1; iteration >= 0; iteration--) {
int halfWidth = sourceWidth / 2;
int halfHeight = sourceHeight / 2;
scaled = new BufferedImage(halfWidth, halfHeight, imageType);
g2 = scaled.createGraphics();
setRenderingHints(g2);
g2.drawImage(source, 0, 0, halfWidth, halfHeight, 0, 0, sourceWidth, sourceHeight,
null);
g2.dispose();
sourceWidth = halfWidth;
sourceHeight = halfHeight;
source = scaled;
iterations--;
}
return scaled;
}
| private static void | setRenderingHints(java.awt.Graphics2D g2)
g2.setRenderingHint(KEY_INTERPOLATION,VALUE_INTERPOLATION_BILINEAR);
g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY);
g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
|
|