Checkpoint commit to getting the png recolor code into nenya. This still needs testing and such.

git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@934 ed5b42cb-e716-0410-a449-f6a68f950b19
This commit is contained in:
Mike Thomas
2010-07-03 01:05:08 +00:00
parent 7752cca5b0
commit 75f00f82ec
4 changed files with 670 additions and 0 deletions
@@ -0,0 +1,167 @@
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image {
import com.threerings.util.Log;
import com.threerings.util.Map;
import com.threerings.util.Maps;
import com.threerings.util.RandomUtil;
import com.threerings.util.StringUtil;
public class ClassRecord
{
private const log :Log = Log.getLog(ClassRecord);
/** An integer identifier for this class. */
public var classId :int;
/** The name of the color class. */
public var name :String;
/** The source color to use when recoloring colors in this class. */
public var source :uint;
/** Data identifying the range of colors around the source color
* that will be recolored when recoloring using this class. */
public var range :Array;
/** The default starting legality value for this color class. See
* {@link ColorRecord#starter}. */
public var starter :Boolean;
/** The default colorId to use for recoloration in this class, or
* 0 if there is no default defined. */
public var defaultId :int;
/** A table of target colors included in this class. */
public var colors :Map = Maps.newMapOf(int);
/** Used when parsing the color definitions. */
public function addColor (record :ColorRecord) :void
{
// validate the color id
if (record.colorId > 127) {
log.warning("Refusing to add color record; colorId > 127",
"class", this, "record", record);
} else if (colors.containsKey(record.colorId)) {
log.warning("Refusing to add duplicate colorId",
"class", this, "record", record, "existing", colors.get(record.colorId));
} else {
record.cclass = this;
colors.put(record.colorId, record);
}
}
/**
* Translates 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.
*/
public function getColorId (name :String) :int
{
// Check if the string is itself a number
var id :int = name as int;
if (colors.containsKey(id)) {
return id;
}
// Look for name matches among all colors
for each (var color :ColorRecord in colors) {
if (StringUtil.compareIgnoreCase(color.name, name) == 0) {
return color.colorId;
}
}
// That input wasn't a color
throw new Error("No color named '" + name + "'", 0);
}
/** Returns a random starting id from the entries in this class. */
public function randomStartingColor () :ColorRecord
{
// figure out our starter ids if we haven't already
if (_starters == null) {
_starters = [];
for each (var color :ColorRecord in colors) {
if (color.starter) {
_starters.push(color);
}
}
}
// sanity check
if (_starters.length < 1) {
log.warning("Requested random starting color from colorless component class",
"class", this);
return null;
}
// return a random entry from the array
return RandomUtil.pickRandom(_starters);
}
/**
* Get the default ColorRecord defined for this color class, or
* null if none.
*/
public function getDefault () :ColorRecord
{
return colors.get(defaultId);
}
public function toString () :String
{
return "[id=" + classId + ", name=" + name + ", source=#" +
StringUtil.toHex(source & 0xFFFFFF, 6) +
", range=" + StringUtil.toString(range) +
", starter=" + starter + ", colors=" +
StringUtil.toString(colors.values()) + "]";
}
public static function fromXml (xml :XML) :ClassRecord
{
var rec :ClassRecord = new ClassRecord();
for each (var colorXml :XML in xml.color) {
rec.colors.put(colorXml.@colorId, ColorRecord.fromXml(colorXml));
}
rec.name = xml.@name;
var srcStr :String = xml.@source;
rec.source = parseInt(srcStr.substring(1, srcStr.length - 1), 16);
rec.range = toNumArray(xml.@range);
rec.defaultId = xml.@defaultId;
return rec;
}
protected static function toNumArray (str :String) :Array
{
if (str == null) {
return null;
}
return str.split(",").map(function(element :*, index :int, arr :Array) :int {
return Number(element);
});
}
protected var _starters :Array;
}
}
@@ -0,0 +1,197 @@
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image {
import com.threerings.util.Log;
import com.threerings.util.Map;
import com.threerings.util.Maps;
public class ColorPository
{
private const log :Log = Log.getLog(ColorPository);
public function getClasses () :Array
{
return _classes.values();
}
/**
* Returns an array containing the records for the colors in the
* specified class.
*/
public function getColors (className :String) :Array
{
// make sure the class exists
var record :ClassRecord = getClassRecordByName(className);
if (record == null) {
return null;
}
// create the array
return record.colors.values();
}
/**
* Returns an array containing the ids of the colors in the specified
* class.
*/
public function getColorIds (className :String) :Array
{
// make sure the class exists
var record :ClassRecord = getClassRecordByName(className);
if (record == null) {
return null;
}
return record.colors.values().map(
function(element :ColorRecord, index :int, arr :Array) :int {
return element.colorId;
});
}
/**
* 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 function isLegalStartColor (classId :int, colorId :int) :Boolean
{
var color :ColorRecord = getColorRecord(classId, colorId);
return (color == null) ? false : color.starter;
}
/**
* Returns a random starting color from the specified color class.
*/
public function getRandomStartingColor (className :String) :ColorRecord
{
// make sure the class exists
var record :ClassRecord = getClassRecordByName(className);
return (record == null) ? null : record.randomStartingColor();
}
/**
* Looks up a colorization by id.
*/
public function getColorization (classId :int, colorId :int) :Colorization
{
var color :ColorRecord = getColorRecord(classId, colorId);
return (color == null) ? null : color.getColorization();
}
/**
* Looks up a colorization by color print.
*/
public function getColorizationByPrint (colorPrint :int) :Colorization
{
return getColorization(colorPrint >> 8, colorPrint & 0xFF);
}
/**
* Looks up a colorization by class and color names.
*/
public function getColorizationByName (className :String, colorName :String) :Colorization
{
var crec :ClassRecord = getClassRecordByName(className);
if (crec != null) {
var colorId :int = crec.getColorId(colorName);
var 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 function getClassRecordByName (className :String) :ClassRecord
{
for each (var crec :ClassRecord in _classes) {
if (crec.name == className) {
return crec;
}
}
log.warning("No such color class", "class", className, new Error());
return null;
}
/**
* Looks up the requested color class record.
*/
public function getClassRecord (classId :int) :ClassRecord
{
return _classes.get(classId);
}
/**
* Looks up the requested color record.
*/
public function getColorRecord (classId :int, colorId :int) :ColorRecord
{
var record :ClassRecord = getClassRecord(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, new Error());
}
return null;
}
return record.colors.get(colorId);
}
/**
* Looks up the requested color record by class & color names.
*/
public function getColorRecordByName (className :String, colorName :String) :ColorRecord
{
var record :ClassRecord = getClassRecordByName(className);
if (record == null) {
log.warning("Requested unknown color class",
"className", className, "colorName", colorName, new Error());
return null;
}
var colorId :int = record.getColorId(colorName);
return record.colors.get(colorId);
}
/**
* Loads up a serialized color pository from the supplied resource
* manager.
*/
public static function fromXml (xml :XML) :ColorPository
{
var pos :ColorPository = new ColorPository();
for each (var classXml :XML in xml.elements("class")) {
pos._classes.put(classXml.@classId, ClassRecord.fromXml(classXml));
}
return pos;
}
/** Our mapping from class names to class records. */
protected var _classes :Map = Maps.newMapOf(int);
}
}
@@ -0,0 +1,100 @@
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image {
import com.threerings.util.Log;
import com.threerings.util.StringUtil;
public class ColorRecord
{
private const log :Log = Log.getLog(ColorRecord);
/** The colorization class to which we belong. */
public var cclass :ClassRecord;
/** A unique colorization identifier (used in fingerprints). */
public var colorId :int;
/** The name of the target color. */
public var name :String;
/** Data indicating the offset (in HSV color space) from the
* source color to recolor to this color. */
public var offsets :Array;
/** 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 var starter :Boolean;
/**
* 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 function getColorPrint () :int
{
return ((cclass.classId << 8) | colorId);
}
/**
* Returns the data in this record configured as a colorization
* instance.
*/
public function getColorization () :Colorization
{
return new Colorization(getColorPrint(), cclass.source,
cclass.range, offsets);
}
public function toString () :String
{
return "[id=" + colorId + ", name=" + name +
", offsets=" + StringUtil.toString(offsets) +
", starter=" + starter + "]";
}
public static function fromXml (xml :XML) :ColorRecord
{
var rec :ColorRecord = new ColorRecord();
rec.colorId = xml.@colorId;
rec.name = xml.@name;
rec.offsets = toNumArray(xml.@offsets);
rec.starter = xml.@starter;
return rec;
}
protected static function toNumArray (str :String) :Array
{
if (str == null) {
return null;
}
return str.split(",").map(function(element :*, index :int, arr :Array) :int {
return Number(element);
});
}
/** Our data represented as a colorization. */
protected var _zation :Colorization;
}
}
@@ -0,0 +1,206 @@
//
// aspirin library - Taking some of the pain out of Actionscript development.
// Copyright (C) 2007-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image {
import flash.utils.ByteArray;
import com.threerings.display.ColorUtil;
import com.threerings.util.Log;
import com.threerings.util.StringUtil;
import com.threerings.util.F;
public class PngRecolorUtil
{
private static const log :Log = Log.getLog(PngRecolorUtil);
protected static function isPng (bytes :ByteArray) :Boolean
{
if (bytes.length < PNG_HEADER.length) {
return false;
}
for (var ii :int = 0; ii < PNG_HEADER.length; ii++) {
if (PNG_HEADER[ii] != bytes[ii]) {
return false;
}
}
return true;
}
/**
* Find the required chunks in the bytes that represent a PNG image. All of the chunks that
* are succesfully found are pushed into the returned array in the order in which they were
* found in the image.
*/
protected static function findChunks (pngBytes :ByteArray, chunkTypes :Array) :Array
{
var chunks :Array = [];
if (chunkTypes.length < 1) {
return chunks;
}
for (var ii :int = PNG_HEADER.length; ii < pngBytes.length;) {
pngBytes.position = ii;
var chunk :PngChunk =
new PngChunk(ii + 8, pngBytes.readInt(), pngBytes.readUTFBytes(4));
var idx :int = chunkTypes.indexOf(chunk.type);
if (idx >= 0) {
chunks.push(chunk);
if (chunkTypes.length == 1) {
return chunks;
} else {
chunkTypes.splice(idx, 1);
}
}
// 4 bytes each for length, chunk type and CRC
ii += 12 + chunk.length;
}
return chunks;
}
public static function recolorPNG (pngBytes :ByteArray,
colors :Array/* of Colorization */) :void
{
if (colors.length == 0) {
return;
}
// first, lets make sure we really have a PNG
if (!isPng(pngBytes)) {
log.warning("recolorPNG received invalid pngBytes", new Error());
return;
}
var chunks :Array = findChunks(pngBytes, [HEADER_CHUNK, PALETTE_CHUNK]);
if (chunks.length != 2 || (chunks[0] as PngChunk).type != HEADER_CHUNK) {
log.warning("recolorPNG received an unexected PNG format", "requiredChunksFound",
chunks.length, new Error());
return;
}
var header :PngChunk = chunks[0] as PngChunk;
var palette :PngChunk = chunks[1] as PngChunk;
// Check to make sure the header declares this to be an indexed-color PNG
var colorType :int = pngBytes[header.idx + COLOR_TYPE_IDX];
if (colorType != INDEXED_COLOR_TYPE) {
log.warning(
"Color Type in PNG header is not indexed-color", "type", colorType, new Error());
return;
}
initializeCRCTable();
// CRC starts with chunk type
var crc :uint = 0xFFFFFFFF;
for (var ii :int = 0; ii < 4; ii++) {
crc = crcCalc(crc, PALETTE_CHUNK.charCodeAt(ii));
}
pngBytes.position = palette.idx;
for (ii = palette.idx; ii < palette.idx + palette.length; ii += 3) {
var red :uint = pngBytes.readUnsignedByte();
var green :uint = pngBytes.readUnsignedByte();
var blue :uint = pngBytes.readUnsignedByte();
var hsb :Array = ColorUtil.RGBtoHSB(red, green, blue);
for each (var color :Colorization in colors) {
var newRgb :uint = color.recolorColor(hsb);
pngBytes[ii] = ColorUtil.getRed(newRgb);
pngBytes[ii + 1] = ColorUtil.getGreen(newRgb);
pngBytes[ii + 2] = ColorUtil.getBlue(newRgb);
}
crc = F.foldl([red, green, blue], crc, crcCalc);
}
crc = uint(crc ^ uint(0xFFFFFFFF));
// write out the CRC for the modified color table
pngBytes.position = palette.idx + palette.length;
pngBytes.writeUnsignedInt(crc);
}
protected static function initializeCRCTable () :void
{
if (_crcTable != null) {
return;
}
_crcTable = [];
for (var n :uint = 0; n < 256; n++)
{
var c :uint = n;
for (var k :uint = 0; k < 8; k++)
{
if (c & 1) {
c = uint(uint(0xedb88320) ^ uint(c >>> 1));
} else {
c = uint(c >>> 1);
}
}
_crcTable[n] = c;
}
}
protected static function crcCalc (crc :uint, byte :uint) :uint
{
return uint(_crcTable[(crc ^ byte) & uint(0xFF)] ^ uint(crc >>> 8));
}
/** The amount to pad error messages by. */
private static const ERROR_PADDING :int = 5;
/** Various Png-recolor related constants. */
protected static const PNG_HEADER :Array =
[ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ];
protected static const PALETTE_CHUNK :String = "PLTE";
protected static const HEADER_CHUNK :String = "IHDR";
protected static const COLOR_TYPE_IDX :int = 9;
protected static const INDEXED_COLOR_TYPE :int = 3;
protected static const JAVA_SHORT_MAX :int = 0x7FFF;
protected static var _crcTable :Array;
}
}
import com.threerings.util.StringUtil;
/** Used for recoloring pngs. */
class PngChunk
{
/** The index of the beginning of the data section of the chunk. */
public var idx :int;
/** The length of the data section of the chunk. */
public var length :int;
/** The type of this chunk. */
public var type :String;
public function PngChunk (idx :int, length :int, type :String)
{
this.idx = idx;
this.length = length;
this.type = type;
}
public function toString () :String
{
return StringUtil.simpleToString(this);
}
}