FileDocCategorySizeDatePackage
ImageUtils.javaAPI DocAndroid 5.1 API14708Thu Mar 12 22:22:44 GMT 2015com.android.layoutlib.bridge.intensive

ImageUtils

public class ImageUtils extends Object
Utilities related to image processing.

Fields Summary
private static final boolean
FAIL_ON_MISSING_THUMBNAIL
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 int
THUMBNAIL_SIZE
private static final double
MAX_PERCENT_DIFFERENCE
Constructors Summary
Methods Summary
public static voidassertImageSimilar(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.StringgetName(java.lang.String relativePath)

        return relativePath.substring(relativePath.lastIndexOf(separatorChar) + 1);
    
private static java.io.FilegetTempDir()
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 voidrequireSimilar(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.StringsaveImageAndAppendMessage(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.BufferedImagescale(java.awt.image.BufferedImage source, double xScale, double yScale)
Resize the given image

param
source the image to be scaled
param
xScale x scale
param
yScale y scale
return
the scaled 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 voidsetRenderingHints(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);