Files
narya/src/java/com/threerings/media/image/ImageManager.java
T
Michael Bayne 60a60d49ec Use runtime adjustments for our image and tile cache size.
git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@2145 542714f4-19e9-0310-aa3c-eee0fc999fb1
2003-01-15 00:47:09 +00:00

509 lines
16 KiB
Java

//
// $Id: ImageManager.java,v 1.38 2003/01/15 00:47:08 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.LRUHashMap;
import com.samskivert.util.RuntimeAdjust;
import com.samskivert.util.StringUtil;
import com.samskivert.util.Throttle;
import com.samskivert.util.Tuple;
import com.threerings.media.Log;
import com.threerings.media.MediaPrefs;
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 = MediaPrefs.config.getValue(
IMAGE_CACHE_SIZE_KEY, 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 config key for our cache size property. */
protected static final String IMAGE_CACHE_SIZE_KEY =
"narya.media.image.cache_size";
/** The maximum number of images that may be cached at any one time. */
protected static final int DEFAULT_IMAGE_CACHE_SIZE = 100;
/** Register our image cache size with the runtime adjustments
* framework. */
protected static RuntimeAdjust.IntAdjust _cacheSize =
new RuntimeAdjust.IntAdjust(
"Size (in images) of the image manager LRU cache [requires reboot]",
IMAGE_CACHE_SIZE_KEY, MediaPrefs.config, DEFAULT_IMAGE_CACHE_SIZE);
}