Files
narya/src/java/com/threerings/media/image/ImageUtil.java
T
Walter Korman 275d35ce07 Added traceImage(), which will doubtless be further optimized, and it
would sure be nice to have a variant that traces the supplied image rather
than always creating a new one, and more flexibility with respect to the
alpha gradient steps might be nice, but all of that sort of thing will
just have to come later.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@1950 542714f4-19e9-0310-aa3c-eee0fc999fb1
2002-11-15 09:29:40 +00:00

548 lines
19 KiB
Java

//
// $Id: ImageUtil.java,v 1.18 2002/11/15 09:29:40 shaper Exp $
package com.threerings.media.util;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Graphics;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.util.Arrays;
import com.samskivert.util.StringUtil;
import com.threerings.media.Log;
/**
* Image related utility functions.
*/
public class ImageUtil
{
/**
* Extracts a subimage from the supplied image with the specified
* dimensions. If the supplied image is an instance of {@link
* BufferedImage}, then the subimage will simply reference the main
* image. If it is not, the subimage will be created and the data will
* be rendered into the newly created image.
*
* @param source the source image.
* @param x the left coordinate of the sub-image.
* @param y the top coordinate of the sub-image.
* @param width the sub-image width.
* @param height the sub-image height.
*
* @return the desired subimage.
*/
public static Image getSubimage (
Image source, int x, int y, int width, int height)
{
if (source instanceof BufferedImage) {
return ((BufferedImage)source).getSubimage(x, y, width, height);
} else {
BufferedImage target = createImage(width, height);
Graphics g = target.getGraphics();
g.drawImage(source, 0, 0, width, height,
x, y, x+width, y+height, null);
g.dispose();
return target;
}
}
/**
* Creates a new blank image with the given dimensions and
* transparency set to {@link Transparency#BITMASK}. The format of
* the created image is compatible with the graphics configuration of
* the default screen device, such that no format conversion will be
* necessary when rendering the image to that device.
*
* @param width the desired image width.
* @param height the desired image height.
*
* @return the blank image.
*/
public static BufferedImage createImage (int width, int height)
{
return createImage(width, height, Transparency.BITMASK);
}
/**
* Creates a new blank image with the given dimensions and
* transparency. The format of the created image is compatible with
* the graphics configuration of the default screen device, such that
* no format conversion will be necessary when rendering the image to
* that device.
*
* @param width the desired image width.
* @param height the desired image height.
* @param transparency the desired image transparency; one of the
* constants in {@link java.awt.Transparency}.
*
* @return the blank image.
*/
public static BufferedImage createImage (
int width, int height, int transparency)
{
return getDefGC().createCompatibleImage(width, height, transparency);
}
/**
* Creates a new buffered image with the same sample model and color
* model as the source image but with the new width and height.
*/
public static BufferedImage createCompatibleImage (
BufferedImage source, int width, int height)
{
WritableRaster raster =
source.getRaster().createCompatibleWritableRaster(width, height);
return new BufferedImage(source.getColorModel(), raster, false, null);
}
/**
* Used to recolor images by shifting bands of color (in HSV color
* space) to a new hue. The source images must be 8-bit color mapped
* images, as the recoloring process works by analysing the color map
* and modifying it.
*/
public static BufferedImage recolorImage (
BufferedImage image, Color rootColor, float[] dists, float[] offsets)
{
return recolorImage(image, new Colorization[] {
new Colorization(-1, rootColor, dists, offsets) });
}
/**
* Recolors the supplied image as in {@link
* #recolorImage(BufferedImage,Color,float[],float[])} obtaining the
* recoloring parameters from the supplied {@link Colorization}
* instance.
*/
public static BufferedImage recolorImage (
BufferedImage image, Colorization cz)
{
return recolorImage(image, new Colorization[] { cz });
}
/**
* Recolors the supplied image using the supplied colorizations.
*/
public static BufferedImage recolorImage (
BufferedImage image, Colorization[] zations)
{
ColorModel cm = image.getColorModel();
if (!(cm instanceof IndexColorModel)) {
String errmsg = "Unable to recolor images with non-index color " +
"model [cm=" + cm.getClass() + "]";
throw new RuntimeException(errmsg);
}
// now process the image
IndexColorModel icm = (IndexColorModel)cm;
int size = icm.getMapSize();
int zcount = zations.length;
int[] rgbs = new int[size];
// fetch the color data
icm.getRGBs(rgbs);
// convert the colors to HSV
float[] hsv = new float[3];
int[] fhsv = new int[3];
int tpixel = -1;
for (int i = 0; i < size; i++) {
int value = rgbs[i];
// don't fiddle with alpha pixels
if ((value & 0xFF000000) == 0) {
tpixel = i;
continue;
}
// convert the color to HSV
int red = (value >> 16) & 0xFF;
int green = (value >> 8) & 0xFF;
int blue = (value >> 0) & 0xFF;
Color.RGBtoHSB(red, green, blue, hsv);
Colorization.toFixedHSV(hsv, fhsv);
// see if this color matches and of our colorizations and
// recolor it if it does
for (int z = 0; z < zcount; z++) {
Colorization cz = zations[z];
if (cz != null && cz.matches(hsv, fhsv)) {
// massage the HSV bands and update the RGBs array
rgbs[i] = cz.recolorColor(hsv);
break;
}
}
}
// create a new image with the adjusted color palette
IndexColorModel nicm = new IndexColorModel(
icm.getPixelSize(), size, rgbs, 0, icm.hasAlpha(),
icm.getTransparentPixel(), icm.getTransferType());
return new BufferedImage(nicm, image.getRaster(), false, null);
}
/**
* Paints multiple copies of the supplied image using the supplied
* graphics context such that the requested width is filled with the
* image.
*/
public static void tileImageAcross (Graphics g, Image image,
int x, int y, int width)
{
int iwidth = image.getWidth(null), iheight = image.getHeight(null);
int tcount = width/iwidth, extra = width % iwidth;
// draw the full copies of the image
for (int ii = 0; ii < tcount; ii++) {
g.drawImage(image, x, y, null);
x += iwidth;
}
// clip the final blit
if (extra > 0) {
Shape oclip = g.getClip();
g.clipRect(x, y, extra, iheight);
g.drawImage(image, x, y, null);
g.setClip(oclip);
}
}
/**
* Paints multiple copies of the supplied image using the supplied
* graphics context such that the requested height is filled with the
* image.
*/
public static void tileImageDown (Graphics g, Image image,
int x, int y, int height)
{
int iwidth = image.getWidth(null), iheight = image.getHeight(null);
int tcount = height/iheight, extra = height % iheight;
// draw the full copies of the image
for (int ii = 0; ii < tcount; ii++) {
g.drawImage(image, x, y, null);
y += iheight;
}
// clip the final blit
if (extra > 0) {
Shape oclip = g.getClip();
g.clipRect(x, y, iwidth, extra);
g.drawImage(image, x, y, null);
g.setClip(oclip);
}
}
/**
* Creates and returns a new image consisting of the supplied image
* traced with the given color and thickness.
*/
public static BufferedImage createTracedImage (
BufferedImage src, Color tcolor, int thickness)
{
return createTracedImage(src, tcolor, thickness, 1.0f, 1.0f);
}
/**
* Creates and returns a new image consisting of the supplied image
* traced with the given color, thickness and alpha transparency.
*/
public static BufferedImage createTracedImage (
BufferedImage src, Color tcolor, int thickness,
float startAlpha, float endAlpha)
{
Raster srcdata = src.getData();
if (srcdata.getNumBands() != 4) {
throw new IllegalArgumentException(
"Can't trace an image with no transparency " +
"[image=" + src + "].");
}
// create the destination image
int wid = src.getWidth(null), hei = src.getHeight(null);
BufferedImage dest = createImage(wid, hei, Transparency.TRANSLUCENT);
// prepare various bits of working data
int srcTrans = src.getColorModel().getTransparency();
int[] tpixel = new int[] {
tcolor.getRed(), tcolor.getGreen(), tcolor.getBlue(),
(int)(startAlpha * 255)};
int[] curpixel = new int[4];
int[] workpixel = new int[4];
WritableRaster destdata = dest.getRaster();
boolean[] traced = new boolean[wid * hei];
int stepAlpha = (thickness <= 1) ? 0 :
(int)(((startAlpha - endAlpha) * 255) / (thickness - 1));
// TODO: this could be made more efficient, e.g., if we made four
// passes through the image in a vertical scan, horizontal scan,
// and opposing diagonal scans, making sure each non-transparent
// pixel found during each scan is traced on both sides of the
// respective scan direction. for now, we just naively check all
// eight pixels surrounding each pixel in the image and fill the
// center pixel with the tracing color if it's transparent but has
// a non-transparent pixel around it.
for (int tt = 0; tt < thickness; tt++) {
if (tt > 0) {
// clear out the array of pixels traced this go-around
Arrays.fill(traced, false);
// use the destination image as our new source
srcdata = dest.getData();
// decrement the trace pixel alpha-level
tpixel[3] = Math.max(0, tpixel[3] - stepAlpha);
}
for (int yy = 0; yy < hei; yy++) {
for (int xx = 0; xx < wid; xx++) {
// get the pixel we're checking
srcdata.getPixel(xx, yy, curpixel);
if (!isTransparentPixel(curpixel)) {
// copy any pixel that isn't transparent
if (tt == 0 && srcTrans == Transparency.BITMASK) {
// give any non-transparent pixel full opacity
curpixel[3] = 255;
}
destdata.setPixel(xx, yy, curpixel);
} else if (bordersNonTransparentPixel(
srcdata, wid, hei, traced,
xx, yy, workpixel)) {
destdata.setPixel(xx, yy, tpixel);
// note that we traced this pixel this pass so
// that it doesn't impact other-pixel borderedness
traced[(yy*wid)+xx] = true;
}
}
}
}
return dest;
}
/**
* Returns whether the given pixel is bordered by any non-transparent
* pixel.
*/
protected static boolean bordersNonTransparentPixel (
Raster data, int wid, int hei, boolean[] traced,
int x, int y, int[] workpixel)
{
// check the three-pixel row above the pixel
if (y > 0) {
for (int rxx = x - 1; rxx <= x + 1; rxx++) {
if (rxx < 0 || rxx >= wid || traced[((y-1)*wid)+rxx]) {
continue;
}
data.getPixel(rxx, y - 1, workpixel);
if (!isTransparentPixel(workpixel)) {
return true;
}
}
}
// check the pixel to the left
if (x > 0 && !traced[(y*wid)+(x-1)]) {
data.getPixel(x - 1, y, workpixel);
if (!isTransparentPixel(workpixel)) {
return true;
}
}
// check the pixel to the right
if (x < wid - 1 && !traced[(y*wid)+(x+1)]) {
data.getPixel(x + 1, y, workpixel);
if (!isTransparentPixel(workpixel)) {
return true;
}
}
// check the three-pixel row below the pixel
if (y < hei - 1) {
for (int rxx = x - 1; rxx <= x + 1; rxx++) {
if (rxx < 0 || rxx >= wid || traced[((y+1)*wid)+rxx]) {
continue;
}
data.getPixel(rxx, y + 1, workpixel);
if (!isTransparentPixel(workpixel)) {
return true;
}
}
}
return false;
}
/**
* Returns whether the given pixel is completely transparent.
*/
protected static boolean isTransparentPixel (int[] pixel)
{
return (pixel[3] == 0);
}
/**
* Create an image using the alpha channel from the first and the RGB
* values from the second.
*/
public static BufferedImage composeMaskedImage (
BufferedImage mask, BufferedImage base)
{
int wid = base.getWidth(null);
int hei = base.getHeight(null);
Raster maskdata = mask.getData();
Raster basedata = base.getData();
// create a new image using the rasters if possible
if (maskdata.getNumBands() == 4 && basedata.getNumBands() >= 3) {
WritableRaster target =
basedata.createCompatibleWritableRaster(wid, hei);
// copy the alpha from the mask image
int[] adata = maskdata.getSamples(0, 0, wid, hei, 3, (int[]) null);
target.setSamples(0, 0, wid, hei, 3, adata);
// copy the RGB from the base image
for (int ii=0; ii < 3; ii++) {
int[] cdata = basedata.getSamples(0, 0, wid, hei,
ii, (int[]) null);
target.setSamples(0, 0, wid, hei, ii, cdata);
}
return new BufferedImage(mask.getColorModel(), target, true, null);
} else {
// otherwise composite them by rendering them with an alpha
// rule
BufferedImage target = createImage(wid, hei);
Graphics2D g2 = target.createGraphics();
try {
g2.drawImage(mask, 0, 0, null);
g2.setComposite(AlphaComposite.SrcIn);
g2.drawImage(base, 0, 0, null);
} finally {
g2.dispose();
}
return target;
}
}
/**
* Returns true if the supplied image contains a non-transparent pixel
* at the specified coordinates, false otherwise.
*/
public static boolean hitTest (Image image, int x, int y)
{
if (image instanceof BufferedImage) {
BufferedImage bimage = (BufferedImage)image;
int argb = bimage.getRGB(x, y);
// int alpha = argb >> 24;
// Log.info("Checking [x=" + x + ", y=" + y + ", " + alpha);
// it's only a hit if the pixel is non-transparent
return (argb >> 24) != 0;
} else {
Log.warning("Can't check for transparent pixel " +
"[image=" + image + "].");
return true;
}
}
/**
* Computes the bounds of the smallest rectangle that contains all
* non-transparent pixels of this image. This isn't extremely
* efficient, so you shouldn't be doing this anywhere exciting.
*/
public static void computeTrimmedBounds (
BufferedImage image, Rectangle tbounds)
{
// this could be more efficient, but it's run as a batch process
// and doesn't really take that long anyway
int width = image.getWidth(), height = image.getHeight();
int firstrow = -1, lastrow = -1, minx = width, maxx = 0;
for (int yy = 0; yy < height; yy++) {
int firstidx = -1, lastidx = -1;
for (int xx = 0; xx < width; xx++) {
// if this pixel is transparent, do nothing
int argb = image.getRGB(xx, yy);
if ((argb >> 24) == 0) {
continue;
}
// otherwise, if we've not seen a non-transparent pixel,
// make a note that this is the first non-transparent
// pixel in the row
if (firstidx == -1) {
firstidx = xx;
}
// keep track of the last non-transparent pixel we saw
lastidx = xx;
}
// if we saw no pixels on this row, we can bail now
if (firstidx == -1) {
continue;
}
// update our min and maxx
minx = Math.min(firstidx, minx);
maxx = Math.max(lastidx, maxx);
// otherwise keep track of the first row on which we see
// pixels and the last row on which we see pixels
if (firstrow == -1) {
firstrow = yy;
}
lastrow = yy;
}
// fill in the dimensions
tbounds.x = minx;
tbounds.y = firstrow;
tbounds.width = maxx - minx + 1;
tbounds.height = lastrow - firstrow + 1;
}
/**
* Obtains the default graphics configuration for this VM.
*/
protected static GraphicsConfiguration getDefGC ()
{
if (_gc == null) {
// obtain information on our graphics environment
GraphicsEnvironment env =
GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice gd = env.getDefaultScreenDevice();
_gc = gd.getDefaultConfiguration();
}
return _gc;
}
/** The graphics configuration for the default screen device. */
protected static GraphicsConfiguration _gc;
}