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