
com.threerings.media.image.ImageUtil Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of nenya Show documentation
Show all versions of nenya Show documentation
Facilities for making networked multiplayer games.
The newest version!
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// https://github.com/threerings/nenya
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image;
import java.util.Arrays;
import java.util.Iterator;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.HeadlessException;
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 com.samskivert.util.Logger;
import com.samskivert.swing.Label;
/**
* Image related utility functions.
*/
public class ImageUtil
{
public static interface ImageCreator
{
/** Used by routines that need to create new images to allow the caller to dictate the
* format (which may mean using createCompatibleImage). */
public BufferedImage createImage (int width, int height, int 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);
}
/**
* Creates an image with the word "Error" written in it.
*/
public static BufferedImage createErrorImage (int width, int height)
{
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED);
Graphics2D g = (Graphics2D)img.getGraphics();
g.setColor(Color.red);
Label l = new Label("Error");
l.layout(g);
Dimension d = l.getSize();
// fill that sucker with errors
for (int yy = 0; yy < height; yy += d.height) {
for (int xx = 0; xx < width; xx += (d.width+5)) {
l.render(g, xx, yy);
}
}
g.dispose();
return img;
}
/**
* 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)) {
throw new RuntimeException(Logger.format(
"Unable to recolor images with non-index color model", "cm", cm.getClass()));
}
// 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];
for (int ii = 0; ii < size; ii++) {
int value = rgbs[ii];
// don't fiddle with alpha pixels
if ((value & 0xFF000000) == 0) {
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[ii] = 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 area is filled with the image.
*/
public static void tileImage (Graphics2D gfx, Mirage image, int x, int y, int width, int height)
{
int iwidth = image.getWidth(), iheight = image.getHeight();
int xnum = width / iwidth, xplus = width % iwidth;
int ynum = height / iheight, yplus = height % iheight;
Shape oclip = gfx.getClip();
for (int ii=0; ii < ynum; ii++) {
// draw the full copies of the image across
int xx = x;
for (int jj=0; jj < xnum; jj++) {
image.paint(gfx, xx, y);
xx += iwidth;
}
if (xplus > 0) {
gfx.clipRect(xx, y, xplus, iheight);
image.paint(gfx, xx, y);
gfx.setClip(oclip);
}
y += iheight;
}
if (yplus > 0) {
int xx = x;
for (int jj=0; jj < xnum; jj++) {
gfx.clipRect(xx, y, iwidth, yplus);
image.paint(gfx, xx, y);
gfx.setClip(oclip);
xx += iwidth;
}
if (xplus > 0) {
gfx.clipRect(xx, y, xplus, yplus);
image.paint(gfx, xx, y);
gfx.setClip(oclip);
}
}
}
/**
* 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 (Graphics2D gfx, Mirage image, int x, int y, int width)
{
tileImage(gfx, image, x, y, width, image.getHeight());
}
/**
* 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 (Graphics2D gfx, Mirage image, int x, int y, int height)
{
tileImage(gfx, image, x, y, image.getWidth(), height);
}
// Not fully added because we're not using it anywhere, plus it's probably a little sketchy
// to create Area objects with all this pixely data.
// Also, the Area was getting zeroed out when it was translated. Something to look into someday
// if anyone wants to use this method.
// /**
// * Creates a mask that is opaque in the non-transparent areas of the source image.
// */
// public static Area createImageMask (BufferedImage src)
// {
// Raster srcdata = src.getData();
// int wid = src.getWidth(), hei = src.getHeight();
// Log.info("creating area of (" + wid + ", " + hei + ")");
// Area a = new Area(new Rectangle(wid, hei));
// Rectangle r = new Rectangle(1, 1);
//
// for (int yy=0; yy < hei; yy++) {
// for (int xx=0; xx < wid; xx++) {
// if (srcdata.getSample(xx, yy, 0) == 0) {
// r.setLocation(xx, yy);
// a.subtract(new Area(r));
// }
// }
// }
//
// return a;
// }
/**
* Creates and returns a new image consisting of the supplied image traced with the given
* color and thickness.
*/
public static BufferedImage createTracedImage (
ImageCreator isrc, BufferedImage src, Color tcolor, int thickness)
{
return createTracedImage(isrc, 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 (
ImageCreator isrc, BufferedImage src, Color tcolor, int thickness,
float startAlpha, float endAlpha)
{
// create the destination image
int wid = src.getWidth(), hei = src.getHeight();
BufferedImage dest = isrc.createImage(wid, hei, Transparency.TRANSLUCENT);
return createTracedImage(src, dest, tcolor, thickness, startAlpha, endAlpha);
}
/**
* 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, BufferedImage dest, Color tcolor, int thickness,
float startAlpha, float endAlpha)
{
// prepare various bits of working data
int wid = src.getWidth(), hei = src.getHeight();
int spixel = (tcolor.getRGB() & RGB_MASK);
int salpha = (int)(startAlpha * 255);
int tpixel = (spixel | (salpha << 24));
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
src = dest;
// decrement the trace pixel alpha-level
salpha -= Math.max(0, stepAlpha);
tpixel = (spixel | (salpha << 24));
}
for (int yy = 0; yy < hei; yy++) {
for (int xx = 0; xx < wid; xx++) {
// get the pixel we're checking
int argb = src.getRGB(xx, yy);
if ((argb & TRANS_MASK) != 0) {
// copy any pixel that isn't transparent
dest.setRGB(xx, yy, argb);
} else if (bordersNonTransparentPixel(src, wid, hei, traced, xx, yy)) {
dest.setRGB(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 (
BufferedImage data, int wid, int hei, boolean[] traced, int x, int y)
{
// 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;
}
if ((data.getRGB(rxx, y - 1) & TRANS_MASK) != 0) {
return true;
}
}
}
// check the pixel to the left
if (x > 0 && !traced[(y*wid)+(x-1)]) {
if ((data.getRGB(x - 1, y) & TRANS_MASK) != 0) {
return true;
}
}
// check the pixel to the right
if (x < wid - 1 && !traced[(y*wid)+(x+1)]) {
if ((data.getRGB(x + 1, y) & TRANS_MASK) != 0) {
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;
}
if ((data.getRGB(rxx, y + 1) & TRANS_MASK) != 0) {
return true;
}
}
}
return false;
}
/**
* Create an image using the alpha channel from the first and the RGB values from the second.
*/
public static BufferedImage composeMaskedImage (
ImageCreator isrc, BufferedImage mask, BufferedImage base)
{
int wid = base.getWidth();
int hei = base.getHeight();
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 = isrc.createImage(wid, hei, Transparency.TRANSLUCENT);
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;
}
}
/**
* Create a new image using the supplied shape as a mask from which to cut out pixels from the
* supplied image. Pixels inside the shape will be added to the final image, pixels outside
* the shape will be clear.
*/
public static BufferedImage composeMaskedImage (
ImageCreator isrc, Shape mask, BufferedImage base)
{
int wid = base.getWidth();
int hei = base.getHeight();
// alternate method for composition:
// 1. create WriteableRaster with base data
// 2. test each pixel with mask.contains() and set the alpha channel to fully-alpha if false
// 3. create buffered image from raster
// (I didn't use this method because it depends on the colormodel of the source image, and
// was booching when the souce image was a cut-up from a tileset, and it seems like it
// would take longer than the method we are using. But it's something to consider)
// composite them by rendering them with an alpha rule
BufferedImage target = isrc.createImage(wid, hei, Transparency.TRANSLUCENT);
Graphics2D g2 = target.createGraphics();
try {
g2.setColor(Color.BLACK); // whatever, really
g2.fill(mask);
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 (BufferedImage image, int x, int y)
{
// it's only a hit if the pixel is non-transparent
int argb = image.getRGB(x, y);
return (argb >> 24) != 0;
}
/**
* 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
if (firstrow != -1) {
tbounds.x = minx;
tbounds.y = firstrow;
tbounds.width = maxx - minx + 1;
tbounds.height = lastrow - firstrow + 1;
} else {
// Entirely blank image. Return 1x1 blank image.
tbounds.x = 0;
tbounds.y = 0;
tbounds.width = 1;
tbounds.height = 1;
}
}
/**
* Returns the estimated memory usage in bytes for the specified image.
*/
public static long getEstimatedMemoryUsage (BufferedImage image)
{
if (image != null) {
return getEstimatedMemoryUsage(image.getRaster());
} else {
return 0;
}
}
/**
* Returns the estimated memory usage in bytes for the specified raster.
*/
public static long getEstimatedMemoryUsage (Raster raster)
{
// we assume that the data buffer stores each element in a byte-rounded memory element;
// maybe the buffer is smarter about things than this, but we're better to err on the safe
// side
DataBuffer db = raster.getDataBuffer();
int bpe = (int)Math.ceil(DataBuffer.getDataTypeSize(db.getDataType()) / 8f);
return bpe * db.getSize();
}
/**
* Returns the estimated memory usage in bytes for all buffered images in the supplied
* iterator.
*/
public static long getEstimatedMemoryUsage (Iterator iter)
{
long size = 0;
while (iter.hasNext()) {
BufferedImage image = iter.next();
size += getEstimatedMemoryUsage(image);
}
return size;
}
/**
* Obtains the default graphics configuration for this VM. If the JVM is in headless mode,
* this method will return null.
*/
protected static GraphicsConfiguration getDefGC ()
{
if (_gc == null) {
// obtain information on our graphics environment
try {
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice gd = env.getDefaultScreenDevice();
_gc = gd.getDefaultConfiguration();
} catch (HeadlessException e) {
// no problem, just return null
}
}
return _gc;
}
/** The graphics configuration for the default screen device. */
protected static GraphicsConfiguration _gc;
/** Used when seeking fully transparent pixels for outlining. */
protected static final int TRANS_MASK = (0xFF << 24);
/** Used when outlining. */
protected static final int RGB_MASK = 0x00FFFFFF;
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy