diff options
author | Deepanshu Gupta <deepanshu@google.com> | 2015-01-05 23:33:18 +0000 |
---|---|---|
committer | Android Git Automerger <android-git-automerger@android.com> | 2015-01-05 23:33:18 +0000 |
commit | a6671b707446963f402a71219725f317a57b42af (patch) | |
tree | 45b4ed2803b6dfaf8f7936d521910d8d45e735e6 /tools | |
parent | 0db2524caa01022629075a113d3370a1b7c3da30 (diff) | |
parent | 6461e05420c6f42b0e70134a4e0d2a626161ab74 (diff) | |
download | frameworks_base-a6671b707446963f402a71219725f317a57b42af.zip frameworks_base-a6671b707446963f402a71219725f317a57b42af.tar.gz frameworks_base-a6671b707446963f402a71219725f317a57b42af.tar.bz2 |
am 6461e054: Merge "Add image comparison to LayoutLib tests." into lmp-dev
* commit '6461e05420c6f42b0e70134a4e0d2a626161ab74':
Add image comparison to LayoutLib tests.
Diffstat (limited to 'tools')
3 files changed, 343 insertions, 0 deletions
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/activity.png b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/activity.png Binary files differnew file mode 100644 index 0000000..943cdf1 --- /dev/null +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/activity.png diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/ImageUtils.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/ImageUtils.java new file mode 100644 index 0000000..e13ad72 --- /dev/null +++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/ImageUtils.java @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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.android.layoutlib.bridge.intensive; + +import com.android.annotations.NonNull; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import javax.imageio.ImageIO; + +import static java.awt.RenderingHints.*; +import static java.awt.image.BufferedImage.TYPE_INT_ARGB; +import static java.io.File.separatorChar; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + + +// Adapted by taking the relevant pieces of code from the following classes: +// +// com.android.tools.idea.rendering.ImageUtils, +// com.android.tools.idea.tests.gui.framework.fixture.layout.ImageFixture and +// com.android.tools.idea.rendering.RenderTestBase +/** + * Utilities related to image processing. + */ +public class ImageUtils { + /** + * Normally, 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 boolean FAIL_ON_MISSING_THUMBNAIL = true; + + private static final int THUMBNAIL_SIZE = 250; + + private static final double MAX_PERCENT_DIFFERENCE = 0.1; + + public static void requireSimilar(@NonNull String relativePath, @NonNull BufferedImage image) + throws IOException { + 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); + } + } + + public static void assertImageSimilar(String relativePath, BufferedImage goldenImage, + BufferedImage image, double maxPercentDifferent) throws IOException { + 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(); + } + + /** + * Resize the given image + * + * @param source the image to be scaled + * @param xScale x scale + * @param yScale y scale + * @return the scaled image + */ + @NonNull + public static BufferedImage scale(@NonNull BufferedImage source, double xScale, double yScale) { + + 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(@NonNull Graphics2D g2) { + g2.setRenderingHint(KEY_INTERPOLATION,VALUE_INTERPOLATION_BILINEAR); + g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY); + g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); + } + + /** + * Temp directory where to write the thumbnails and deltas. + */ + @NonNull + private static File getTempDir() { + 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$ + } + + /** + * Saves the generated thumbnail image and appends the info message to an initial message + */ + @NonNull + private static String saveImageAndAppendMessage(@NonNull BufferedImage image, + @NonNull String initialMessage, @NonNull String relativePath) throws IOException { + 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; + } + + private static String getName(@NonNull String relativePath) { + return relativePath.substring(relativePath.lastIndexOf(separatorChar) + 1); + } +} diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java index a2588a6..b439b34 100644 --- a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java +++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java @@ -39,6 +39,7 @@ import org.junit.Test; import java.io.File; import java.io.FileFilter; +import java.io.IOException; import java.net.URL; import java.util.Arrays; import java.util.Comparator; @@ -280,6 +281,12 @@ public class Main { getLogger().error(session.getResult().getException(), session.getResult().getErrorMessage()); } + String goldenDataDir = APP_TEST_RES + "/../../../golden/"; + try { + ImageUtils.requireSimilar(goldenDataDir + "activity.png", session.getImage()); + } catch (IOException e) { + getLogger().error(e, e.getMessage()); + } } /** |