ttx.py revision 823f8cd15f16bb9dc3991c2672f16dd90579711b
1#! /usr/bin/env python 2 3"""\ 4usage: ttx [options] inputfile1 [... inputfileN] 5 6 TTX %s -- From OpenType To XML And Back 7 8 If an input file is a TrueType or OpenType font file, it will be 9 dumped to an TTX file (an XML-based text format). 10 If an input file is a TTX file, it will be compiled to a TrueType 11 or OpenType font file. 12 13 Output files are created so they are unique: an existing file is 14 never overwrritten. 15 16 General options: 17 -h Help: print this message 18 -d <outputfolder> Specify a directory where the output files are 19 to be created. 20 -v Verbose: more messages will be written to stdout about what 21 is being done. 22 -a allow virtual glyphs ID's on compile or decompile. 23 24 Dump options: 25 -l List table info: instead of dumping to a TTX file, list some 26 minimal info about each table. 27 -t <table> Specify a table to dump. Multiple -t options 28 are allowed. When no -t option is specified, all tables 29 will be dumped. 30 -x <table> Specify a table to exclude from the dump. Multiple 31 -x options are allowed. -t and -x are mutually exclusive. 32 -s Split tables: save the TTX data into separate TTX files per 33 table and write one small TTX file that contains references 34 to the individual table dumps. This file can be used as 35 input to ttx, as long as the table files are in the 36 same directory. 37 -i Do NOT disassemble TT instructions: when this option is given, 38 all TrueType programs (glyph programs, the font program and the 39 pre-program) will be written to the TTX file as hex data 40 instead of assembly. This saves some time and makes the TTX 41 file smaller. 42 43 Compile options: 44 -m Merge with TrueType-input-file: specify a TrueType or OpenType 45 font file to be merged with the TTX file. This option is only 46 valid when at most one TTX file is specified. 47 -b Don't recalc glyph boundig boxes: use the values in the TTX 48 file as-is. 49""" 50 51 52import sys 53import os 54import getopt 55import re 56from fontTools.ttLib import TTFont 57from fontTools.ttLib.tables.otBase import OTLOffsetOverflowError 58from fontTools.ttLib.tables.otTables import fixLookupOverFlows, fixSubTableOverFlows 59from fontTools import version 60 61def usage(): 62 print __doc__ % version 63 sys.exit(2) 64 65 66if sys.platform == "darwin" and sys.version_info[:3] == (2, 2, 0): 67 # the Mac support of Jaguar's Python 2.2 is broken 68 have_broken_macsupport = 1 69else: 70 have_broken_macsupport = 0 71 72numberAddedRE = re.compile("(.*)#\d+$") 73 74def makeOutputFileName(input, outputDir, extension): 75 dir, file = os.path.split(input) 76 file, ext = os.path.splitext(file) 77 if outputDir: 78 dir = outputDir 79 output = os.path.join(dir, file + extension) 80 m = numberAddedRE.match(file) 81 if m: 82 file = m.group(1) 83 n = 1 84 while os.path.exists(output): 85 output = os.path.join(dir, file + "#" + repr(n) + extension) 86 n = n + 1 87 return output 88 89 90class Options: 91 92 listTables = 0 93 outputDir = None 94 verbose = 0 95 splitTables = 0 96 disassembleInstructions = 1 97 mergeFile = None 98 recalcBBoxes = 1 99 allowVID = 0 100 101 def __init__(self, rawOptions, numFiles): 102 self.onlyTables = [] 103 self.skipTables = [] 104 for option, value in rawOptions: 105 # general options 106 if option == "-h": 107 print __doc__ % version 108 sys.exit(0) 109 elif option == "-d": 110 if not os.path.isdir(value): 111 print "The -d option value must be an existing directory" 112 sys.exit(2) 113 self.outputDir = value 114 elif option == "-v": 115 self.verbose = 1 116 # dump options 117 elif option == "-l": 118 self.listTables = 1 119 elif option == "-t": 120 self.onlyTables.append(value) 121 elif option == "-x": 122 self.skipTables.append(value) 123 elif option == "-s": 124 self.splitTables = 1 125 elif option == "-i": 126 self.disassembleInstructions = 0 127 # compile options 128 elif option == "-m": 129 self.mergeFile = value 130 elif option == "-b": 131 self.recalcBBoxes = 0 132 elif option == "-a": 133 self.allowVID = 1 134 if self.onlyTables and self.skipTables: 135 print "-t and -x options are mutually exclusive" 136 sys.exit(2) 137 if self.mergeFile and numFiles > 1: 138 print "Must specify exactly one TTX source file when using -m" 139 sys.exit(2) 140 141 142def ttList(input, output, options): 143 import string 144 ttf = TTFont(input) 145 reader = ttf.reader 146 tags = reader.keys() 147 tags.sort() 148 print 'Listing table info for "%s":' % input 149 format = " %4s %10s %7s %7s" 150 print format % ("tag ", " checksum", " length", " offset") 151 print format % ("----", "----------", "-------", "-------") 152 for tag in tags: 153 entry = reader.tables[tag] 154 checkSum = long(entry.checkSum) 155 if checkSum < 0: 156 checkSum = checkSum + 0x100000000L 157 checksum = "0x" + string.zfill(hex(checkSum)[2:-1], 8) 158 print format % (tag, checksum, entry.length, entry.offset) 159 print 160 ttf.close() 161 162 163def ttDump(input, output, options): 164 print 'Dumping "%s" to "%s"...' % (input, output) 165 ttf = TTFont(input, 0, verbose=options.verbose, allowVID=options.allowVID) 166 ttf.saveXML(output, 167 tables=options.onlyTables, 168 skipTables=options.skipTables, 169 splitTables=options.splitTables, 170 disassembleInstructions=options.disassembleInstructions) 171 ttf.close() 172 173 174def ttCompile(input, output, options): 175 print 'Compiling "%s" to "%s"...' % (input, output) 176 ttf = TTFont(options.mergeFile, 177 recalcBBoxes=options.recalcBBoxes, 178 verbose=options.verbose, allowVID=options.allowVID) 179 ttf.importXML(input) 180 try: 181 ttf.save(output) 182 except OTLOffsetOverflowError, e: 183 overflowRecord = e.value 184 print "Attempting to fix OTLOffsetOverflowError", e 185 lastItem = overflowRecord 186 while 1: 187 ok = 0 188 if overflowRecord.itemName == None: 189 ok = fixLookupOverFlows(ttf, overflowRecord) 190 else: 191 ok = fixSubTableOverFlows(ttf, overflowRecord) 192 if not ok: 193 raise 194 195 try: 196 ttf.save(output) 197 break 198 except OTLOffsetOverflowError, e: 199 print "Attempting to fix OTLOffsetOverflowError", e 200 overflowRecord = e.value 201 if overflowRecord == lastItem: 202 raise 203 204 if options.verbose: 205 import time 206 print "finished at", time.strftime("%H:%M:%S", time.localtime(time.time())) 207 208 209def guessFileType(fileName): 210 base, ext = os.path.splitext(fileName) 211 try: 212 f = open(fileName, "rb") 213 except IOError: 214 return None 215 if not have_broken_macsupport: 216 try: 217 import MacOS 218 except ImportError: 219 pass 220 else: 221 cr, tp = MacOS.GetCreatorAndType(fileName) 222 if tp in ("sfnt", "FFIL"): 223 return "TTF" 224 if ext == ".dfont": 225 return "TTF" 226 header = f.read(256) 227 head = header[:4] 228 if head == "OTTO": 229 return "OTF" 230 elif head in ("\0\1\0\0", "true"): 231 return "TTF" 232 elif head.lower() == "<?xm": 233 if header.find('sfntVersion="OTTO"') > 0: 234 return "OTX" 235 else: 236 return "TTX" 237 return None 238 239 240def parseOptions(args): 241 try: 242 rawOptions, files = getopt.getopt(args, "ld:vht:x:sim:ba") 243 except getopt.GetoptError: 244 usage() 245 246 if not files: 247 usage() 248 249 options = Options(rawOptions, len(files)) 250 jobs = [] 251 252 for input in files: 253 tp = guessFileType(input) 254 if tp in ("OTF", "TTF"): 255 extension = ".ttx" 256 if options.listTables: 257 action = ttList 258 else: 259 action = ttDump 260 elif tp == "TTX": 261 extension = ".ttf" 262 action = ttCompile 263 elif tp == "OTX": 264 extension = ".otf" 265 action = ttCompile 266 else: 267 print 'Unknown file type: "%s"' % input 268 continue 269 270 output = makeOutputFileName(input, options.outputDir, extension) 271 jobs.append((action, input, output)) 272 return jobs, options 273 274 275def process(jobs, options): 276 for action, input, output in jobs: 277 action(input, output, options) 278 279 280def waitForKeyPress(): 281 """Force the DOS Prompt window to stay open so the user gets 282 a chance to see what's wrong.""" 283 import msvcrt 284 print '(Hit any key to exit)' 285 while not msvcrt.kbhit(): 286 pass 287 288 289def main(args): 290 jobs, options = parseOptions(args) 291 try: 292 process(jobs, options) 293 except KeyboardInterrupt: 294 print "(Cancelled.)" 295 except SystemExit: 296 if sys.platform == "win32": 297 waitForKeyPress() 298 else: 299 raise 300 except: 301 if sys.platform == "win32": 302 import traceback 303 traceback.print_exc() 304 waitForKeyPress() 305 else: 306 raise 307 308 309if __name__ == "__main__": 310 main(sys.argv[1:]) 311