diff options
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.java | 979 |
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; + } +} |