ttx.py revision 45d1f3b3b552297484bc2b8e9a2e999630bb5e50
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 overflowRecord = e.value 185 print "Attempting to fix OTLOffsetOverflowError", e 186 lastItem = overflowRecord 187 while 1: 188 ok = 0 189 if overflowRecord.itemName == None: 190 ok = fixLookupOverFlows(ttf, overflowRecord) 191 else: 192 ok = fixSubTableOverFlows(ttf, overflowRecord) 193 if not ok: 194 raise 195 196 try: 197 ttf.save(output) 198 break 199 except OTLOffsetOverflowError, e: 200 print "Attempting to fix OTLOffsetOverflowError", e 201 overflowRecord = e.value 202 if overflowRecord == lastItem: 203 raise 204 205 if options.verbose: 206 import time 207 print "finished at", time.strftime("%H:%M:%S", time.localtime(time.time())) 208 209 210def guessFileType(fileName): 211 base, ext = os.path.splitext(fileName) 212 try: 213 f = open(fileName, "rb") 214 except IOError: 215 return None 216 cr, tp = getMacCreatorAndType(fileName) 217 if tp in ("sfnt", "FFIL"): 218 return "TTF" 219 if ext == ".dfont": 220 return "TTF" 221 header = f.read(256) 222 head = header[:4] 223 if head == "OTTO": 224 return "OTF" 225 elif head in ("\0\1\0\0", "true"): 226 return "TTF" 227 elif head.lower() == "<?xm": 228 if header.find('sfntVersion="OTTO"') > 0: 229 return "OTX" 230 else: 231 return "TTX" 232 return None 233 234 235def parseOptions(args): 236 try: 237 rawOptions, files = getopt.getopt(args, "ld:vht:x:sim:bae") 238 except getopt.GetoptError: 239 usage() 240 241 if not files: 242 usage() 243 244 options = Options(rawOptions, len(files)) 245 jobs = [] 246 247 for input in files: 248 tp = guessFileType(input) 249 if tp in ("OTF", "TTF"): 250 extension = ".ttx" 251 if options.listTables: 252 action = ttList 253 else: 254 action = ttDump 255 elif tp == "TTX": 256 extension = ".ttf" 257 action = ttCompile 258 elif tp == "OTX": 259 extension = ".otf" 260 action = ttCompile 261 else: 262 print 'Unknown file type: "%s"' % input 263 continue 264 265 output = makeOutputFileName(input, options.outputDir, extension) 266 jobs.append((action, input, output)) 267 return jobs, options 268 269 270def process(jobs, options): 271 for action, input, output in jobs: 272 action(input, output, options) 273 274 275def waitForKeyPress(): 276 """Force the DOS Prompt window to stay open so the user gets 277 a chance to see what's wrong.""" 278 import msvcrt 279 print '(Hit any key to exit)' 280 while not msvcrt.kbhit(): 281 pass 282 283 284def main(args): 285 jobs, options = parseOptions(args) 286 try: 287 process(jobs, options) 288 except KeyboardInterrupt: 289 print "(Cancelled.)" 290 except SystemExit: 291 if sys.platform == "win32": 292 waitForKeyPress() 293 else: 294 raise 295 except: 296 if sys.platform == "win32": 297 import traceback 298 traceback.print_exc() 299 waitForKeyPress() 300 else: 301 raise 302 303 304if __name__ == "__main__": 305 main(sys.argv[1:]) 306