| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.assetstudiolib; |
| |
| import java.awt.AlphaComposite; |
| import java.awt.Color; |
| import java.awt.Composite; |
| import java.awt.Graphics; |
| import java.awt.Graphics2D; |
| import java.awt.Image; |
| import java.awt.Paint; |
| import java.awt.Rectangle; |
| import java.awt.image.BufferedImage; |
| import java.awt.image.BufferedImageOp; |
| import java.awt.image.ConvolveOp; |
| import java.awt.image.Kernel; |
| import java.awt.image.Raster; |
| import java.awt.image.RescaleOp; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * A set of utility classes for manipulating {@link BufferedImage} objects and drawing them to |
| * {@link Graphics2D} canvases. |
| */ |
| public class Util { |
| /** |
| * Scales the given rectangle by the given scale factor. |
| * |
| * @param rect The rectangle to scale. |
| * @param scaleFactor The factor to scale by. |
| * @return The scaled rectangle. |
| */ |
| public static Rectangle scaleRectangle(Rectangle rect, float scaleFactor) { |
| return new Rectangle( |
| (int) Math.round(rect.x * scaleFactor), |
| (int) Math.round(rect.y * scaleFactor), |
| (int) Math.round(rect.width * scaleFactor), |
| (int) Math.round(rect.height * scaleFactor)); |
| } |
| |
| /** |
| * Creates a new ARGB {@link BufferedImage} of the given width and height. |
| * |
| * @param width The width of the new image. |
| * @param height The height of the new image. |
| * @return The newly created image. |
| */ |
| public static BufferedImage newArgbBufferedImage(int width, int height) { |
| return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); |
| } |
| |
| /** |
| * Smoothly scales the given {@link BufferedImage} to the given width and height using the |
| * {@link Image#SCALE_SMOOTH} algorithm (generally bicubic resampling or bilinear filtering). |
| * |
| * @param source The source image. |
| * @param width The destination width to scale to. |
| * @param height The destination height to scale to. |
| * @return A new, scaled image. |
| */ |
| public static BufferedImage scaledImage(BufferedImage source, int width, int height) { |
| Image scaledImage = source.getScaledInstance(width, height, Image.SCALE_SMOOTH); |
| BufferedImage scaledBufImage = new BufferedImage(width, height, |
| BufferedImage.TYPE_INT_ARGB); |
| Graphics g = scaledBufImage.createGraphics(); |
| g.drawImage(scaledImage, 0, 0, null); |
| g.dispose(); |
| return scaledBufImage; |
| } |
| |
| /** |
| * Applies a gaussian blur of the given radius to the given {@link BufferedImage} using a kernel |
| * convolution. |
| * |
| * @param source The source image. |
| * @param radius The blur radius, in pixels. |
| * @return A new, blurred image, or the source image if no blur is performed. |
| */ |
| public static BufferedImage blurredImage(BufferedImage source, double radius) { |
| if (radius == 0) { |
| return source; |
| } |
| |
| final int r = (int) Math.ceil(radius); |
| final int rows = r * 2 + 1; |
| final float[] kernelData = new float[rows * rows]; |
| |
| final double sigma = radius / 3; |
| final double sigma22 = 2 * sigma * sigma; |
| final double sqrtPiSigma22 = Math.sqrt(Math.PI * sigma22); |
| final double radius2 = radius * radius; |
| |
| double total = 0; |
| int index = 0; |
| double distance2; |
| |
| int x, y; |
| for (y = -r; y <= r; y++) { |
| for (x = -r; x <= r; x++) { |
| distance2 = 1.0 * x * x + 1.0 * y * y; |
| if (distance2 > radius2) { |
| kernelData[index] = 0; |
| } else { |
| kernelData[index] = (float) (Math.exp(-distance2 / sigma22) / sqrtPiSigma22); |
| } |
| total += kernelData[index]; |
| ++index; |
| } |
| } |
| |
| for (index = 0; index < kernelData.length; index++) { |
| kernelData[index] /= total; |
| } |
| |
| // We first pad the image so the kernel can operate at the edges. |
| BufferedImage paddedSource = paddedImage(source, r); |
| BufferedImage blurredPaddedImage = operatedImage(paddedSource, new ConvolveOp( |
| new Kernel(rows, rows, kernelData), ConvolveOp.EDGE_ZERO_FILL, null)); |
| return blurredPaddedImage.getSubimage(r, r, source.getWidth(), source.getHeight()); |
| } |
| |
| /** |
| * Inverts the alpha channel of the given {@link BufferedImage}. RGB data for the inverted area |
| * are undefined, so it's generally best to fill the resulting image with a color. |
| * |
| * @param source The source image. |
| * @return A new image with an alpha channel inverted from the original. |
| */ |
| public static BufferedImage invertedAlphaImage(BufferedImage source) { |
| final float[] scaleFactors = new float[]{1, 1, 1, -1}; |
| final float[] offsets = new float[]{0, 0, 0, 255}; |
| |
| return operatedImage(source, new RescaleOp(scaleFactors, offsets, null)); |
| } |
| |
| /** |
| * Applies a {@link BufferedImageOp} on the given {@link BufferedImage}. |
| * |
| * @param source The source image. |
| * @param op The operation to perform. |
| * @return A new image with the operation performed. |
| */ |
| public static BufferedImage operatedImage(BufferedImage source, BufferedImageOp op) { |
| BufferedImage newImage = newArgbBufferedImage(source.getWidth(), source.getHeight()); |
| Graphics2D g = (Graphics2D) newImage.getGraphics(); |
| g.drawImage(source, op, 0, 0); |
| return newImage; |
| } |
| |
| /** |
| * Fills the given {@link BufferedImage} with a {@link Paint}, preserving its alpha channel. |
| * |
| * @param source The source image. |
| * @param paint The paint to fill with. |
| * @return A new, painted/filled image. |
| */ |
| public static BufferedImage filledImage(BufferedImage source, Paint paint) { |
| BufferedImage newImage = newArgbBufferedImage(source.getWidth(), source.getHeight()); |
| Graphics2D g = (Graphics2D) newImage.getGraphics(); |
| g.drawImage(source, 0, 0, null); |
| g.setComposite(AlphaComposite.SrcAtop); |
| g.setPaint(paint); |
| g.fillRect(0, 0, source.getWidth(), source.getHeight()); |
| return newImage; |
| } |
| |
| /** |
| * Pads the given {@link BufferedImage} on all sides by the given padding amount. |
| * |
| * @param source The source image. |
| * @param padding The amount to pad on all sides, in pixels. |
| * @return A new, padded image, or the source image if no padding is performed. |
| */ |
| public static BufferedImage paddedImage(BufferedImage source, int padding) { |
| if (padding == 0) { |
| return source; |
| } |
| |
| BufferedImage newImage = newArgbBufferedImage( |
| source.getWidth() + padding * 2, source.getHeight() + padding * 2); |
| Graphics2D g = (Graphics2D) newImage.getGraphics(); |
| g.drawImage(source, padding, padding, null); |
| return newImage; |
| } |
| |
| /** |
| * Trims the transparent pixels from the given {@link BufferedImage} (returns a sub-image). |
| * |
| * @param source The source image. |
| * @return A new, trimmed image, or the source image if no trim is performed. |
| */ |
| public static BufferedImage trimmedImage(BufferedImage source) { |
| final int minAlpha = 1; |
| final int srcWidth = source.getWidth(); |
| final int srcHeight = source.getHeight(); |
| Raster raster = source.getRaster(); |
| int l = srcWidth, t = srcHeight, r = 0, b = 0; |
| |
| int alpha, x, y; |
| int[] pixel = new int[4]; |
| for (y = 0; y < srcHeight; y++) { |
| for (x = 0; x < srcWidth; x++) { |
| raster.getPixel(x, y, pixel); |
| alpha = pixel[3]; |
| if (alpha >= minAlpha) { |
| l = Math.min(x, l); |
| t = Math.min(y, t); |
| r = Math.max(x, r); |
| b = Math.max(y, b); |
| } |
| } |
| } |
| |
| if (l > r || t > b) { |
| // No pixels, couldn't trim |
| return source; |
| } |
| |
| return source.getSubimage(l, t, r - l + 1, b - t + 1); |
| } |
| |
| /** |
| * Draws the given {@link BufferedImage} to the canvas, at the given coordinates, with the given |
| * {@link Effect}s applied. Note that drawn effects may be outside the bounds of the source |
| * image. |
| * |
| * @param g The destination canvas. |
| * @param source The source image. |
| * @param x The x offset at which to draw the image. |
| * @param y The y offset at which to draw the image. |
| * @param effects The list of effects to apply. |
| */ |
| public static void drawEffects(Graphics2D g, BufferedImage source, int x, int y, |
| Effect[] effects) { |
| List<ShadowEffect> shadowEffects = new ArrayList<ShadowEffect>(); |
| List<FillEffect> fillEffects = new ArrayList<FillEffect>(); |
| |
| for (Effect effect : effects) { |
| if (effect instanceof ShadowEffect) { |
| shadowEffects.add((ShadowEffect) effect); |
| } else if (effect instanceof FillEffect) { |
| fillEffects.add((FillEffect) effect); |
| } |
| } |
| |
| Composite oldComposite = g.getComposite(); |
| for (ShadowEffect effect : shadowEffects) { |
| if (effect.inner) { |
| continue; |
| } |
| |
| // Outer shadow |
| g.setComposite(AlphaComposite.getInstance( |
| AlphaComposite.SRC_OVER, (float) effect.opacity)); |
| g.drawImage( |
| filledImage( |
| blurredImage(source, effect.radius), |
| effect.color), |
| (int) effect.xOffset, (int) effect.yOffset, null); |
| } |
| g.setComposite(oldComposite); |
| |
| // Inner shadow & fill effects. |
| final Rectangle imageRect = new Rectangle(0, 0, source.getWidth(), source.getHeight()); |
| BufferedImage out = newArgbBufferedImage(imageRect.width, imageRect.height); |
| Graphics2D g2 = (Graphics2D) out.getGraphics(); |
| double fillOpacity = 1.0; |
| |
| g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f)); |
| g2.drawImage(source, 0, 0, null); |
| g2.setComposite(AlphaComposite.SrcAtop); |
| |
| // Gradient fill |
| for (FillEffect effect : fillEffects) { |
| g2.setPaint(effect.paint); |
| g2.fillRect(0, 0, imageRect.width, imageRect.height); |
| fillOpacity = Math.max(0, Math.min(1, effect.opacity)); |
| } |
| |
| // Inner shadows |
| for (ShadowEffect effect : shadowEffects) { |
| if (!effect.inner) { |
| continue; |
| } |
| |
| BufferedImage innerShadowImage = newArgbBufferedImage( |
| imageRect.width, imageRect.height); |
| Graphics2D g3 = (Graphics2D) innerShadowImage.getGraphics(); |
| g3.drawImage(source, (int) effect.xOffset, (int) effect.yOffset, null); |
| g2.setComposite(AlphaComposite.getInstance( |
| AlphaComposite.SRC_ATOP, (float) effect.opacity)); |
| g2.drawImage( |
| filledImage( |
| blurredImage(invertedAlphaImage(innerShadowImage), effect.radius), |
| effect.color), |
| 0, 0, null); |
| } |
| |
| g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) fillOpacity)); |
| g.drawImage(out, x, y, null); |
| g.setComposite(oldComposite); |
| } |
| |
| /** |
| * Draws the given {@link BufferedImage} to the canvas, centered, wholly contained within the |
| * bounds defined by the destination rectangle, and with preserved aspect ratio. |
| * |
| * @param g The destination canvas. |
| * @param source The source image. |
| * @param dstRect The destination rectangle in the destination canvas into which to draw the |
| * image. |
| */ |
| public static void drawCenterInside(Graphics2D g, BufferedImage source, Rectangle dstRect) { |
| final int srcWidth = source.getWidth(); |
| final int srcHeight = source.getHeight(); |
| if (srcWidth * 1.0 / srcHeight > dstRect.width * 1.0 / dstRect.height) { |
| final int scaledWidth = Math.max(1, dstRect.width); |
| final int scaledHeight = Math.max(1, dstRect.width * srcHeight / srcWidth); |
| Image scaledImage = scaledImage(source, scaledWidth, scaledHeight); |
| g.drawImage(scaledImage, |
| dstRect.x, |
| dstRect.y + (dstRect.height - scaledHeight) / 2, |
| dstRect.x + dstRect.width, |
| dstRect.y + (dstRect.height - scaledHeight) / 2 + scaledHeight, |
| 0, |
| 0, |
| 0 + scaledWidth, |
| 0 + scaledHeight, |
| null); |
| } else { |
| final int scaledWidth = Math.max(1, dstRect.height * srcWidth / srcHeight); |
| final int scaledHeight = Math.max(1, dstRect.height); |
| Image scaledImage = scaledImage(source, scaledWidth, scaledHeight); |
| g.drawImage(scaledImage, |
| dstRect.x + (dstRect.width - scaledWidth) / 2, |
| dstRect.y, |
| dstRect.x + (dstRect.width - scaledWidth) / 2 + scaledWidth, |
| dstRect.y + dstRect.height, |
| 0, |
| 0, |
| 0 + scaledWidth, |
| 0 + scaledHeight, |
| null); |
| } |
| } |
| |
| /** |
| * Draws the given {@link BufferedImage} to the canvas, centered and cropped to fill the |
| * bounds defined by the destination rectangle, and with preserved aspect ratio. |
| * |
| * @param g The destination canvas. |
| * @param source The source image. |
| * @param dstRect The destination rectangle in the destination canvas into which to draw the |
| * image. |
| */ |
| public static void drawCenterCrop(Graphics2D g, BufferedImage source, Rectangle dstRect) { |
| final int srcWidth = source.getWidth(); |
| final int srcHeight = source.getHeight(); |
| if (srcWidth * 1.0 / srcHeight > dstRect.width * 1.0 / dstRect.height) { |
| final int scaledWidth = dstRect.height * srcWidth / srcHeight; |
| final int scaledHeight = dstRect.height; |
| Image scaledImage = scaledImage(source, scaledWidth, scaledHeight); |
| g.drawImage(scaledImage, |
| dstRect.x, |
| dstRect.y, |
| dstRect.x + dstRect.width, |
| dstRect.y + dstRect.height, |
| 0 + (scaledWidth - dstRect.width) / 2, |
| 0, |
| 0 + (scaledWidth - dstRect.width) / 2 + dstRect.width, |
| 0 + dstRect.height, |
| null); |
| } else { |
| final int scaledWidth = dstRect.width; |
| final int scaledHeight = dstRect.width * srcHeight / srcWidth; |
| Image scaledImage = scaledImage(source, scaledWidth, scaledHeight); |
| g.drawImage(scaledImage, |
| dstRect.x, |
| dstRect.y, |
| dstRect.x + dstRect.width, |
| dstRect.y + dstRect.height, |
| 0, |
| 0 + (scaledHeight - dstRect.height) / 2, |
| 0 + dstRect.width, |
| 0 + (scaledHeight - dstRect.height) / 2 + dstRect.height, |
| null); |
| } |
| } |
| |
| /** |
| * An effect to apply in |
| * {@link Util#drawEffects(java.awt.Graphics2D, java.awt.image.BufferedImage, int, int, Util.Effect[])} |
| */ |
| public static abstract class Effect { |
| } |
| |
| /** |
| * An inner or outer shadow. |
| */ |
| public static class ShadowEffect extends Effect { |
| public double xOffset; |
| public double yOffset; |
| public double radius; |
| public Color color; |
| public double opacity; |
| public boolean inner; |
| |
| public ShadowEffect(double xOffset, double yOffset, double radius, Color color, |
| double opacity, boolean inner) { |
| this.xOffset = xOffset; |
| this.yOffset = yOffset; |
| this.radius = radius; |
| this.color = color; |
| this.opacity = opacity; |
| this.inner = inner; |
| } |
| } |
| |
| /** |
| * A fill, defined by a paint. |
| */ |
| public static class FillEffect extends Effect { |
| public Paint paint; |
| public double opacity; |
| |
| public FillEffect(Paint paint, double opacity) { |
| this.paint = paint; |
| this.opacity = opacity; |
| } |
| |
| public FillEffect(Paint paint) { |
| this.paint = paint; |
| this.opacity = 1.0; |
| } |
| } |
| } |