Home | History | Annotate | Download | only in assetstudiolib
      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