/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php * * 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.ide.eclipse.adt.internal.editors.layout.gle2; import static com.android.SdkConstants.DOT_9PNG; import static com.android.SdkConstants.DOT_BMP; import static com.android.SdkConstants.DOT_GIF; import static com.android.SdkConstants.DOT_JPG; import static com.android.SdkConstants.DOT_PNG; import static com.android.utils.SdkUtils.endsWithIgnoreCase; import static java.awt.RenderingHints.KEY_ANTIALIASING; import static java.awt.RenderingHints.KEY_INTERPOLATION; import static java.awt.RenderingHints.KEY_RENDERING; import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON; import static java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR; import static java.awt.RenderingHints.VALUE_RENDER_QUALITY; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.api.Rect; import com.android.ide.eclipse.adt.AdtPlugin; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.graphics.Rectangle; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.io.IOException; import java.io.InputStream; import java.util.Iterator; import java.util.List; import javax.imageio.ImageIO; /** * Utilities related to image processing. */ public class ImageUtils { /** * Returns true if the given image has no dark pixels * * @param image the image to be checked for dark pixels * @return true if no dark pixels were found */ public static boolean containsDarkPixels(BufferedImage image) { for (int y = 0, height = image.getHeight(); y < height; y++) { for (int x = 0, width = image.getWidth(); x < width; x++) { int pixel = image.getRGB(x, y); if ((pixel & 0xFF000000) != 0) { int r = (pixel & 0xFF0000) >> 16; int g = (pixel & 0x00FF00) >> 8; int b = (pixel & 0x0000FF); // One perceived luminance formula is (0.299*red + 0.587*green + 0.114*blue) // In order to keep this fast since we don't need a very accurate // measure, I'll just estimate this with integer math: long brightness = (299L*r + 587*g + 114*b) / 1000; if (brightness < 128) { return true; } } } } return false; } /** * Returns the perceived brightness of the given RGB integer on a scale from 0 to 255 * * @param rgb the RGB triplet, 8 bits each * @return the perceived brightness, with 0 maximally dark and 255 maximally bright */ public static int getBrightness(int rgb) { if ((rgb & 0xFFFFFF) != 0) { int r = (rgb & 0xFF0000) >> 16; int g = (rgb & 0x00FF00) >> 8; int b = (rgb & 0x0000FF); // See the containsDarkPixels implementation for details return (int) ((299L*r + 587*g + 114*b) / 1000); } return 0; } /** * Converts an alpha-red-green-blue integer color into an {@link RGB} color. *
* NOTE - this will drop the alpha value since {@link RGB} objects do not * contain transparency information. * * @param rgb the RGB integer to convert to a color description * @return the color description corresponding to the integer */ public static RGB intToRgb(int rgb) { return new RGB((rgb & 0xFF0000) >>> 16, (rgb & 0xFF00) >>> 8, rgb & 0xFF); } /** * Converts an {@link RGB} color into a alpha-red-green-blue integer * * @param rgb the RGB color descriptor to convert * @param alpha the amount of alpha to add into the color integer (since the * {@link RGB} objects do not contain an alpha channel) * @return an integer corresponding to the {@link RGB} color */ public static int rgbToInt(RGB rgb, int alpha) { return alpha << 24 | (rgb.red << 16) | (rgb.green << 8) | rgb.blue; } /** * Crops blank pixels from the edges of the image and returns the cropped result. We * crop off pixels that are blank (meaning they have an alpha value = 0). Note that * this is not the same as pixels that aren't opaque (an alpha value other than 255). * * @param image the image to be cropped * @param initialCrop If not null, specifies a rectangle which contains an initial * crop to continue. This can be used to crop an image where you already * know about margins in the image * @return a cropped version of the source image, or null if the whole image was blank * and cropping completely removed everything */ @Nullable public static BufferedImage cropBlank( @NonNull BufferedImage image, @Nullable Rect initialCrop) { return cropBlank(image, initialCrop, image.getType()); } /** * Crops blank pixels from the edges of the image and returns the cropped result. We * crop off pixels that are blank (meaning they have an alpha value = 0). Note that * this is not the same as pixels that aren't opaque (an alpha value other than 255). * * @param image the image to be cropped * @param initialCrop If not null, specifies a rectangle which contains an initial * crop to continue. This can be used to crop an image where you already * know about margins in the image * @param imageType the type of {@link BufferedImage} to create * @return a cropped version of the source image, or null if the whole image was blank * and cropping completely removed everything */ public static BufferedImage cropBlank(BufferedImage image, Rect initialCrop, int imageType) { CropFilter filter = new CropFilter() { @Override public boolean crop(BufferedImage bufferedImage, int x, int y) { int rgb = bufferedImage.getRGB(x, y); return (rgb & 0xFF000000) == 0x00000000; // TODO: Do a threshold of 80 instead of just 0? Might give better // visual results -- e.g. check <= 0x80000000 } }; return crop(image, filter, initialCrop, imageType); } /** * Crops pixels of a given color from the edges of the image and returns the cropped * result. * * @param image the image to be cropped * @param blankArgb the color considered to be blank, as a 32 pixel integer with 8 * bits of alpha, red, green and blue * @param initialCrop If not null, specifies a rectangle which contains an initial * crop to continue. This can be used to crop an image where you already * know about margins in the image * @return a cropped version of the source image, or null if the whole image was blank * and cropping completely removed everything */ @Nullable public static BufferedImage cropColor( @NonNull BufferedImage image, final int blankArgb, @Nullable Rect initialCrop) { return cropColor(image, blankArgb, initialCrop, image.getType()); } /** * Crops pixels of a given color from the edges of the image and returns the cropped * result. * * @param image the image to be cropped * @param blankArgb the color considered to be blank, as a 32 pixel integer with 8 * bits of alpha, red, green and blue * @param initialCrop If not null, specifies a rectangle which contains an initial * crop to continue. This can be used to crop an image where you already * know about margins in the image * @param imageType the type of {@link BufferedImage} to create * @return a cropped version of the source image, or null if the whole image was blank * and cropping completely removed everything */ public static BufferedImage cropColor(BufferedImage image, final int blankArgb, Rect initialCrop, int imageType) { CropFilter filter = new CropFilter() { @Override public boolean crop(BufferedImage bufferedImage, int x, int y) { return blankArgb == bufferedImage.getRGB(x, y); } }; return crop(image, filter, initialCrop, imageType); } /** * Interface implemented by cropping functions that determine whether * a pixel should be cropped or not. */ private static interface CropFilter { /** * Returns true if the pixel is should be cropped. * * @param image the image containing the pixel in question * @param x the x position of the pixel * @param y the y position of the pixel * @return true if the pixel should be cropped (for example, is blank) */ boolean crop(BufferedImage image, int x, int y); } private static BufferedImage crop(BufferedImage image, CropFilter filter, Rect initialCrop, int imageType) { if (image == null) { return null; } // First, determine the dimensions of the real image within the image int x1, y1, x2, y2; if (initialCrop != null) { x1 = initialCrop.x; y1 = initialCrop.y; x2 = initialCrop.x + initialCrop.w; y2 = initialCrop.y + initialCrop.h; } else { x1 = 0; y1 = 0; x2 = image.getWidth(); y2 = image.getHeight(); } // Nothing left to crop if (x1 == x2 || y1 == y2) { return null; } // This algorithm is a bit dumb -- it just scans along the edges looking for // a pixel that shouldn't be cropped. I could maybe try to make it smarter by // for example doing a binary search to quickly eliminate large empty areas to // the right and bottom -- but this is slightly tricky with components like the // AnalogClock where I could accidentally end up finding a blank horizontal or // vertical line somewhere in the middle of the rendering of the clock, so for now // we do the dumb thing -- not a big deal since we tend to crop reasonably // small images. // First determine top edge topEdge: for (; y1 < y2; y1++) { for (int x = x1; x < x2; x++) { if (!filter.crop(image, x, y1)) { break topEdge; } } } if (y1 == image.getHeight()) { // The image is blank return null; } // Next determine left edge leftEdge: for (; x1 < x2; x1++) { for (int y = y1; y < y2; y++) { if (!filter.crop(image, x1, y)) { break leftEdge; } } } // Next determine right edge rightEdge: for (; x2 > x1; x2--) { for (int y = y1; y < y2; y++) { if (!filter.crop(image, x2 - 1, y)) { break rightEdge; } } } // Finally determine bottom edge bottomEdge: for (; y2 > y1; y2--) { for (int x = x1; x < x2; x++) { if (!filter.crop(image, x, y2 - 1)) { break bottomEdge; } } } // No need to crop? if (x1 == 0 && y1 == 0 && x2 == image.getWidth() && y2 == image.getHeight()) { return image; } if (x1 == x2 || y1 == y2) { // Nothing left after crop -- blank image return null; } int width = x2 - x1; int height = y2 - y1; // Now extract the sub-image if (imageType == -1) { imageType = image.getType(); } if (imageType == BufferedImage.TYPE_CUSTOM) { imageType = BufferedImage.TYPE_INT_ARGB; } BufferedImage cropped = new BufferedImage(width, height, imageType); Graphics g = cropped.getGraphics(); g.drawImage(image, 0, 0, width, height, x1, y1, x2, y2, null); g.dispose(); return cropped; } /** * Creates a drop shadow of a given image and returns a new image which shows the * input image on top of its drop shadow. *
* NOTE: If the shape is rectangular and opaque, consider using * {@link #drawRectangleShadow(Graphics, int, int, int, int)} instead. * * @param source the source image to be shadowed * @param shadowSize the size of the shadow in pixels * @param shadowOpacity the opacity of the shadow, with 0=transparent and 1=opaque * @param shadowRgb the RGB int to use for the shadow color * @return a new image with the source image on top of its shadow */ public static BufferedImage createDropShadow(BufferedImage source, int shadowSize, float shadowOpacity, int shadowRgb) { // This code is based on // http://www.jroller.com/gfx/entry/non_rectangular_shadow BufferedImage image = new BufferedImage(source.getWidth() + shadowSize * 2, source.getHeight() + shadowSize * 2, BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = image.createGraphics(); g2.drawImage(source, null, shadowSize, shadowSize); int dstWidth = image.getWidth(); int dstHeight = image.getHeight(); int left = (shadowSize - 1) >> 1; int right = shadowSize - left; int xStart = left; int xStop = dstWidth - right; int yStart = left; int yStop = dstHeight - right; shadowRgb = shadowRgb & 0x00FFFFFF; int[] aHistory = new int[shadowSize]; int historyIdx = 0; int aSum; int[] dataBuffer = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); int lastPixelOffset = right * dstWidth; float sumDivider = shadowOpacity / shadowSize; // horizontal pass for (int y = 0, bufferOffset = 0; y < dstHeight; y++, bufferOffset = y * dstWidth) { aSum = 0; historyIdx = 0; for (int x = 0; x < shadowSize; x++, bufferOffset++) { int a = dataBuffer[bufferOffset] >>> 24; aHistory[x] = a; aSum += a; } bufferOffset -= right; for (int x = xStart; x < xStop; x++, bufferOffset++) { int a = (int) (aSum * sumDivider); dataBuffer[bufferOffset] = a << 24 | shadowRgb; // subtract the oldest pixel from the sum aSum -= aHistory[historyIdx]; // get the latest pixel a = dataBuffer[bufferOffset + right] >>> 24; aHistory[historyIdx] = a; aSum += a; if (++historyIdx >= shadowSize) { historyIdx -= shadowSize; } } } // vertical pass for (int x = 0, bufferOffset = 0; x < dstWidth; x++, bufferOffset = x) { aSum = 0; historyIdx = 0; for (int y = 0; y < shadowSize; y++, bufferOffset += dstWidth) { int a = dataBuffer[bufferOffset] >>> 24; aHistory[y] = a; aSum += a; } bufferOffset -= lastPixelOffset; for (int y = yStart; y < yStop; y++, bufferOffset += dstWidth) { int a = (int) (aSum * sumDivider); dataBuffer[bufferOffset] = a << 24 | shadowRgb; // subtract the oldest pixel from the sum aSum -= aHistory[historyIdx]; // get the latest pixel a = dataBuffer[bufferOffset + lastPixelOffset] >>> 24; aHistory[historyIdx] = a; aSum += a; if (++historyIdx >= shadowSize) { historyIdx -= shadowSize; } } } g2.drawImage(source, null, 0, 0); g2.dispose(); return image; } /** * Draws a rectangular drop shadow (of size {@link #SHADOW_SIZE} by * {@link #SHADOW_SIZE} around the given source and returns a new image with * both combined * * @param source the source image * @return the source image with a drop shadow on the bottom and right */ public static BufferedImage createRectangularDropShadow(BufferedImage source) { int type = source.getType(); if (type == BufferedImage.TYPE_CUSTOM) { type = BufferedImage.TYPE_INT_ARGB; } int width = source.getWidth(); int height = source.getHeight(); BufferedImage image = new BufferedImage(width + SHADOW_SIZE, height + SHADOW_SIZE, type); Graphics g = image.getGraphics(); g.drawImage(source, 0, 0, width, height, null); ImageUtils.drawRectangleShadow(image, 0, 0, width, height); g.dispose(); return image; } /** * Draws a drop shadow for the given rectangle into the given context. It * will not draw anything if the rectangle is smaller than a minimum * determined by the assets used to draw the shadow graphics. * The size of the shadow is {@link #SHADOW_SIZE}. * * @param image the image to draw the shadow into * @param x the left coordinate of the left hand side of the rectangle * @param y the top coordinate of the top of the rectangle * @param width the width of the rectangle * @param height the height of the rectangle */ public static final void drawRectangleShadow(BufferedImage image, int x, int y, int width, int height) { Graphics gc = image.getGraphics(); try { drawRectangleShadow(gc, x, y, width, height); } finally { gc.dispose(); } } /** * Draws a small drop shadow for the given rectangle into the given context. It * will not draw anything if the rectangle is smaller than a minimum * determined by the assets used to draw the shadow graphics. * The size of the shadow is {@link #SMALL_SHADOW_SIZE}. * * @param image the image to draw the shadow into * @param x the left coordinate of the left hand side of the rectangle * @param y the top coordinate of the top of the rectangle * @param width the width of the rectangle * @param height the height of the rectangle */ public static final void drawSmallRectangleShadow(BufferedImage image, int x, int y, int width, int height) { Graphics gc = image.getGraphics(); try { drawSmallRectangleShadow(gc, x, y, width, height); } finally { gc.dispose(); } } /** * The width and height of the drop shadow painted by * {@link #drawRectangleShadow(Graphics, int, int, int, int)} */ public static final int SHADOW_SIZE = 20; // DO NOT EDIT. This corresponds to bitmap graphics /** * The width and height of the drop shadow painted by * {@link #drawSmallRectangleShadow(Graphics, int, int, int, int)} */ public static final int SMALL_SHADOW_SIZE = 10; // DO NOT EDIT. Corresponds to bitmap graphics /** * Draws a drop shadow for the given rectangle into the given context. It * will not draw anything if the rectangle is smaller than a minimum * determined by the assets used to draw the shadow graphics. *
* This corresponds to * {@link SwtUtils#drawRectangleShadow(org.eclipse.swt.graphics.GC, int, int, int, int)}, * but applied to an AWT graphics object instead, such that no image * conversion has to be performed. *
* Make sure to keep changes in the visual appearance here in sync with the * AWT version in * {@link SwtUtils#drawRectangleShadow(org.eclipse.swt.graphics.GC, int, int, int, int)}. * * @param gc the graphics context to draw into * @param x the left coordinate of the left hand side of the rectangle * @param y the top coordinate of the top of the rectangle * @param width the width of the rectangle * @param height the height of the rectangle */ public static final void drawRectangleShadow(Graphics gc, int x, int y, int width, int height) { if (sShadowBottomLeft == null) { // Shadow graphics. This was generated by creating a drop shadow in // Gimp, using the parameters x offset=10, y offset=10, blur radius=10, // color=black, and opacity=51. These values attempt to make a shadow // that is legible both for dark and light themes, on top of the // canvas background (rgb(150,150,150). Darker shadows would tend to // blend into the foreground for a dark holo screen, and lighter shadows // would be hard to spot on the canvas background. If you make adjustments, // make sure to check the shadow with both dark and light themes. // // After making the graphics, I cut out the top right, bottom left // and bottom right corners as 20x20 images, and these are reproduced by // painting them in the corresponding places in the target graphics context. // I then grabbed a single horizontal gradient line from the middle of the // right edge,and a single vertical gradient line from the bottom. These // are then painted scaled/stretched in the target to fill the gaps between // the three corner images. // // Filenames: bl=bottom left, b=bottom, br=bottom right, r=right, tr=top right sShadowBottomLeft = readImage("shadow-bl.png"); //$NON-NLS-1$ sShadowBottom = readImage("shadow-b.png"); //$NON-NLS-1$ sShadowBottomRight = readImage("shadow-br.png"); //$NON-NLS-1$ sShadowRight = readImage("shadow-r.png"); //$NON-NLS-1$ sShadowTopRight = readImage("shadow-tr.png"); //$NON-NLS-1$ assert sShadowBottomLeft != null; assert sShadowBottomRight.getWidth() == SHADOW_SIZE; assert sShadowBottomRight.getHeight() == SHADOW_SIZE; } int blWidth = sShadowBottomLeft.getWidth(); int trHeight = sShadowTopRight.getHeight(); if (width < blWidth) { return; } if (height < trHeight) { return; } gc.drawImage(sShadowBottomLeft, x, y + height, null); gc.drawImage(sShadowBottomRight, x + width, y + height, null); gc.drawImage(sShadowTopRight, x + width, y, null); gc.drawImage(sShadowBottom, x + sShadowBottomLeft.getWidth(), y + height, x + width, y + height + sShadowBottom.getHeight(), 0, 0, sShadowBottom.getWidth(), sShadowBottom.getHeight(), null); gc.drawImage(sShadowRight, x + width, y + sShadowTopRight.getHeight(), x + width + sShadowRight.getWidth(), y + height, 0, 0, sShadowRight.getWidth(), sShadowRight.getHeight(), null); } /** * Draws a small drop shadow for the given rectangle into the given context. It * will not draw anything if the rectangle is smaller than a minimum * determined by the assets used to draw the shadow graphics. *
*
* @param gc the graphics context to draw into
* @param x the left coordinate of the left hand side of the rectangle
* @param y the top coordinate of the top of the rectangle
* @param width the width of the rectangle
* @param height the height of the rectangle
*/
public static final void drawSmallRectangleShadow(Graphics gc,
int x, int y, int width, int height) {
if (sShadow2BottomLeft == null) {
// Shadow graphics. This was generated by creating a drop shadow in
// Gimp, using the parameters x offset=5, y offset=%, blur radius=5,
// color=black, and opacity=51. These values attempt to make a shadow
// that is legible both for dark and light themes, on top of the
// canvas background (rgb(150,150,150). Darker shadows would tend to
// blend into the foreground for a dark holo screen, and lighter shadows
// would be hard to spot on the canvas background. If you make adjustments,
// make sure to check the shadow with both dark and light themes.
//
// After making the graphics, I cut out the top right, bottom left
// and bottom right corners as 20x20 images, and these are reproduced by
// painting them in the corresponding places in the target graphics context.
// I then grabbed a single horizontal gradient line from the middle of the
// right edge,and a single vertical gradient line from the bottom. These
// are then painted scaled/stretched in the target to fill the gaps between
// the three corner images.
//
// Filenames: bl=bottom left, b=bottom, br=bottom right, r=right, tr=top right
sShadow2BottomLeft = readImage("shadow2-bl.png"); //$NON-NLS-1$
sShadow2Bottom = readImage("shadow2-b.png"); //$NON-NLS-1$
sShadow2BottomRight = readImage("shadow2-br.png"); //$NON-NLS-1$
sShadow2Right = readImage("shadow2-r.png"); //$NON-NLS-1$
sShadow2TopRight = readImage("shadow2-tr.png"); //$NON-NLS-1$
assert sShadow2BottomLeft != null;
assert sShadow2TopRight != null;
assert sShadow2BottomRight.getWidth() == SMALL_SHADOW_SIZE;
assert sShadow2BottomRight.getHeight() == SMALL_SHADOW_SIZE;
}
int blWidth = sShadow2BottomLeft.getWidth();
int trHeight = sShadow2TopRight.getHeight();
if (width < blWidth) {
return;
}
if (height < trHeight) {
return;
}
gc.drawImage(sShadow2BottomLeft, x, y + height, null);
gc.drawImage(sShadow2BottomRight, x + width, y + height, null);
gc.drawImage(sShadow2TopRight, x + width, y, null);
gc.drawImage(sShadow2Bottom,
x + sShadow2BottomLeft.getWidth(), y + height,
x + width, y + height + sShadow2Bottom.getHeight(),
0, 0, sShadow2Bottom.getWidth(), sShadow2Bottom.getHeight(),
null);
gc.drawImage(sShadow2Right,
x + width, y + sShadow2TopRight.getHeight(),
x + width + sShadow2Right.getWidth(), y + height,
0, 0, sShadow2Right.getWidth(), sShadow2Right.getHeight(),
null);
}
/**
* Reads the given image from the plugin folder
*
* @param name the name of the image (including file extension)
* @return the corresponding image, or null if something goes wrong
*/
@Nullable
public static BufferedImage readImage(@NonNull String name) {
InputStream stream = ImageUtils.class.getResourceAsStream("/icons/" + name); //$NON-NLS-1$
if (stream != null) {
try {
return ImageIO.read(stream);
} catch (IOException e) {
AdtPlugin.log(e, "Could not read %1$s", name);
} finally {
try {
stream.close();
} catch (IOException e) {
// Dumb API
}
}
}
return null;
}
// Normal drop shadow
private static BufferedImage sShadowBottomLeft;
private static BufferedImage sShadowBottom;
private static BufferedImage sShadowBottomRight;
private static BufferedImage sShadowRight;
private static BufferedImage sShadowTopRight;
// Small drop shadow
private static BufferedImage sShadow2BottomLeft;
private static BufferedImage sShadow2Bottom;
private static BufferedImage sShadow2BottomRight;
private static BufferedImage sShadow2Right;
private static BufferedImage sShadow2TopRight;
/**
* Returns a bounding rectangle for the given list of rectangles. If the list is
* empty, the bounding rectangle is null.
*
* @param items the list of rectangles to compute a bounding rectangle for (may not be
* null)
* @return a bounding rectangle of the passed in rectangles, or null if the list is
* empty
*/
public static Rectangle getBoundingRectangle(List