aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java979
1 files changed, 979 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java
new file mode 100644
index 000000000..b5bc9aa72
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java
@@ -0,0 +1,979 @@
+/*
+ * 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.
+ * <p>
+ * <b>NOTE</b> - 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.
+ * <p>
+ * <b>NOTE: If the shape is rectangular and opaque, consider using
+ * {@link #drawRectangleShadow(Graphics, int, int, int, int)} instead.</b>
+ *
+ * @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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ *
+ * @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<Rectangle> items) {
+ Iterator<Rectangle> iterator = items.iterator();
+ if (!iterator.hasNext()) {
+ return null;
+ }
+
+ Rectangle bounds = iterator.next();
+ Rectangle union = new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height);
+ while (iterator.hasNext()) {
+ union.add(iterator.next());
+ }
+
+ return union;
+ }
+
+ /**
+ * Returns a new image which contains of the sub image given by the rectangle (x1,y1)
+ * to (x2,y2)
+ *
+ * @param source the source image
+ * @param x1 top left X coordinate
+ * @param y1 top left Y coordinate
+ * @param x2 bottom right X coordinate
+ * @param y2 bottom right Y coordinate
+ * @return a new image containing the pixels in the given range
+ */
+ public static BufferedImage subImage(BufferedImage source, int x1, int y1, int x2, int y2) {
+ int width = x2 - x1;
+ int height = y2 - y1;
+ int imageType = source.getType();
+ if (imageType == BufferedImage.TYPE_CUSTOM) {
+ imageType = BufferedImage.TYPE_INT_ARGB;
+ }
+ BufferedImage sub = new BufferedImage(width, height, imageType);
+ Graphics g = sub.getGraphics();
+ g.drawImage(source, 0, 0, width, height, x1, y1, x2, y2, null);
+ g.dispose();
+
+ return sub;
+ }
+
+ /**
+ * Returns the color value represented by the given string value
+ * @param value the color value
+ * @return the color as an int
+ * @throw NumberFormatException if the conversion failed.
+ */
+ public static int getColor(String value) {
+ // Copied from ResourceHelper in layoutlib
+ if (value != null) {
+ if (value.startsWith("#") == false) { //$NON-NLS-1$
+ throw new NumberFormatException(
+ String.format("Color value '%s' must start with #", value));
+ }
+
+ value = value.substring(1);
+
+ // make sure it's not longer than 32bit
+ if (value.length() > 8) {
+ throw new NumberFormatException(String.format(
+ "Color value '%s' is too long. Format is either" +
+ "#AARRGGBB, #RRGGBB, #RGB, or #ARGB",
+ value));
+ }
+
+ if (value.length() == 3) { // RGB format
+ char[] color = new char[8];
+ color[0] = color[1] = 'F';
+ color[2] = color[3] = value.charAt(0);
+ color[4] = color[5] = value.charAt(1);
+ color[6] = color[7] = value.charAt(2);
+ value = new String(color);
+ } else if (value.length() == 4) { // ARGB format
+ char[] color = new char[8];
+ color[0] = color[1] = value.charAt(0);
+ color[2] = color[3] = value.charAt(1);
+ color[4] = color[5] = value.charAt(2);
+ color[6] = color[7] = value.charAt(3);
+ value = new String(color);
+ } else if (value.length() == 6) {
+ value = "FF" + value; //$NON-NLS-1$
+ }
+
+ // this is a RRGGBB or AARRGGBB value
+
+ // Integer.parseInt will fail to parse strings like "ff191919", so we use
+ // a Long, but cast the result back into an int, since we know that we're only
+ // dealing with 32 bit values.
+ return (int)Long.parseLong(value, 16);
+ }
+
+ throw new NumberFormatException();
+ }
+
+ /**
+ * Resize the given image
+ *
+ * @param source the image to be scaled
+ * @param xScale x scale
+ * @param yScale y scale
+ * @return the scaled image
+ */
+ public static BufferedImage scale(BufferedImage source, double xScale, double yScale) {
+ return scale(source, xScale, yScale, 0, 0);
+ }
+
+ /**
+ * Resize the given image
+ *
+ * @param source the image to be scaled
+ * @param xScale x scale
+ * @param yScale y scale
+ * @param rightMargin extra margin to add on the right
+ * @param bottomMargin extra margin to add on the bottom
+ * @return the scaled image
+ */
+ public static BufferedImage scale(BufferedImage source, double xScale, double yScale,
+ int rightMargin, int bottomMargin) {
+ 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 + rightMargin, destHeight + bottomMargin, imageType);
+ Graphics2D g2 = scaled.createGraphics();
+ g2.setComposite(AlphaComposite.Src);
+ g2.setColor(new Color(0, true));
+ g2.fillRect(0, 0, destWidth + rightMargin, destHeight + bottomMargin);
+ g2.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BILINEAR);
+ g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY);
+ g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
+ 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++;
+ }
+
+ // If we're supposed to add in margins, we need to do it in the initial resizing
+ // operation if we don't have any subsequent resizing operations.
+ if (iterations == 0) {
+ nearestWidth += rightMargin;
+ nearestHeight += bottomMargin;
+ }
+
+ BufferedImage scaled = new BufferedImage(nearestWidth, nearestHeight, imageType);
+ Graphics2D g2 = scaled.createGraphics();
+ g2.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BILINEAR);
+ g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY);
+ g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
+ 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;
+ if (iteration == 0) { // Last iteration: Add margins in final image
+ scaled = new BufferedImage(halfWidth + rightMargin, halfHeight + bottomMargin,
+ imageType);
+ } else {
+ scaled = new BufferedImage(halfWidth, halfHeight, imageType);
+ }
+ g2 = scaled.createGraphics();
+ g2.setRenderingHint(KEY_INTERPOLATION,VALUE_INTERPOLATION_BILINEAR);
+ g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY);
+ g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
+ g2.drawImage(source, 0, 0,
+ halfWidth, halfHeight, 0, 0,
+ sourceWidth, sourceHeight,
+ null);
+ g2.dispose();
+
+ sourceWidth = halfWidth;
+ sourceHeight = halfHeight;
+ source = scaled;
+ iterations--;
+ }
+ return scaled;
+ }
+ }
+
+ /**
+ * Returns true if the given file path points to an image file recognized by
+ * Android. See http://developer.android.com/guide/appendix/media-formats.html
+ * for details.
+ *
+ * @param path the filename to be tested
+ * @return true if the file represents an image file
+ */
+ public static boolean hasImageExtension(String path) {
+ return endsWithIgnoreCase(path, DOT_PNG)
+ || endsWithIgnoreCase(path, DOT_9PNG)
+ || endsWithIgnoreCase(path, DOT_GIF)
+ || endsWithIgnoreCase(path, DOT_JPG)
+ || endsWithIgnoreCase(path, DOT_BMP);
+ }
+
+ /**
+ * Creates a new image of the given size filled with the given color
+ *
+ * @param width the width of the image
+ * @param height the height of the image
+ * @param color the color of the image
+ * @return a new image of the given size filled with the given color
+ */
+ public static BufferedImage createColoredImage(int width, int height, RGB color) {
+ BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ Graphics g = image.getGraphics();
+ g.setColor(new Color(color.red, color.green, color.blue));
+ g.fillRect(0, 0, image.getWidth(), image.getHeight());
+ g.dispose();
+ return image;
+ }
+}