Files
narya/src/java/com/threerings/media/image/ImageManager.java
T
Michael Bayne f26cfb322d Temporary hack to make all images translucent since that works around
strange slow rendering problems we see when we allow mixed BITMASK and
TRANSLUCENT images to be rendered.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@2140 542714f4-19e9-0310-aa3c-eee0fc999fb1
2003-01-14 04:05:56 +00:00

497 lines
16 KiB
Java

//
// $Id: ImageManager.java,v 1.37 2003/01/14 04:05:56 mdb Exp $
package com.threerings.media.image;
import java.awt.Component;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import com.sun.imageio.plugins.png.PNGImageReaderSpi;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;
import com.samskivert.io.NestableIOException;
import com.samskivert.util.ConfigUtil;
import com.samskivert.util.LRUHashMap;
import com.samskivert.util.StringUtil;
import com.samskivert.util.Throttle;
import com.samskivert.util.Tuple;
import com.threerings.media.Log;
import com.threerings.resource.ResourceManager;
/**
* Provides a single point of access for image retrieval and caching.
*/
public class ImageManager
{
/**
* Used to identify an image for caching and reconstruction.
*/
public static class ImageKey
{
/** The data provider from which this image's data is loaded. */
public ImageDataProvider daprov;
/** The path used to identify the image to the data provider. */
public String path;
protected ImageKey (ImageDataProvider daprov, String path)
{
this.daprov = daprov;
this.path = path;
}
public int hashCode ()
{
return path.hashCode() ^ daprov.getIdent().hashCode();
}
public boolean equals (Object other)
{
if (other == null || !(other instanceof ImageKey)) {
return false;
}
ImageKey okey = (ImageKey)other;
return ((okey.daprov.getIdent().equals(daprov.getIdent())) &&
(okey.path.equals(path)));
}
public String toString ()
{
return daprov.getIdent() + ":" + path;
}
}
/**
* Construct an image manager with the specified {@link
* ResourceManager} from which it will obtain its data. A non-null
* <code>context</code> must be provided if there is any expectation
* that the image manager will not be able to load images via the
* ImageIO services and will have to fallback to Toolkit-style
* loading.
*/
public ImageManager (ResourceManager rmgr, Component context)
{
_rmgr = rmgr;
// create our image cache
int icsize = ConfigUtil.getSystemProperty(
"narya.media.image.cache_size", DEFAULT_IMAGE_CACHE_SIZE);
Log.debug("Creating image cache [size=" + icsize + "].");
_ccache = new LRUHashMap(icsize);
// try to figure out which image loader we'll be using
try {
_loader = (ImageLoader)Class.forName(IMAGEIO_LOADER).newInstance();
} catch (Throwable t) {
Log.info("Unable to use ImageIO to load images. " +
"Falling back to Toolkit [error=" + t + "].");
_loader = new ToolkitLoader(context);
}
// obtain our graphics configuration
if (context != null) {
_gc = context.getGraphicsConfiguration();
} else {
_gc = ImageUtil.getDefGC();
}
}
/**
* Creates a buffered image, optimized for display on our graphics
* device.
*/
public BufferedImage createImage (int width, int height, int transparency)
{
// DEBUG: override transparency for the moment on all images
transparency = Transparency.TRANSLUCENT;
return _gc.createCompatibleImage(width, height, transparency);
}
/**
* Loads (and caches) the specified image from the resource manager
* using the supplied path to identify the image.
*/
public BufferedImage getImage (String path)
{
return getImage(null, path, null);
}
/**
* Like {@link #getImage(String)} but the specified colorizations are
* applied to the image before it is returned.
*/
public BufferedImage getImage (String path, Colorization[] zations)
{
return getImage(null, path, zations);
}
/**
* Like {@link #getImage(String)} but the image is loaded from the
* specified resource set rathern than the default resource set.
*/
public BufferedImage getImage (String rset, String path)
{
return getImage(rset, path, null);
}
/**
* Like {@link #getImage(String,String)} but the specified
* colorizations are applied to the image before it is returned.
*/
public BufferedImage getImage (String rset, String path,
Colorization[] zations)
{
if (StringUtil.blank(path)) {
String errmsg = "Invalid image path [rset=" + rset +
", path=" + path + "]";
throw new IllegalArgumentException(errmsg);
}
return getImage(getImageKey(rset, path), zations);
}
/**
* Returns an image key that can be used to fetch the image identified
* by the specified resource set and image path.
*/
public ImageKey getImageKey (String rset, String path)
{
return getImageKey(getDataProvider(rset), path);
}
/**
* Returns an image key that can be used to fetch the image identified
* by the specified data provider and image path.
*/
public ImageKey getImageKey (ImageDataProvider daprov, String path)
{
return new ImageKey(daprov, path);
}
/**
* Obtains the image identified by the specified key, caching if
* possible. The image will be recolored using the supplied
* colorizations if requested.
*/
public BufferedImage getImage (ImageKey key, Colorization[] zations)
{
CacheRecord crec = (CacheRecord)_ccache.get(key);
if (crec != null) {
// Log.info("Cache hit [key=" + key + ", crec=" + crec + "].");
return crec.getImage(zations);
}
// Log.info("Cache miss [key=" + key + ", crec=" + crec + "].");
// load up the raw image
BufferedImage image = loadImage(key);
if (image == null) {
Log.warning("Failed to load image " + key + ".");
}
// create a cache record
crec = new CacheRecord(image);
_ccache.put(key, crec);
_keySet.add(key);
// periodically report our image cache performance
reportCachePerformance();
return crec.getImage(zations);
}
/**
* Returns the data provider configured to obtain image data from the
* specified resource set.
*/
protected ImageDataProvider getDataProvider (final String rset)
{
if (rset == null) {
return _defaultProvider;
}
ImageDataProvider dprov = (ImageDataProvider)_providers.get(rset);
if (dprov == null) {
dprov = new ImageDataProvider() {
public ImageInputStream loadImageData (String path)
throws IOException {
// first attempt to load the image from the specified
// resource set
try {
return _rmgr.getImageResource(rset, path);
} catch (FileNotFoundException fnfe) {
// fall back to trying the classpath
return _rmgr.getImageResource(path);
}
}
public String getIdent () {
return "rmgr:" + rset;
}
};
_providers.put(rset, dprov);
}
return dprov;
}
/**
* Loads and returns the image with the specified key from the
* supplied data provider.
*/
protected BufferedImage loadImage (ImageKey key)
{
BufferedImage image = null;
ImageInputStream imgin = null;
try {
Log.debug("Loading image " + key + ".");
imgin = key.daprov.loadImageData(key.path);
image = loadImage(imgin);
} catch (Exception e) {
Log.warning("Unable to load image '" + key + "'.");
Log.logStackTrace(e);
// create a blank image in its stead
image = createImage(1, 1, Transparency.OPAQUE);
}
if (imgin != null) {
try {
imgin.close();
} catch (IOException ioe) {
// jesus fucking hoppalong cassidy christ on a polyester
// pogo stick! those fuckers at sun have
// ImageInputStreamImpl.close() throwing a fucking
// IOException if it's already closed; there's no way to
// find out if it's already closed or not, so we have to
// check for their bullshit exception; as if we should
// just "trust" ImageIO.read() to close the fucking input
// stream when it's done, especially after the goddamned
// fiasco with PNGImageReader not closing it's fucking
// inflaters; for the love of humanity
if (!"closed".equals(ioe.getMessage())) {
Log.warning("Failure closing image input '" + key + "'.");
Log.logStackTrace(ioe);
}
}
}
return image;
}
/**
* Called by {@link #loadImage(ImageKey)} to do the actual image
* loading.
*/
protected BufferedImage loadImage (ImageInputStream imgin)
throws IOException
{
return ImageIO.read(imgin);
// _reader.setInput(imgin, false, false);
// return _reader.read(0, null);
}
/**
* Creates a mirage which is an image optimized for display on our
* current display device and which will be stored into video memory
* if possible.
*/
public Mirage getMirage (ImageKey key)
{
return getMirage(key, null, null);
}
/**
* Like {@link #getMirage(ImageKey)} but that only the specified
* subimage of the source image is used to build the mirage.
*/
public Mirage getMirage (ImageKey key, Rectangle bounds)
{
return getMirage(key, bounds, null);
}
/**
* Like {@link #getMirage(ImageKey)} but the supplied colorizations
* are applied to the source image before creating the mirage.
*/
public Mirage getMirage (ImageKey key, Colorization[] zations)
{
return getMirage(key, null, zations);
}
/**
* Like {@link #getMirage(ImageKey,Colorization[])} except that the
* mirage is created using only the specified subset of the original
* image.
*/
public Mirage getMirage (ImageKey key, Rectangle bounds,
Colorization[] zations)
{
if (bounds == null) {
BufferedImage src = getImage(key, zations);
bounds = new Rectangle(0, 0, src.getWidth(), src.getHeight());
// return new BufferedMirage(src);
// } else {
// BufferedImage src = getImage(key, zations);
// src = src.getSubimage(bounds.x, bounds.y,
// bounds.width, bounds.height);
// return new BufferedMirage(src);
}
return new CachedVolatileMirage(this, key, bounds, zations);
// return new BlankMirage(bounds.width, bounds.height);
}
/**
* Returns the graphics configuration that should be used to optimize
* images for display.
*/
public GraphicsConfiguration getGraphicsConfiguration ()
{
return _gc;
}
/**
* Reports statistics detailing the image manager cache performance
* and the current size of the cached images.
*/
protected void reportCachePerformance ()
{
if (/* Log.getLevel() != Log.log.DEBUG || */
_cacheStatThrottle.throttleOp()) {
return;
}
// compute our estimated memory usage
long size = 0;
Iterator iter = _ccache.values().iterator();
while (iter.hasNext()) {
size += ((CacheRecord)iter.next()).getEstimatedMemoryUsage();
}
int[] eff = _ccache.getTrackedEffectiveness();
Log.info("ImageManager LRU [mem=" + (size / 1024) + "k" +
", size=" + _ccache.size() + ", hits=" + eff[0] +
", misses=" + eff[1] + ", totalKeys=" + _keySet.size() + "].");
}
/** Maintains a source image and a set of colorized versions in the
* image cache. */
protected static class CacheRecord
{
public CacheRecord (BufferedImage source)
{
_source = source;
}
public BufferedImage getImage (Colorization[] zations)
{
if (zations == null) {
return _source;
}
if (_colorized == null) {
_colorized = new ArrayList();
}
// we search linearly through our list of colorized copies
// because it is not likely to be very long
int csize = _colorized.size();
for (int ii = 0; ii < csize; ii++) {
Tuple tup = (Tuple)_colorized.get(ii);
Colorization[] tzations = (Colorization[])tup.left;
if (Arrays.equals(zations, tzations)) {
return (BufferedImage)tup.right;
}
}
BufferedImage cimage = ImageUtil.recolorImage(_source, zations);
_colorized.add(new Tuple(zations, cimage));
return cimage;
}
public long getEstimatedMemoryUsage ()
{
return ImageUtil.getEstimatedMemoryUsage(_source);
}
public String toString ()
{
return "[wid=" + _source.getWidth() +
", hei=" + _source.getHeight() +
", ccount=" + ((_colorized == null) ? 0 : _colorized.size()) +
"]";
}
protected BufferedImage _source;
protected ArrayList _colorized;
}
/** A reference to the resource manager via which we load image data
* by default. */
protected ResourceManager _rmgr;
/** The image loader via which we convert an input stream into an
* image. */
protected ImageLoader _loader;
/** A cache of loaded images. */
protected LRUHashMap _ccache;
/** The set of all keys we've ever seen. */
protected HashSet _keySet = new HashSet();
/** Throttle our cache status logging to once every 30 seconds. */
protected Throttle _cacheStatThrottle = new Throttle(1, 30000L);
/** The graphics configuration for the default screen device. */
protected GraphicsConfiguration _gc;
/** Our default data provider. */
protected ImageDataProvider _defaultProvider = new ImageDataProvider() {
public ImageInputStream loadImageData (String path)
throws IOException {
return _rmgr.getImageResource(path);
}
public String getIdent () {
return "rmgr:default";
}
};
// /** Temporary hacked PNG reader that actually closes its
// * InflaterInputStream when it's done with it. */
// protected PNGImageReader _reader =
// new PNGImageReader(new PNGImageReaderSpi());
/** Data providers for different resource sets. */
protected HashMap _providers = new HashMap();
/** The classname of the ImageIO-based image loader which we attempt
* to use but fallback from if we're not running a JVM that has
* ImageIO support. */
protected static final String IMAGEIO_LOADER =
"com.threerings.media.image.ImageIOLoader";
/** The maximum number of images that may be cached at any one time. */
protected static final int DEFAULT_IMAGE_CACHE_SIZE = 100;
}