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