Files
narya/src/java/com/threerings/media/image/ImageUtil.java
T
Michael Bayne 78f6342dbd Revamped the recoloration code such that it is much more efficient.
git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@1108 542714f4-19e9-0310-aa3c-eee0fc999fb1
2002-03-08 21:05:01 +00:00

226 lines
7.5 KiB
Java

//
// $Id: ImageUtil.java,v 1.7 2002/03/08 21:05:01 mdb Exp $
package com.threerings.media.util;
import java.awt.Color;
import java.awt.Image;
import java.awt.Graphics;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
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 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 _gc.createCompatibleImage(width, height, transparency);
}
/**
* 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)
{
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);
}
// first convert the root color to HSV for later comparison
float[] rHSV = Color.RGBtoHSB(rootColor.getRed(), rootColor.getGreen(),
rootColor.getBlue(), null);
int[] frHSV = toFixedHSV(rHSV, null);
int[] rgb = new int[3];
// now process the image
IndexColorModel icm = (IndexColorModel)cm;
int size = icm.getMapSize();
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);
// check to see that this color is sufficiently "close" to the
// root color based on the supplied distance parameters
toFixedHSV(hsv, fhsv);
if (distance(fhsv[0], frHSV[0], Short.MAX_VALUE) >=
dists[0] * Short.MAX_VALUE) {
continue;
}
// saturation and value don't wrap around like hue
if (Math.abs(rHSV[1] - hsv[1]) >= dists[1] ||
Math.abs(rHSV[2] - hsv[2]) >= dists[2]) {
continue;
}
// massage the HSV bands and update the RGBs array
rgbs[i] = recolorColor(hsv, offsets);
}
// 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);
}
/**
* Adjusts the supplied color by the specified offests, taking the
* appropriate measures for hue (wrapping it around) and saturation
* and value (clipping).
*
* @return the RGB value of the recolored color.
*/
public static int recolorColor (float[] hsv, float[] offsets)
{
// for hue, we wrap around
hsv[0] += offsets[0];
if (hsv[0] > 1.0) {
hsv[0] -= 1.0;
}
// otherwise we clip
hsv[1] = Math.min(Math.max(hsv[1] + offsets[1], 0), 1);
hsv[2] = Math.min(Math.max(hsv[2] + offsets[2], 0), 1);
// convert back to RGB space
return Color.HSBtoRGB(hsv[0], hsv[1], hsv[2]);
}
/**
* Converts floating point HSV values to a fixed point integer
* representation.
*/
protected static int[] toFixedHSV (float[] hsv, int[] fhsv)
{
if (fhsv == null) {
fhsv = new int[hsv.length];
}
for (int i = 0; i < hsv.length; i++) {
// fhsv[i] = (int)(hsv[i]*Integer.MAX_VALUE);
fhsv[i] = (int)(hsv[i]*Short.MAX_VALUE);
}
return fhsv;
}
/**
* Returns the distance between the supplied to numbers modulo N.
*/
protected static int distance (int a, int b, int N)
{
return (a > b) ? Math.min(a-b, b+N-a) : Math.min(b-a, a+N-b);
}
/** The graphics configuration for the default screen device. */
protected static GraphicsConfiguration _gc;
static {
// obtain information on our graphics environment
GraphicsEnvironment env =
GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice gd = env.getDefaultScreenDevice();
_gc = gd.getDefaultConfiguration();
};
}