• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
containsDarkPixels(BufferedImage image)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      */
getBrightness(int rgb)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      */
intToRgb(int rgb)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      */
rgbToInt(RGB rgb, int alpha)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      */
cropBlank(BufferedImage image, Rect initialCrop)127     public static BufferedImage cropBlank(BufferedImage image, Rect initialCrop) {
128         return cropBlank(image, initialCrop, image.getType());
129     }
130 
131     /**
132      * Crops blank pixels from the edges of the image and returns the cropped result. We
133      * crop off pixels that are blank (meaning they have an alpha value = 0). Note that
134      * this is not the same as pixels that aren't opaque (an alpha value other than 255).
135      *
136      * @param image the image to be cropped
137      * @param initialCrop If not null, specifies a rectangle which contains an initial
138      *            crop to continue. This can be used to crop an image where you already
139      *            know about margins in the image
140      * @param imageType the type of {@link BufferedImage} to create
141      * @return a cropped version of the source image, or null if the whole image was blank
142      *         and cropping completely removed everything
143      */
cropBlank(BufferedImage image, Rect initialCrop, int imageType)144     public static BufferedImage cropBlank(BufferedImage image, Rect initialCrop, int imageType) {
145         CropFilter filter = new CropFilter() {
146             @Override
147             public boolean crop(BufferedImage bufferedImage, int x, int y) {
148                 int rgb = bufferedImage.getRGB(x, y);
149                 return (rgb & 0xFF000000) == 0x00000000;
150                 // TODO: Do a threshold of 80 instead of just 0? Might give better
151                 // visual results -- e.g. check <= 0x80000000
152             }
153         };
154         return crop(image, filter, initialCrop, imageType);
155     }
156 
157     /**
158      * Crops pixels of a given color from the edges of the image and returns the cropped
159      * result.
160      *
161      * @param image the image to be cropped
162      * @param blankArgb the color considered to be blank, as a 32 pixel integer with 8
163      *            bits of alpha, red, green and blue
164      * @param initialCrop If not null, specifies a rectangle which contains an initial
165      *            crop to continue. This can be used to crop an image where you already
166      *            know about margins in the image
167      * @return a cropped version of the source image, or null if the whole image was blank
168      *         and cropping completely removed everything
169      */
cropColor(BufferedImage image, final int blankArgb, Rect initialCrop)170     public static BufferedImage cropColor(BufferedImage image,
171             final int blankArgb, Rect initialCrop) {
172         return cropColor(image, blankArgb, initialCrop, image.getType());
173     }
174 
175     /**
176      * Crops pixels of a given color from the edges of the image and returns the cropped
177      * result.
178      *
179      * @param image the image to be cropped
180      * @param blankArgb the color considered to be blank, as a 32 pixel integer with 8
181      *            bits of alpha, red, green and blue
182      * @param initialCrop If not null, specifies a rectangle which contains an initial
183      *            crop to continue. This can be used to crop an image where you already
184      *            know about margins in the image
185      * @param imageType the type of {@link BufferedImage} to create
186      * @return a cropped version of the source image, or null if the whole image was blank
187      *         and cropping completely removed everything
188      */
cropColor(BufferedImage image, final int blankArgb, Rect initialCrop, int imageType)189     public static BufferedImage cropColor(BufferedImage image,
190             final int blankArgb, Rect initialCrop, int imageType) {
191         CropFilter filter = new CropFilter() {
192             @Override
193             public boolean crop(BufferedImage bufferedImage, int x, int y) {
194                 return blankArgb == bufferedImage.getRGB(x, y);
195             }
196         };
197         return crop(image, filter, initialCrop, imageType);
198     }
199 
200     /**
201      * Interface implemented by cropping functions that determine whether
202      * a pixel should be cropped or not.
203      */
204     private static interface CropFilter {
205         /**
206          * Returns true if the pixel is should be cropped.
207          *
208          * @param image the image containing the pixel in question
209          * @param x the x position of the pixel
210          * @param y the y position of the pixel
211          * @return true if the pixel should be cropped (for example, is blank)
212          */
crop(BufferedImage image, int x, int y)213         boolean crop(BufferedImage image, int x, int y);
214     }
215 
crop(BufferedImage image, CropFilter filter, Rect initialCrop, int imageType)216     private static BufferedImage crop(BufferedImage image, CropFilter filter, Rect initialCrop,
217             int imageType) {
218         if (image == null) {
219             return null;
220         }
221 
222         // First, determine the dimensions of the real image within the image
223         int x1, y1, x2, y2;
224         if (initialCrop != null) {
225             x1 = initialCrop.x;
226             y1 = initialCrop.y;
227             x2 = initialCrop.x + initialCrop.w;
228             y2 = initialCrop.y + initialCrop.h;
229         } else {
230             x1 = 0;
231             y1 = 0;
232             x2 = image.getWidth();
233             y2 = image.getHeight();
234         }
235 
236         // Nothing left to crop
237         if (x1 == x2 || y1 == y2) {
238             return null;
239         }
240 
241         // This algorithm is a bit dumb -- it just scans along the edges looking for
242         // a pixel that shouldn't be cropped. I could maybe try to make it smarter by
243         // for example doing a binary search to quickly eliminate large empty areas to
244         // the right and bottom -- but this is slightly tricky with components like the
245         // AnalogClock where I could accidentally end up finding a blank horizontal or
246         // vertical line somewhere in the middle of the rendering of the clock, so for now
247         // we do the dumb thing -- not a big deal since we tend to crop reasonably
248         // small images.
249 
250         // First determine top edge
251         topEdge: for (; y1 < y2; y1++) {
252             for (int x = x1; x < x2; x++) {
253                 if (!filter.crop(image, x, y1)) {
254                     break topEdge;
255                 }
256             }
257         }
258 
259         if (y1 == image.getHeight()) {
260             // The image is blank
261             return null;
262         }
263 
264         // Next determine left edge
265         leftEdge: for (; x1 < x2; x1++) {
266             for (int y = y1; y < y2; y++) {
267                 if (!filter.crop(image, x1, y)) {
268                     break leftEdge;
269                 }
270             }
271         }
272 
273         // Next determine right edge
274         rightEdge: for (; x2 > x1; x2--) {
275             for (int y = y1; y < y2; y++) {
276                 if (!filter.crop(image, x2 - 1, y)) {
277                     break rightEdge;
278                 }
279             }
280         }
281 
282         // Finally determine bottom edge
283         bottomEdge: for (; y2 > y1; y2--) {
284             for (int x = x1; x < x2; x++) {
285                 if (!filter.crop(image, x, y2 - 1)) {
286                     break bottomEdge;
287                 }
288             }
289         }
290 
291         // No need to crop?
292         if (x1 == 0 && y1 == 0 && x2 == image.getWidth() && y2 == image.getHeight()) {
293             return image;
294         }
295 
296         if (x1 == x2 || y1 == y2) {
297             // Nothing left after crop -- blank image
298             return null;
299         }
300 
301         int width = x2 - x1;
302         int height = y2 - y1;
303 
304         // Now extract the sub-image
305         BufferedImage cropped = new BufferedImage(width, height,
306                 imageType != -1 ? imageType : image.getType());
307         Graphics g = cropped.getGraphics();
308         g.drawImage(image, 0, 0, width, height, x1, y1, x2, y2, null);
309 
310         g.dispose();
311 
312         return cropped;
313     }
314 
315     /**
316      * Creates a drop shadow of a given image and returns a new image which shows the
317      * input image on top of its drop shadow.
318      *
319      * @param source the source image to be shadowed
320      * @param shadowSize the size of the shadow in pixels
321      * @param shadowOpacity the opacity of the shadow, with 0=transparent and 1=opaque
322      * @param shadowRgb the RGB int to use for the shadow color
323      * @return a new image with the source image on top of its shadow
324      */
createDropShadow(BufferedImage source, int shadowSize, float shadowOpacity, int shadowRgb)325     public static BufferedImage createDropShadow(BufferedImage source, int shadowSize,
326             float shadowOpacity, int shadowRgb) {
327 
328         // This code is based on
329         //      http://www.jroller.com/gfx/entry/non_rectangular_shadow
330 
331         BufferedImage image = new BufferedImage(source.getWidth() + shadowSize * 2,
332                 source.getHeight() + shadowSize * 2,
333                 BufferedImage.TYPE_INT_ARGB);
334 
335         Graphics2D g2 = image.createGraphics();
336         g2.drawImage(source, null, shadowSize, shadowSize);
337 
338         int dstWidth = image.getWidth();
339         int dstHeight = image.getHeight();
340 
341         int left = (shadowSize - 1) >> 1;
342         int right = shadowSize - left;
343         int xStart = left;
344         int xStop = dstWidth - right;
345         int yStart = left;
346         int yStop = dstHeight - right;
347 
348         shadowRgb = shadowRgb & 0x00FFFFFF;
349 
350         int[] aHistory = new int[shadowSize];
351         int historyIdx = 0;
352 
353         int aSum;
354 
355         int[] dataBuffer = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
356         int lastPixelOffset = right * dstWidth;
357         float sumDivider = shadowOpacity / shadowSize;
358 
359         // horizontal pass
360         for (int y = 0, bufferOffset = 0; y < dstHeight; y++, bufferOffset = y * dstWidth) {
361             aSum = 0;
362             historyIdx = 0;
363             for (int x = 0; x < shadowSize; x++, bufferOffset++) {
364                 int a = dataBuffer[bufferOffset] >>> 24;
365                 aHistory[x] = a;
366                 aSum += a;
367             }
368 
369             bufferOffset -= right;
370 
371             for (int x = xStart; x < xStop; x++, bufferOffset++) {
372                 int a = (int) (aSum * sumDivider);
373                 dataBuffer[bufferOffset] = a << 24 | shadowRgb;
374 
375                 // subtract the oldest pixel from the sum
376                 aSum -= aHistory[historyIdx];
377 
378                 // get the latest pixel
379                 a = dataBuffer[bufferOffset + right] >>> 24;
380                 aHistory[historyIdx] = a;
381                 aSum += a;
382 
383                 if (++historyIdx >= shadowSize) {
384                     historyIdx -= shadowSize;
385                 }
386             }
387         }
388         // vertical pass
389         for (int x = 0, bufferOffset = 0; x < dstWidth; x++, bufferOffset = x) {
390             aSum = 0;
391             historyIdx = 0;
392             for (int y = 0; y < shadowSize; y++, bufferOffset += dstWidth) {
393                 int a = dataBuffer[bufferOffset] >>> 24;
394                 aHistory[y] = a;
395                 aSum += a;
396             }
397 
398             bufferOffset -= lastPixelOffset;
399 
400             for (int y = yStart; y < yStop; y++, bufferOffset += dstWidth) {
401                 int a = (int) (aSum * sumDivider);
402                 dataBuffer[bufferOffset] = a << 24 | shadowRgb;
403 
404                 // subtract the oldest pixel from the sum
405                 aSum -= aHistory[historyIdx];
406 
407                 // get the latest pixel
408                 a = dataBuffer[bufferOffset + lastPixelOffset] >>> 24;
409                 aHistory[historyIdx] = a;
410                 aSum += a;
411 
412                 if (++historyIdx >= shadowSize) {
413                     historyIdx -= shadowSize;
414                 }
415             }
416         }
417 
418         g2.drawImage(source, null, 0, 0);
419         g2.dispose();
420 
421         return image;
422     }
423 
424     /**
425      * Returns a bounding rectangle for the given list of rectangles. If the list is
426      * empty, the bounding rectangle is null.
427      *
428      * @param items the list of rectangles to compute a bounding rectangle for (may not be
429      *            null)
430      * @return a bounding rectangle of the passed in rectangles, or null if the list is
431      *         empty
432      */
getBoundingRectangle(List<Rectangle> items)433     public static Rectangle getBoundingRectangle(List<Rectangle> items) {
434         Iterator<Rectangle> iterator = items.iterator();
435         if (!iterator.hasNext()) {
436             return null;
437         }
438 
439         Rectangle bounds = iterator.next();
440         Rectangle union = new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height);
441         while (iterator.hasNext()) {
442             union.add(iterator.next());
443         }
444 
445         return union;
446     }
447 
448     /**
449      * Returns a new image which contains of the sub image given by the rectangle (x1,y1)
450      * to (x2,y2)
451      *
452      * @param source the source image
453      * @param x1 top left X coordinate
454      * @param y1 top left Y coordinate
455      * @param x2 bottom right X coordinate
456      * @param y2 bottom right Y coordinate
457      * @return a new image containing the pixels in the given range
458      */
subImage(BufferedImage source, int x1, int y1, int x2, int y2)459     public static BufferedImage subImage(BufferedImage source, int x1, int y1, int x2, int y2) {
460         int width = x2 - x1;
461         int height = y2 - y1;
462         BufferedImage sub = new BufferedImage(width, height, source.getType());
463         Graphics g = sub.getGraphics();
464         g.drawImage(source, 0, 0, width, height, x1, y1, x2, y2, null);
465         g.dispose();
466 
467         return sub;
468     }
469 
470     /**
471      * Returns the color value represented by the given string value
472      * @param value the color value
473      * @return the color as an int
474      * @throw NumberFormatException if the conversion failed.
475      */
getColor(String value)476     public static int getColor(String value) {
477         // Copied from ResourceHelper in layoutlib
478         if (value != null) {
479             if (value.startsWith("#") == false) { //$NON-NLS-1$
480                 throw new NumberFormatException(
481                         String.format("Color value '%s' must start with #", value));
482             }
483 
484             value = value.substring(1);
485 
486             // make sure it's not longer than 32bit
487             if (value.length() > 8) {
488                 throw new NumberFormatException(String.format(
489                         "Color value '%s' is too long. Format is either" +
490                         "#AARRGGBB, #RRGGBB, #RGB, or #ARGB",
491                         value));
492             }
493 
494             if (value.length() == 3) { // RGB format
495                 char[] color = new char[8];
496                 color[0] = color[1] = 'F';
497                 color[2] = color[3] = value.charAt(0);
498                 color[4] = color[5] = value.charAt(1);
499                 color[6] = color[7] = value.charAt(2);
500                 value = new String(color);
501             } else if (value.length() == 4) { // ARGB format
502                 char[] color = new char[8];
503                 color[0] = color[1] = value.charAt(0);
504                 color[2] = color[3] = value.charAt(1);
505                 color[4] = color[5] = value.charAt(2);
506                 color[6] = color[7] = value.charAt(3);
507                 value = new String(color);
508             } else if (value.length() == 6) {
509                 value = "FF" + value; //$NON-NLS-1$
510             }
511 
512             // this is a RRGGBB or AARRGGBB value
513 
514             // Integer.parseInt will fail to parse strings like "ff191919", so we use
515             // a Long, but cast the result back into an int, since we know that we're only
516             // dealing with 32 bit values.
517             return (int)Long.parseLong(value, 16);
518         }
519 
520         throw new NumberFormatException();
521     }
522 
523     /**
524      * Resize the given image
525      *
526      * @param source the image to be scaled
527      * @param xScale x scale
528      * @param yScale y scale
529      * @return the scaled image
530      */
scale(BufferedImage source, double xScale, double yScale)531     public static BufferedImage scale(BufferedImage source, double xScale, double yScale) {
532         int sourceWidth = source.getWidth();
533         int sourceHeight = source.getHeight();
534         int destWidth = Math.max(1, (int) (xScale * sourceWidth));
535         int destHeight = Math.max(1, (int) (yScale * sourceHeight));
536         BufferedImage scaled = new BufferedImage(destWidth, destHeight, source.getType());
537         Graphics2D g2 = scaled.createGraphics();
538         g2.setComposite(AlphaComposite.Src);
539         g2.setColor(new Color(0, true));
540         g2.fillRect(0, 0, destWidth, destHeight);
541         g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
542                 RenderingHints.VALUE_INTERPOLATION_BILINEAR);
543         g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
544         g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
545         g2.drawImage(source, 0, 0, destWidth, destHeight, 0, 0, sourceWidth, sourceHeight, null);
546         g2.dispose();
547 
548         return scaled;
549     }
550 
551     /**
552      * Returns true if the given file path points to an image file recognized by
553      * Android. See http://developer.android.com/guide/appendix/media-formats.html
554      * for details.
555      *
556      * @param path the filename to be tested
557      * @return true if the file represents an image file
558      */
hasImageExtension(String path)559     public static boolean hasImageExtension(String path) {
560         return endsWithIgnoreCase(path, DOT_PNG)
561             || endsWithIgnoreCase(path, DOT_9PNG)
562             || endsWithIgnoreCase(path, DOT_GIF)
563             || endsWithIgnoreCase(path, DOT_JPG)
564             || endsWithIgnoreCase(path, DOT_BMP);
565     }
566 
567     /**
568      * Creates a new image of the given size filled with the given color
569      *
570      * @param width the width of the image
571      * @param height the height of the image
572      * @param color the color of the image
573      * @return a new image of the given size filled with the given color
574      */
createColoredImage(int width, int height, RGB color)575     public static BufferedImage createColoredImage(int width, int height, RGB color) {
576         BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
577         Graphics g = image.getGraphics();
578         g.setColor(new Color(color.red, color.green, color.blue));
579         g.fillRect(0, 0, image.getWidth(), image.getHeight());
580         g.dispose();
581         return image;
582     }
583 }
584