My texture atlas tool
10 Aug 2012I’m not a fan of XML. Even less a fan of dealing with it in a static, compiled language. And when it comes to load or run time in a game, the thought of spending the time to parse XML abhors me.
However, all the good texture packing tools output XML, so I have to deal with it at some point. To that end, I wrote a small script to compile the “Generic XML” output from TexturePacker into a binary format that I can quickly load in game without having to deal with parsing any plain text.
The structure of the file is very simple. I haven’t had a need yet for any of the fancier features of TexturePacker like rotation, trimming, etc, so I don’t try and support them. It’s simply a short header, followed by N AtlasSprite instances:
/* The .atlas file format is simply a Header structure, followed by numFrames
* frame structures.
*
* All multibyte values are defined to be little endian.
*/
struct AtlasHeader {
uint32_t magic; // (1635019891, or 'atls')
uint16_t textureWidth;
uint16_t textureHeight;
uint16_t numFrames;
char textureName[64];
};
struct AtlasFrame {
char name[64];
uint16_t width, height;
float u1, v1;
float u2, v2;
};
The script that parses the files is a short Python script:
#!/usr/bin/env python
#
# Compiles texture atlases exported from TexturePacker (in the Generic XML format)
# into a binary format for fast/easy loading in game code. At the moment this doesn't
# support trimming or rotation, 'cause I don't really need 'em.
#
# The file format (all multibyte values are little endian):
# struct AtlasHeader {
# uint32_t magic; // (1635019891, or 'atls')
# uint16_t textureWidth;
# uint16_t textureHeight;
# uint16_t numFrames;
# char textureName[64];
# };
# sizeof(struct AtlasHeader) == 74;
#
# struct AtlasFrame {
# char name[64];
# uint16_t width, height;
# float u1, v1;
# float u2, v2;
# };
# sizeof(struct AtlasFrame) == 84;
import struct
import xml.sax
from optparse import OptionParser
class Texture:
def __init__(self, xml_attrs):
self.name = xml_attrs.getValue('imagePath')
self.width = int(xml_attrs.getValue('width'))
self.height = int(xml_attrs.getValue('height'))
class Frame:
def __init__(self, xml_attrs):
self.name = xml_attrs.getValue('n')
self.x = float(xml_attrs.getValue('x'))
self.y = float(xml_attrs.getValue('y'))
self.width = float(xml_attrs.getValue('w'))
self.height = float(xml_attrs.getValue('h'))
self.u1 = 0
self.v1 = 0
self.u2 = 0
self.v2 = 0
def make_bytes(self):
"""
Returns the byte-array representation of the frame.
"""
frame_fmt = '<64s2H4f'
return struct.pack(frame_fmt, str(self.name), int(self.width), int(self.height), self.u1, self.v1, self.u2, self.v2)
class AtlasHandler(xml.sax.ContentHandler):
def __init__(self):
xml.sax.ContentHandler.__init__(self)
self.texture = None
self.frames = []
def startElement(self, name, attrs):
if name == 'TextureAtlas':
self.texture = Texture(attrs)
elif name == 'sprite':
self.frames.append(Frame(attrs))
def endElement(self, name):
if name == 'TextureAtlas':
for frame in self.frames:
frame.u1 = frame.x / self.texture.width
frame.v1 = frame.y / self.texture.height
frame.u2 = (frame.x + frame.width) / self.texture.width
frame.v2 = (frame.y + frame.height) / self.texture.height
def dumpDescription(self):
if self.texture:
print("Texture: %s (%s, %s)" % (self.texture.name, self.texture.width, self.texture.height))
else:
print("No texture!")
for frame in self.frames:
print(" Frame: %s" % frame.name)
print(" x: %s" % frame.x)
print(" y: %s" % frame.y)
print(" width: %s" % frame.width)
print(" height: %s" % frame.height)
print(" u1: %s" % frame.u1)
print(" v1: %s" % frame.v1)
print(" u2: %s" % frame.u2)
print(" v2: %s" % frame.v2)
def make_header(self):
"""
Returns a byte array representation of the binary file header.
"""
header_fmt = "<ccccHHH64s"
return struct.pack(header_fmt, 'a', 't', 'l', 's', self.texture.width, self.texture.height, len(self.frames), str(self.texture.name))
def write_file(self, f):
"""
Writes out the byte array representation of the texture atlas to the given file.
"""
f.write(self.make_header())
for frame in self.frames:
f.write(frame.make_bytes())
def parse_file(filename):
source = open(filename)
handler = AtlasHandler()
xml.sax.parse(source, handler)
return handler
USAGE="""
Usage: atlasc.py -o <output-filename> <input-filename>
"""
def handle_commandline():
"""
Handles command-line options and arguments and does the right things.
"""
parser = OptionParser(usage=USAGE)
parser.add_option('-o', '--output', dest='output', default='out.atlas')
(options, args) = parser.parse_args()
if len(args) == 0:
print(USAGE)
return -1
atlas = parse_file(args[0])
out = open(options.output, 'w')
atlas.write_file(out)
out.close()
return 0
if __name__ == '__main__':
return handle_commandline()
And sample Objective-C code to read in the files is included in the same gist on Github.