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