diff --git a/src/as/com/threerings/media/image/ClassRecord.as b/src/as/com/threerings/media/image/ClassRecord.as new file mode 100644 index 00000000..4147f7a5 --- /dev/null +++ b/src/as/com/threerings/media/image/ClassRecord.as @@ -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; +} +} \ No newline at end of file diff --git a/src/as/com/threerings/media/image/ColorPository.as b/src/as/com/threerings/media/image/ColorPository.as new file mode 100644 index 00000000..37698cb8 --- /dev/null +++ b/src/as/com/threerings/media/image/ColorPository.as @@ -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); +} +} \ No newline at end of file diff --git a/src/as/com/threerings/media/image/ColorRecord.as b/src/as/com/threerings/media/image/ColorRecord.as new file mode 100644 index 00000000..61166886 --- /dev/null +++ b/src/as/com/threerings/media/image/ColorRecord.as @@ -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; +} +} \ No newline at end of file diff --git a/src/as/com/threerings/media/image/PngRecolorUtil.as b/src/as/com/threerings/media/image/PngRecolorUtil.as new file mode 100644 index 00000000..e7d97719 --- /dev/null +++ b/src/as/com/threerings/media/image/PngRecolorUtil.as @@ -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); + } +} \ No newline at end of file