Revamped image loading such that the ResourceManager handles loading images

given a resource path so that it can be smart about loading from files versus
streams depending on what works based based on where the resources are coming
from. Also moved FastImageIO into com.threerings.resource to avoid a dependence
on com.threerings.media in resource.


git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@310 ed5b42cb-e716-0410-a449-f6a68f950b19
This commit is contained in:
Michael Bayne
2007-10-26 19:50:53 +00:00
parent 3e47134b06
commit ecc173d6ad
11 changed files with 86 additions and 87 deletions
@@ -0,0 +1,174 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.resource;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.IndexColorModel;
import java.awt.image.PixelInterleavedSampleModel;
import java.awt.image.WritableRaster;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.IntBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
/**
* Provides routines for writing and reading uncompressed 8-bit color
* mapped images in a manner that is extremely fast and generates a
* minimal amount of garbage during the loading process.
*/
public class FastImageIO
{
/** A suffix for use when storing raw images in bundles or on the file
* system. */
public static final String FILE_SUFFIX = ".raw";
/**
* Returns true if the supplied image is of a format that is supported
* by the fast image I/O services, false if not.
*/
public static boolean canWrite (BufferedImage image)
{
return (image.getColorModel() instanceof IndexColorModel) &&
(image.getRaster().getDataBuffer() instanceof DataBufferByte);
}
/**
* Writes the supplied image to the supplied output stream.
*
* @exception IOException thrown if an error occurs writing to the
* output stream.
*/
public static void write (BufferedImage image, OutputStream out)
throws IOException
{
DataOutputStream dout = new DataOutputStream(out);
// write the image dimensions
int width = image.getWidth(), height = image.getHeight();
dout.writeInt(width);
dout.writeInt(height);
// write the color model information
IndexColorModel cmodel = (IndexColorModel)image.getColorModel();
int tpixel = cmodel.getTransparentPixel();
dout.writeInt(tpixel);
int msize = cmodel.getMapSize();
int[] map = new int[msize];
cmodel.getRGBs(map);
dout.writeInt(msize);
for (int ii = 0; ii < map.length; ii++) {
dout.writeInt(map[ii]);
}
// write the raster data
DataBufferByte dbuf = (DataBufferByte)image.getRaster().getDataBuffer();
byte[] data = dbuf.getData();
if (data.length != width * height) {
String errmsg = "Raster data not same size as image! [" +
width + "x" + height + " != " + data.length + "]";
throw new IllegalStateException(errmsg);
}
dout.write(data);
dout.flush();
}
/**
* Reads an image from the supplied file (which must contain an image
* previously written via a call to {@link #write}).
*
* @exception IOException thrown if an error occurs reading from the
* file.
*/
public static BufferedImage read (File file)
throws IOException
{
RandomAccessFile raf = new RandomAccessFile(file, "r");
FileChannel fchan = raf.getChannel();
try {
MappedByteBuffer mbuf = fchan.map(
FileChannel.MapMode.READ_ONLY, 0, file.length());
// read in our integer fields
IntBuffer ibuf = mbuf.asIntBuffer();
int width = ibuf.get();
int height = ibuf.get();
int tpixel = ibuf.get();
int msize = ibuf.get();
if (width > Short.MAX_VALUE || width < 0 ||
height > Short.MAX_VALUE || height < 0) {
throw new IOException("Bogus image size " +
width + "x" + height);
}
IndexColorModel cmodel;
synchronized (_origin) { // any old object will do
// make sure our colormap array is big enough
if (_cmap == null || _cmap.length < msize) {
_cmap = new int[msize];
}
// read in the data and create our colormap
ibuf.get(_cmap, 0, msize);
cmodel = new IndexColorModel(
8, msize, _cmap, 0, DataBuffer.TYPE_BYTE, null);
}
// advance the byte buffer accordingly
mbuf.position(ibuf.position() * 4);
// read in the image data itself
byte[] data = new byte[width*height];
mbuf.get(data);
// create the image from our component parts
DataBuffer dbuf = new DataBufferByte(data, data.length, 0);
int[] offsets = new int[] { 0 };
PixelInterleavedSampleModel smodel =
new PixelInterleavedSampleModel(
DataBuffer.TYPE_BYTE, width, height, 1, width, offsets);
WritableRaster raster = WritableRaster.createWritableRaster(
smodel, dbuf, _origin);
return new BufferedImage(cmodel, raster, false, null);
} finally {
fchan.close();
raf.close();
}
}
/** Used when loading our color map. */
protected static int[] _cmap;
/** Used when creating our writable raster. */
protected static Point _origin = new Point(0, 0);
}
@@ -171,12 +171,10 @@ public class Handler extends URLStreamHandler
throw new FileNotFoundException(path);
}
// locate the tile image, then write that subimage back out in PNG
// format into memory and return an input stream for that
ImageInputStream stream =
StringUtil.isBlank(bundle) ? _rmgr.getImageResource(path)
: _rmgr.getImageResource(bundle, path);
BufferedImage src = ImageIO.read(stream);
// locate the tile image, then write that subimage back out in PNG format into memory and
// return an input stream for that
BufferedImage src = StringUtil.isBlank(bundle) ?
_rmgr.getImageResource(path) : _rmgr.getImageResource(bundle, path);
Rectangle trect = GeomUtil.getTile(
src.getWidth(), src.getHeight(), width, height, tidx);
BufferedImage tile = src.getSubimage(
@@ -21,6 +21,8 @@
package com.threerings.resource;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
@@ -251,8 +253,7 @@ public class ResourceBundle
}
// copy the resource into the temporary file
BufferedOutputStream fout =
new BufferedOutputStream(new FileOutputStream(tfile));
BufferedOutputStream fout = new BufferedOutputStream(new FileOutputStream(tfile));
InputStream jin = _jarSource.getInputStream(entry);
IOUtils.copy(jin, fout);
jin.close();
@@ -261,6 +262,16 @@ public class ResourceBundle
return tfile;
}
/**
* Decodes and returns the specified image resource. Returns null if no resource exists at the
* specified path.
*/
public BufferedImage getImageResource (String path)
throws IOException
{
return ResourceManager.loadImage(getResourceFile(path));
}
/**
* Returns true if this resource bundle contains the resource with the
* specified path. This avoids actually loading the resource, in the
@@ -22,6 +22,7 @@
package com.threerings.resource;
import java.awt.EventQueue;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
@@ -43,9 +44,7 @@ import java.util.StringTokenizer;
import java.security.AccessController;
import java.security.PrivilegedAction;
import javax.imageio.stream.FileImageInputStream;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import javax.imageio.ImageIO;
import com.samskivert.net.PathUtil;
import com.samskivert.util.ResultListener;
@@ -449,28 +448,27 @@ public class ResourceManager
}
/**
* Fetches the specified resource as an {@link ImageInputStream} and one that takes advantage,
* if possible, of caching of unpacked resources on the local filesystem.
* Fetches and decodes the specified resource into a {@link BufferedImage}.
*
* @exception FileNotFoundException thrown if the resource could not be located in any of the
* bundles in the specified set, or if the specified set does not exist.
* @exception IOException thrown if a problem occurs locating or reading the resource.
*/
public ImageInputStream getImageResource (String path)
public BufferedImage getImageResource (String path)
throws IOException
{
// first look for this resource in our default resource bundle
for (int i = 0; i < _default.length; i++) {
File file = _default[i].getResourceFile(path);
if (file != null) {
return new FileImageInputStream(file);
return loadImage(file);
}
}
// fallback next to an unpacked resource file
File file = getResourceFile(path);
if (file != null && file.exists()) {
return new FileImageInputStream(file);
return loadImage(file);
}
// first try a locale-specific file
@@ -483,7 +481,7 @@ public class ResourceManager
}
});
if (in != null) {
return new MemoryCacheImageInputStream(new BufferedInputStream(in));
return loadImage(in);
}
}
@@ -495,7 +493,7 @@ public class ResourceManager
}
});
if (in != null) {
return new MemoryCacheImageInputStream(new BufferedInputStream(in));
return loadImage(in);
}
// if we still haven't found it, we throw an exception
@@ -549,14 +547,13 @@ public class ResourceManager
}
/**
* Fetches the specified resource as an {@link ImageInputStream} and one that takes advantage,
* if possible, of caching of unpacked resources on the local filesystem.
* Fetches and decodes the specified resource into a {@link BufferedImage}.
*
* @exception FileNotFoundException thrown if the resource could not be located in any of the
* bundles in the specified set, or if the specified set does not exist.
* @exception IOException thrown if a problem occurs locating or reading the resource.
*/
public ImageInputStream getImageResource (String rset, String path)
public BufferedImage getImageResource (String rset, String path)
throws IOException
{
// grab the resource bundles in the specified resource set
@@ -569,22 +566,21 @@ public class ResourceManager
// look for the resource in any of the bundles
int size = bundles.length;
for (int ii = 0; ii < size; ii++) {
File file = null;
BufferedImage image = null;
// try a localized version first
if (_localePrefix != null) {
file = bundles[ii].getResourceFile(PathUtil.appendPath(_localePrefix, path));
image = bundles[ii].getImageResource(PathUtil.appendPath(_localePrefix, path));
}
// if we didn't find that, try generic
if (file == null) {
file = bundles[ii].getResourceFile(path);
if (image == null) {
image = bundles[ii].getImageResource(path);
}
if (file != null) {
if (image != null) {
// Log.info("Found image resource [rset=" + rset +
// ", bundle=" + bundles[ii].getSource() +
// ", path=" + path + ", file=" + file + "].");
return new FileImageInputStream(file);
// ", bundle=" + bundles[ii].getSource() + ", path=" + path + "].");
return image;
}
}
@@ -653,6 +649,47 @@ public class ResourceManager
}
}
/**
* Loads an image from the supplied file. Supports {@link FastImageIO} files and formats
* supported by {@link ImageIO}.
*/
protected static BufferedImage loadImage (File file)
throws IOException
{
if (file == null) {
return null;
} else if (file.getName().endsWith(FastImageIO.FILE_SUFFIX)) {
return FastImageIO.read(file);
} else {
return ImageIO.read(file);
}
}
/**
* Loads an image from the supplied input stream. Supports formats supported by {@link ImageIO}
* but not {@link FastImageIO}.
*/
protected static BufferedImage loadImage (InputStream iis)
throws IOException
{
BufferedImage image = ImageIO.read(iis);
try {
iis.close();
} catch (IOException ioe) {
// jesus fucking hoppalong cassidy christ on a polyester pogo stick!
// ImageInputStreamImpl.close() throws 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 its fucking inflaters; for the love of humanity
if (!"closed".equals(ioe.getMessage())) {
Log.warning("Failure closing image input '" + iis + "'.");
Log.logStackTrace(ioe);
}
}
return image;
}
/** Used to unpack bundles on a separate thread. */
protected static class Unpacker extends Thread
{