I’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.