1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 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 17 package com.android.assetstudiolib; 18 19 import java.awt.AlphaComposite; 20 import java.awt.Color; 21 import java.awt.Composite; 22 import java.awt.Graphics; 23 import java.awt.Graphics2D; 24 import java.awt.Image; 25 import java.awt.Paint; 26 import java.awt.Rectangle; 27 import java.awt.image.BufferedImage; 28 import java.awt.image.BufferedImageOp; 29 import java.awt.image.ConvolveOp; 30 import java.awt.image.Kernel; 31 import java.awt.image.Raster; 32 import java.awt.image.RescaleOp; 33 import java.util.ArrayList; 34 import java.util.List; 35 36 /** 37 * A set of utility classes for manipulating {@link BufferedImage} objects and drawing them to 38 * {@link Graphics2D} canvases. 39 */ 40 public class Util { 41 /** 42 * Scales the given rectangle by the given scale factor. 43 * 44 * @param rect The rectangle to scale. 45 * @param scaleFactor The factor to scale by. 46 * @return The scaled rectangle. 47 */ 48 public static Rectangle scaleRectangle(Rectangle rect, float scaleFactor) { 49 return new Rectangle( 50 (int) (rect.x * scaleFactor), 51 (int) (rect.y * scaleFactor), 52 (int) (rect.width * scaleFactor), 53 (int) (rect.height * scaleFactor)); 54 } 55 56 /** 57 * Creates a new ARGB {@link BufferedImage} of the given width and height. 58 * 59 * @param width The width of the new image. 60 * @param height The height of the new image. 61 * @return The newly created image. 62 */ 63 public static BufferedImage newArgbBufferedImage(int width, int height) { 64 return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 65 } 66 67 /** 68 * Smoothly scales the given {@link BufferedImage} to the given width and height using the 69 * {@link Image#SCALE_SMOOTH} algorithm (generally bicubic resampling or bilinear filtering). 70 * 71 * @param source The source image. 72 * @param width The destination width to scale to. 73 * @param height The destination height to scale to. 74 * @return A new, scaled image. 75 */ 76 public static BufferedImage scaledImage(BufferedImage source, int width, int height) { 77 Image scaledImage = source.getScaledInstance(width, height, Image.SCALE_SMOOTH); 78 BufferedImage scaledBufImage = new BufferedImage(width, height, 79 BufferedImage.TYPE_INT_ARGB); 80 Graphics g = scaledBufImage.createGraphics(); 81 g.drawImage(scaledImage, 0, 0, null); 82 g.dispose(); 83 return scaledBufImage; 84 } 85 86 /** 87 * Applies a gaussian blur of the given radius to the given {@link BufferedImage} using a kernel 88 * convolution. 89 * 90 * @param source The source image. 91 * @param radius The blur radius, in pixels. 92 * @return A new, blurred image, or the source image if no blur is performed. 93 */ 94 public static BufferedImage blurredImage(BufferedImage source, double radius) { 95 if (radius == 0) { 96 return source; 97 } 98 99 final int r = (int) Math.ceil(radius); 100 final int rows = r * 2 + 1; 101 final float[] kernelData = new float[rows * rows]; 102 103 final double sigma = radius / 3; 104 final double sigma22 = 2 * sigma * sigma; 105 final double sqrtPiSigma22 = Math.sqrt(Math.PI * sigma22); 106 final double radius2 = radius * radius; 107 108 double total = 0; 109 int index = 0; 110 double distance2; 111 112 int x, y; 113 for (y = -r; y <= r; y++) { 114 for (x = -r; x <= r; x++) { 115 distance2 = 1.0 * x * x + 1.0 * y * y; 116 if (distance2 > radius2) { 117 kernelData[index] = 0; 118 } else { 119 kernelData[index] = (float) (Math.exp(-distance2 / sigma22) / sqrtPiSigma22); 120 } 121 total += kernelData[index]; 122 ++index; 123 } 124 } 125 126 for (index = 0; index < kernelData.length; index++) { 127 kernelData[index] /= total; 128 } 129 130 // We first pad the image so the kernel can operate at the edges. 131 BufferedImage paddedSource = paddedImage(source, r); 132 BufferedImage blurredPaddedImage = operatedImage(paddedSource, new ConvolveOp( 133 new Kernel(rows, rows, kernelData), ConvolveOp.EDGE_ZERO_FILL, null)); 134 return blurredPaddedImage.getSubimage(r, r, source.getWidth(), source.getHeight()); 135 } 136 137 /** 138 * Inverts the alpha channel of the given {@link BufferedImage}. RGB data for the inverted area 139 * are undefined, so it's generally best to fill the resulting image with a color. 140 * 141 * @param source The source image. 142 * @return A new image with an alpha channel inverted from the original. 143 */ 144 public static BufferedImage invertedAlphaImage(BufferedImage source) { 145 final float[] scaleFactors = new float[]{1, 1, 1, -1}; 146 final float[] offsets = new float[]{0, 0, 0, 255}; 147 148 return operatedImage(source, new RescaleOp(scaleFactors, offsets, null)); 149 } 150 151 /** 152 * Applies a {@link BufferedImageOp} on the given {@link BufferedImage}. 153 * 154 * @param source The source image. 155 * @param op The operation to perform. 156 * @return A new image with the operation performed. 157 */ 158 public static BufferedImage operatedImage(BufferedImage source, BufferedImageOp op) { 159 BufferedImage newImage = newArgbBufferedImage(source.getWidth(), source.getHeight()); 160 Graphics2D g = (Graphics2D) newImage.getGraphics(); 161 g.drawImage(source, op, 0, 0); 162 return newImage; 163 } 164 165 /** 166 * Fills the given {@link BufferedImage} with a {@link Paint}, preserving its alpha channel. 167 * 168 * @param source The source image. 169 * @param paint The paint to fill with. 170 * @return A new, painted/filled image. 171 */ 172 public static BufferedImage filledImage(BufferedImage source, Paint paint) { 173 BufferedImage newImage = newArgbBufferedImage(source.getWidth(), source.getHeight()); 174 Graphics2D g = (Graphics2D) newImage.getGraphics(); 175 g.drawImage(source, 0, 0, null); 176 g.setComposite(AlphaComposite.SrcAtop); 177 g.setPaint(paint); 178 g.fillRect(0, 0, source.getWidth(), source.getHeight()); 179 return newImage; 180 } 181 182 /** 183 * Pads the given {@link BufferedImage} on all sides by the given padding amount. 184 * 185 * @param source The source image. 186 * @param padding The amount to pad on all sides, in pixels. 187 * @return A new, padded image, or the source image if no padding is performed. 188 */ 189 public static BufferedImage paddedImage(BufferedImage source, int padding) { 190 if (padding == 0) { 191 return source; 192 } 193 194 BufferedImage newImage = newArgbBufferedImage( 195 source.getWidth() + padding * 2, source.getHeight() + padding * 2); 196 Graphics2D g = (Graphics2D) newImage.getGraphics(); 197 g.drawImage(source, padding, padding, null); 198 return newImage; 199 } 200 201 /** 202 * Trims the transparent pixels from the given {@link BufferedImage} (returns a sub-image). 203 * 204 * @param source The source image. 205 * @return A new, trimmed image, or the source image if no trim is performed. 206 */ 207 public static BufferedImage trimmedImage(BufferedImage source) { 208 final int minAlpha = 1; 209 final int srcWidth = source.getWidth(); 210 final int srcHeight = source.getHeight(); 211 Raster raster = source.getRaster(); 212 int l = srcWidth, t = srcHeight, r = 0, b = 0; 213 214 int alpha, x, y; 215 int[] pixel = new int[4]; 216 for (y = 0; y < srcHeight; y++) { 217 for (x = 0; x < srcWidth; x++) { 218 raster.getPixel(x, y, pixel); 219 alpha = pixel[3]; 220 if (alpha >= minAlpha) { 221 l = Math.min(x, l); 222 t = Math.min(y, t); 223 r = Math.max(x, r); 224 b = Math.max(y, b); 225 } 226 } 227 } 228 229 if (l > r || t > b) { 230 // No pixels, couldn't trim 231 return source; 232 } 233 234 return source.getSubimage(l, t, r - l + 1, b - t + 1); 235 } 236 237 /** 238 * Draws the given {@link BufferedImage} to the canvas, at the given coordinates, with the given 239 * {@link Effect}s applied. Note that drawn effects may be outside the bounds of the source 240 * image. 241 * 242 * @param g The destination canvas. 243 * @param source The source image. 244 * @param x The x offset at which to draw the image. 245 * @param y The y offset at which to draw the image. 246 * @param effects The list of effects to apply. 247 */ 248 public static void drawEffects(Graphics2D g, BufferedImage source, int x, int y, 249 Effect[] effects) { 250 List<ShadowEffect> shadowEffects = new ArrayList<ShadowEffect>(); 251 List<FillEffect> fillEffects = new ArrayList<FillEffect>(); 252 253 for (Effect effect : effects) { 254 if (effect instanceof ShadowEffect) { 255 shadowEffects.add((ShadowEffect) effect); 256 } else if (effect instanceof FillEffect) { 257 fillEffects.add((FillEffect) effect); 258 } 259 } 260 261 Composite oldComposite = g.getComposite(); 262 for (ShadowEffect effect : shadowEffects) { 263 if (effect.inner) { 264 continue; 265 } 266 267 // Outer shadow 268 g.setComposite(AlphaComposite.getInstance( 269 AlphaComposite.SRC_OVER, (float) effect.opacity)); 270 g.drawImage( 271 filledImage( 272 blurredImage(source, effect.radius), 273 effect.color), 274 (int) effect.xOffset, (int) effect.yOffset, null); 275 } 276 g.setComposite(oldComposite); 277 278 // Inner shadow & fill effects. 279 final Rectangle imageRect = new Rectangle(0, 0, source.getWidth(), source.getHeight()); 280 BufferedImage out = newArgbBufferedImage(imageRect.width, imageRect.height); 281 Graphics2D g2 = (Graphics2D) out.getGraphics(); 282 283 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f)); 284 g2.drawImage(source, 0, 0, null); 285 g2.setComposite(AlphaComposite.SrcAtop); 286 287 // Gradient fill 288 for (FillEffect effect : fillEffects) { 289 g2.setPaint(effect.paint); 290 g2.fillRect(0, 0, imageRect.width, imageRect.height); 291 } 292 293 // Inner shadows 294 for (ShadowEffect effect : shadowEffects) { 295 if (!effect.inner) { 296 continue; 297 } 298 299 BufferedImage innerShadowImage = newArgbBufferedImage( 300 imageRect.width, imageRect.height); 301 Graphics2D g3 = (Graphics2D) innerShadowImage.getGraphics(); 302 g3.drawImage(source, (int) effect.xOffset, (int) effect.yOffset, null); 303 g2.setComposite(AlphaComposite.getInstance( 304 AlphaComposite.SRC_ATOP, (float) effect.opacity)); 305 g2.drawImage( 306 filledImage( 307 blurredImage(invertedAlphaImage(innerShadowImage), effect.radius), 308 effect.color), 309 0, 0, null); 310 } 311 312 g.drawImage(out, x, y, null); 313 } 314 315 /** 316 * Draws the given {@link BufferedImage} to the canvas, centered, wholly contained within the 317 * bounds defined by the destination rectangle, and with preserved aspect ratio. 318 * 319 * @param g The destination canvas. 320 * @param source The source image. 321 * @param dstRect The destination rectangle in the destination canvas into which to draw the 322 * image. 323 */ 324 public static void drawCenterInside(Graphics2D g, BufferedImage source, Rectangle dstRect) { 325 final int srcWidth = source.getWidth(); 326 final int srcHeight = source.getHeight(); 327 if (srcWidth * 1.0 / srcHeight > dstRect.width * 1.0 / dstRect.height) { 328 final int scaledWidth = Math.max(1, dstRect.width); 329 final int scaledHeight = Math.max(1, dstRect.width * srcHeight / srcWidth); 330 Image scaledImage = scaledImage(source, scaledWidth, scaledHeight); 331 g.drawImage(scaledImage, 332 dstRect.x, 333 dstRect.y + (dstRect.height - scaledHeight) / 2, 334 dstRect.x + dstRect.width, 335 dstRect.y + (dstRect.height - scaledHeight) / 2 + scaledHeight, 336 0, 337 0, 338 0 + scaledWidth, 339 0 + scaledHeight, 340 null); 341 } else { 342 final int scaledWidth = Math.max(1, dstRect.height * srcWidth / srcHeight); 343 final int scaledHeight = Math.max(1, dstRect.height); 344 Image scaledImage = scaledImage(source, scaledWidth, scaledHeight); 345 g.drawImage(scaledImage, 346 dstRect.x + (dstRect.width - scaledWidth) / 2, 347 dstRect.y, 348 dstRect.x + (dstRect.width - scaledWidth) / 2 + scaledWidth, 349 dstRect.y + dstRect.height, 350 0, 351 0, 352 0 + scaledWidth, 353 0 + scaledHeight, 354 null); 355 } 356 } 357 358 /** 359 * Draws the given {@link BufferedImage} to the canvas, centered and cropped to fill the 360 * bounds defined by the destination rectangle, and with preserved aspect ratio. 361 * 362 * @param g The destination canvas. 363 * @param source The source image. 364 * @param dstRect The destination rectangle in the destination canvas into which to draw the 365 * image. 366 */ 367 public static void drawCenterCrop(Graphics2D g, BufferedImage source, Rectangle dstRect) { 368 final int srcWidth = source.getWidth(); 369 final int srcHeight = source.getHeight(); 370 if (srcWidth * 1.0 / srcHeight > dstRect.width * 1.0 / dstRect.height) { 371 final int scaledWidth = dstRect.height * srcWidth / srcHeight; 372 final int scaledHeight = dstRect.height; 373 Image scaledImage = scaledImage(source, scaledWidth, scaledHeight); 374 g.drawImage(scaledImage, 375 dstRect.x, 376 dstRect.y, 377 dstRect.x + dstRect.width, 378 dstRect.y + dstRect.height, 379 0 + (scaledWidth - dstRect.width) / 2, 380 0, 381 0 + (scaledWidth - dstRect.width) / 2 + dstRect.width, 382 0 + dstRect.height, 383 null); 384 } else { 385 final int scaledWidth = dstRect.width; 386 final int scaledHeight = dstRect.width * srcHeight / srcWidth; 387 Image scaledImage = scaledImage(source, scaledWidth, scaledHeight); 388 g.drawImage(scaledImage, 389 dstRect.x, 390 dstRect.y, 391 dstRect.x + dstRect.width, 392 dstRect.y + dstRect.height, 393 0, 394 0 + (scaledHeight - dstRect.height) / 2, 395 0 + dstRect.width, 396 0 + (scaledHeight - dstRect.height) / 2 + dstRect.height, 397 null); 398 } 399 } 400 401 /** 402 * An effect to apply in 403 * {@link Util#drawEffects(java.awt.Graphics2D, java.awt.image.BufferedImage, int, int, Util.Effect[])} 404 */ 405 public static abstract class Effect { 406 } 407 408 /** 409 * An inner or outer shadow. 410 */ 411 public static class ShadowEffect extends Effect { 412 public double xOffset; 413 public double yOffset; 414 public double radius; 415 public Color color; 416 public double opacity; 417 public boolean inner; 418 419 public ShadowEffect(double xOffset, double yOffset, double radius, Color color, 420 double opacity, boolean inner) { 421 this.xOffset = xOffset; 422 this.yOffset = yOffset; 423 this.radius = radius; 424 this.color = color; 425 this.opacity = opacity; 426 this.inner = inner; 427 } 428 } 429 430 /** 431 * A fill, defined by a paint. 432 */ 433 public static class FillEffect extends Effect { 434 public Paint paint; 435 436 public FillEffect(Paint paint) { 437 this.paint = paint; 438 } 439 } 440 } 441