Home | History | Annotate | Download | only in gle2
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
     17 
     18 import static com.android.SdkConstants.DOT_9PNG;
     19 import static com.android.SdkConstants.DOT_BMP;
     20 import static com.android.SdkConstants.DOT_GIF;
     21 import static com.android.SdkConstants.DOT_JPG;
     22 import static com.android.SdkConstants.DOT_PNG;
     23 import static com.android.utils.SdkUtils.endsWithIgnoreCase;
     24 import static java.awt.RenderingHints.KEY_ANTIALIASING;
     25 import static java.awt.RenderingHints.KEY_INTERPOLATION;
     26 import static java.awt.RenderingHints.KEY_RENDERING;
     27 import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON;
     28 import static java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR;
     29 import static java.awt.RenderingHints.VALUE_RENDER_QUALITY;
     30 
     31 import com.android.annotations.NonNull;
     32 import com.android.annotations.Nullable;
     33 import com.android.ide.common.api.Rect;
     34 import com.android.ide.eclipse.adt.AdtPlugin;
     35 
     36 import org.eclipse.swt.graphics.RGB;
     37 import org.eclipse.swt.graphics.Rectangle;
     38 
     39 import java.awt.AlphaComposite;
     40 import java.awt.Color;
     41 import java.awt.Graphics;
     42 import java.awt.Graphics2D;
     43 import java.awt.image.BufferedImage;
     44 import java.awt.image.DataBufferInt;
     45 import java.io.IOException;
     46 import java.io.InputStream;
     47 import java.util.Iterator;
     48 import java.util.List;
     49 
     50 import javax.imageio.ImageIO;
     51 
     52 /**
     53  * Utilities related to image processing.
     54  */
     55 public class ImageUtils {
     56     /**
     57      * Returns true if the given image has no dark pixels
     58      *
     59      * @param image the image to be checked for dark pixels
     60      * @return true if no dark pixels were found
     61      */
     62     public static boolean containsDarkPixels(BufferedImage image) {
     63         for (int y = 0, height = image.getHeight(); y < height; y++) {
     64             for (int x = 0, width = image.getWidth(); x < width; x++) {
     65                 int pixel = image.getRGB(x, y);
     66                 if ((pixel & 0xFF000000) != 0) {
     67                     int r = (pixel & 0xFF0000) >> 16;
     68                     int g = (pixel & 0x00FF00) >> 8;
     69                     int b = (pixel & 0x0000FF);
     70 
     71                     // One perceived luminance formula is (0.299*red + 0.587*green + 0.114*blue)
     72                     // In order to keep this fast since we don't need a very accurate
     73                     // measure, I'll just estimate this with integer math:
     74                     long brightness = (299L*r + 587*g + 114*b) / 1000;
     75                     if (brightness < 128) {
     76                         return true;
     77                     }
     78                 }
     79             }
     80         }
     81         return false;
     82     }
     83 
     84     /**
     85      * Returns the perceived brightness of the given RGB integer on a scale from 0 to 255
     86      *
     87      * @param rgb the RGB triplet, 8 bits each
     88      * @return the perceived brightness, with 0 maximally dark and 255 maximally bright
     89      */
     90     public static int getBrightness(int rgb) {
     91         if ((rgb & 0xFFFFFF) != 0) {
     92             int r = (rgb & 0xFF0000) >> 16;
     93             int g = (rgb & 0x00FF00) >> 8;
     94             int b = (rgb & 0x0000FF);
     95             // See the containsDarkPixels implementation for details
     96             return (int) ((299L*r + 587*g + 114*b) / 1000);
     97         }
     98 
     99         return 0;
    100     }
    101 
    102     /**
    103      * Converts an alpha-red-green-blue integer color into an {@link RGB} color.
    104      * <p>
    105      * <b>NOTE</b> - this will drop the alpha value since {@link RGB} objects do not
    106      * contain transparency information.
    107      *
    108      * @param rgb the RGB integer to convert to a color description
    109      * @return the color description corresponding to the integer
    110      */
    111     public static RGB intToRgb(int rgb) {
    112         return new RGB((rgb & 0xFF0000) >>> 16, (rgb & 0xFF00) >>> 8, rgb & 0xFF);
    113     }
    114 
    115     /**
    116      * Converts an {@link RGB} color into a alpha-red-green-blue integer
    117      *
    118      * @param rgb the RGB color descriptor to convert
    119      * @param alpha the amount of alpha to add into the color integer (since the
    120      *            {@link RGB} objects do not contain an alpha channel)
    121      * @return an integer corresponding to the {@link RGB} color
    122      */
    123     public static int rgbToInt(RGB rgb, int alpha) {
    124         return alpha << 24 | (rgb.red << 16) | (rgb.green << 8) | rgb.blue;
    125     }
    126 
    127     /**
    128      * Crops blank pixels from the edges of the image and returns the cropped result. We
    129      * crop off pixels that are blank (meaning they have an alpha value = 0). Note that
    130      * this is not the same as pixels that aren't opaque (an alpha value other than 255).
    131      *
    132      * @param image the image to be cropped
    133      * @param initialCrop If not null, specifies a rectangle which contains an initial
    134      *            crop to continue. This can be used to crop an image where you already
    135      *            know about margins in the image
    136      * @return a cropped version of the source image, or null if the whole image was blank
    137      *         and cropping completely removed everything
    138      */
    139     @Nullable
    140     public static BufferedImage cropBlank(
    141             @NonNull BufferedImage image,
    142             @Nullable Rect initialCrop) {
    143         return cropBlank(image, initialCrop, image.getType());
    144     }
    145 
    146     /**
    147      * Crops blank pixels from the edges of the image and returns the cropped result. We
    148      * crop off pixels that are blank (meaning they have an alpha value = 0). Note that
    149      * this is not the same as pixels that aren't opaque (an alpha value other than 255).
    150      *
    151      * @param image the image to be cropped
    152      * @param initialCrop If not null, specifies a rectangle which contains an initial
    153      *            crop to continue. This can be used to crop an image where you already
    154      *            know about margins in the image
    155      * @param imageType the type of {@link BufferedImage} to create
    156      * @return a cropped version of the source image, or null if the whole image was blank
    157      *         and cropping completely removed everything
    158      */
    159     public static BufferedImage cropBlank(BufferedImage image, Rect initialCrop, int imageType) {
    160         CropFilter filter = new CropFilter() {
    161             @Override
    162             public boolean crop(BufferedImage bufferedImage, int x, int y) {
    163                 int rgb = bufferedImage.getRGB(x, y);
    164                 return (rgb & 0xFF000000) == 0x00000000;
    165                 // TODO: Do a threshold of 80 instead of just 0? Might give better
    166                 // visual results -- e.g. check <= 0x80000000
    167             }
    168         };
    169         return crop(image, filter, initialCrop, imageType);
    170     }
    171 
    172     /**
    173      * Crops pixels of a given color from the edges of the image and returns the cropped
    174      * result.
    175      *
    176      * @param image the image to be cropped
    177      * @param blankArgb the color considered to be blank, as a 32 pixel integer with 8
    178      *            bits of alpha, red, green and blue
    179      * @param initialCrop If not null, specifies a rectangle which contains an initial
    180      *            crop to continue. This can be used to crop an image where you already
    181      *            know about margins in the image
    182      * @return a cropped version of the source image, or null if the whole image was blank
    183      *         and cropping completely removed everything
    184      */
    185     @Nullable
    186     public static BufferedImage cropColor(
    187             @NonNull BufferedImage image,
    188             final int blankArgb,
    189             @Nullable Rect initialCrop) {
    190         return cropColor(image, blankArgb, initialCrop, image.getType());
    191     }
    192 
    193     /**
    194      * Crops pixels of a given color from the edges of the image and returns the cropped
    195      * result.
    196      *
    197      * @param image the image to be cropped
    198      * @param blankArgb the color considered to be blank, as a 32 pixel integer with 8
    199      *            bits of alpha, red, green and blue
    200      * @param initialCrop If not null, specifies a rectangle which contains an initial
    201      *            crop to continue. This can be used to crop an image where you already
    202      *            know about margins in the image
    203      * @param imageType the type of {@link BufferedImage} to create
    204      * @return a cropped version of the source image, or null if the whole image was blank
    205      *         and cropping completely removed everything
    206      */
    207     public static BufferedImage cropColor(BufferedImage image,
    208             final int blankArgb, Rect initialCrop, int imageType) {
    209         CropFilter filter = new CropFilter() {
    210             @Override
    211             public boolean crop(BufferedImage bufferedImage, int x, int y) {
    212                 return blankArgb == bufferedImage.getRGB(x, y);
    213             }
    214         };
    215         return crop(image, filter, initialCrop, imageType);
    216     }
    217 
    218     /**
    219      * Interface implemented by cropping functions that determine whether
    220      * a pixel should be cropped or not.
    221      */
    222     private static interface CropFilter {
    223         /**
    224          * Returns true if the pixel is should be cropped.
    225          *
    226          * @param image the image containing the pixel in question
    227          * @param x the x position of the pixel
    228          * @param y the y position of the pixel
    229          * @return true if the pixel should be cropped (for example, is blank)
    230          */
    231         boolean crop(BufferedImage image, int x, int y);
    232     }
    233 
    234     private static BufferedImage crop(BufferedImage image, CropFilter filter, Rect initialCrop,
    235             int imageType) {
    236         if (image == null) {
    237             return null;
    238         }
    239 
    240         // First, determine the dimensions of the real image within the image
    241         int x1, y1, x2, y2;
    242         if (initialCrop != null) {
    243             x1 = initialCrop.x;
    244             y1 = initialCrop.y;
    245             x2 = initialCrop.x + initialCrop.w;
    246             y2 = initialCrop.y + initialCrop.h;
    247         } else {
    248             x1 = 0;
    249             y1 = 0;
    250             x2 = image.getWidth();
    251             y2 = image.getHeight();
    252         }
    253 
    254         // Nothing left to crop
    255         if (x1 == x2 || y1 == y2) {
    256             return null;
    257         }
    258 
    259         // This algorithm is a bit dumb -- it just scans along the edges looking for
    260         // a pixel that shouldn't be cropped. I could maybe try to make it smarter by
    261         // for example doing a binary search to quickly eliminate large empty areas to
    262         // the right and bottom -- but this is slightly tricky with components like the
    263         // AnalogClock where I could accidentally end up finding a blank horizontal or
    264         // vertical line somewhere in the middle of the rendering of the clock, so for now
    265         // we do the dumb thing -- not a big deal since we tend to crop reasonably
    266         // small images.
    267 
    268         // First determine top edge
    269         topEdge: for (; y1 < y2; y1++) {
    270             for (int x = x1; x < x2; x++) {
    271                 if (!filter.crop(image, x, y1)) {
    272                     break topEdge;
    273                 }
    274             }
    275         }
    276 
    277         if (y1 == image.getHeight()) {
    278             // The image is blank
    279             return null;
    280         }
    281 
    282         // Next determine left edge
    283         leftEdge: for (; x1 < x2; x1++) {
    284             for (int y = y1; y < y2; y++) {
    285                 if (!filter.crop(image, x1, y)) {
    286                     break leftEdge;
    287                 }
    288             }
    289         }
    290 
    291         // Next determine right edge
    292         rightEdge: for (; x2 > x1; x2--) {
    293             for (int y = y1; y < y2; y++) {
    294                 if (!filter.crop(image, x2 - 1, y)) {
    295                     break rightEdge;
    296                 }
    297             }
    298         }
    299 
    300         // Finally determine bottom edge
    301         bottomEdge: for (; y2 > y1; y2--) {
    302             for (int x = x1; x < x2; x++) {
    303                 if (!filter.crop(image, x, y2 - 1)) {
    304                     break bottomEdge;
    305                 }
    306             }
    307         }
    308 
    309         // No need to crop?
    310         if (x1 == 0 && y1 == 0 && x2 == image.getWidth() && y2 == image.getHeight()) {
    311             return image;
    312         }
    313 
    314         if (x1 == x2 || y1 == y2) {
    315             // Nothing left after crop -- blank image
    316             return null;
    317         }
    318 
    319         int width = x2 - x1;
    320         int height = y2 - y1;
    321 
    322         // Now extract the sub-image
    323         if (imageType == -1) {
    324             imageType = image.getType();
    325         }
    326         if (imageType == BufferedImage.TYPE_CUSTOM) {
    327             imageType = BufferedImage.TYPE_INT_ARGB;
    328         }
    329         BufferedImage cropped = new BufferedImage(width, height, imageType);
    330         Graphics g = cropped.getGraphics();
    331         g.drawImage(image, 0, 0, width, height, x1, y1, x2, y2, null);
    332 
    333         g.dispose();
    334 
    335         return cropped;
    336     }
    337 
    338     /**
    339      * Creates a drop shadow of a given image and returns a new image which shows the
    340      * input image on top of its drop shadow.
    341      * <p>
    342      * <b>NOTE: If the shape is rectangular and opaque, consider using
    343      * {@link #drawRectangleShadow(Graphics, int, int, int, int)} instead.</b>
    344      *
    345      * @param source the source image to be shadowed
    346      * @param shadowSize the size of the shadow in pixels
    347      * @param shadowOpacity the opacity of the shadow, with 0=transparent and 1=opaque
    348      * @param shadowRgb the RGB int to use for the shadow color
    349      * @return a new image with the source image on top of its shadow
    350      */
    351     public static BufferedImage createDropShadow(BufferedImage source, int shadowSize,
    352             float shadowOpacity, int shadowRgb) {
    353 
    354         // This code is based on
    355         //      http://www.jroller.com/gfx/entry/non_rectangular_shadow
    356 
    357         BufferedImage image = new BufferedImage(source.getWidth() + shadowSize * 2,
    358                 source.getHeight() + shadowSize * 2,
    359                 BufferedImage.TYPE_INT_ARGB);
    360 
    361         Graphics2D g2 = image.createGraphics();
    362         g2.drawImage(source, null, shadowSize, shadowSize);
    363 
    364         int dstWidth = image.getWidth();
    365         int dstHeight = image.getHeight();
    366 
    367         int left = (shadowSize - 1) >> 1;
    368         int right = shadowSize - left;
    369         int xStart = left;
    370         int xStop = dstWidth - right;
    371         int yStart = left;
    372         int yStop = dstHeight - right;
    373 
    374         shadowRgb = shadowRgb & 0x00FFFFFF;
    375 
    376         int[] aHistory = new int[shadowSize];
    377         int historyIdx = 0;
    378 
    379         int aSum;
    380 
    381         int[] dataBuffer = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
    382         int lastPixelOffset = right * dstWidth;
    383         float sumDivider = shadowOpacity / shadowSize;
    384 
    385         // horizontal pass
    386         for (int y = 0, bufferOffset = 0; y < dstHeight; y++, bufferOffset = y * dstWidth) {
    387             aSum = 0;
    388             historyIdx = 0;
    389             for (int x = 0; x < shadowSize; x++, bufferOffset++) {
    390                 int a = dataBuffer[bufferOffset] >>> 24;
    391                 aHistory[x] = a;
    392                 aSum += a;
    393             }
    394 
    395             bufferOffset -= right;
    396 
    397             for (int x = xStart; x < xStop; x++, bufferOffset++) {
    398                 int a = (int) (aSum * sumDivider);
    399                 dataBuffer[bufferOffset] = a << 24 | shadowRgb;
    400 
    401                 // subtract the oldest pixel from the sum
    402                 aSum -= aHistory[historyIdx];
    403 
    404                 // get the latest pixel
    405                 a = dataBuffer[bufferOffset + right] >>> 24;
    406                 aHistory[historyIdx] = a;
    407                 aSum += a;
    408 
    409                 if (++historyIdx >= shadowSize) {
    410                     historyIdx -= shadowSize;
    411                 }
    412             }
    413         }
    414         // vertical pass
    415         for (int x = 0, bufferOffset = 0; x < dstWidth; x++, bufferOffset = x) {
    416             aSum = 0;
    417             historyIdx = 0;
    418             for (int y = 0; y < shadowSize; y++, bufferOffset += dstWidth) {
    419                 int a = dataBuffer[bufferOffset] >>> 24;
    420                 aHistory[y] = a;
    421                 aSum += a;
    422             }
    423 
    424             bufferOffset -= lastPixelOffset;
    425 
    426             for (int y = yStart; y < yStop; y++, bufferOffset += dstWidth) {
    427                 int a = (int) (aSum * sumDivider);
    428                 dataBuffer[bufferOffset] = a << 24 | shadowRgb;
    429 
    430                 // subtract the oldest pixel from the sum
    431                 aSum -= aHistory[historyIdx];
    432 
    433                 // get the latest pixel
    434                 a = dataBuffer[bufferOffset + lastPixelOffset] >>> 24;
    435                 aHistory[historyIdx] = a;
    436                 aSum += a;
    437 
    438                 if (++historyIdx >= shadowSize) {
    439                     historyIdx -= shadowSize;
    440                 }
    441             }
    442         }
    443 
    444         g2.drawImage(source, null, 0, 0);
    445         g2.dispose();
    446 
    447         return image;
    448     }
    449 
    450     /**
    451      * Draws a rectangular drop shadow (of size {@link #SHADOW_SIZE} by
    452      * {@link #SHADOW_SIZE} around the given source and returns a new image with
    453      * both combined
    454      *
    455      * @param source the source image
    456      * @return the source image with a drop shadow on the bottom and right
    457      */
    458     public static BufferedImage createRectangularDropShadow(BufferedImage source) {
    459         int type = source.getType();
    460         if (type == BufferedImage.TYPE_CUSTOM) {
    461             type = BufferedImage.TYPE_INT_ARGB;
    462         }
    463 
    464         int width = source.getWidth();
    465         int height = source.getHeight();
    466         BufferedImage image = new BufferedImage(width + SHADOW_SIZE, height + SHADOW_SIZE, type);
    467         Graphics g = image.getGraphics();
    468         g.drawImage(source, 0, 0, width, height, null);
    469         ImageUtils.drawRectangleShadow(image, 0, 0, width, height);
    470         g.dispose();
    471 
    472         return image;
    473     }
    474 
    475     /**
    476      * Draws a drop shadow for the given rectangle into the given context. It
    477      * will not draw anything if the rectangle is smaller than a minimum
    478      * determined by the assets used to draw the shadow graphics.
    479      * The size of the shadow is {@link #SHADOW_SIZE}.
    480      *
    481      * @param image the image to draw the shadow into
    482      * @param x the left coordinate of the left hand side of the rectangle
    483      * @param y the top coordinate of the top of the rectangle
    484      * @param width the width of the rectangle
    485      * @param height the height of the rectangle
    486      */
    487     public static final void drawRectangleShadow(BufferedImage image,
    488             int x, int y, int width, int height) {
    489         Graphics gc = image.getGraphics();
    490         try {
    491             drawRectangleShadow(gc, x, y, width, height);
    492         } finally {
    493             gc.dispose();
    494         }
    495     }
    496 
    497     /**
    498      * Draws a small drop shadow for the given rectangle into the given context. It
    499      * will not draw anything if the rectangle is smaller than a minimum
    500      * determined by the assets used to draw the shadow graphics.
    501      * The size of the shadow is {@link #SMALL_SHADOW_SIZE}.
    502      *
    503      * @param image the image to draw the shadow into
    504      * @param x the left coordinate of the left hand side of the rectangle
    505      * @param y the top coordinate of the top of the rectangle
    506      * @param width the width of the rectangle
    507      * @param height the height of the rectangle
    508      */
    509     public static final void drawSmallRectangleShadow(BufferedImage image,
    510             int x, int y, int width, int height) {
    511         Graphics gc = image.getGraphics();
    512         try {
    513             drawSmallRectangleShadow(gc, x, y, width, height);
    514         } finally {
    515             gc.dispose();
    516         }
    517     }
    518 
    519     /**
    520      * The width and height of the drop shadow painted by
    521      * {@link #drawRectangleShadow(Graphics, int, int, int, int)}
    522      */
    523     public static final int SHADOW_SIZE = 20; // DO NOT EDIT. This corresponds to bitmap graphics
    524 
    525     /**
    526      * The width and height of the drop shadow painted by
    527      * {@link #drawSmallRectangleShadow(Graphics, int, int, int, int)}
    528      */
    529     public static final int SMALL_SHADOW_SIZE = 10; // DO NOT EDIT. Corresponds to bitmap graphics
    530 
    531     /**
    532      * Draws a drop shadow for the given rectangle into the given context. It
    533      * will not draw anything if the rectangle is smaller than a minimum
    534      * determined by the assets used to draw the shadow graphics.
    535      * <p>
    536      * This corresponds to
    537      * {@link SwtUtils#drawRectangleShadow(org.eclipse.swt.graphics.GC, int, int, int, int)},
    538      * but applied to an AWT graphics object instead, such that no image
    539      * conversion has to be performed.
    540      * <p>
    541      * Make sure to keep changes in the visual appearance here in sync with the
    542      * AWT version in
    543      * {@link SwtUtils#drawRectangleShadow(org.eclipse.swt.graphics.GC, int, int, int, int)}.
    544      *
    545      * @param gc the graphics context to draw into
    546      * @param x the left coordinate of the left hand side of the rectangle
    547      * @param y the top coordinate of the top of the rectangle
    548      * @param width the width of the rectangle
    549      * @param height the height of the rectangle
    550      */
    551     public static final void drawRectangleShadow(Graphics gc,
    552             int x, int y, int width, int height) {
    553         if (sShadowBottomLeft == null) {
    554             // Shadow graphics. This was generated by creating a drop shadow in
    555             // Gimp, using the parameters x offset=10, y offset=10, blur radius=10,
    556             // color=black, and opacity=51. These values attempt to make a shadow
    557             // that is legible both for dark and light themes, on top of the
    558             // canvas background (rgb(150,150,150). Darker shadows would tend to
    559             // blend into the foreground for a dark holo screen, and lighter shadows
    560             // would be hard to spot on the canvas background. If you make adjustments,
    561             // make sure to check the shadow with both dark and light themes.
    562             //
    563             // After making the graphics, I cut out the top right, bottom left
    564             // and bottom right corners as 20x20 images, and these are reproduced by
    565             // painting them in the corresponding places in the target graphics context.
    566             // I then grabbed a single horizontal gradient line from the middle of the
    567             // right edge,and a single vertical gradient line from the bottom. These
    568             // are then painted scaled/stretched in the target to fill the gaps between
    569             // the three corner images.
    570             //
    571             // Filenames: bl=bottom left, b=bottom, br=bottom right, r=right, tr=top right
    572             sShadowBottomLeft  = readImage("shadow-bl.png"); //$NON-NLS-1$
    573             sShadowBottom      = readImage("shadow-b.png");  //$NON-NLS-1$
    574             sShadowBottomRight = readImage("shadow-br.png"); //$NON-NLS-1$
    575             sShadowRight       = readImage("shadow-r.png");  //$NON-NLS-1$
    576             sShadowTopRight    = readImage("shadow-tr.png"); //$NON-NLS-1$
    577             assert sShadowBottomLeft != null;
    578             assert sShadowBottomRight.getWidth() == SHADOW_SIZE;
    579             assert sShadowBottomRight.getHeight() == SHADOW_SIZE;
    580         }
    581 
    582         int blWidth = sShadowBottomLeft.getWidth();
    583         int trHeight = sShadowTopRight.getHeight();
    584         if (width < blWidth) {
    585             return;
    586         }
    587         if (height < trHeight) {
    588             return;
    589         }
    590 
    591         gc.drawImage(sShadowBottomLeft, x, y + height, null);
    592         gc.drawImage(sShadowBottomRight, x + width, y + height, null);
    593         gc.drawImage(sShadowTopRight, x + width, y, null);
    594         gc.drawImage(sShadowBottom,
    595                 x + sShadowBottomLeft.getWidth(), y + height,
    596                 x + width, y + height + sShadowBottom.getHeight(),
    597                 0, 0, sShadowBottom.getWidth(), sShadowBottom.getHeight(),
    598                 null);
    599         gc.drawImage(sShadowRight,
    600                 x + width, y + sShadowTopRight.getHeight(),
    601                 x + width + sShadowRight.getWidth(), y + height,
    602                 0, 0, sShadowRight.getWidth(), sShadowRight.getHeight(),
    603                 null);
    604     }
    605 
    606     /**
    607      * Draws a small drop shadow for the given rectangle into the given context. It
    608      * will not draw anything if the rectangle is smaller than a minimum
    609      * determined by the assets used to draw the shadow graphics.
    610      * <p>
    611      *
    612      * @param gc the graphics context to draw into
    613      * @param x the left coordinate of the left hand side of the rectangle
    614      * @param y the top coordinate of the top of the rectangle
    615      * @param width the width of the rectangle
    616      * @param height the height of the rectangle
    617      */
    618     public static final void drawSmallRectangleShadow(Graphics gc,
    619             int x, int y, int width, int height) {
    620         if (sShadow2BottomLeft == null) {
    621             // Shadow graphics. This was generated by creating a drop shadow in
    622             // Gimp, using the parameters x offset=5, y offset=%, blur radius=5,
    623             // color=black, and opacity=51. These values attempt to make a shadow
    624             // that is legible both for dark and light themes, on top of the
    625             // canvas background (rgb(150,150,150). Darker shadows would tend to
    626             // blend into the foreground for a dark holo screen, and lighter shadows
    627             // would be hard to spot on the canvas background. If you make adjustments,
    628             // make sure to check the shadow with both dark and light themes.
    629             //
    630             // After making the graphics, I cut out the top right, bottom left
    631             // and bottom right corners as 20x20 images, and these are reproduced by
    632             // painting them in the corresponding places in the target graphics context.
    633             // I then grabbed a single horizontal gradient line from the middle of the
    634             // right edge,and a single vertical gradient line from the bottom. These
    635             // are then painted scaled/stretched in the target to fill the gaps between
    636             // the three corner images.
    637             //
    638             // Filenames: bl=bottom left, b=bottom, br=bottom right, r=right, tr=top right
    639             sShadow2BottomLeft  = readImage("shadow2-bl.png"); //$NON-NLS-1$
    640             sShadow2Bottom      = readImage("shadow2-b.png");  //$NON-NLS-1$
    641             sShadow2BottomRight = readImage("shadow2-br.png"); //$NON-NLS-1$
    642             sShadow2Right       = readImage("shadow2-r.png");  //$NON-NLS-1$
    643             sShadow2TopRight    = readImage("shadow2-tr.png"); //$NON-NLS-1$
    644             assert sShadow2BottomLeft != null;
    645             assert sShadow2TopRight != null;
    646             assert sShadow2BottomRight.getWidth() == SMALL_SHADOW_SIZE;
    647             assert sShadow2BottomRight.getHeight() == SMALL_SHADOW_SIZE;
    648         }
    649 
    650         int blWidth = sShadow2BottomLeft.getWidth();
    651         int trHeight = sShadow2TopRight.getHeight();
    652         if (width < blWidth) {
    653             return;
    654         }
    655         if (height < trHeight) {
    656             return;
    657         }
    658 
    659         gc.drawImage(sShadow2BottomLeft, x, y + height, null);
    660         gc.drawImage(sShadow2BottomRight, x + width, y + height, null);
    661         gc.drawImage(sShadow2TopRight, x + width, y, null);
    662         gc.drawImage(sShadow2Bottom,
    663                 x + sShadow2BottomLeft.getWidth(), y + height,
    664                 x + width, y + height + sShadow2Bottom.getHeight(),
    665                 0, 0, sShadow2Bottom.getWidth(), sShadow2Bottom.getHeight(),
    666                 null);
    667         gc.drawImage(sShadow2Right,
    668                 x + width, y + sShadow2TopRight.getHeight(),
    669                 x + width + sShadow2Right.getWidth(), y + height,
    670                 0, 0, sShadow2Right.getWidth(), sShadow2Right.getHeight(),
    671                 null);
    672     }
    673 
    674     /**
    675      * Reads the given image from the plugin folder
    676      *
    677      * @param name the name of the image (including file extension)
    678      * @return the corresponding image, or null if something goes wrong
    679      */
    680     @Nullable
    681     public static BufferedImage readImage(@NonNull String name) {
    682         InputStream stream = ImageUtils.class.getResourceAsStream("/icons/" + name); //$NON-NLS-1$
    683         if (stream != null) {
    684             try {
    685                 return ImageIO.read(stream);
    686             } catch (IOException e) {
    687                 AdtPlugin.log(e, "Could not read %1$s", name);
    688             } finally {
    689                 try {
    690                     stream.close();
    691                 } catch (IOException e) {
    692                     // Dumb API
    693                 }
    694             }
    695         }
    696 
    697         return null;
    698     }
    699 
    700     // Normal drop shadow
    701     private static BufferedImage sShadowBottomLeft;
    702     private static BufferedImage sShadowBottom;
    703     private static BufferedImage sShadowBottomRight;
    704     private static BufferedImage sShadowRight;
    705     private static BufferedImage sShadowTopRight;
    706 
    707     // Small drop shadow
    708     private static BufferedImage sShadow2BottomLeft;
    709     private static BufferedImage sShadow2Bottom;
    710     private static BufferedImage sShadow2BottomRight;
    711     private static BufferedImage sShadow2Right;
    712     private static BufferedImage sShadow2TopRight;
    713 
    714     /**
    715      * Returns a bounding rectangle for the given list of rectangles. If the list is
    716      * empty, the bounding rectangle is null.
    717      *
    718      * @param items the list of rectangles to compute a bounding rectangle for (may not be
    719      *            null)
    720      * @return a bounding rectangle of the passed in rectangles, or null if the list is
    721      *         empty
    722      */
    723     public static Rectangle getBoundingRectangle(List<Rectangle> items) {
    724         Iterator<Rectangle> iterator = items.iterator();
    725         if (!iterator.hasNext()) {
    726             return null;
    727         }
    728 
    729         Rectangle bounds = iterator.next();
    730         Rectangle union = new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height);
    731         while (iterator.hasNext()) {
    732             union.add(iterator.next());
    733         }
    734 
    735         return union;
    736     }
    737 
    738     /**
    739      * Returns a new image which contains of the sub image given by the rectangle (x1,y1)
    740      * to (x2,y2)
    741      *
    742      * @param source the source image
    743      * @param x1 top left X coordinate
    744      * @param y1 top left Y coordinate
    745      * @param x2 bottom right X coordinate
    746      * @param y2 bottom right Y coordinate
    747      * @return a new image containing the pixels in the given range
    748      */
    749     public static BufferedImage subImage(BufferedImage source, int x1, int y1, int x2, int y2) {
    750         int width = x2 - x1;
    751         int height = y2 - y1;
    752         int imageType = source.getType();
    753         if (imageType == BufferedImage.TYPE_CUSTOM) {
    754             imageType = BufferedImage.TYPE_INT_ARGB;
    755         }
    756         BufferedImage sub = new BufferedImage(width, height, imageType);
    757         Graphics g = sub.getGraphics();
    758         g.drawImage(source, 0, 0, width, height, x1, y1, x2, y2, null);
    759         g.dispose();
    760 
    761         return sub;
    762     }
    763 
    764     /**
    765      * Returns the color value represented by the given string value
    766      * @param value the color value
    767      * @return the color as an int
    768      * @throw NumberFormatException if the conversion failed.
    769      */
    770     public static int getColor(String value) {
    771         // Copied from ResourceHelper in layoutlib
    772         if (value != null) {
    773             if (value.startsWith("#") == false) { //$NON-NLS-1$
    774                 throw new NumberFormatException(
    775                         String.format("Color value '%s' must start with #", value));
    776             }
    777 
    778             value = value.substring(1);
    779 
    780             // make sure it's not longer than 32bit
    781             if (value.length() > 8) {
    782                 throw new NumberFormatException(String.format(
    783                         "Color value '%s' is too long. Format is either" +
    784                         "#AARRGGBB, #RRGGBB, #RGB, or #ARGB",
    785                         value));
    786             }
    787 
    788             if (value.length() == 3) { // RGB format
    789                 char[] color = new char[8];
    790                 color[0] = color[1] = 'F';
    791                 color[2] = color[3] = value.charAt(0);
    792                 color[4] = color[5] = value.charAt(1);
    793                 color[6] = color[7] = value.charAt(2);
    794                 value = new String(color);
    795             } else if (value.length() == 4) { // ARGB format
    796                 char[] color = new char[8];
    797                 color[0] = color[1] = value.charAt(0);
    798                 color[2] = color[3] = value.charAt(1);
    799                 color[4] = color[5] = value.charAt(2);
    800                 color[6] = color[7] = value.charAt(3);
    801                 value = new String(color);
    802             } else if (value.length() == 6) {
    803                 value = "FF" + value; //$NON-NLS-1$
    804             }
    805 
    806             // this is a RRGGBB or AARRGGBB value
    807 
    808             // Integer.parseInt will fail to parse strings like "ff191919", so we use
    809             // a Long, but cast the result back into an int, since we know that we're only
    810             // dealing with 32 bit values.
    811             return (int)Long.parseLong(value, 16);
    812         }
    813 
    814         throw new NumberFormatException();
    815     }
    816 
    817     /**
    818      * Resize the given image
    819      *
    820      * @param source the image to be scaled
    821      * @param xScale x scale
    822      * @param yScale y scale
    823      * @return the scaled image
    824      */
    825     public static BufferedImage scale(BufferedImage source, double xScale, double yScale) {
    826        return scale(source, xScale, yScale, 0, 0);
    827     }
    828 
    829     /**
    830      * Resize the given image
    831      *
    832      * @param source the image to be scaled
    833      * @param xScale x scale
    834      * @param yScale y scale
    835      * @param rightMargin extra margin to add on the right
    836      * @param bottomMargin extra margin to add on the bottom
    837      * @return the scaled image
    838      */
    839     public static BufferedImage scale(BufferedImage source, double xScale, double yScale,
    840             int rightMargin, int bottomMargin) {
    841         int sourceWidth = source.getWidth();
    842         int sourceHeight = source.getHeight();
    843         int destWidth = Math.max(1, (int) (xScale * sourceWidth));
    844         int destHeight = Math.max(1, (int) (yScale * sourceHeight));
    845         int imageType = source.getType();
    846         if (imageType == BufferedImage.TYPE_CUSTOM) {
    847             imageType = BufferedImage.TYPE_INT_ARGB;
    848         }
    849         if (xScale > 0.5 && yScale > 0.5) {
    850             BufferedImage scaled =
    851                     new BufferedImage(destWidth + rightMargin, destHeight + bottomMargin, imageType);
    852             Graphics2D g2 = scaled.createGraphics();
    853             g2.setComposite(AlphaComposite.Src);
    854             g2.setColor(new Color(0, true));
    855             g2.fillRect(0, 0, destWidth + rightMargin, destHeight + bottomMargin);
    856             g2.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BILINEAR);
    857             g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY);
    858             g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
    859             g2.drawImage(source, 0, 0, destWidth, destHeight, 0, 0, sourceWidth, sourceHeight,
    860                     null);
    861             g2.dispose();
    862             return scaled;
    863         } else {
    864             // When creating a thumbnail, using the above code doesn't work very well;
    865             // you get some visible artifacts, especially for text. Instead use the
    866             // technique of repeatedly scaling the image into half; this will cause
    867             // proper averaging of neighboring pixels, and will typically (for the kinds
    868             // of screen sizes used by this utility method in the layout editor) take
    869             // about 3-4 iterations to get the result since we are logarithmically reducing
    870             // the size. Besides, each successive pass in operating on much fewer pixels
    871             // (a reduction of 4 in each pass).
    872             //
    873             // However, we may not be resizing to a size that can be reached exactly by
    874             // successively diving in half. Therefore, once we're within a factor of 2 of
    875             // the final size, we can do a resize to the exact target size.
    876             // However, we can get even better results if we perform this final resize
    877             // up front. Let's say we're going from width 1000 to a destination width of 85.
    878             // The first approach would cause a resize from 1000 to 500 to 250 to 125, and
    879             // then a resize from 125 to 85. That last resize can distort/blur a lot.
    880             // Instead, we can start with the destination width, 85, and double it
    881             // successfully until we're close to the initial size: 85, then 170,
    882             // then 340, and finally 680. (The next one, 1360, is larger than 1000).
    883             // So, now we *start* the thumbnail operation by resizing from width 1000 to
    884             // width 680, which will preserve a lot of visual details such as text.
    885             // Then we can successively resize the image in half, 680 to 340 to 170 to 85.
    886             // We end up with the expected final size, but we've been doing an exact
    887             // divide-in-half resizing operation at the end so there is less distortion.
    888 
    889 
    890             int iterations = 0; // Number of halving operations to perform after the initial resize
    891             int nearestWidth = destWidth; // Width closest to source width that = 2^x, x is integer
    892             int nearestHeight = destHeight;
    893             while (nearestWidth < sourceWidth / 2) {
    894                 nearestWidth *= 2;
    895                 nearestHeight *= 2;
    896                 iterations++;
    897             }
    898 
    899             // If we're supposed to add in margins, we need to do it in the initial resizing
    900             // operation if we don't have any subsequent resizing operations.
    901             if (iterations == 0) {
    902                 nearestWidth += rightMargin;
    903                 nearestHeight += bottomMargin;
    904             }
    905 
    906             BufferedImage scaled = new BufferedImage(nearestWidth, nearestHeight, imageType);
    907             Graphics2D g2 = scaled.createGraphics();
    908             g2.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BILINEAR);
    909             g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY);
    910             g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
    911             g2.drawImage(source, 0, 0, nearestWidth, nearestHeight,
    912                     0, 0, sourceWidth, sourceHeight, null);
    913             g2.dispose();
    914 
    915             sourceWidth = nearestWidth;
    916             sourceHeight = nearestHeight;
    917             source = scaled;
    918 
    919             for (int iteration = iterations - 1; iteration >= 0; iteration--) {
    920                 int halfWidth = sourceWidth / 2;
    921                 int halfHeight = sourceHeight / 2;
    922                 if (iteration == 0) { // Last iteration: Add margins in final image
    923                     scaled = new BufferedImage(halfWidth + rightMargin, halfHeight + bottomMargin,
    924                             imageType);
    925                 } else {
    926                     scaled = new BufferedImage(halfWidth, halfHeight, imageType);
    927                 }
    928                 g2 = scaled.createGraphics();
    929                 g2.setRenderingHint(KEY_INTERPOLATION,VALUE_INTERPOLATION_BILINEAR);
    930                 g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY);
    931                 g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
    932                 g2.drawImage(source, 0, 0,
    933                         halfWidth, halfHeight, 0, 0,
    934                         sourceWidth, sourceHeight,
    935                         null);
    936                 g2.dispose();
    937 
    938                 sourceWidth = halfWidth;
    939                 sourceHeight = halfHeight;
    940                 source = scaled;
    941                 iterations--;
    942             }
    943             return scaled;
    944         }
    945     }
    946 
    947     /**
    948      * Returns true if the given file path points to an image file recognized by
    949      * Android. See http://developer.android.com/guide/appendix/media-formats.html
    950      * for details.
    951      *
    952      * @param path the filename to be tested
    953      * @return true if the file represents an image file
    954      */
    955     public static boolean hasImageExtension(String path) {
    956         return endsWithIgnoreCase(path, DOT_PNG)
    957             || endsWithIgnoreCase(path, DOT_9PNG)
    958             || endsWithIgnoreCase(path, DOT_GIF)
    959             || endsWithIgnoreCase(path, DOT_JPG)
    960             || endsWithIgnoreCase(path, DOT_BMP);
    961     }
    962 
    963     /**
    964      * Creates a new image of the given size filled with the given color
    965      *
    966      * @param width the width of the image
    967      * @param height the height of the image
    968      * @param color the color of the image
    969      * @return a new image of the given size filled with the given color
    970      */
    971     public static BufferedImage createColoredImage(int width, int height, RGB color) {
    972         BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
    973         Graphics g = image.getGraphics();
    974         g.setColor(new Color(color.red, color.green, color.blue));
    975         g.fillRect(0, 0, image.getWidth(), image.getHeight());
    976         g.dispose();
    977         return image;
    978     }
    979 }
    980