• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
scaleRectangle(Rectangle rect, float scaleFactor)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      */
newArgbBufferedImage(int width, int height)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      */
scaledImage(BufferedImage source, int width, int height)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      */
blurredImage(BufferedImage source, double radius)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      */
invertedAlphaImage(BufferedImage source)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      */
operatedImage(BufferedImage source, BufferedImageOp op)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      */
filledImage(BufferedImage source, Paint paint)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      */
paddedImage(BufferedImage source, int padding)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      */
trimmedImage(BufferedImage source)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      */
drawEffects(Graphics2D g, BufferedImage source, int x, int y, Effect[] effects)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      */
drawCenterInside(Graphics2D g, BufferedImage source, Rectangle dstRect)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      */
drawCenterCrop(Graphics2D g, BufferedImage source, Rectangle dstRect)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 
ShadowEffect(double xOffset, double yOffset, double radius, Color color, double opacity, boolean inner)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 
FillEffect(Paint paint, double opacity)441         public FillEffect(Paint paint, double opacity) {
442             this.paint = paint;
443             this.opacity = opacity;
444         }
445 
FillEffect(Paint paint)446         public FillEffect(Paint paint) {
447             this.paint = paint;
448             this.opacity = 1.0;
449         }
450     }
451 }
452