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      */
containsDarkPixels(BufferedImage image)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      */
getBrightness(int rgb)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      */
intToRgb(int rgb)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      */
rgbToInt(RGB rgb, int alpha)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
cropBlank( @onNull BufferedImage image, @Nullable Rect initialCrop)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      */
cropBlank(BufferedImage image, Rect initialCrop, int imageType)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
cropColor( @onNull BufferedImage image, final int blankArgb, @Nullable Rect initialCrop)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      */
cropColor(BufferedImage image, final int blankArgb, Rect initialCrop, int imageType)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          */
crop(BufferedImage image, int x, int y)231         boolean crop(BufferedImage image, int x, int y);
232     }
233 
crop(BufferedImage image, CropFilter filter, Rect initialCrop, int imageType)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      */
createDropShadow(BufferedImage source, int shadowSize, float shadowOpacity, int shadowRgb)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      */
createRectangularDropShadow(BufferedImage source)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      */
drawRectangleShadow(BufferedImage image, int x, int y, int width, int height)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      */
drawSmallRectangleShadow(BufferedImage image, int x, int y, int width, int height)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      */
drawRectangleShadow(Graphics gc, int x, int y, int width, int height)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      */
drawSmallRectangleShadow(Graphics gc, int x, int y, int width, int height)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
readImage(@onNull String name)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      */
getBoundingRectangle(List<Rectangle> items)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      */
subImage(BufferedImage source, int x1, int y1, int x2, int y2)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      */
getColor(String value)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      */
scale(BufferedImage source, double xScale, double yScale)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      */
scale(BufferedImage source, double xScale, double yScale, int rightMargin, int bottomMargin)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      */
hasImageExtension(String path)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      */
createColoredImage(int width, int height, RGB color)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