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) Math.round(rect.x * scaleFactor), 51 (int) Math.round(rect.y * scaleFactor), 52 (int) Math.round(rect.width * scaleFactor), 53 (int) Math.round(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 double fillOpacity = 1.0; 283 284 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f)); 285 g2.drawImage(source, 0, 0, null); 286 g2.setComposite(AlphaComposite.SrcAtop); 287 288 // Gradient fill 289 for (FillEffect effect : fillEffects) { 290 g2.setPaint(effect.paint); 291 g2.fillRect(0, 0, imageRect.width, imageRect.height); 292 fillOpacity = Math.max(0, Math.min(1, effect.opacity)); 293 } 294 295 // Inner shadows 296 for (ShadowEffect effect : shadowEffects) { 297 if (!effect.inner) { 298 continue; 299 } 300 301 BufferedImage innerShadowImage = newArgbBufferedImage( 302 imageRect.width, imageRect.height); 303 Graphics2D g3 = (Graphics2D) innerShadowImage.getGraphics(); 304 g3.drawImage(source, (int) effect.xOffset, (int) effect.yOffset, null); 305 g2.setComposite(AlphaComposite.getInstance( 306 AlphaComposite.SRC_ATOP, (float) effect.opacity)); 307 g2.drawImage( 308 filledImage( 309 blurredImage(invertedAlphaImage(innerShadowImage), effect.radius), 310 effect.color), 311 0, 0, null); 312 } 313 314 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) fillOpacity)); 315 g.drawImage(out, x, y, null); 316 g.setComposite(oldComposite); 317 } 318 319 /** 320 * Draws the given {@link BufferedImage} to the canvas, centered, wholly contained within the 321 * bounds defined by the destination rectangle, and with preserved aspect ratio. 322 * 323 * @param g The destination canvas. 324 * @param source The source image. 325 * @param dstRect The destination rectangle in the destination canvas into which to draw the 326 * image. 327 */ 328 public static void drawCenterInside(Graphics2D g, BufferedImage source, Rectangle dstRect) { 329 final int srcWidth = source.getWidth(); 330 final int srcHeight = source.getHeight(); 331 if (srcWidth * 1.0 / srcHeight > dstRect.width * 1.0 / dstRect.height) { 332 final int scaledWidth = Math.max(1, dstRect.width); 333 final int scaledHeight = Math.max(1, dstRect.width * srcHeight / srcWidth); 334 Image scaledImage = scaledImage(source, scaledWidth, scaledHeight); 335 g.drawImage(scaledImage, 336 dstRect.x, 337 dstRect.y + (dstRect.height - scaledHeight) / 2, 338 dstRect.x + dstRect.width, 339 dstRect.y + (dstRect.height - scaledHeight) / 2 + scaledHeight, 340 0, 341 0, 342 0 + scaledWidth, 343 0 + scaledHeight, 344 null); 345 } else { 346 final int scaledWidth = Math.max(1, dstRect.height * srcWidth / srcHeight); 347 final int scaledHeight = Math.max(1, dstRect.height); 348 Image scaledImage = scaledImage(source, scaledWidth, scaledHeight); 349 g.drawImage(scaledImage, 350 dstRect.x + (dstRect.width - scaledWidth) / 2, 351 dstRect.y, 352 dstRect.x + (dstRect.width - scaledWidth) / 2 + scaledWidth, 353 dstRect.y + dstRect.height, 354 0, 355 0, 356 0 + scaledWidth, 357 0 + scaledHeight, 358 null); 359 } 360 } 361 362 /** 363 * Draws the given {@link BufferedImage} to the canvas, centered and cropped to fill the 364 * bounds defined by the destination rectangle, and with preserved aspect ratio. 365 * 366 * @param g The destination canvas. 367 * @param source The source image. 368 * @param dstRect The destination rectangle in the destination canvas into which to draw the 369 * image. 370 */ 371 public static void drawCenterCrop(Graphics2D g, BufferedImage source, Rectangle dstRect) { 372 final int srcWidth = source.getWidth(); 373 final int srcHeight = source.getHeight(); 374 if (srcWidth * 1.0 / srcHeight > dstRect.width * 1.0 / dstRect.height) { 375 final int scaledWidth = dstRect.height * srcWidth / srcHeight; 376 final int scaledHeight = dstRect.height; 377 Image scaledImage = scaledImage(source, scaledWidth, scaledHeight); 378 g.drawImage(scaledImage, 379 dstRect.x, 380 dstRect.y, 381 dstRect.x + dstRect.width, 382 dstRect.y + dstRect.height, 383 0 + (scaledWidth - dstRect.width) / 2, 384 0, 385 0 + (scaledWidth - dstRect.width) / 2 + dstRect.width, 386 0 + dstRect.height, 387 null); 388 } else { 389 final int scaledWidth = dstRect.width; 390 final int scaledHeight = dstRect.width * srcHeight / srcWidth; 391 Image scaledImage = scaledImage(source, scaledWidth, scaledHeight); 392 g.drawImage(scaledImage, 393 dstRect.x, 394 dstRect.y, 395 dstRect.x + dstRect.width, 396 dstRect.y + dstRect.height, 397 0, 398 0 + (scaledHeight - dstRect.height) / 2, 399 0 + dstRect.width, 400 0 + (scaledHeight - dstRect.height) / 2 + dstRect.height, 401 null); 402 } 403 } 404 405 /** 406 * An effect to apply in 407 * {@link Util#drawEffects(java.awt.Graphics2D, java.awt.image.BufferedImage, int, int, Util.Effect[])} 408 */ 409 public static abstract class Effect { 410 } 411 412 /** 413 * An inner or outer shadow. 414 */ 415 public static class ShadowEffect extends Effect { 416 public double xOffset; 417 public double yOffset; 418 public double radius; 419 public Color color; 420 public double opacity; 421 public boolean inner; 422 423 public ShadowEffect(double xOffset, double yOffset, double radius, Color color, 424 double opacity, boolean inner) { 425 this.xOffset = xOffset; 426 this.yOffset = yOffset; 427 this.radius = radius; 428 this.color = color; 429 this.opacity = opacity; 430 this.inner = inner; 431 } 432 } 433 434 /** 435 * A fill, defined by a paint. 436 */ 437 public static class FillEffect extends Effect { 438 public Paint paint; 439 public double opacity; 440 441 public FillEffect(Paint paint, double opacity) { 442 this.paint = paint; 443 this.opacity = opacity; 444 } 445 446 public FillEffect(Paint paint) { 447 this.paint = paint; 448 this.opacity = 1.0; 449 } 450 } 451 } 452