ttx.py revision 142506b07df7382f7a599cb661273bdaaef0f67c
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 -e Don't ignore decompilation errors, but show a full traceback 43 and abort. 44 45 Compile options: 46 -m Merge with TrueType-input-file: specify a TrueType or OpenType 47 font file to be merged with the TTX file. This option is only 48 valid when at most one TTX file is specified. 49 -b Don't recalc glyph boundig boxes: use the values in the TTX 50 file as-is. 51""" 52 53 54import sys 55import os 56import getopt 57import re 58from fontTools.ttLib import TTFont 59from fontTools.ttLib.tables.otBase import OTLOffsetOverflowError 60from fontTools.ttLib.tables.otTables import fixLookupOverFlows, fixSubTableOverFlows 61from fontTools.misc.macCreatorType import getMacCreatorAndType 62from fontTools import version 63 64def usage(): 65 print __doc__ % version 66 sys.exit(2) 67 68 69numberAddedRE = re.compile("(.*)#\d+$") 70 71def makeOutputFileName(input, outputDir, extension): 72 dir, file = os.path.split(input) 73 file, ext = os.path.splitext(file) 74 if outputDir: 75 dir = outputDir 76 output = os.path.join(dir, file + extension) 77 m = numberAddedRE.match(file) 78 if m: 79 file = m.group(1) 80 n = 1 81 while os.path.exists(output): 82 output = os.path.join(dir, file + "#" + repr(n) + extension) 83 n = n + 1 84 return output 85 86 87class Options: 88 89 listTables = 0 90 outputDir = None 91 verbose = 0 92 splitTables = 0 93 disassembleInstructions = 1 94 mergeFile = None 95 recalcBBoxes = 1 96 allowVID = 0 97 ignoreDecompileErrors = True 98 99 def __init__(self, rawOptions, numFiles): 100 self.onlyTables = [] 101 self.skipTables = [] 102 for option, value in rawOptions: 103 # general options 104 if option == "-h": 105 print __doc__ % version 106 sys.exit(0) 107 elif option == "-d": 108 if not os.path.isdir(value): 109 print "The -d option value must be an existing directory" 110 sys.exit(2) 111 self.outputDir = value 112 elif option == "-v": 113 self.verbose = 1 114 # dump options 115 elif option == "-l": 116 self.listTables = 1 117 elif option == "-t": 118 self.onlyTables.append(value) 119 elif option == "-x": 120 self.skipTables.append(value) 121 elif option == "-s": 122 self.splitTables = 1 123 elif option == "-i": 124 self.disassembleInstructions = 0 125 # compile options 126 elif option == "-m": 127 self.mergeFile = value 128 elif option == "-b": 129 self.recalcBBoxes = 0 130 elif option == "-a": 131 self.allowVID = 1 132 elif option == "-e": 133 self.ignoreDecompileErrors = False 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 ignoreDecompileErrors=options.ignoreDecompileErrors) 167 ttf.saveXML(output, 168 tables=options.onlyTables, 169 skipTables=options.skipTables, 170 splitTables=options.splitTables, 171 disassembleInstructions=options.disassembleInstructions) 172 ttf.close() 173 174 175def ttCompile(input, output, options): 176 print 'Compiling "%s" to "%s"...' % (input, output) 177 ttf = TTFont(options.mergeFile, 178 recalcBBoxes=options.recalcBBoxes, 179 verbose=options.verbose, allowVID=options.allowVID) 180 ttf.importXML(input) 181 try: 182 ttf.save(output) 183 except OTLOffsetOverflowError, e: 184 # XXX This shouldn't be here at all, it should be as close to the 185 # OTL code as possible. 186 overflowRecord = e.value 187 print "Attempting to fix OTLOffsetOverflowError", e 188 lastItem = overflowRecord 189 while 1: 190 ok = 0 191 if overflowRecord.itemName == None: 192 ok = fixLookupOverFlows(ttf, overflowRecord) 193 else: 194 ok = fixSubTableOverFlows(ttf, overflowRecord) 195 if not ok: 196 raise 197 198 try: 199 ttf.save(output) 200 break 201 except OTLOffsetOverflowError, e: 202 print "Attempting to fix OTLOffsetOverflowError", e 203 overflowRecord = e.value 204 if overflowRecord == lastItem: 205 raise 206 207 if options.verbose: 208 import time 209 print "finished at", time.strftime("%H:%M:%S", time.localtime(time.time())) 210 211 212def guessFileType(fileName): 213 base, ext = os.path.splitext(fileName) 214 try: 215 f = open(fileName, "rb") 216 except IOError: 217 return None 218 cr, tp = getMacCreatorAndType(fileName) 219 if tp in ("sfnt", "FFIL"): 220 return "TTF" 221 if ext == ".dfont": 222 return "TTF" 223 header = f.read(256) 224 head = header[:4] 225 if head == "OTTO": 226 return "OTF" 227 elif head in ("\0\1\0\0", "true"): 228 return "TTF" 229 elif head.lower() == "<?xm": 230 if header.find('sfntVersion="OTTO"') > 0: 231 return "OTX" 232 else: 233 return "TTX" 234 return None 235 236 237def parseOptions(args): 238 try: 239 rawOptions, files = getopt.getopt(args, "ld:vht:x:sim:bae") 240 except getopt.GetoptError: 241 usage() 242 243 if not files: 244 usage() 245 246 options = Options(rawOptions, len(files)) 247 jobs = [] 248 249 for input in files: 250 tp = guessFileType(input) 251 if tp in ("OTF", "TTF"): 252 extension = ".ttx" 253 if options.listTables: 254 action = ttList 255 else: 256 action = ttDump 257 elif tp == "TTX": 258 extension = ".ttf" 259 action = ttCompile 260 elif tp == "OTX": 261 extension = ".otf" 262 action = ttCompile 263 else: 264 print 'Unknown file type: "%s"' % input 265 continue 266 267 output = makeOutputFileName(input, options.outputDir, extension) 268 jobs.append((action, input, output)) 269 return jobs, options 270 271 272def process(jobs, options): 273 for action, input, output in jobs: 274 action(input, output, options) 275 276 277def waitForKeyPress(): 278 """Force the DOS Prompt window to stay open so the user gets 279 a chance to see what's wrong.""" 280 import msvcrt 281 print '(Hit any key to exit)' 282 while not msvcrt.kbhit(): 283 pass 284 285 286def main(args): 287 jobs, options = parseOptions(args) 288 try: 289 process(jobs, options) 290 except KeyboardInterrupt: 291 print "(Cancelled.)" 292 except SystemExit: 293 if sys.platform == "win32": 294 waitForKeyPress() 295 else: 296 raise 297 except: 298 if sys.platform == "win32": 299 import traceback 300 traceback.print_exc() 301 waitForKeyPress() 302 else: 303 raise 304 305 306if __name__ == "__main__": 307 main(sys.argv[1:]) 308