Behold, Nenya, Ring of Water and repository for our media and animation related
goodies, both Java 2D and LWJGL/JME 3D. git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@1 ed5b42cb-e716-0410-a449-f6a68f950b19
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// 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.awt.Component;
|
||||
import java.awt.GraphicsConfiguration;
|
||||
import java.awt.GraphicsDevice;
|
||||
import java.awt.Transparency;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
/**
|
||||
* If the user of the image manager services intends to create and display
|
||||
* images using the AWT, they can use this image creator which will use the
|
||||
* AWT to determine the optimal image format.
|
||||
*/
|
||||
public class AWTImageCreator
|
||||
implements ImageManager.OptimalImageCreator
|
||||
{
|
||||
/**
|
||||
* Create an image creator that will rely on the AWT to determine the
|
||||
* optimal image format.
|
||||
*
|
||||
* @param context if non-null, the graphics configuration will be obtained
|
||||
* therefrom; otherwise {@link GraphicsDevice#getDefaultConfiguration}
|
||||
* will be used.
|
||||
*/
|
||||
public AWTImageCreator (Component context)
|
||||
{
|
||||
// obtain our graphics configuration
|
||||
if (context != null) {
|
||||
_gc = context.getGraphicsConfiguration();
|
||||
} else {
|
||||
_gc = ImageUtil.getDefGC();
|
||||
}
|
||||
}
|
||||
|
||||
// documentation inherited from interface ImageManager.OptimalImageCreator
|
||||
public BufferedImage createImage (int width, int height, int trans)
|
||||
{
|
||||
// DEBUG: override transparency for the moment on all images
|
||||
trans = Transparency.TRANSLUCENT;
|
||||
if (_gc != null) {
|
||||
return _gc.createCompatibleImage(width, height, trans);
|
||||
} else {
|
||||
// if we're running in headless mode, do everything in 24-bit
|
||||
return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
}
|
||||
}
|
||||
|
||||
/** The graphics configuration for the default screen device. */
|
||||
protected GraphicsConfiguration _gc;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// $Id: BackedVolatileMirage.java 4191 2006-06-13 22:42:20Z ray $
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// 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.awt.Graphics;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import com.threerings.media.Log;
|
||||
|
||||
/**
|
||||
* Provides a volatile mirage that is backed by a buffered image that is
|
||||
* not obtained from the image manager but is instead provided at
|
||||
* construct time and completely circumvents the image manager's cache. As
|
||||
* such, this should not be used unless you know what you're doing.
|
||||
*/
|
||||
public class BackedVolatileMirage extends VolatileMirage
|
||||
{
|
||||
/**
|
||||
* Creates a mirage with the supplied regeneration informoation and
|
||||
* prepared image.
|
||||
*/
|
||||
public BackedVolatileMirage (ImageManager imgr, BufferedImage source)
|
||||
{
|
||||
super(imgr, new Rectangle(0, 0, source.getWidth(), source.getHeight()));
|
||||
_source = source;
|
||||
|
||||
// create our volatile image for the first time
|
||||
createVolatileImage();
|
||||
}
|
||||
|
||||
// documentation inherited
|
||||
protected int getTransparency ()
|
||||
{
|
||||
return _source.getColorModel().getTransparency();
|
||||
}
|
||||
|
||||
// documentation inherited
|
||||
protected void refreshVolatileImage ()
|
||||
{
|
||||
Graphics gfx = null;
|
||||
try {
|
||||
gfx = _image.getGraphics();
|
||||
gfx.drawImage(_source, -_bounds.x, -_bounds.y, null);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.warning("Failure refreshing mirage " + this + ".");
|
||||
Log.logStackTrace(e);
|
||||
|
||||
} finally {
|
||||
gfx.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// documentation inherited
|
||||
protected void toString (StringBuilder buf)
|
||||
{
|
||||
super.toString(buf);
|
||||
buf.append(", src=").append(_source);
|
||||
}
|
||||
|
||||
protected BufferedImage _source;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// $Id: BlankMirage.java 4191 2006-06-13 22:42:20Z ray $
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// 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.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
/**
|
||||
* A mirage implementation that contains no image data. Generally only
|
||||
* useful for testing.
|
||||
*/
|
||||
public class BlankMirage implements Mirage
|
||||
{
|
||||
public BlankMirage (int width, int height)
|
||||
{
|
||||
_width = width;
|
||||
_height = height;
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public void paint (Graphics2D gfx, int x, int y)
|
||||
{
|
||||
// nothing doing
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public int getWidth ()
|
||||
{
|
||||
return _width;
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public int getHeight ()
|
||||
{
|
||||
return _height;
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public boolean hitTest (int x, int y)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public long getEstimatedMemoryUsage ()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public BufferedImage getSnapshot ()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
protected int _width;
|
||||
protected int _height;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// $Id: BufferedMirage.java 4191 2006-06-13 22:42:20Z ray $
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// 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.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
/**
|
||||
* A simple mirage implementation that uses a buffered image.
|
||||
*/
|
||||
public class BufferedMirage implements Mirage
|
||||
{
|
||||
public BufferedMirage (BufferedImage image)
|
||||
{
|
||||
_image = image;
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public void paint (Graphics2D gfx, int x, int y)
|
||||
{
|
||||
gfx.drawImage(_image, x, y, null);
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public int getWidth ()
|
||||
{
|
||||
return _image.getWidth();
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public int getHeight ()
|
||||
{
|
||||
return _image.getHeight();
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public boolean hitTest (int x, int y)
|
||||
{
|
||||
return ImageUtil.hitTest(_image, x, y);
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public long getEstimatedMemoryUsage ()
|
||||
{
|
||||
return ImageUtil.getEstimatedMemoryUsage(_image.getRaster());
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public BufferedImage getSnapshot ()
|
||||
{
|
||||
return _image;
|
||||
}
|
||||
|
||||
protected BufferedImage _image;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
//
|
||||
// $Id: CachedVolatileMirage.java 4191 2006-06-13 22:42:20Z ray $
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// 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.awt.Graphics;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.Transparency;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import com.threerings.media.Log;
|
||||
|
||||
/**
|
||||
* A mirage implementation which allows the image to be maintained in
|
||||
* video memory and refetched from the image manager in the event that our
|
||||
* target screen resolution changes or we are flushed from video memory
|
||||
* for some other reason.
|
||||
*
|
||||
* <p> These objects are never created directly, but always obtained from
|
||||
* the {@link ImageManager}.
|
||||
*/
|
||||
public class CachedVolatileMirage extends VolatileMirage
|
||||
{
|
||||
/**
|
||||
* Creates a mirage with the supplied regeneration informoation and
|
||||
* prepared image.
|
||||
*/
|
||||
protected CachedVolatileMirage (
|
||||
ImageManager imgr, ImageManager.ImageKey source,
|
||||
Rectangle bounds, Colorization[] zations)
|
||||
{
|
||||
super(imgr, bounds);
|
||||
|
||||
_source = source;
|
||||
_zations = zations;
|
||||
|
||||
// create our volatile image for the first time
|
||||
createVolatileImage();
|
||||
}
|
||||
|
||||
// documentation inherited
|
||||
protected int getTransparency ()
|
||||
{
|
||||
BufferedImage source = _imgr.getImage(_source, _zations);
|
||||
return (source == null) ? Transparency.OPAQUE :
|
||||
source.getColorModel().getTransparency();
|
||||
}
|
||||
|
||||
// documentation inherited
|
||||
protected void refreshVolatileImage ()
|
||||
{
|
||||
Graphics gfx = null;
|
||||
try {
|
||||
BufferedImage source = _imgr.getImage(_source, _zations);
|
||||
if (source != null) {
|
||||
gfx = _image.getGraphics();
|
||||
gfx.drawImage(source, -_bounds.x, -_bounds.y, null);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.warning("Failure refreshing mirage " + this + ".");
|
||||
Log.logStackTrace(e);
|
||||
|
||||
} finally {
|
||||
if (gfx != null) {
|
||||
gfx.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// documentation inherited
|
||||
protected void toString (StringBuilder buf)
|
||||
{
|
||||
super.toString(buf);
|
||||
buf.append(", key=").append(_source);
|
||||
buf.append(", zations=").append(_zations);
|
||||
}
|
||||
|
||||
/** The key that identifies the image data used to create our volatile
|
||||
* image. */
|
||||
protected ImageManager.ImageKey _source;
|
||||
|
||||
/** Optional colorizations that are applied to our source image when
|
||||
* creating our mirage. */
|
||||
protected Colorization[] _zations;
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
//
|
||||
// $Id: ColorPository.java 4188 2006-06-13 18:03:48Z mdb $
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// 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.awt.Color;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.Serializable;
|
||||
|
||||
import java.text.ParseException;
|
||||
|
||||
import com.samskivert.util.HashIntMap;
|
||||
import com.samskivert.util.RandomUtil;
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
import com.threerings.media.Log;
|
||||
import com.threerings.resource.ResourceManager;
|
||||
import com.threerings.util.CompiledConfig;
|
||||
|
||||
/**
|
||||
* A repository of image recoloration information. It was called the
|
||||
* recolor repository but the re-s cancelled one another out.
|
||||
*/
|
||||
public class ColorPository implements Serializable
|
||||
{
|
||||
/**
|
||||
* Used to store information on a class of colors. These are public to
|
||||
* simplify the XML parsing process, so pay them no mind.
|
||||
*/
|
||||
public static class ClassRecord implements Serializable
|
||||
{
|
||||
/** An integer identifier for this class. */
|
||||
public int classId;
|
||||
|
||||
/** The name of the color class. */
|
||||
public String name;
|
||||
|
||||
/** The source color to use when recoloring colors in this class. */
|
||||
public Color source;
|
||||
|
||||
/** Data identifying the range of colors around the source color
|
||||
* that will be recolored when recoloring using this class. */
|
||||
public float[] range;
|
||||
|
||||
/** The default starting legality value for this color class. See
|
||||
* {@link ColorRecord#starter}. */
|
||||
public boolean starter;
|
||||
|
||||
/** The default colorId to use for recoloration in this class, or
|
||||
* 0 if there is no default defined. */
|
||||
public int defaultId;
|
||||
|
||||
/** A table of target colors included in this class. */
|
||||
public HashIntMap colors = new HashIntMap();
|
||||
|
||||
/** Used when parsing the color definitions. */
|
||||
public void addColor (ColorRecord record)
|
||||
{
|
||||
// validate the color id
|
||||
if (record.colorId > 255) {
|
||||
Log.warning("Refusing to add color record; colorId > 255 " +
|
||||
"[class=" + this + ", record=" + record + "].");
|
||||
} else {
|
||||
record.cclass = this;
|
||||
colors.put(record.colorId, record);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translants a color identified in string form into the id that
|
||||
* should be used to look up its information. Throws an
|
||||
* exception if no color could be found that associates with
|
||||
* that name.
|
||||
*
|
||||
* FIXME: This lookup could be sped up a lot with some cached
|
||||
* data tables if it looked like this function would get called
|
||||
* in time critical situations.
|
||||
*/
|
||||
public int getColorId (String name)
|
||||
throws ParseException
|
||||
{
|
||||
// Check if the string is itself a number
|
||||
try {
|
||||
int id = Integer.parseInt(name);
|
||||
if (colors.containsKey(id)) {
|
||||
return id;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// Guess it must be something else
|
||||
}
|
||||
|
||||
// Look for name matches among all colors
|
||||
Iterator iter = colors.values().iterator();
|
||||
while (iter.hasNext()) {
|
||||
ColorRecord color = (ColorRecord)iter.next();
|
||||
if (color.name.equalsIgnoreCase(name)) {
|
||||
return color.colorId;
|
||||
}
|
||||
}
|
||||
|
||||
// That input wasn't a color
|
||||
throw new ParseException("No color named '" + name + "'", 0);
|
||||
}
|
||||
|
||||
/** Returns a random starting id from the entries in this
|
||||
* class. */
|
||||
public ColorRecord randomStartingColor ()
|
||||
{
|
||||
// figure out our starter ids if we haven't already
|
||||
if (_starters == null) {
|
||||
ArrayList list = new ArrayList();
|
||||
Iterator iter = colors.values().iterator();
|
||||
while (iter.hasNext()) {
|
||||
ColorRecord color = (ColorRecord)iter.next();
|
||||
if (color.starter) {
|
||||
list.add(color);
|
||||
}
|
||||
}
|
||||
_starters = (ColorRecord[])
|
||||
list.toArray(new ColorRecord[list.size()]);
|
||||
}
|
||||
|
||||
// sanity check
|
||||
if (_starters.length < 1) {
|
||||
Log.warning("Requested random starting color from " +
|
||||
"colorless component class " + this + "].");
|
||||
return null;
|
||||
}
|
||||
|
||||
// return a random entry from the array
|
||||
return _starters[RandomUtil.getInt(_starters.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default ColorRecord defined for this color class, or
|
||||
* null if none.
|
||||
*/
|
||||
public ColorRecord getDefault ()
|
||||
{
|
||||
return (ColorRecord) colors.get(defaultId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of this instance.
|
||||
*/
|
||||
public String toString ()
|
||||
{
|
||||
return "[id=" + classId + ", name=" + name + ", source=#" +
|
||||
Integer.toString(source.getRGB() & 0xFFFFFF, 16) +
|
||||
", range=" + StringUtil.toString(range) +
|
||||
", starter=" + starter + ", colors=" +
|
||||
StringUtil.toString(colors.values().iterator()) + "]";
|
||||
}
|
||||
|
||||
protected transient ColorRecord[] _starters;
|
||||
|
||||
/** Increase this value when object's serialized state is impacted
|
||||
* by a class change (modification of fields, inheritance). */
|
||||
private static final long serialVersionUID = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to store information on a particular color. These are public
|
||||
* to simplify the XML parsing process, so pay them no mind.
|
||||
*/
|
||||
public static class ColorRecord implements Serializable
|
||||
{
|
||||
/** The colorization class to which we belong. */
|
||||
public ClassRecord cclass;
|
||||
|
||||
/** A unique colorization identifier (used in fingerprints). */
|
||||
public int colorId;
|
||||
|
||||
/** The name of the target color. */
|
||||
public String name;
|
||||
|
||||
/** Data indicating the offset (in HSV color space) from the
|
||||
* source color to recolor to this color. */
|
||||
public float[] offsets;
|
||||
|
||||
/** Tags this color as a legal starting color or not. This is a
|
||||
* shameful copout, placing application-specific functionality
|
||||
* into a general purpose library class. */
|
||||
public boolean starter;
|
||||
|
||||
/**
|
||||
* Returns a value that is the composite of our class id and color
|
||||
* id which can be used to identify a colorization record. This
|
||||
* value will always be a positive integer that fits into 16 bits.
|
||||
*/
|
||||
public int getColorPrint ()
|
||||
{
|
||||
return ((cclass.classId << 8) | colorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data in this record configured as a colorization
|
||||
* instance.
|
||||
*/
|
||||
public Colorization getColorization ()
|
||||
{
|
||||
// if (_zation == null) {
|
||||
// _zation = new Colorization(getColorPrint(), cclass.source,
|
||||
// cclass.range, offsets);
|
||||
// }
|
||||
// return _zation;
|
||||
return new Colorization(getColorPrint(), cclass.source,
|
||||
cclass.range, offsets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of this instance.
|
||||
*/
|
||||
public String toString ()
|
||||
{
|
||||
return "[id=" + colorId + ", name=" + name +
|
||||
", offsets=" + StringUtil.toString(offsets) +
|
||||
", starter=" + starter + "]";
|
||||
}
|
||||
|
||||
/** Our data represented as a colorization. */
|
||||
protected transient Colorization _zation;
|
||||
|
||||
/** Increase this value when object's serialized state is impacted
|
||||
* by a class change (modification of fields, inheritance). */
|
||||
private static final long serialVersionUID = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator over all color classes in this pository.
|
||||
*/
|
||||
public Iterator enumerateClasses ()
|
||||
{
|
||||
return _classes.values().iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing the records for the colors in the
|
||||
* specified class.
|
||||
*/
|
||||
public ColorRecord[] enumerateColors (String className)
|
||||
{
|
||||
// make sure the class exists
|
||||
ClassRecord record = getClassRecord(className);
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// create the array
|
||||
ColorRecord[] crecs = new ColorRecord[record.colors.size()];
|
||||
Iterator iter = record.colors.values().iterator();
|
||||
for (int i = 0; iter.hasNext(); i++) {
|
||||
crecs[i] = ((ColorRecord)iter.next());
|
||||
}
|
||||
return crecs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing the ids of the colors in the specified
|
||||
* class.
|
||||
*/
|
||||
public int[] enumerateColorIds (String className)
|
||||
{
|
||||
// make sure the class exists
|
||||
ClassRecord record = getClassRecord(className);
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int[] cids = new int[record.colors.size()];
|
||||
Iterator crecs = record.colors.values().iterator();
|
||||
for (int i = 0; crecs.hasNext(); i++) {
|
||||
cids[i] = ((ColorRecord)crecs.next()).colorId;
|
||||
}
|
||||
return cids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the specified color is legal for use at character
|
||||
* creation time. false is always returned for non-existent colors or
|
||||
* classes.
|
||||
*/
|
||||
public boolean isLegalStartColor (int classId, int colorId)
|
||||
{
|
||||
ColorRecord color = getColorRecord(classId, colorId);
|
||||
return (color == null) ? false : color.starter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random starting color from the specified color class.
|
||||
*/
|
||||
public ColorRecord getRandomStartingColor (String className)
|
||||
{
|
||||
// make sure the class exists
|
||||
ClassRecord record = getClassRecord(className);
|
||||
return (record == null) ? null : record.randomStartingColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up a colorization by id.
|
||||
*/
|
||||
public Colorization getColorization (int classId, int colorId)
|
||||
{
|
||||
ColorRecord color = getColorRecord(classId, colorId);
|
||||
return (color == null) ? null : color.getColorization();
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up a colorization by color print.
|
||||
*/
|
||||
public Colorization getColorization (int colorPrint)
|
||||
{
|
||||
return getColorization(colorPrint >> 8, colorPrint & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up a colorization by name.
|
||||
*/
|
||||
public Colorization getColorization (String className, int colorId)
|
||||
{
|
||||
ClassRecord crec = getClassRecord(className);
|
||||
if (crec != null) {
|
||||
ColorRecord color = (ColorRecord)crec.colors.get(colorId);
|
||||
if (color != null) {
|
||||
return color.getColorization();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads up a colorization class by name and logs a warning if it
|
||||
* doesn't exist.
|
||||
*/
|
||||
public ClassRecord getClassRecord (String className)
|
||||
{
|
||||
Iterator iter = _classes.values().iterator();
|
||||
while (iter.hasNext()) {
|
||||
ClassRecord crec = (ClassRecord)iter.next();
|
||||
if (crec.name.equals(className)) {
|
||||
return crec;
|
||||
}
|
||||
}
|
||||
Log.warning("No such color class [class=" + className + "].");
|
||||
Thread.dumpStack();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the requested color record.
|
||||
*/
|
||||
public ColorRecord getColorRecord (int classId, int colorId)
|
||||
{
|
||||
ClassRecord record = (ClassRecord)_classes.get(classId);
|
||||
if (record == null) {
|
||||
// if they request color class zero, we assume they're just
|
||||
// decoding a blank colorprint, otherwise we complain
|
||||
if (classId != 0) {
|
||||
Log.warning("Requested unknown color class " +
|
||||
"[classId=" + classId +
|
||||
", colorId=" + colorId + "].");
|
||||
Thread.dumpStack();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return (ColorRecord)record.colors.get(colorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a fully configured color class record to the pository. This is
|
||||
* only called by the XML parsing code, so pay it no mind.
|
||||
*/
|
||||
public void addClass (ClassRecord record)
|
||||
{
|
||||
// validate the class id
|
||||
if (record.classId > 255) {
|
||||
Log.warning("Refusing to add class; classId > 255 " + record + ".");
|
||||
} else {
|
||||
_classes.put(record.classId, record);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads up a serialized color pository from the supplied resource
|
||||
* manager.
|
||||
*/
|
||||
public static ColorPository loadColorPository (ResourceManager rmgr)
|
||||
{
|
||||
try {
|
||||
return loadColorPository(rmgr.getResource(CONFIG_PATH));
|
||||
} catch (IOException ioe) {
|
||||
Log.warning("Failure loading color pository [path=" + CONFIG_PATH +
|
||||
", error=" + ioe + "].");
|
||||
return new ColorPository();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads up a serialized color pository from the supplied resource
|
||||
* manager.
|
||||
*/
|
||||
public static ColorPository loadColorPository (InputStream source)
|
||||
{
|
||||
try {
|
||||
return (ColorPository)CompiledConfig.loadConfig(source);
|
||||
} catch (IOException ioe) {
|
||||
Log.warning("Failure loading color pository: " + ioe + ".");
|
||||
return new ColorPository();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes and saves color pository to the supplied file.
|
||||
*/
|
||||
public static void saveColorPository (ColorPository posit, File root)
|
||||
{
|
||||
File path = new File(root, CONFIG_PATH);
|
||||
try {
|
||||
CompiledConfig.saveConfig(path, posit);
|
||||
} catch (IOException ioe) {
|
||||
Log.warning("Failure saving color pository " +
|
||||
"[path=" + path + ", error=" + ioe + "].");
|
||||
}
|
||||
}
|
||||
|
||||
/** Our mapping from class names to class records. */
|
||||
protected HashIntMap _classes = new HashIntMap();
|
||||
|
||||
/** Increase this value when object's serialized state is impacted by
|
||||
* a class change (modification of fields, inheritance). */
|
||||
private static final long serialVersionUID = 1;
|
||||
|
||||
/**
|
||||
* The path (relative to the resource directory) at which the
|
||||
* serialized recolorization repository should be loaded and stored.
|
||||
*/
|
||||
protected static final String CONFIG_PATH = "config/media/colordefs.dat";
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// $Id: ColorUtil.java 4191 2006-06-13 22:42:20Z ray $
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// 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.awt.Color;
|
||||
|
||||
/**
|
||||
* Utilities to manipulate colors.
|
||||
*/
|
||||
public class ColorUtil
|
||||
{
|
||||
/**
|
||||
* Blends the two supplied colors.
|
||||
*
|
||||
* @return a color halfway between the two colors.
|
||||
*/
|
||||
public static final Color blend (Color c1, Color c2)
|
||||
{
|
||||
return new Color((c1.getRed() + c2.getRed()) >> 1,
|
||||
(c1.getGreen() + c2.getGreen()) >> 1,
|
||||
(c1.getBlue() + c2.getBlue()) >> 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Blends the two supplied colors, using the supplied percentage
|
||||
* as the amount of the first color to use.
|
||||
*
|
||||
* @param firstperc The percentage of the first color to use, from 0.0f
|
||||
* to 1.0f inclusive.
|
||||
*/
|
||||
public static final Color blend (Color c1, Color c2, float firstperc)
|
||||
{
|
||||
float p2 = 1.0f - firstperc;
|
||||
return new Color((int) (c1.getRed() * firstperc + c2.getRed() * p2),
|
||||
(int) (c1.getGreen() * firstperc + c2.getGreen() * p2),
|
||||
(int) (c1.getBlue() * firstperc + c2.getBlue() * p2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
//
|
||||
// $Id: Colorization.java 4191 2006-06-13 22:42:20Z ray $
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// 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.awt.Color;
|
||||
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
/**
|
||||
* Used to support recoloring images.
|
||||
*/
|
||||
public class Colorization
|
||||
{
|
||||
/** Every colorization must have a unique id that can be used to
|
||||
* compare a particular colorization record with another. */
|
||||
public int colorizationId;
|
||||
|
||||
/** The root color for the colorization. */
|
||||
public Color rootColor;
|
||||
|
||||
/** The range around the root color that will be colorized (in
|
||||
* delta-hue, delta-saturation, delta-value. */
|
||||
public float[] range;
|
||||
|
||||
/** The adjustments to make to hue, saturation and value. */
|
||||
public float[] offsets;
|
||||
|
||||
/**
|
||||
* Constructs a colorization record with the specified identifier.
|
||||
*/
|
||||
public Colorization (int colorizationId, Color rootColor,
|
||||
float[] range, float[] offsets)
|
||||
{
|
||||
this.colorizationId = colorizationId;
|
||||
this.rootColor = rootColor;
|
||||
this.range = range;
|
||||
this.offsets = offsets;
|
||||
|
||||
// compute our HSV and fixed HSV
|
||||
_hsv = Color.RGBtoHSB(rootColor.getRed(), rootColor.getGreen(),
|
||||
rootColor.getBlue(), null);
|
||||
_fhsv = toFixedHSV(_hsv, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root color adjusted by the colorization.
|
||||
*/
|
||||
public Color getColorizedRoot ()
|
||||
{
|
||||
return new Color(recolorColor(_hsv));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the supplied color by the offests in this colorization,
|
||||
* taking the appropriate measures for hue (wrapping it around) and
|
||||
* saturation and value (clipping).
|
||||
*
|
||||
* @return the RGB value of the recolored color.
|
||||
*/
|
||||
public int recolorColor (float[] hsv)
|
||||
{
|
||||
// for hue, we wrap around
|
||||
float hue = hsv[0] + offsets[0];
|
||||
if (hue > 1.0) {
|
||||
hue -= 1.0;
|
||||
}
|
||||
|
||||
// otherwise we clip
|
||||
float sat = Math.min(Math.max(hsv[1] + offsets[1], 0), 1);
|
||||
float val = Math.min(Math.max(hsv[2] + offsets[2], 0), 1);
|
||||
|
||||
// convert back to RGB space
|
||||
return Color.HSBtoRGB(hue, sat, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this colorization matches the supplied color, false
|
||||
* otherwise.
|
||||
*
|
||||
* @param hsv the HSV values for the color in question.
|
||||
* @param fhsv the HSV values converted to fixed point via {@link
|
||||
* #toFixedHSV} for the color in question.
|
||||
*/
|
||||
public boolean matches (float[] hsv, int[] fhsv)
|
||||
{
|
||||
// check to see that this color is sufficiently "close" to the
|
||||
// root color based on the supplied distance parameters
|
||||
if (distance(fhsv[0], _fhsv[0], Short.MAX_VALUE) >=
|
||||
range[0] * Short.MAX_VALUE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// saturation and value don't wrap around like hue
|
||||
if (Math.abs(_hsv[1] - hsv[1]) >= range[1] ||
|
||||
Math.abs(_hsv[2] - hsv[2]) >= range[2]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// documentation inherited
|
||||
public int hashCode ()
|
||||
{
|
||||
return colorizationId ^ rootColor.hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this colorization to another based on id.
|
||||
*/
|
||||
public boolean equals (Object other)
|
||||
{
|
||||
if (other instanceof Colorization) {
|
||||
return ((Colorization)other).colorizationId == colorizationId;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of this colorization.
|
||||
*/
|
||||
public String toString ()
|
||||
{
|
||||
return String.valueOf(colorizationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a long string representation of this colorization.
|
||||
*/
|
||||
public String toVerboseString ()
|
||||
{
|
||||
return StringUtil.fieldsToString(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts floating point HSV values to a fixed point integer
|
||||
* representation.
|
||||
*
|
||||
* @param hsv the HSV values to be converted.
|
||||
* @param fhsv the destination array into which the fixed values will
|
||||
* be stored. If this is null, a new array will be created of the
|
||||
* appropriate length.
|
||||
*
|
||||
* @return the <code>fhsv</code> parameter if it was non-null or the
|
||||
* newly created target array.
|
||||
*/
|
||||
public static int[] toFixedHSV (float[] hsv, int[] fhsv)
|
||||
{
|
||||
if (fhsv == null) {
|
||||
fhsv = new int[hsv.length];
|
||||
}
|
||||
for (int i = 0; i < hsv.length; i++) {
|
||||
// fhsv[i] = (int)(hsv[i]*Integer.MAX_VALUE);
|
||||
fhsv[i] = (int)(hsv[i]*Short.MAX_VALUE);
|
||||
}
|
||||
return fhsv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the distance between the supplied to numbers modulo N.
|
||||
*/
|
||||
public static int distance (int a, int b, int N)
|
||||
{
|
||||
return (a > b) ? Math.min(a-b, b+N-a) : Math.min(b-a, a+N-b);
|
||||
}
|
||||
|
||||
/** Fixed HSV values for our root color; used when calculating
|
||||
* recolorizations using this colorization. */
|
||||
protected int[] _fhsv;
|
||||
|
||||
/** HSV values for our root color; used when calculating
|
||||
* recolorizations using this colorization. */
|
||||
protected float[] _hsv;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.threerings.media.image;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
public class CompositeMirage implements Mirage
|
||||
{
|
||||
public CompositeMirage (Mirage[] mirages)
|
||||
{
|
||||
_mirages = mirages;
|
||||
}
|
||||
|
||||
public CompositeMirage (Mirage mirage1, Mirage mirage2)
|
||||
{
|
||||
_mirages = new Mirage[]{mirage1, mirage2};
|
||||
}
|
||||
|
||||
// documentation inherited from interface Mirage
|
||||
public long getEstimatedMemoryUsage ()
|
||||
{
|
||||
// Return the total memory of our component mirages.
|
||||
long mem = 0;
|
||||
for (Mirage m : _mirages) {
|
||||
mem += m.getEstimatedMemoryUsage();
|
||||
}
|
||||
|
||||
return mem;
|
||||
}
|
||||
|
||||
// documentation inherited from interface Mirage
|
||||
public int getHeight ()
|
||||
{
|
||||
// Return the maximal height of our component mirages.
|
||||
int height = 0;
|
||||
for (Mirage m : _mirages) {
|
||||
height = Math.max(height, m.getHeight());
|
||||
}
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
// documentation inherited from interface Mirage
|
||||
public int getWidth ()
|
||||
{
|
||||
// Return the maximal width of our component mirages.
|
||||
int width = 0;
|
||||
for (Mirage m : _mirages) {
|
||||
width = Math.max(width, m.getWidth());
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
// documentation inheritd from interface Mirage
|
||||
public BufferedImage getSnapshot ()
|
||||
{
|
||||
BufferedImage img = new BufferedImage(getWidth(), getHeight(),
|
||||
BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D gfx = img.createGraphics();
|
||||
|
||||
try {
|
||||
for (Mirage m : _mirages) {
|
||||
m.paint(gfx, 0, 0);
|
||||
}
|
||||
} finally {
|
||||
gfx.dispose();
|
||||
}
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
// documentation inheritd from interface Mirage
|
||||
public boolean hitTest (int x, int y)
|
||||
{
|
||||
// If it hits any of our mirages, it hits us.
|
||||
for (Mirage m : _mirages) {
|
||||
if (m.hitTest(x, y)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// documentation inheritd from interface Mirage
|
||||
public void paint (Graphics2D gfx, int x, int y)
|
||||
{
|
||||
// Paint everyone.
|
||||
for (Mirage m : _mirages) {
|
||||
m.paint(gfx, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
/** All the component mirages we're made up of. */
|
||||
protected Mirage[] _mirages;
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
//
|
||||
// $Id: FastImageIO.java 4191 2006-06-13 22:42:20Z ray $
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// 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.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);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// $Id: ImageDataProvider.java 4191 2006-06-13 22:42:20Z ray $
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// 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.io.IOException;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
/**
|
||||
* Provides access to image data for the image with the specified
|
||||
* path. Images loaded from different data providers (which are
|
||||
* differentiated by reference equality) will be considered distinct
|
||||
* images with respect to caching.
|
||||
*/
|
||||
public interface ImageDataProvider
|
||||
{
|
||||
/**
|
||||
* Returns a string identifier for this image data provider which wil
|
||||
* be used to differentiate it from other providers and thus should be
|
||||
* unique.
|
||||
*/
|
||||
public String getIdent ();
|
||||
|
||||
/**
|
||||
* Returns the image at the specified path.
|
||||
*/
|
||||
public BufferedImage loadImage (String path) throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
//
|
||||
// $Id: ImageManager.java 4007 2006-04-10 08:59:30Z mdb $
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// 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.awt.Component;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.Transparency;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
|
||||
import com.samskivert.swing.RuntimeAdjust;
|
||||
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.media.MediaPrefs;
|
||||
import com.threerings.resource.ResourceManager;
|
||||
|
||||
/**
|
||||
* Provides a single point of access for image retrieval and caching.
|
||||
*/
|
||||
public class ImageManager
|
||||
implements ImageUtil.ImageCreator
|
||||
{
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface allows the image manager to create images that are in a
|
||||
* format optimal for rendering to the screen.
|
||||
*/
|
||||
public interface OptimalImageCreator
|
||||
{
|
||||
/**
|
||||
* Requests that a blank image be created that is in a format and of a
|
||||
* depth that are optimal for rendering to the screen.
|
||||
*/
|
||||
public BufferedImage createImage (int width, int height, int trans);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an image manager with the specified {@link ResourceManager}
|
||||
* from which it will obtain its data.
|
||||
*/
|
||||
public ImageManager (ResourceManager rmgr, OptimalImageCreator icreator)
|
||||
{
|
||||
_rmgr = rmgr;
|
||||
_icreator = icreator;
|
||||
|
||||
// create our image cache
|
||||
int icsize = _cacheSize.getValue();
|
||||
Log.debug("Creating image cache [size=" + icsize + "k].");
|
||||
_ccache = new LRUHashMap(icsize * 1024, new LRUHashMap.ItemSizer() {
|
||||
public int computeSize (Object value) {
|
||||
return (int)((CacheRecord)value).getEstimatedMemoryUsage();
|
||||
}
|
||||
});
|
||||
_ccache.setTracking(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience constructor that creates an {@link AWTImageCreator} for
|
||||
* use by the image manager.
|
||||
*/
|
||||
public ImageManager (ResourceManager rmgr, Component context)
|
||||
{
|
||||
this(rmgr, new AWTImageCreator(context));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a buffered image, optimized for display on our graphics
|
||||
* device.
|
||||
*/
|
||||
public BufferedImage createImage (int width, int height, int transparency)
|
||||
{
|
||||
return _icreator.createImage(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.isBlank(path)) {
|
||||
String errmsg = "Invalid image path [rset=" + rset +
|
||||
", path=" + path + "]";
|
||||
throw new IllegalArgumentException(errmsg);
|
||||
}
|
||||
|
||||
return getImage(getImageKey(rset, path), zations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads (and caches) the specified image from the resource manager
|
||||
* using the supplied path to identify the image.
|
||||
*
|
||||
* <p> Additionally the image is optimized for display in the current
|
||||
* graphics configuration. Consider using {@link #getMirage(ImageKey)}
|
||||
* instead of prepared images as they (some day) will automatically
|
||||
* use volatile images to increase performance.
|
||||
*/
|
||||
public BufferedImage getPreparedImage (String path)
|
||||
{
|
||||
return getPreparedImage(null, path, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads (and caches) the specified image from the resource manager,
|
||||
* obtaining the image from the supplied resource set.
|
||||
*
|
||||
* <p> Additionally the image is optimized for display in the current
|
||||
* graphics configuration. Consider using {@link #getMirage(ImageKey)}
|
||||
* instead of prepared images as they (some day) will automatically
|
||||
* use volatile images to increase performance.
|
||||
*/
|
||||
public BufferedImage getPreparedImage (String rset, String path)
|
||||
{
|
||||
return getPreparedImage(rset, path, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads (and caches) the specified image from the resource manager,
|
||||
* obtaining the image from the supplied resource set and applying the
|
||||
* using the supplied path to identify the image.
|
||||
*
|
||||
* <p> Additionally the image is optimized for display in the current
|
||||
* graphics configuration. Consider using {@link
|
||||
* #getMirage(ImageKey,Colorization[])} instead of prepared images as
|
||||
* they (some day) will automatically use volatile images to increase
|
||||
* performance.
|
||||
*/
|
||||
public BufferedImage getPreparedImage (
|
||||
String rset, String path, Colorization[] zations)
|
||||
{
|
||||
BufferedImage image = getImage(rset, path, zations);
|
||||
BufferedImage prepped = null;
|
||||
if (image != null) {
|
||||
prepped = createImage(image.getWidth(), image.getHeight(),
|
||||
image.getColorModel().getTransparency());
|
||||
Graphics2D pg = prepped.createGraphics();
|
||||
pg.drawImage(image, 0, 0, null);
|
||||
pg.dispose();
|
||||
}
|
||||
return prepped;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = null;
|
||||
synchronized (_ccache) {
|
||||
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 blank image instead
|
||||
image = new BufferedImage(10, 10, BufferedImage.TYPE_BYTE_INDEXED);
|
||||
}
|
||||
|
||||
// Log.info("Loaded " + key.path + ", image=" + image +
|
||||
// ", size=" + ImageUtil.getEstimatedMemoryUsage(image));
|
||||
|
||||
// create a cache record
|
||||
crec = new CacheRecord(key, image);
|
||||
synchronized (_ccache) {
|
||||
_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 BufferedImage loadImage (String path)
|
||||
throws IOException {
|
||||
// first attempt to load the image from the specified
|
||||
// resource set
|
||||
try {
|
||||
return read(_rmgr.getImageResource(rset, path));
|
||||
} catch (FileNotFoundException fnfe) {
|
||||
// fall back to trying the classpath
|
||||
return read(_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)
|
||||
{
|
||||
// if (EventQueue.isDispatchThread()) {
|
||||
// Log.info("Loading image on AWT thread " + key + ".");
|
||||
// }
|
||||
|
||||
BufferedImage image = null;
|
||||
try {
|
||||
Log.debug("Loading image " + key + ".");
|
||||
image = key.daprov.loadImage(key.path);
|
||||
if (image == null) {
|
||||
Log.warning("ImageIO.read(" + key + ") returned null.");
|
||||
}
|
||||
|
||||
} 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);
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for loading images.
|
||||
*/
|
||||
protected static BufferedImage read (ImageInputStream iis)
|
||||
throws IOException
|
||||
{
|
||||
BufferedImage image = ImageIO.read(iis);
|
||||
closeIIS(iis);
|
||||
return image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for closing image input streams.
|
||||
*/
|
||||
protected static void closeIIS (ImageInputStream iis)
|
||||
{
|
||||
if (iis == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 it's fucking inflaters; for the
|
||||
// love of humanity
|
||||
if (!"closed".equals(ioe.getMessage())) {
|
||||
Log.warning("Failure closing image input '" + iis + "'.");
|
||||
Log.logStackTrace(ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
BufferedImage src = null;
|
||||
|
||||
if (bounds == null) {
|
||||
// if they specified no bounds, we need to load up the raw
|
||||
// image and determine its bounds so that we can pass those
|
||||
// along to the created mirage
|
||||
src = getImage(key, zations);
|
||||
bounds = new Rectangle(0, 0, src.getWidth(), src.getHeight());
|
||||
|
||||
} else if (!_prepareImages.getValue()) {
|
||||
src = getImage(key, zations);
|
||||
src = src.getSubimage(bounds.x, bounds.y,
|
||||
bounds.width, bounds.height);
|
||||
}
|
||||
|
||||
if (_runBlank.getValue()) {
|
||||
return new BlankMirage(bounds.width, bounds.height);
|
||||
} else if (_prepareImages.getValue()) {
|
||||
return new CachedVolatileMirage(this, key, bounds, zations);
|
||||
} else {
|
||||
return new BufferedMirage(src);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the image creator that can be used to create buffered images
|
||||
* optimized for rendering to the screen.
|
||||
*/
|
||||
public OptimalImageCreator getImageCreator ()
|
||||
{
|
||||
return _icreator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
int[] eff = null;
|
||||
synchronized (_ccache) {
|
||||
Iterator iter = _ccache.values().iterator();
|
||||
while (iter.hasNext()) {
|
||||
size += ((CacheRecord)iter.next()).getEstimatedMemoryUsage();
|
||||
}
|
||||
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 (ImageKey key, BufferedImage source)
|
||||
{
|
||||
_key = key;
|
||||
_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;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
BufferedImage cimage = ImageUtil.recolorImage(_source, zations);
|
||||
_colorized.add(new Tuple(zations, cimage));
|
||||
return cimage;
|
||||
|
||||
} catch (Exception re) {
|
||||
Log.warning("Failure recoloring image [source" + _key +
|
||||
", zations=" + StringUtil.toString(zations) +
|
||||
", error=" + re + "].");
|
||||
// return the uncolorized version
|
||||
return _source;
|
||||
}
|
||||
}
|
||||
|
||||
public long getEstimatedMemoryUsage ()
|
||||
{
|
||||
return ImageUtil.getEstimatedMemoryUsage(_source);
|
||||
}
|
||||
|
||||
public String toString ()
|
||||
{
|
||||
return "[key=" + _key + ", wid=" + _source.getWidth() +
|
||||
", hei=" + _source.getHeight() +
|
||||
", ccount=" + ((_colorized == null) ? 0 : _colorized.size()) +
|
||||
"]";
|
||||
}
|
||||
|
||||
protected ImageKey _key;
|
||||
protected BufferedImage _source;
|
||||
protected ArrayList _colorized;
|
||||
}
|
||||
|
||||
/** A reference to the resource manager via which we load image data
|
||||
* by default. */
|
||||
protected ResourceManager _rmgr;
|
||||
|
||||
/** We use this to create images optimized for rendering. */
|
||||
protected OptimalImageCreator _icreator;
|
||||
|
||||
/** 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 300 seconds. */
|
||||
protected Throttle _cacheStatThrottle = new Throttle(1, 300000L);
|
||||
|
||||
/** Our default data provider. */
|
||||
protected ImageDataProvider _defaultProvider = new ImageDataProvider() {
|
||||
public BufferedImage loadImage (String path) throws IOException {
|
||||
return read(_rmgr.getImageResource(path));
|
||||
}
|
||||
|
||||
public String getIdent () {
|
||||
return "rmgr:default";
|
||||
}
|
||||
};
|
||||
|
||||
/** Data providers for different resource sets. */
|
||||
protected HashMap _providers = new HashMap();
|
||||
|
||||
/** Register our image cache size with the runtime adjustments
|
||||
* framework. */
|
||||
protected static RuntimeAdjust.IntAdjust _cacheSize =
|
||||
new RuntimeAdjust.IntAdjust(
|
||||
"Size (in kb of memory used) of the image manager LRU cache " +
|
||||
"[requires restart]", "narya.media.image.cache_size",
|
||||
MediaPrefs.config, 2048);
|
||||
|
||||
/** Controls whether or not we prepare images or use raw versions. */
|
||||
protected static RuntimeAdjust.BooleanAdjust _prepareImages =
|
||||
new RuntimeAdjust.BooleanAdjust(
|
||||
"Cause image manager to optimize all images for display.",
|
||||
"narya.media.image.prep_images", MediaPrefs.config, true);
|
||||
|
||||
/** A debug toggle for running entirely without rendering images. */
|
||||
protected static RuntimeAdjust.BooleanAdjust _runBlank =
|
||||
new RuntimeAdjust.BooleanAdjust(
|
||||
"Cause image manager to return blank images.",
|
||||
"narya.media.image.run_blank", MediaPrefs.config, false);
|
||||
}
|
||||
@@ -0,0 +1,647 @@
|
||||
//
|
||||
// $Id: ImageUtil.java 3965 2006-03-21 02:32:27Z mthomas $
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// 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.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 java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
|
||||
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)) {
|
||||
String errmsg = "Unable to recolor images with non-index color " +
|
||||
"model [cm=" + cm.getClass() + "]";
|
||||
throw new RuntimeException(errmsg);
|
||||
}
|
||||
|
||||
// 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];
|
||||
int tpixel = -1;
|
||||
for (int i = 0; i < size; i++) {
|
||||
int value = rgbs[i];
|
||||
|
||||
// don't fiddle with alpha pixels
|
||||
if ((value & 0xFF000000) == 0) {
|
||||
tpixel = i;
|
||||
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[i] = 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 = (BufferedImage)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;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// $Id: Mirage.java 4191 2006-06-13 22:42:20Z ray $
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// 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.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
/**
|
||||
* Provides an interface via which images can be accessed in a way that
|
||||
* allows them to optionally be located in video memory where that affords
|
||||
* performance improvements.
|
||||
*/
|
||||
public interface Mirage
|
||||
{
|
||||
/**
|
||||
* Renders this mirage at the specified position in the supplied
|
||||
* graphics context.
|
||||
*/
|
||||
public void paint (Graphics2D gfx, int x, int y);
|
||||
|
||||
/**
|
||||
* Returns the width of this mirage.
|
||||
*/
|
||||
public int getWidth ();
|
||||
|
||||
/**
|
||||
* Returns the height of this mirage.
|
||||
*/
|
||||
public int getHeight ();
|
||||
|
||||
/**
|
||||
* Returns true if this mirage contains a non-transparent pixel at the
|
||||
* specified coordinate.
|
||||
*/
|
||||
public boolean hitTest (int x, int y);
|
||||
|
||||
/**
|
||||
* Returns a snapshot of this mirage as a buffered image. The snapshot
|
||||
* should <em>not</em> be modified by the caller.
|
||||
*/
|
||||
public BufferedImage getSnapshot ();
|
||||
|
||||
/**
|
||||
* Returns an estimate of the memory consumed by this mirage's image
|
||||
* raster data.
|
||||
*/
|
||||
public long getEstimatedMemoryUsage ();
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// $Id: MirageIcon.java 4191 2006-06-13 22:42:20Z ray $
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// 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.awt.Component;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Graphics;
|
||||
|
||||
import javax.swing.Icon;
|
||||
|
||||
/**
|
||||
* Implements the Swing {@link Icon} interface with a mirage providing the
|
||||
* image information.
|
||||
*/
|
||||
public class MirageIcon implements Icon
|
||||
{
|
||||
public MirageIcon (Mirage mirage)
|
||||
{
|
||||
_mirage = mirage;
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public void paintIcon (Component c, Graphics g, int x, int y)
|
||||
{
|
||||
_mirage.paint((Graphics2D)g, x, y);
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public int getIconWidth()
|
||||
{
|
||||
return _mirage.getWidth();
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public int getIconHeight()
|
||||
{
|
||||
return _mirage.getHeight();
|
||||
}
|
||||
|
||||
protected Mirage _mirage;
|
||||
}
|
||||
@@ -0,0 +1,776 @@
|
||||
//
|
||||
// $Id: Quantize.java 4031 2006-04-18 20:35:28Z ray $
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// 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;
|
||||
|
||||
/*
|
||||
* @(#)Quantize.java 0.90 9/19/00 Adam Doppelt
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calculates a reduced color
|
||||
*
|
||||
* Three Rings note: Code taken from
|
||||
* <a href="http://www.gurge.com/amd/java/quantize/">Adam Doppelt</a>, who
|
||||
* adapted it from other code. Feel the love.<p>
|
||||
*
|
||||
* RenderingHints is supposed to provide a way to block dithering, but I have
|
||||
* not been able to get that to work. It always dithers, so we use this
|
||||
* class instead.
|
||||
* <p>
|
||||
*
|
||||
* The following modifications were added to the original code:
|
||||
* - Made it work with image data with transparent pixels.
|
||||
* - Clarified documentation of the main method.
|
||||
* - Changed the 'QUICK' constant to false for better quantization.
|
||||
* - Fixed an integer overflow that caused a bug quantizing large images.
|
||||
*
|
||||
* <p><p>
|
||||
*
|
||||
* Original headers follow:
|
||||
* </pre>
|
||||
*
|
||||
*
|
||||
*
|
||||
* An efficient color quantization algorithm, adapted from the C++
|
||||
* implementation quantize.c in <a
|
||||
* href="http://www.imagemagick.org/">ImageMagick</a>. The pixels for
|
||||
* an image are placed into an oct tree. The oct tree is reduced in
|
||||
* size, and the pixels from the original image are reassigned to the
|
||||
* nodes in the reduced tree.<p>
|
||||
*
|
||||
* Here is the copyright notice from ImageMagick:
|
||||
*
|
||||
* <pre>
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
% Permission is hereby granted, free of charge, to any person obtaining a %
|
||||
% copy of this software and associated documentation files ("ImageMagick"), %
|
||||
% to deal in ImageMagick without restriction, including without limitation %
|
||||
% the rights to use, copy, modify, merge, publish, distribute, sublicense, %
|
||||
% and/or sell copies of ImageMagick, and to permit persons to whom the %
|
||||
% ImageMagick is furnished to do so, subject to the following conditions: %
|
||||
% %
|
||||
% The above copyright notice and this permission notice shall be included in %
|
||||
% all copies or substantial portions of ImageMagick. %
|
||||
% %
|
||||
% The software is provided "as is", without warranty of any kind, express or %
|
||||
% implied, including but not limited to the warranties of merchantability, %
|
||||
% fitness for a particular purpose and noninfringement. In no event shall %
|
||||
% E. I. du Pont de Nemours and Company be liable for any claim, damages or %
|
||||
% other liability, whether in an action of contract, tort or otherwise, %
|
||||
% arising from, out of or in connection with ImageMagick or the use or other %
|
||||
% dealings in ImageMagick. %
|
||||
% %
|
||||
% Except as contained in this notice, the name of the E. I. du Pont de %
|
||||
% Nemours and Company shall not be used in advertising or otherwise to %
|
||||
% promote the sale, use or other dealings in ImageMagick without prior %
|
||||
% written authorization from the E. I. du Pont de Nemours and Company. %
|
||||
% %
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
</pre>
|
||||
*
|
||||
*
|
||||
* @version 0.90 19 Sep 2000
|
||||
* @author <a href="http://www.gurge.com/amd/">Adam Doppelt</a>
|
||||
*/
|
||||
public class Quantize {
|
||||
|
||||
/*
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
% %
|
||||
% %
|
||||
% %
|
||||
% QQQ U U AAA N N TTTTT IIIII ZZZZZ EEEEE %
|
||||
% Q Q U U A A NN N T I ZZ E %
|
||||
% Q Q U U AAAAA N N N T I ZZZ EEEEE %
|
||||
% Q QQ U U A A N NN T I ZZ E %
|
||||
% QQQQ UUU A A N N T IIIII ZZZZZ EEEEE %
|
||||
% %
|
||||
% %
|
||||
% Reduce the Number of Unique Colors in an Image %
|
||||
% %
|
||||
% %
|
||||
% Software Design %
|
||||
% John Cristy %
|
||||
% July 1992 %
|
||||
% %
|
||||
% %
|
||||
% Copyright 1998 E. I. du Pont de Nemours and Company %
|
||||
% %
|
||||
% Permission is hereby granted, free of charge, to any person obtaining a %
|
||||
% copy of this software and associated documentation files ("ImageMagick"), %
|
||||
% to deal in ImageMagick without restriction, including without limitation %
|
||||
% the rights to use, copy, modify, merge, publish, distribute, sublicense, %
|
||||
% and/or sell copies of ImageMagick, and to permit persons to whom the %
|
||||
% ImageMagick is furnished to do so, subject to the following conditions: %
|
||||
% %
|
||||
% The above copyright notice and this permission notice shall be included in %
|
||||
% all copies or substantial portions of ImageMagick. %
|
||||
% %
|
||||
% The software is provided "as is", without warranty of any kind, express or %
|
||||
% implied, including but not limited to the warranties of merchantability, %
|
||||
% fitness for a particular purpose and noninfringement. In no event shall %
|
||||
% E. I. du Pont de Nemours and Company be liable for any claim, damages or %
|
||||
% other liability, whether in an action of contract, tort or otherwise, %
|
||||
% arising from, out of or in connection with ImageMagick or the use or other %
|
||||
% dealings in ImageMagick. %
|
||||
% %
|
||||
% Except as contained in this notice, the name of the E. I. du Pont de %
|
||||
% Nemours and Company shall not be used in advertising or otherwise to %
|
||||
% promote the sale, use or other dealings in ImageMagick without prior %
|
||||
% written authorization from the E. I. du Pont de Nemours and Company. %
|
||||
% %
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%
|
||||
% Realism in computer graphics typically requires using 24 bits/pixel to
|
||||
% generate an image. Yet many graphic display devices do not contain
|
||||
% the amount of memory necessary to match the spatial and color
|
||||
% resolution of the human eye. The QUANTIZE program takes a 24 bit
|
||||
% image and reduces the number of colors so it can be displayed on
|
||||
% raster device with less bits per pixel. In most instances, the
|
||||
% quantized image closely resembles the original reference image.
|
||||
%
|
||||
% A reduction of colors in an image is also desirable for image
|
||||
% transmission and real-time animation.
|
||||
%
|
||||
% Function Quantize takes a standard RGB or monochrome images and quantizes
|
||||
% them down to some fixed number of colors.
|
||||
%
|
||||
% For purposes of color allocation, an image is a set of n pixels, where
|
||||
% each pixel is a point in RGB space. RGB space is a 3-dimensional
|
||||
% vector space, and each pixel, pi, is defined by an ordered triple of
|
||||
% red, green, and blue coordinates, (ri, gi, bi).
|
||||
%
|
||||
% Each primary color component (red, green, or blue) represents an
|
||||
% intensity which varies linearly from 0 to a maximum value, cmax, which
|
||||
% corresponds to full saturation of that color. Color allocation is
|
||||
% defined over a domain consisting of the cube in RGB space with
|
||||
% opposite vertices at (0,0,0) and (cmax,cmax,cmax). QUANTIZE requires
|
||||
% cmax = 255.
|
||||
%
|
||||
% The algorithm maps this domain onto a tree in which each node
|
||||
% represents a cube within that domain. In the following discussion
|
||||
% these cubes are defined by the coordinate of two opposite vertices:
|
||||
% The vertex nearest the origin in RGB space and the vertex farthest
|
||||
% from the origin.
|
||||
%
|
||||
% The tree's root node represents the the entire domain, (0,0,0) through
|
||||
% (cmax,cmax,cmax). Each lower level in the tree is generated by
|
||||
% subdividing one node's cube into eight smaller cubes of equal size.
|
||||
% This corresponds to bisecting the parent cube with planes passing
|
||||
% through the midpoints of each edge.
|
||||
%
|
||||
% The basic algorithm operates in three phases: Classification,
|
||||
% Reduction, and Assignment. Classification builds a color
|
||||
% description tree for the image. Reduction collapses the tree until
|
||||
% the number it represents, at most, the number of colors desired in the
|
||||
% output image. Assignment defines the output image's color map and
|
||||
% sets each pixel's color by reclassification in the reduced tree.
|
||||
% Our goal is to minimize the numerical discrepancies between the original
|
||||
% colors and quantized colors (quantization error).
|
||||
%
|
||||
% Classification begins by initializing a color description tree of
|
||||
% sufficient depth to represent each possible input color in a leaf.
|
||||
% However, it is impractical to generate a fully-formed color
|
||||
% description tree in the classification phase for realistic values of
|
||||
% cmax. If colors components in the input image are quantized to k-bit
|
||||
% precision, so that cmax= 2k-1, the tree would need k levels below the
|
||||
% root node to allow representing each possible input color in a leaf.
|
||||
% This becomes prohibitive because the tree's total number of nodes is
|
||||
% 1 + sum(i=1,k,8k).
|
||||
%
|
||||
% A complete tree would require 19,173,961 nodes for k = 8, cmax = 255.
|
||||
% Therefore, to avoid building a fully populated tree, QUANTIZE: (1)
|
||||
% Initializes data structures for nodes only as they are needed; (2)
|
||||
% Chooses a maximum depth for the tree as a function of the desired
|
||||
% number of colors in the output image (currently log2(colormap size)).
|
||||
%
|
||||
% For each pixel in the input image, classification scans downward from
|
||||
% the root of the color description tree. At each level of the tree it
|
||||
% identifies the single node which represents a cube in RGB space
|
||||
% containing the pixel's color. It updates the following data for each
|
||||
% such node:
|
||||
%
|
||||
% n1: Number of pixels whose color is contained in the RGB cube
|
||||
% which this node represents;
|
||||
%
|
||||
% n2: Number of pixels whose color is not represented in a node at
|
||||
% lower depth in the tree; initially, n2 = 0 for all nodes except
|
||||
% leaves of the tree.
|
||||
%
|
||||
% Sr, Sg, Sb: Sums of the red, green, and blue component values for
|
||||
% all pixels not classified at a lower depth. The combination of
|
||||
% these sums and n2 will ultimately characterize the mean color of a
|
||||
% set of pixels represented by this node.
|
||||
%
|
||||
% E: The distance squared in RGB space between each pixel contained
|
||||
% within a node and the nodes' center. This represents the quantization
|
||||
% error for a node.
|
||||
%
|
||||
% Reduction repeatedly prunes the tree until the number of nodes with
|
||||
% n2 > 0 is less than or equal to the maximum number of colors allowed
|
||||
% in the output image. On any given iteration over the tree, it selects
|
||||
% those nodes whose E count is minimal for pruning and merges their
|
||||
% color statistics upward. It uses a pruning threshold, Ep, to govern
|
||||
% node selection as follows:
|
||||
%
|
||||
% Ep = 0
|
||||
% while number of nodes with (n2 > 0) > required maximum number of colors
|
||||
% prune all nodes such that E <= Ep
|
||||
% Set Ep to minimum E in remaining nodes
|
||||
%
|
||||
% This has the effect of minimizing any quantization error when merging
|
||||
% two nodes together.
|
||||
%
|
||||
% When a node to be pruned has offspring, the pruning procedure invokes
|
||||
% itself recursively in order to prune the tree from the leaves upward.
|
||||
% n2, Sr, Sg, and Sb in a node being pruned are always added to the
|
||||
% corresponding data in that node's parent. This retains the pruned
|
||||
% node's color characteristics for later averaging.
|
||||
%
|
||||
% For each node, n2 pixels exist for which that node represents the
|
||||
% smallest volume in RGB space containing those pixel's colors. When n2
|
||||
% > 0 the node will uniquely define a color in the output image. At the
|
||||
% beginning of reduction, n2 = 0 for all nodes except a the leaves of
|
||||
% the tree which represent colors present in the input image.
|
||||
%
|
||||
% The other pixel count, n1, indicates the total number of colors
|
||||
% within the cubic volume which the node represents. This includes n1 -
|
||||
% n2 pixels whose colors should be defined by nodes at a lower level in
|
||||
% the tree.
|
||||
%
|
||||
% Assignment generates the output image from the pruned tree. The
|
||||
% output image consists of two parts: (1) A color map, which is an
|
||||
% array of color descriptions (RGB triples) for each color present in
|
||||
% the output image; (2) A pixel array, which represents each pixel as
|
||||
% an index into the color map array.
|
||||
%
|
||||
% First, the assignment phase makes one pass over the pruned color
|
||||
% description tree to establish the image's color map. For each node
|
||||
% with n2 > 0, it divides Sr, Sg, and Sb by n2 . This produces the
|
||||
% mean color of all pixels that classify no lower than this node. Each
|
||||
% of these colors becomes an entry in the color map.
|
||||
%
|
||||
% Finally, the assignment phase reclassifies each pixel in the pruned
|
||||
% tree to identify the deepest node containing the pixel's color. The
|
||||
% pixel's value in the pixel array becomes the index of this node's mean
|
||||
% color in the color map.
|
||||
%
|
||||
% With the permission of USC Information Sciences Institute, 4676 Admiralty
|
||||
% Way, Marina del Rey, California 90292, this code was adapted from module
|
||||
% ALCOLS written by Paul Raveling.
|
||||
%
|
||||
% The names of ISI and USC are not used in advertising or publicity
|
||||
% pertaining to distribution of the software without prior specific
|
||||
% written permission from ISI.
|
||||
%
|
||||
*/
|
||||
|
||||
final static boolean QUICK = false;
|
||||
|
||||
final static int MAX_RGB = 255;
|
||||
final static int MAX_NODES = 266817;
|
||||
final static int MAX_TREE_DEPTH = 8;
|
||||
|
||||
// these are precomputed in advance
|
||||
static int SQUARES[];
|
||||
static int SHIFT[];
|
||||
|
||||
static {
|
||||
SQUARES = new int[MAX_RGB + MAX_RGB + 1];
|
||||
for (int i= -MAX_RGB; i <= MAX_RGB; i++) {
|
||||
SQUARES[i + MAX_RGB] = i * i;
|
||||
}
|
||||
|
||||
SHIFT = new int[MAX_TREE_DEPTH + 1];
|
||||
for (int i = 0; i < MAX_TREE_DEPTH + 1; ++i) {
|
||||
SHIFT[i] = 1 << (15 - i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce the image to the given number of colors.
|
||||
*
|
||||
* @param pixels an in/out parameter that should initially contain
|
||||
* [A]RGB values but that will contain color palette indicies upon return.
|
||||
*
|
||||
* @return The new color palette.
|
||||
*/
|
||||
public static int[] quantizeImage(int pixels[][], int max_colors) {
|
||||
Cube cube = new Cube(pixels, max_colors);
|
||||
cube.classification();
|
||||
cube.reduction();
|
||||
cube.assignment();
|
||||
return cube.colormap;
|
||||
}
|
||||
|
||||
static class Cube {
|
||||
int pixels[][];
|
||||
int max_colors;
|
||||
int colormap[];
|
||||
|
||||
// do we have transparent pixels?
|
||||
boolean hasTrans = false;
|
||||
|
||||
Node root;
|
||||
int depth;
|
||||
|
||||
// counter for the number of colors in the cube. this gets
|
||||
// recalculated often.
|
||||
int colors;
|
||||
|
||||
// counter for the number of nodes in the tree
|
||||
int nodes;
|
||||
|
||||
Cube(int pixels[][], int max_colors) {
|
||||
this.pixels = pixels;
|
||||
this.max_colors = max_colors;
|
||||
|
||||
int i = max_colors;
|
||||
// tree_depth = log max_colors
|
||||
// 4
|
||||
for (depth = 1; i != 0; depth++) {
|
||||
i /= 4;
|
||||
}
|
||||
if (depth > 1) {
|
||||
--depth;
|
||||
}
|
||||
if (depth > MAX_TREE_DEPTH) {
|
||||
depth = MAX_TREE_DEPTH;
|
||||
} else if (depth < 2) {
|
||||
depth = 2;
|
||||
}
|
||||
|
||||
root = new Node(this);
|
||||
}
|
||||
|
||||
/*
|
||||
* Procedure Classification begins by initializing a color
|
||||
* description tree of sufficient depth to represent each
|
||||
* possible input color in a leaf. However, it is impractical
|
||||
* to generate a fully-formed color description tree in the
|
||||
* classification phase for realistic values of cmax. If
|
||||
* colors components in the input image are quantized to k-bit
|
||||
* precision, so that cmax= 2k-1, the tree would need k levels
|
||||
* below the root node to allow representing each possible
|
||||
* input color in a leaf. This becomes prohibitive because the
|
||||
* tree's total number of nodes is 1 + sum(i=1,k,8k).
|
||||
*
|
||||
* A complete tree would require 19,173,961 nodes for k = 8,
|
||||
* cmax = 255. Therefore, to avoid building a fully populated
|
||||
* tree, QUANTIZE: (1) Initializes data structures for nodes
|
||||
* only as they are needed; (2) Chooses a maximum depth for
|
||||
* the tree as a function of the desired number of colors in
|
||||
* the output image (currently log2(colormap size)).
|
||||
*
|
||||
* For each pixel in the input image, classification scans
|
||||
* downward from the root of the color description tree. At
|
||||
* each level of the tree it identifies the single node which
|
||||
* represents a cube in RGB space containing It updates the
|
||||
* following data for each such node:
|
||||
*
|
||||
* number_pixels : Number of pixels whose color is contained
|
||||
* in the RGB cube which this node represents;
|
||||
*
|
||||
* unique : Number of pixels whose color is not represented
|
||||
* in a node at lower depth in the tree; initially, n2 = 0
|
||||
* for all nodes except leaves of the tree.
|
||||
*
|
||||
* total_red/green/blue : Sums of the red, green, and blue
|
||||
* component values for all pixels not classified at a lower
|
||||
* depth. The combination of these sums and n2 will
|
||||
* ultimately characterize the mean color of a set of pixels
|
||||
* represented by this node.
|
||||
*/
|
||||
void classification() {
|
||||
int pixels[][] = this.pixels;
|
||||
|
||||
int width = pixels.length;
|
||||
int height = pixels[0].length;
|
||||
|
||||
// convert to indexed color
|
||||
for (int x = width; x-- > 0; ) {
|
||||
for (int y = height; y-- > 0; ) {
|
||||
int pixel = pixels[x][y];
|
||||
int alpha = (pixel >> 24) & 0xFF;
|
||||
if (alpha != 255) {
|
||||
hasTrans = true;
|
||||
continue; // don't add transparent pixels to the cube
|
||||
}
|
||||
int red = (pixel >> 16) & 0xFF;
|
||||
int green = (pixel >> 8) & 0xFF;
|
||||
int blue = (pixel >> 0) & 0xFF;
|
||||
|
||||
// a hard limit on the number of nodes in the tree
|
||||
if (nodes > MAX_NODES) {
|
||||
System.out.println("pruning");
|
||||
root.pruneLevel();
|
||||
--depth;
|
||||
}
|
||||
|
||||
// walk the tree to depth, increasing the
|
||||
// number_pixels count for each node
|
||||
Node node = root;
|
||||
for (int level = 1; level <= depth; ++level) {
|
||||
int id = (((red > node.mid_red ? 1 : 0) << 0) |
|
||||
((green > node.mid_green ? 1 : 0) << 1) |
|
||||
((blue > node.mid_blue ? 1 : 0) << 2));
|
||||
if (node.child[id] == null) {
|
||||
new Node(node, id, level);
|
||||
}
|
||||
node = node.child[id];
|
||||
node.number_pixels += SHIFT[level];
|
||||
}
|
||||
|
||||
++node.unique;
|
||||
node.total_red += red;
|
||||
node.total_green += green;
|
||||
node.total_blue += blue;
|
||||
}
|
||||
}
|
||||
|
||||
// if we have transparent pixels, that cuts into the number
|
||||
// of other colors we can use.
|
||||
if (hasTrans) {
|
||||
this.max_colors--;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* reduction repeatedly prunes the tree until the number of
|
||||
* nodes with unique > 0 is less than or equal to the maximum
|
||||
* number of colors allowed in the output image.
|
||||
*
|
||||
* When a node to be pruned has offspring, the pruning
|
||||
* procedure invokes itself recursively in order to prune the
|
||||
* tree from the leaves upward. The statistics of the node
|
||||
* being pruned are always added to the corresponding data in
|
||||
* that node's parent. This retains the pruned node's color
|
||||
* characteristics for later averaging.
|
||||
*/
|
||||
void reduction() {
|
||||
long threshold = 1;
|
||||
while (colors > max_colors) {
|
||||
colors = 0;
|
||||
threshold = root.reduce(threshold, Long.MAX_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of a closest color search.
|
||||
*/
|
||||
static class Search {
|
||||
int distance;
|
||||
int color_number;
|
||||
}
|
||||
|
||||
/*
|
||||
* Procedure assignment generates the output image from the
|
||||
* pruned tree. The output image consists of two parts: (1) A
|
||||
* color map, which is an array of color descriptions (RGB
|
||||
* triples) for each color present in the output image; (2) A
|
||||
* pixel array, which represents each pixel as an index into
|
||||
* the color map array.
|
||||
*
|
||||
* First, the assignment phase makes one pass over the pruned
|
||||
* color description tree to establish the image's color map.
|
||||
* For each node with n2 > 0, it divides Sr, Sg, and Sb by n2.
|
||||
* This produces the mean color of all pixels that classify no
|
||||
* lower than this node. Each of these colors becomes an entry
|
||||
* in the color map.
|
||||
*
|
||||
* Finally, the assignment phase reclassifies each pixel in
|
||||
* the pruned tree to identify the deepest node containing the
|
||||
* pixel's color. The pixel's value in the pixel array becomes
|
||||
* the index of this node's mean color in the color map.
|
||||
*/
|
||||
void assignment() {
|
||||
colormap = new int[colors];
|
||||
colors = 0;
|
||||
root.colormap();
|
||||
|
||||
int pixels[][] = this.pixels;
|
||||
|
||||
int width = pixels.length;
|
||||
int height = pixels[0].length;
|
||||
|
||||
Search search = new Search();
|
||||
|
||||
int transPad = hasTrans ? 1 : 0;
|
||||
|
||||
// convert to indexed color
|
||||
for (int x = width; x-- > 0; ) {
|
||||
for (int y = height; y-- > 0; ) {
|
||||
int pixel = pixels[x][y];
|
||||
int alpha = (pixel >> 24) & 0xFF;
|
||||
if (alpha != 255) {
|
||||
pixels[x][y] = 0; // transparent
|
||||
continue;
|
||||
}
|
||||
int red = (pixel >> 16) & 0xFF;
|
||||
int green = (pixel >> 8) & 0xFF;
|
||||
int blue = (pixel >> 0) & 0xFF;
|
||||
|
||||
// walk the tree to find the cube containing that color
|
||||
Node node = root;
|
||||
for ( ; ; ) {
|
||||
int id = (((red > node.mid_red ? 1 : 0) << 0) |
|
||||
((green > node.mid_green ? 1 : 0) << 1) |
|
||||
((blue > node.mid_blue ? 1 : 0) << 2) );
|
||||
if (node.child[id] == null) {
|
||||
break;
|
||||
}
|
||||
node = node.child[id];
|
||||
}
|
||||
|
||||
if (QUICK) {
|
||||
// if QUICK is set, just use that
|
||||
// node. Strictly speaking, this isn't
|
||||
// necessarily best match.
|
||||
pixels[x][y] = node.color_number + transPad;
|
||||
} else {
|
||||
// Find the closest color.
|
||||
search.distance = Integer.MAX_VALUE;
|
||||
node.parent.closestColor(red, green, blue, search);
|
||||
pixels[x][y] = search.color_number + transPad;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// expand the colormap by one to account for the transparent
|
||||
if (hasTrans) {
|
||||
int[] newcmap = new int[colormap.length + 1];
|
||||
System.arraycopy(colormap, 0, newcmap, 1, colormap.length);
|
||||
colormap = newcmap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A single Node in the tree.
|
||||
*/
|
||||
static class Node {
|
||||
Cube cube;
|
||||
|
||||
// parent node
|
||||
Node parent;
|
||||
|
||||
// child nodes
|
||||
Node child[];
|
||||
int nchild;
|
||||
|
||||
// our index within our parent
|
||||
int id;
|
||||
// our level within the tree
|
||||
int level;
|
||||
// our color midpoint
|
||||
int mid_red;
|
||||
int mid_green;
|
||||
int mid_blue;
|
||||
|
||||
// the pixel count for this node and all children
|
||||
long number_pixels;
|
||||
|
||||
// the pixel count for this node
|
||||
int unique;
|
||||
// the sum of all pixels contained in this node
|
||||
int total_red;
|
||||
int total_green;
|
||||
int total_blue;
|
||||
|
||||
// used to build the colormap
|
||||
int color_number;
|
||||
|
||||
Node(Cube cube) {
|
||||
this.cube = cube;
|
||||
this.parent = this;
|
||||
this.child = new Node[8];
|
||||
this.id = 0;
|
||||
this.level = 0;
|
||||
|
||||
this.number_pixels = Long.MAX_VALUE;
|
||||
|
||||
this.mid_red = (MAX_RGB + 1) >> 1;
|
||||
this.mid_green = (MAX_RGB + 1) >> 1;
|
||||
this.mid_blue = (MAX_RGB + 1) >> 1;
|
||||
}
|
||||
|
||||
Node(Node parent, int id, int level) {
|
||||
this.cube = parent.cube;
|
||||
this.parent = parent;
|
||||
this.child = new Node[8];
|
||||
this.id = id;
|
||||
this.level = level;
|
||||
|
||||
// add to the cube
|
||||
++cube.nodes;
|
||||
if (level == cube.depth) {
|
||||
++cube.colors;
|
||||
}
|
||||
|
||||
// add to the parent
|
||||
++parent.nchild;
|
||||
parent.child[id] = this;
|
||||
|
||||
// figure out our midpoint
|
||||
int bi = (1 << (MAX_TREE_DEPTH - level)) >> 1;
|
||||
mid_red = parent.mid_red + ((id & 1) > 0 ? bi : -bi);
|
||||
mid_green = parent.mid_green + ((id & 2) > 0 ? bi : -bi);
|
||||
mid_blue = parent.mid_blue + ((id & 4) > 0 ? bi : -bi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove this child node, and make sure our parent
|
||||
* absorbs our pixel statistics.
|
||||
*/
|
||||
void pruneChild() {
|
||||
--parent.nchild;
|
||||
parent.unique += unique;
|
||||
parent.total_red += total_red;
|
||||
parent.total_green += total_green;
|
||||
parent.total_blue += total_blue;
|
||||
parent.child[id] = null;
|
||||
--cube.nodes;
|
||||
cube = null;
|
||||
parent = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prune the lowest layer of the tree.
|
||||
*/
|
||||
void pruneLevel() {
|
||||
if (nchild != 0) {
|
||||
for (int id = 0; id < 8; id++) {
|
||||
if (child[id] != null) {
|
||||
child[id].pruneLevel();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (level == cube.depth) {
|
||||
pruneChild();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any nodes that have fewer than threshold
|
||||
* pixels. Also, as long as we're walking the tree:
|
||||
*
|
||||
* - figure out the color with the fewest pixels
|
||||
* - recalculate the total number of colors in the tree
|
||||
*/
|
||||
long reduce(long threshold, long next_threshold) {
|
||||
if (nchild != 0) {
|
||||
for (int id = 0; id < 8; id++) {
|
||||
if (child[id] != null) {
|
||||
next_threshold = child[id].reduce(threshold, next_threshold);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (number_pixels <= threshold) {
|
||||
pruneChild();
|
||||
} else {
|
||||
if (unique != 0) {
|
||||
cube.colors++;
|
||||
}
|
||||
if (number_pixels < next_threshold) {
|
||||
next_threshold = number_pixels;
|
||||
}
|
||||
}
|
||||
return next_threshold;
|
||||
}
|
||||
|
||||
/*
|
||||
* colormap traverses the color cube tree and notes each
|
||||
* colormap entry. A colormap entry is any node in the
|
||||
* color cube tree where the number of unique colors is
|
||||
* not zero.
|
||||
*/
|
||||
void colormap() {
|
||||
if (nchild != 0) {
|
||||
for (int id = 0; id < 8; id++) {
|
||||
if (child[id] != null) {
|
||||
child[id].colormap();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unique != 0) {
|
||||
int r = ((total_red + (unique >> 1)) / unique);
|
||||
int g = ((total_green + (unique >> 1)) / unique);
|
||||
int b = ((total_blue + (unique >> 1)) / unique);
|
||||
cube.colormap[cube.colors] = ((( 0xFF) << 24) |
|
||||
((r & 0xFF) << 16) |
|
||||
((g & 0xFF) << 8) |
|
||||
((b & 0xFF) << 0));
|
||||
color_number = cube.colors++;
|
||||
}
|
||||
}
|
||||
|
||||
/* ClosestColor traverses the color cube tree at a
|
||||
* particular node and determines which colormap entry
|
||||
* best represents the input color.
|
||||
*/
|
||||
void closestColor(int red, int green, int blue, Search search) {
|
||||
if (nchild != 0) {
|
||||
for (int id = 0; id < 8; id++) {
|
||||
if (child[id] != null) {
|
||||
child[id].closestColor(red, green, blue, search);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (unique != 0) {
|
||||
int color = cube.colormap[color_number];
|
||||
int distance = distance(color, red, green, blue);
|
||||
if (distance < search.distance) {
|
||||
search.distance = distance;
|
||||
search.color_number = color_number;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out the distance between this node and som color.
|
||||
*/
|
||||
final static int distance(int color, int r, int g, int b) {
|
||||
return (SQUARES[((color >> 16) & 0xFF) - r + MAX_RGB] +
|
||||
SQUARES[((color >> 8) & 0xFF) - g + MAX_RGB] +
|
||||
SQUARES[((color >> 0) & 0xFF) - b + MAX_RGB]);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
if (parent == this) {
|
||||
buf.append("root");
|
||||
} else {
|
||||
buf.append("node");
|
||||
}
|
||||
buf.append(' ');
|
||||
buf.append(level);
|
||||
buf.append(" [");
|
||||
buf.append(mid_red);
|
||||
buf.append(',');
|
||||
buf.append(mid_green);
|
||||
buf.append(',');
|
||||
buf.append(mid_blue);
|
||||
buf.append(']');
|
||||
return new String(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// 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.awt.Dimension;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Point;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.NoninvertibleTransformException;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import com.threerings.media.Log;
|
||||
|
||||
/**
|
||||
* Draws a mirage combined with an arbitrary AffineTransform.
|
||||
*/
|
||||
public class TransformedMirage
|
||||
implements Mirage
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public TransformedMirage (Mirage base, AffineTransform transform)
|
||||
{
|
||||
_base = base;
|
||||
|
||||
// clone the transform so that it doesn't get changed on us.
|
||||
_transform = (AffineTransform) transform.clone();
|
||||
computeTransformedBounds();
|
||||
_transform.preConcatenate(
|
||||
AffineTransform.getTranslateInstance(-_bounds.x, -_bounds.y));
|
||||
}
|
||||
|
||||
// documentation inherited from interface Mirage
|
||||
public void paint (Graphics2D gfx, int x, int y)
|
||||
{
|
||||
AffineTransform otrans = gfx.getTransform();
|
||||
gfx.translate(x, y);
|
||||
gfx.transform(_transform);
|
||||
_base.paint(gfx, 0, 0);
|
||||
gfx.setTransform(otrans);
|
||||
}
|
||||
|
||||
// documentation inherited from interface Mirage
|
||||
public int getWidth ()
|
||||
{
|
||||
return _bounds.width;
|
||||
}
|
||||
|
||||
// documentation inherited from interface Mirage
|
||||
public int getHeight ()
|
||||
{
|
||||
return _bounds.height;
|
||||
}
|
||||
|
||||
// documentation inherited from interface Mirage
|
||||
public boolean hitTest (int x, int y)
|
||||
{
|
||||
Point p = new Point(x, y);
|
||||
try {
|
||||
_transform.createInverse().transform(p, p);
|
||||
return _base.hitTest(p.x, p.y);
|
||||
|
||||
} catch (NoninvertibleTransformException nte) {
|
||||
// grumble, grumble
|
||||
// TODO: log something?
|
||||
return ImageUtil.hitTest(getSnapshot(), x, y);
|
||||
}
|
||||
}
|
||||
|
||||
// documentation inherited from interface Mirage
|
||||
public BufferedImage getSnapshot ()
|
||||
{
|
||||
BufferedImage baseSnap = _base.getSnapshot();
|
||||
BufferedImage img = new BufferedImage(_bounds.width, _bounds.height,
|
||||
baseSnap.getType());
|
||||
Graphics2D gfx = (Graphics2D) img.getGraphics();
|
||||
try {
|
||||
gfx.transform(_transform);
|
||||
gfx.drawImage(baseSnap, 0, 0, null);
|
||||
} finally {
|
||||
gfx.dispose();
|
||||
}
|
||||
return img;
|
||||
}
|
||||
|
||||
// documentation inherited from interface Mirage
|
||||
public long getEstimatedMemoryUsage ()
|
||||
{
|
||||
return _base.getEstimatedMemoryUsage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the bounds of the base Mirage after it has been
|
||||
* transformed.
|
||||
*/
|
||||
protected void computeTransformedBounds ()
|
||||
{
|
||||
int w = _base.getWidth();
|
||||
int h = _base.getHeight();
|
||||
Point[] points = new Point[] {
|
||||
new Point(0, 0), new Point(w, 0), new Point(0, h), new Point(w, h) };
|
||||
_transform.transform(points, 0, points, 0, 4);
|
||||
int minX, minY, maxX, maxY;
|
||||
minX = minY = Integer.MAX_VALUE;
|
||||
maxX = maxY = Integer.MIN_VALUE;
|
||||
for (int ii=0; ii < 4; ii++) {
|
||||
minX = Math.min(minX, points[ii].x);
|
||||
maxX = Math.max(maxX, points[ii].x);
|
||||
minY = Math.min(minY, points[ii].y);
|
||||
maxY = Math.max(maxY, points[ii].y);
|
||||
}
|
||||
|
||||
_bounds = new Rectangle(minX, minY, maxX - minX, maxY - minY);
|
||||
}
|
||||
|
||||
/** The base mirage. */
|
||||
protected Mirage _base;
|
||||
|
||||
/** Our transformed bounds. */
|
||||
protected Rectangle _bounds;
|
||||
|
||||
/** The transform we apply when painting the base mirage. */
|
||||
protected AffineTransform _transform;
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
//
|
||||
// $Id: VolatileMirage.java 4191 2006-06-13 22:42:20Z ray $
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// 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.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
/**
|
||||
* A mirage implementation which allows the image to be maintained in
|
||||
* video memory and rebuilt from some source image or images in the event
|
||||
* that our target screen resolution changes or we are flushed from video
|
||||
* memory for some other reason.
|
||||
*/
|
||||
public abstract class VolatileMirage implements Mirage
|
||||
{
|
||||
/**
|
||||
* Informs the base class of its image manager and image bounds.
|
||||
*/
|
||||
protected VolatileMirage (ImageManager imgr, Rectangle bounds)
|
||||
{
|
||||
_imgr = imgr;
|
||||
_bounds = bounds;
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public void paint (Graphics2D gfx, int x, int y)
|
||||
{
|
||||
// create our volatile image for the first time if necessary
|
||||
if (_image == null) {
|
||||
createVolatileImage();
|
||||
}
|
||||
|
||||
// int renders = 0;
|
||||
// do {
|
||||
// // validate that our image is compatible with the target GC
|
||||
// switch (_image.validate(_imgr.getGraphicsConfiguration())) {
|
||||
// case VolatileImage.IMAGE_RESTORED:
|
||||
// refreshVolatileImage(); // need to rerender it
|
||||
// break;
|
||||
// case VolatileImage.IMAGE_INCOMPATIBLE:
|
||||
// createVolatileImage(); // need to recreate it
|
||||
// break;
|
||||
// }
|
||||
|
||||
// // now we can render it
|
||||
// gfx.drawImage(_image, x, y, null);
|
||||
// renders++;
|
||||
|
||||
// // don't try forever
|
||||
// } while (_image.contentsLost() && (renders < 10));
|
||||
|
||||
if (IMAGE_DEBUG) {
|
||||
gfx.setColor(new Color(_image.getRGB(_bounds.width/2,
|
||||
_bounds.height/2)));
|
||||
gfx.fillRect(x, y, _bounds.width, _bounds.height);
|
||||
} else {
|
||||
gfx.drawImage(_image, x, y, null);
|
||||
}
|
||||
|
||||
// TODO: note number of attempted renders for performance
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the x offset into our source image, which is generally zero but
|
||||
* may be non-zero for a mirage that obtains its data from a region of its
|
||||
* source image.
|
||||
*/
|
||||
public int getX ()
|
||||
{
|
||||
return _bounds.x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the y offset into our source image, which is generally zero but
|
||||
* may be non-zero for a mirage that obtains its data from a region of its
|
||||
* source image.
|
||||
*/
|
||||
public int getY ()
|
||||
{
|
||||
return _bounds.y;
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public int getWidth ()
|
||||
{
|
||||
return _bounds.width;
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public int getHeight ()
|
||||
{
|
||||
return _bounds.height;
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public boolean hitTest (int x, int y)
|
||||
{
|
||||
// return ImageUtil.hitTest(_image.getSnapshot(), x, y);
|
||||
return ImageUtil.hitTest(_image, x, y);
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public long getEstimatedMemoryUsage ()
|
||||
{
|
||||
return ImageUtil.getEstimatedMemoryUsage(_image.getRaster());
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public BufferedImage getSnapshot ()
|
||||
{
|
||||
// return _image.getSnapshot();
|
||||
return _image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates our volatile image from the information in our source
|
||||
* image.
|
||||
*/
|
||||
protected void createVolatileImage ()
|
||||
{
|
||||
// release any previous volatile image we might hold
|
||||
if (_image != null) {
|
||||
_image.flush();
|
||||
}
|
||||
|
||||
// create a new, compatible, volatile image
|
||||
// _image = _imgr.createVolatileImage(
|
||||
// _bounds.width, _bounds.height, getTransparency());
|
||||
_image = _imgr.createImage(
|
||||
_bounds.width, _bounds.height, getTransparency());
|
||||
|
||||
// render our source image into the volatile image
|
||||
refreshVolatileImage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the transparency that should be used when creating our
|
||||
* volatile image.
|
||||
*/
|
||||
protected abstract int getTransparency ();
|
||||
|
||||
/**
|
||||
* Rerenders our volatile image from the its source image data.
|
||||
*/
|
||||
protected abstract void refreshVolatileImage ();
|
||||
|
||||
/**
|
||||
* Generates a string representation of this instance.
|
||||
*/
|
||||
public String toString ()
|
||||
{
|
||||
StringBuilder buf = new StringBuilder("[");
|
||||
toString(buf);
|
||||
return buf.append("]").toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a string representation of this instance.
|
||||
*/
|
||||
protected void toString (StringBuilder buf)
|
||||
{
|
||||
buf.append("bounds=").append(StringUtil.toString(_bounds));
|
||||
}
|
||||
|
||||
/** The image manager with whom we interoperate. */
|
||||
protected ImageManager _imgr;
|
||||
|
||||
/** The bounds of the region of our source image which we desire for
|
||||
* this mirage (possibly the whole thing). */
|
||||
protected Rectangle _bounds;
|
||||
|
||||
/** Our volatile image which lives in video memory and can go away at
|
||||
* any time. */
|
||||
// protected VolatileImage _image;
|
||||
protected BufferedImage _image;
|
||||
|
||||
/** Turns off image rendering for testing. */
|
||||
protected static final boolean IMAGE_DEBUG = false;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// $Id: DumpColorPository.java 4191 2006-06-13 22:42:20Z ray $
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// 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.tools;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
|
||||
import com.threerings.media.image.ColorPository;
|
||||
|
||||
/**
|
||||
* Simple tool for dumping a serialized color pository.
|
||||
*/
|
||||
public class DumpColorPository
|
||||
{
|
||||
public static void main (String[] args)
|
||||
{
|
||||
if (args.length == 0) {
|
||||
System.err.println("Usage: DumpColorPository colorpos.dat");
|
||||
System.exit(-1);
|
||||
}
|
||||
|
||||
try {
|
||||
ColorPository pos = ColorPository.loadColorPository(
|
||||
new FileInputStream(args[0]));
|
||||
Iterator iter = pos.enumerateClasses();
|
||||
while (iter.hasNext()) {
|
||||
System.out.println(iter.next());
|
||||
}
|
||||
|
||||
} catch (IOException ioe) {
|
||||
ioe.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// $Id: ColorPositoryParser.java 4191 2006-06-13 22:42:20Z ray $
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// 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.tools.xml;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.xml.sax.Attributes;
|
||||
import org.apache.commons.digester.Digester;
|
||||
import org.apache.commons.digester.Rule;
|
||||
import com.samskivert.xml.SetPropertyFieldsRule;
|
||||
|
||||
import com.threerings.tools.xml.CompiledConfigParser;
|
||||
|
||||
import com.threerings.media.image.ColorPository.ClassRecord;
|
||||
import com.threerings.media.image.ColorPository.ColorRecord;
|
||||
import com.threerings.media.image.ColorPository;
|
||||
|
||||
/**
|
||||
* Parses the XML color repository definition and creates a {@link
|
||||
* ColorPository} instance that reflects its contents.
|
||||
*/
|
||||
public class ColorPositoryParser extends CompiledConfigParser
|
||||
{
|
||||
// documentation inherited
|
||||
protected Serializable createConfigObject ()
|
||||
{
|
||||
return new ColorPository();
|
||||
}
|
||||
|
||||
// documentation inherited
|
||||
protected void addRules (Digester digest)
|
||||
{
|
||||
// create and configure class record instances
|
||||
String prefix = "colors/class";
|
||||
digest.addObjectCreate(prefix, ClassRecord.class.getName());
|
||||
digest.addRule(prefix, new SetPropertyFieldsRule());
|
||||
digest.addSetNext(prefix, "addClass", ClassRecord.class.getName());
|
||||
|
||||
// create and configure color record instances
|
||||
prefix += "/color";
|
||||
digest.addRule(prefix, new Rule() {
|
||||
public void begin (String namespace, String name,
|
||||
Attributes attributes) throws Exception {
|
||||
// we want to inherit settings from the color class when
|
||||
// creating the record, so we do some custom stuff
|
||||
ColorRecord record = new ColorRecord();
|
||||
ClassRecord clrec = (ClassRecord)digester.peek();
|
||||
record.starter = clrec.starter;
|
||||
digester.push(record);
|
||||
}
|
||||
|
||||
public void end (String namespace, String name) throws Exception {
|
||||
digester.pop();
|
||||
}
|
||||
});
|
||||
digest.addRule(prefix, new SetPropertyFieldsRule());
|
||||
digest.addSetNext(prefix, "addColor", ColorRecord.class.getName());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user