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