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.ide.eclipse.adt.AdtConstants.DOT_9PNG;
     19 import static com.android.ide.eclipse.adt.AdtConstants.DOT_BMP;
     20 import static com.android.ide.eclipse.adt.AdtConstants.DOT_GIF;
     21 import static com.android.ide.eclipse.adt.AdtConstants.DOT_JPG;
     22 import static com.android.ide.eclipse.adt.AdtConstants.DOT_PNG;
     23 import static com.android.ide.eclipse.adt.AdtUtils.endsWithIgnoreCase;
     24 
     25 import com.android.ide.common.api.Rect;
     26 
     27 import org.eclipse.swt.graphics.RGB;
     28 import org.eclipse.swt.graphics.Rectangle;
     29 
     30 import java.awt.AlphaComposite;
     31 import java.awt.Color;
     32 import java.awt.Graphics;
     33 import java.awt.Graphics2D;
     34 import java.awt.RenderingHints;
     35 import java.awt.image.BufferedImage;
     36 import java.awt.image.DataBufferInt;
     37 import java.util.Iterator;
     38 import java.util.List;
     39 
     40 /**
     41  * Utilities related to image processing.
     42  */
     43 public class ImageUtils {
     44     /**
     45      * Returns true if the given image has no dark pixels
     46      *
     47      * @param image the image to be checked for dark pixels
     48      * @return true if no dark pixels were found
     49      */
     50     public static boolean containsDarkPixels(BufferedImage image) {
     51         for (int y = 0, height = image.getHeight(); y < height; y++) {
     52             for (int x = 0, width = image.getWidth(); x < width; x++) {
     53                 int pixel = image.getRGB(x, y);
     54                 if ((pixel & 0xFF000000) != 0) {
     55                     int r = (pixel & 0xFF0000) >> 16;
     56                     int g = (pixel & 0x00FF00) >> 8;
     57                     int b = (pixel & 0x0000FF);
     58 
     59                     // One perceived luminance formula is (0.299*red + 0.587*green + 0.114*blue)
     60                     // In order to keep this fast since we don't need a very accurate
     61                     // measure, I'll just estimate this with integer math:
     62                     long brightness = (299L*r + 587*g + 114*b) / 1000;
     63                     if (brightness < 128) {
     64                         return true;
     65                     }
     66                 }
     67             }
     68         }
     69         return false;
     70     }
     71 
     72     /**
     73      * Returns the perceived brightness of the given RGB integer on a scale from 0 to 255
     74      *
     75      * @param rgb the RGB triplet, 8 bits each
     76      * @return the perceived brightness, with 0 maximally dark and 255 maximally bright
     77      */
     78     public static int getBrightness(int rgb) {
     79         if ((rgb & 0xFFFFFF) != 0) {
     80             int r = (rgb & 0xFF0000) >> 16;
     81             int g = (rgb & 0x00FF00) >> 8;
     82             int b = (rgb & 0x0000FF);
     83             // See the containsDarkPixels implementation for details
     84             return (int) ((299L*r + 587*g + 114*b) / 1000);
     85         }
     86 
     87         return 0;
     88     }
     89 
     90     /**
     91      * Converts an alpha-red-green-blue integer color into an {@link RGB} color.
     92      * <p>
     93      * <b>NOTE</b> - this will drop the alpha value since {@link RGB} objects do not
     94      * contain transparency information.
     95      *
     96      * @param rgb the RGB integer to convert to a color description
     97      * @return the color description corresponding to the integer
     98      */
     99     public static RGB intToRgb(int rgb) {
    100         return new RGB((rgb & 0xFF0000) >>> 16, (rgb & 0xFF00) >>> 8, rgb & 0xFF);
    101     }
    102 
    103     /**
    104      * Converts an {@link RGB} color into a alpha-red-green-blue integer
    105      *
    106      * @param rgb the RGB color descriptor to convert
    107      * @param alpha the amount of alpha to add into the color integer (since the
    108      *            {@link RGB} objects do not contain an alpha channel)
    109      * @return an integer corresponding to the {@link RGB} color
    110      */
    111     public static int rgbToInt(RGB rgb, int alpha) {
    112         return alpha << 24 | (rgb.red << 16) | (rgb.green << 8) | rgb.blue;
    113     }
    114 
    115     /**
    116      * Crops blank pixels from the edges of the image and returns the cropped result. We
    117      * crop off pixels that are blank (meaning they have an alpha value = 0). Note that
    118      * this is not the same as pixels that aren't opaque (an alpha value other than 255).
    119      *
    120      * @param image the image to be cropped
    121      * @param initialCrop If not null, specifies a rectangle which contains an initial
    122      *            crop to continue. This can be used to crop an image where you already
    123      *            know about margins in the image
    124      * @return a cropped version of the source image, or null if the whole image was blank
    125      *         and cropping completely removed everything
    126      */
    127     public static BufferedImage cropBlank(BufferedImage image, Rect initialCrop) {
    128         CropFilter filter = new CropFilter() {
    129             public boolean crop(BufferedImage bufferedImage, int x, int y) {
    130                 int rgb = bufferedImage.getRGB(x, y);
    131                 return (rgb & 0xFF000000) == 0x00000000;
    132                 // TODO: Do a threshold of 80 instead of just 0? Might give better
    133                 // visual results -- e.g. check <= 0x80000000
    134             }
    135         };
    136         return crop(image, filter, initialCrop);
    137     }
    138 
    139     /**
    140      * Crops pixels of a given color from the edges of the image and returns the cropped
    141      * result.
    142      *
    143      * @param image the image to be cropped
    144      * @param blankArgb the color considered to be blank, as a 32 pixel integer with 8
    145      *            bits of alpha, red, green and blue
    146      * @param initialCrop If not null, specifies a rectangle which contains an initial
    147      *            crop to continue. This can be used to crop an image where you already
    148      *            know about margins in the image
    149      * @return a cropped version of the source image, or null if the whole image was blank
    150      *         and cropping completely removed everything
    151      */
    152     public static BufferedImage cropColor(BufferedImage image,
    153             final int blankArgb, Rect initialCrop) {
    154         CropFilter filter = new CropFilter() {
    155             public boolean crop(BufferedImage bufferedImage, int x, int y) {
    156                 return blankArgb == bufferedImage.getRGB(x, y);
    157             }
    158         };
    159         return crop(image, filter, initialCrop);
    160     }
    161 
    162     /**
    163      * Interface implemented by cropping functions that determine whether
    164      * a pixel should be cropped or not.
    165      */
    166     private static interface CropFilter {
    167         /**
    168          * Returns true if the pixel is should be cropped.
    169          *
    170          * @param image the image containing the pixel in question
    171          * @param x the x position of the pixel
    172          * @param y the y position of the pixel
    173          * @return true if the pixel should be cropped (for example, is blank)
    174          */
    175         boolean crop(BufferedImage image, int x, int y);
    176     }
    177 
    178     private static BufferedImage crop(BufferedImage image, CropFilter filter, Rect initialCrop) {
    179         if (image == null) {
    180             return null;
    181         }
    182 
    183         // First, determine the dimensions of the real image within the image
    184         int x1, y1, x2, y2;
    185         if (initialCrop != null) {
    186             x1 = initialCrop.x;
    187             y1 = initialCrop.y;
    188             x2 = initialCrop.x + initialCrop.w;
    189             y2 = initialCrop.y + initialCrop.h;
    190         } else {
    191             x1 = 0;
    192             y1 = 0;
    193             x2 = image.getWidth();
    194             y2 = image.getHeight();
    195         }
    196 
    197         // Nothing left to crop
    198         if (x1 == x2 || y1 == y2) {
    199             return null;
    200         }
    201 
    202         // This algorithm is a bit dumb -- it just scans along the edges looking for
    203         // a pixel that shouldn't be cropped. I could maybe try to make it smarter by
    204         // for example doing a binary search to quickly eliminate large empty areas to
    205         // the right and bottom -- but this is slightly tricky with components like the
    206         // AnalogClock where I could accidentally end up finding a blank horizontal or
    207         // vertical line somewhere in the middle of the rendering of the clock, so for now
    208         // we do the dumb thing -- not a big deal since we tend to crop reasonably
    209         // small images.
    210 
    211         // First determine top edge
    212         topEdge: for (; y1 < y2; y1++) {
    213             for (int x = x1; x < x2; x++) {
    214                 if (!filter.crop(image, x, y1)) {
    215                     break topEdge;
    216                 }
    217             }
    218         }
    219 
    220         if (y1 == image.getHeight()) {
    221             // The image is blank
    222             return null;
    223         }
    224 
    225         // Next determine left edge
    226         leftEdge: for (; x1 < x2; x1++) {
    227             for (int y = y1; y < y2; y++) {
    228                 if (!filter.crop(image, x1, y)) {
    229                     break leftEdge;
    230                 }
    231             }
    232         }
    233 
    234         // Next determine right edge
    235         rightEdge: for (; x2 > x1; x2--) {
    236             for (int y = y1; y < y2; y++) {
    237                 if (!filter.crop(image, x2 - 1, y)) {
    238                     break rightEdge;
    239                 }
    240             }
    241         }
    242 
    243         // Finally determine bottom edge
    244         bottomEdge: for (; y2 > y1; y2--) {
    245             for (int x = x1; x < x2; x++) {
    246                 if (!filter.crop(image, x, y2 - 1)) {
    247                     break bottomEdge;
    248                 }
    249             }
    250         }
    251 
    252         // No need to crop?
    253         if (x1 == 0 && y1 == 0 && x2 == image.getWidth() && y2 == image.getHeight()) {
    254             return image;
    255         }
    256 
    257         if (x1 == x2 || y1 == y2) {
    258             // Nothing left after crop -- blank image
    259             return null;
    260         }
    261 
    262         int width = x2 - x1;
    263         int height = y2 - y1;
    264 
    265         // Now extract the sub-image
    266         BufferedImage cropped = new BufferedImage(width, height, image.getType());
    267         Graphics g = cropped.getGraphics();
    268         g.drawImage(image, 0, 0, width, height, x1, y1, x2, y2, null);
    269 
    270         g.dispose();
    271 
    272         return cropped;
    273     }
    274 
    275     /**
    276      * Creates a drop shadow of a given image and returns a new image which shows the
    277      * input image on top of its drop shadow.
    278      *
    279      * @param source the source image to be shadowed
    280      * @param shadowSize the size of the shadow in pixels
    281      * @param shadowOpacity the opacity of the shadow, with 0=transparent and 1=opaque
    282      * @param shadowRgb the RGB int to use for the shadow color
    283      * @return a new image with the source image on top of its shadow
    284      */
    285     public static BufferedImage createDropShadow(BufferedImage source, int shadowSize,
    286             float shadowOpacity, int shadowRgb) {
    287 
    288         // This code is based on
    289         //      http://www.jroller.com/gfx/entry/non_rectangular_shadow
    290 
    291         BufferedImage image = new BufferedImage(source.getWidth() + shadowSize * 2,
    292                 source.getHeight() + shadowSize * 2,
    293                 BufferedImage.TYPE_INT_ARGB);
    294 
    295         Graphics2D g2 = image.createGraphics();
    296         g2.drawImage(source, null, shadowSize, shadowSize);
    297 
    298         int dstWidth = image.getWidth();
    299         int dstHeight = image.getHeight();
    300 
    301         int left = (shadowSize - 1) >> 1;
    302         int right = shadowSize - left;
    303         int xStart = left;
    304         int xStop = dstWidth - right;
    305         int yStart = left;
    306         int yStop = dstHeight - right;
    307 
    308         shadowRgb = shadowRgb & 0x00FFFFFF;
    309 
    310         int[] aHistory = new int[shadowSize];
    311         int historyIdx = 0;
    312 
    313         int aSum;
    314 
    315         int[] dataBuffer = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
    316         int lastPixelOffset = right * dstWidth;
    317         float sumDivider = shadowOpacity / shadowSize;
    318 
    319         // horizontal pass
    320         for (int y = 0, bufferOffset = 0; y < dstHeight; y++, bufferOffset = y * dstWidth) {
    321             aSum = 0;
    322             historyIdx = 0;
    323             for (int x = 0; x < shadowSize; x++, bufferOffset++) {
    324                 int a = dataBuffer[bufferOffset] >>> 24;
    325                 aHistory[x] = a;
    326                 aSum += a;
    327             }
    328 
    329             bufferOffset -= right;
    330 
    331             for (int x = xStart; x < xStop; x++, bufferOffset++) {
    332                 int a = (int) (aSum * sumDivider);
    333                 dataBuffer[bufferOffset] = a << 24 | shadowRgb;
    334 
    335                 // subtract the oldest pixel from the sum
    336                 aSum -= aHistory[historyIdx];
    337 
    338                 // get the latest pixel
    339                 a = dataBuffer[bufferOffset + right] >>> 24;
    340                 aHistory[historyIdx] = a;
    341                 aSum += a;
    342 
    343                 if (++historyIdx >= shadowSize) {
    344                     historyIdx -= shadowSize;
    345                 }
    346             }
    347         }
    348         // vertical pass
    349         for (int x = 0, bufferOffset = 0; x < dstWidth; x++, bufferOffset = x) {
    350             aSum = 0;
    351             historyIdx = 0;
    352             for (int y = 0; y < shadowSize; y++, bufferOffset += dstWidth) {
    353                 int a = dataBuffer[bufferOffset] >>> 24;
    354                 aHistory[y] = a;
    355                 aSum += a;
    356             }
    357 
    358             bufferOffset -= lastPixelOffset;
    359 
    360             for (int y = yStart; y < yStop; y++, bufferOffset += dstWidth) {
    361                 int a = (int) (aSum * sumDivider);
    362                 dataBuffer[bufferOffset] = a << 24 | shadowRgb;
    363 
    364                 // subtract the oldest pixel from the sum
    365                 aSum -= aHistory[historyIdx];
    366 
    367                 // get the latest pixel
    368                 a = dataBuffer[bufferOffset + lastPixelOffset] >>> 24;
    369                 aHistory[historyIdx] = a;
    370                 aSum += a;
    371 
    372                 if (++historyIdx >= shadowSize) {
    373                     historyIdx -= shadowSize;
    374                 }
    375             }
    376         }
    377 
    378         g2.drawImage(source, null, 0, 0);
    379         g2.dispose();
    380 
    381         return image;
    382     }
    383 
    384     /**
    385      * Returns a bounding rectangle for the given list of rectangles. If the list is
    386      * empty, the bounding rectangle is null.
    387      *
    388      * @param items the list of rectangles to compute a bounding rectangle for (may not be
    389      *            null)
    390      * @return a bounding rectangle of the passed in rectangles, or null if the list is
    391      *         empty
    392      */
    393     public static Rectangle getBoundingRectangle(List<Rectangle> items) {
    394         Iterator<Rectangle> iterator = items.iterator();
    395         if (!iterator.hasNext()) {
    396             return null;
    397         }
    398 
    399         Rectangle bounds = iterator.next();
    400         Rectangle union = new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height);
    401         while (iterator.hasNext()) {
    402             union.add(iterator.next());
    403         }
    404 
    405         return union;
    406     }
    407 
    408     /**
    409      * Returns a new image which contains of the sub image given by the rectangle (x1,y1)
    410      * to (x2,y2)
    411      *
    412      * @param source the source image
    413      * @param x1 top left X coordinate
    414      * @param y1 top left Y coordinate
    415      * @param x2 bottom right X coordinate
    416      * @param y2 bottom right Y coordinate
    417      * @return a new image containing the pixels in the given range
    418      */
    419     public static BufferedImage subImage(BufferedImage source, int x1, int y1, int x2, int y2) {
    420         int width = x2 - x1;
    421         int height = y2 - y1;
    422         BufferedImage sub = new BufferedImage(width, height, source.getType());
    423         Graphics g = sub.getGraphics();
    424         g.drawImage(source, 0, 0, width, height, x1, y1, x2, y2, null);
    425         g.dispose();
    426 
    427         return sub;
    428     }
    429 
    430     /**
    431      * Returns the color value represented by the given string value
    432      * @param value the color value
    433      * @return the color as an int
    434      * @throw NumberFormatException if the conversion failed.
    435      */
    436     public static int getColor(String value) {
    437         // Copied from ResourceHelper in layoutlib
    438         if (value != null) {
    439             if (value.startsWith("#") == false) { //$NON-NLS-1$
    440                 throw new NumberFormatException(
    441                         String.format("Color value '%s' must start with #", value));
    442             }
    443 
    444             value = value.substring(1);
    445 
    446             // make sure it's not longer than 32bit
    447             if (value.length() > 8) {
    448                 throw new NumberFormatException(String.format(
    449                         "Color value '%s' is too long. Format is either" +
    450                         "#AARRGGBB, #RRGGBB, #RGB, or #ARGB",
    451                         value));
    452             }
    453 
    454             if (value.length() == 3) { // RGB format
    455                 char[] color = new char[8];
    456                 color[0] = color[1] = 'F';
    457                 color[2] = color[3] = value.charAt(0);
    458                 color[4] = color[5] = value.charAt(1);
    459                 color[6] = color[7] = value.charAt(2);
    460                 value = new String(color);
    461             } else if (value.length() == 4) { // ARGB format
    462                 char[] color = new char[8];
    463                 color[0] = color[1] = value.charAt(0);
    464                 color[2] = color[3] = value.charAt(1);
    465                 color[4] = color[5] = value.charAt(2);
    466                 color[6] = color[7] = value.charAt(3);
    467                 value = new String(color);
    468             } else if (value.length() == 6) {
    469                 value = "FF" + value; //$NON-NLS-1$
    470             }
    471 
    472             // this is a RRGGBB or AARRGGBB value
    473 
    474             // Integer.parseInt will fail to parse strings like "ff191919", so we use
    475             // a Long, but cast the result back into an int, since we know that we're only
    476             // dealing with 32 bit values.
    477             return (int)Long.parseLong(value, 16);
    478         }
    479 
    480         throw new NumberFormatException();
    481     }
    482 
    483     /**
    484      * Resize the given image
    485      *
    486      * @param source the image to be scaled
    487      * @param xScale x scale
    488      * @param yScale y scale
    489      * @return the scaled image
    490      */
    491     public static BufferedImage scale(BufferedImage source, double xScale, double yScale) {
    492         int sourceWidth = source.getWidth();
    493         int sourceHeight = source.getHeight();
    494         int destWidth = Math.max(1, (int) (xScale * sourceWidth));
    495         int destHeight = Math.max(1, (int) (yScale * sourceHeight));
    496         BufferedImage scaled = new BufferedImage(destWidth, destHeight, source.getType());
    497         Graphics2D g2 = scaled.createGraphics();
    498         g2.setComposite(AlphaComposite.Src);
    499         g2.setColor(new Color(0, true));
    500         g2.fillRect(0, 0, destWidth, destHeight);
    501         g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
    502                 RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    503         g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    504         g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    505         g2.drawImage(source, 0, 0, destWidth, destHeight, 0, 0, sourceWidth, sourceHeight, null);
    506         g2.dispose();
    507 
    508         return scaled;
    509     }
    510 
    511     /**
    512      * Returns true if the given file path points to an image file recognized by
    513      * Android. See http://developer.android.com/guide/appendix/media-formats.html
    514      * for details.
    515      *
    516      * @param path the filename to be tested
    517      * @return true if the file represents an image file
    518      */
    519     public static boolean hasImageExtension(String path) {
    520         return endsWithIgnoreCase(path, DOT_PNG)
    521             || endsWithIgnoreCase(path, DOT_9PNG)
    522             || endsWithIgnoreCase(path, DOT_GIF)
    523             || endsWithIgnoreCase(path, DOT_JPG)
    524             || endsWithIgnoreCase(path, DOT_BMP);
    525     }
    526 
    527     /**
    528      * Creates a new image of the given size filled with the given color
    529      *
    530      * @param width the width of the image
    531      * @param height the height of the image
    532      * @param color the color of the image
    533      * @return a new image of the given size filled with the given color
    534      */
    535     public static BufferedImage createColoredImage(int width, int height, RGB color) {
    536         BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
    537         Graphics g = image.getGraphics();
    538         g.setColor(new Color(color.red, color.green, color.blue));
    539         g.fillRect(0, 0, image.getWidth(), image.getHeight());
    540         g.dispose();
    541         return image;
    542     }
    543 }
    544