// // $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.jme.util; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.logging.Level; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.awt.Graphics2D; import java.awt.color.ColorSpace; import java.awt.geom.AffineTransform; import java.awt.image.BandCombineOp; import java.awt.image.BufferedImage; import java.awt.image.ComponentColorModel; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.Raster; import com.jme.image.Image; import com.jmex.bui.BImage; import com.threerings.media.image.Colorization; import com.threerings.media.image.ImageUtil; import com.threerings.resource.ResourceManager; import static com.threerings.jme.Log.log; /** * Manages a weak cache of image data to make life simpler for callers that don't want to worry * about coordinating shared use of the same images. */ public class ImageCache { /** * Creates a JME-compatible image from the supplied buffered image. */ public static Image createImage (BufferedImage bufimg, boolean flip) { return createImage(bufimg, 1f, flip); } /** * Colorizes the supplied buffered image (which must be an 8-bit colormapped image), then * converts the colorized image into a form that JME can display. */ public static Image createImage (BufferedImage bufimg, Colorization[] zations, boolean flip) { return createImage(bufimg, zations, 1f, flip); } /** * Colorizes the supplied buffered image (which must be an 8-bit colormapped image), then * converts the colorized image into a form that JME can display. */ public static Image createImage (BufferedImage bufimg, Colorization[] zations, float scale, boolean flip) { return createImage(ImageUtil.recolorImage(bufimg, zations), scale, flip); } /** * Creates a JME-compatible image from the supplied buffered image. */ public static Image createImage (BufferedImage bufimg, float scale, boolean flip) { // make sure images are square powers of two int width = (int)(bufimg.getWidth() * scale); int height = (int)(bufimg.getHeight() * scale); int tsize = nextPOT(Math.max(width, height)); // convert the the image to the format that OpenGL prefers BufferedImage dispimg = createCompatibleImage( tsize, tsize, bufimg.getColorModel().hasAlpha()); // flip the image to convert into OpenGL's coordinate system AffineTransform tx = null; if (flip) { tx = AffineTransform.getScaleInstance(scale, -scale); tx.translate((tsize - width) / 2, (height - tsize) / 2 - bufimg.getHeight()); } // "convert" the image by rendering the old into the new Graphics2D gfx = (Graphics2D)dispimg.getGraphics(); gfx.drawImage(bufimg, tx, null); gfx.dispose(); // now extract the image data into a JME image return convertImage(dispimg); } /** * Creates a buffered image in a format compatible with LWJGL with the specified dimensions. * * @param transparent if true, the image will be four bytes per pixel (RGBA), if false it will * be three (and have no alpha channel). */ public static BufferedImage createCompatibleImage (int width, int height, boolean transparent) { if (transparent) { return new BufferedImage(GL_ALPHA_MODEL, Raster.createInterleavedRaster( DataBuffer.TYPE_BYTE, width, height, 4, null), false, null); } else { return new BufferedImage(GL_OPAQUE_MODEL, Raster.createInterleavedRaster( DataBuffer.TYPE_BYTE, width, height, 3, null), false, null); } } /** * Converts an image that was created with {@link #createCompatibleImage} into a JME {@link * Image}. The data is assumed to have already been "flipped". */ public static Image convertImage (BufferedImage bufimg) { Image image = new Image(); image.setType(bufimg.getColorModel().hasAlpha() ? Image.RGBA8888 : Image.RGB888); image.setWidth(bufimg.getWidth()); image.setHeight(bufimg.getHeight()); image.setData(convertImage(bufimg, null)); return image; } /** * Converts the supplied image (which must have been created with {@link * #createCompatibleImage}) into a {@link ByteBuffer} that can be passed to {@link * Image#setData}. * * @param target the results of a previous call to {@link #convertImage} that will be * overwritten or null if a new buffer should be allocated. Of course a reused buffer must be * used with the same image or one with the exact same configuration. */ public static ByteBuffer convertImage (BufferedImage image, ByteBuffer target) { DataBufferByte dbuf = (DataBufferByte)image.getRaster().getDataBuffer(); byte[] data = dbuf.getData(); if (target == null) { target = ByteBuffer.allocateDirect(data.length); target.order(ByteOrder.nativeOrder()); } target.clear(); target.put(data); target.flip(); return target; } /** * Creates an image cache that will obtain its image data from the supplied resource manager. */ public ImageCache (ResourceManager rsrcmgr) { _rsrcmgr = rsrcmgr; } /** * See {@link #getImage(String,boolean)}. */ public Image getImage (String rsrcPath) { return getImage(rsrcPath, 1f, true); } /** * See {@link #getImage(String,float,boolean)}. */ public Image getImage (String rsrcPath, float scale) { return getImage(rsrcPath, scale, true); } /** * Loads up an image from the cache if possible or from the resource manager otherwise, in * which case it is prepared for use by JME and OpenGL. Note: these images are cached * separately from the {@link BImage} and {@link BufferedImage} caches. * * @param flip whether or not to convert the image from normal computer coordinates into OpenGL * coordinates when loading. Note: this information is not cached, an image must * always be requested as flipped or not flipped. */ public Image getImage (String rsrcPath, boolean flip) { return getImage(rsrcPath, 1f, flip); } /** * Loads up an image from the cache if possible or from the resource manager otherwise, in * which case it is prepared for use by JME and OpenGL. Note: these images are cached * separately from the {@link BImage} and {@link BufferedImage} caches. * * @param flip whether or not to convert the image from normal computer coordinates into OpenGL * coordinates when loading. Note: this information is not cached, an image must * always be requested as flipped or not flipped. * @param scale a scale factor to apply to the image. As with the flip setting, the scale * factor is not cached. */ public Image getImage (String rsrcPath, float scale, boolean flip) { // first check the cache WeakReference iref = _imgcache.get(rsrcPath); Image image; if (iref != null && (image = iref.get()) != null) { return image; } // load the image data from the resource manager BufferedImage bufimg; try { bufimg = _rsrcmgr.getImageResource(rsrcPath); } catch (Throwable t) { log.log(Level.WARNING, "Unable to load image resource " + "[path=" + ifile + "].", t); // cope; return an error image of abitrary size bufimg = ImageUtil.createErrorImage(64, 64); } // create and cache a new JME image with the appropriate data image = createImage(bufimg, scale, flip); _imgcache.put(rsrcPath, new WeakReference(image)); return image; } /** * Loads up an image from the cache if possible or from the resource manager otherwise, in * which case it is prepared for use by BUI. Note: these images are cached separately * from the {@link Image} and {@link BufferedImage} caches. */ public BImage getBImage (String rsrcPath) { return getBImage(rsrcPath, 1f, false); } /** * Loads up an image from the cache if possible or from the resource manager otherwise, in * which case it is prepared for use by BUI. Note: these images are cached separately * from the {@link Image} and {@link BufferedImage} caches. * * @param scale If none-unity the image will be scaled by the supplied factor before being * converted into a {@link BImage}. * @param returnNull If set to true, will return a null instead of generating an error image on * failure */ public BImage getBImage (String rsrcPath, float scale, boolean returnNull) { // first check the cache String key = rsrcPath + ":" + scale; WeakReference iref = _buicache.get(key); BImage image; if (iref != null && (image = iref.get()) != null) { return image; } // load the image data from the resource manager BufferedImage bufimg; try { bufimg = _rsrcmgr.getImageResource(rsrcPath); } catch (Throwable t) { if (returnNull) { return null; } log.log(Level.WARNING, "Unable to load image resource [path=" + ifile + "].", t); // cope; return an error image of abitrary size bufimg = ImageUtil.createErrorImage(64, 64); } // create and cache a new BUI image with the appropriate (scaled if necessary) data image = new BImage(scale == 1f ? bufimg : bufimg.getScaledInstance(Math.round(bufimg.getWidth()*scale), Math.round(bufimg.getHeight()*scale), BufferedImage.SCALE_SMOOTH), true); _buicache.put(key, new WeakReference(image)); return image; } /** * Loads up a silhouette image (in which all non-transparent pixels are set to black) from the * cache if possible or from the resource manager otherwise, in which case it is prepared for * use by BUI. */ public BImage getSilhouetteBImage (String rsrcPath, boolean returnNull) { // first check the cache String key = "silhouette:" + rsrcPath; WeakReference iref = _buicache.get(key); BImage image; if (iref != null && (image = iref.get()) != null) { return image; } // load the image data from the resource manager BufferedImage bufimg, silimg; try { bufimg = _rsrcmgr.getImageResource(rsrcPath); // now turn it into a silhouette silimg = new BufferedImage(bufimg.getWidth(), bufimg.getHeight(), BufferedImage.TYPE_4BYTE_ABGR); new BandCombineOp(NON_ALPHA_TO_BROWN, null).filter( bufimg.getRaster(), silimg.getRaster()); } catch (Throwable t) { if (returnNull) { return null; } log.log(Level.WARNING, "Unable to load image resource [path=" + ifile + "].", t); // cope; return an error image of abitrary size silimg = ImageUtil.createErrorImage(64, 64); } // create and cache a new BUI image with the appropriate data image = new BImage(silimg, true); _buicache.put(key, new WeakReference(image)); return image; } /** * Colorizes the image with supplied path (which must be an 8-bit colormapped image), then * converts the colorized image into a form that JME can display. */ public BImage createColorizedBImage (String path, Colorization[] zations, boolean flip) { return new BImage(ImageUtil.recolorImage(getBufferedImage(path), zations), flip); } /** * Loads up a buffered image from the cache if possible or from the resource manager * otherwise. Note: these images are cached separately from the {@link Image} and * {@link BImage} caches. */ public BufferedImage getBufferedImage (String rsrcPath) { // first check the cache WeakReference iref = _bufcache.get(rsrcPath); BufferedImage image; if (iref != null && (image = iref.get()) != null) { return image; } // load the image data from the resource manager try { image = _rsrcmgr.getImageResource(rsrcPath); } catch (Throwable t) { log.log(Level.WARNING, "Unable to load image resource [path=" + ifile + "].", t); // cope; return an error image of abitrary size image = ImageUtil.createErrorImage(64, 64); } _bufcache.put(rsrcPath, new WeakReference(image)); return image; } /** Rounds the supplied value up to a power of two. */ protected static int nextPOT (int value) { return (Integer.bitCount(value) > 1) ? (Integer.highestOneBit(value) << 1) : value; } /** Provides access to our image data. */ protected ResourceManager _rsrcmgr; /** A cache of {@link Image} instances. */ protected HashMap> _imgcache = new HashMap>(); /** A cache of {@link BImage} instances. */ protected HashMap> _buicache = new HashMap>(); /** A cache of {@link BufferedImage} instances. */ protected HashMap> _bufcache = new HashMap>(); /** Used to create buffered images in a format compatible with OpenGL. */ protected static ComponentColorModel GL_ALPHA_MODEL = new ComponentColorModel( ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] { 8, 8, 8, 8 }, true, false, ComponentColorModel.TRANSLUCENT, DataBuffer.TYPE_BYTE); /** Used to create buffered images in a format compatible with OpenGL. */ protected static ComponentColorModel GL_OPAQUE_MODEL = new ComponentColorModel( ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] { 8, 8, 8, 0 }, false, false, ComponentColorModel.OPAQUE, DataBuffer.TYPE_BYTE); /** An operation that converts all channels except alpha to brown. */ protected static final float[][] NON_ALPHA_TO_BROWN = { { 0, 0, 0, 0x66/255f }, { 0, 0, 0, 0x57/255f }, { 0, 0, 0, 0x34/255f }, { 0, 0, 0, 0.5f } }; }