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