ttx.py revision ac1b4359467ca3deab03186a15eae1d55eb35567
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 = sorted(reader.keys()) 177 print 'Listing table info for "%s":' % input 178 format = " %4s %10s %7s %7s" 179 print format % ("tag ", " checksum", " length", " offset") 180 print format % ("----", "----------", "-------", "-------") 181 for tag in tags: 182 entry = reader.tables[tag] 183 checkSum = int(entry.checkSum) 184 if checkSum < 0: 185 checkSum = checkSum + 0x100000000 186 checksum = "0x" + string.zfill(hex(checkSum)[2:-1], 8) 187 print format % (tag, checksum, entry.length, entry.offset) 188 print 189 ttf.close() 190 191 192def ttDump(input, output, options): 193 if not options.quiet: 194 print 'Dumping "%s" to "%s"...' % (input, output) 195 ttf = TTFont(input, 0, verbose=options.verbose, allowVID=options.allowVID, 196 lazy=False, 197 quiet=options.quiet, 198 ignoreDecompileErrors=options.ignoreDecompileErrors, 199 fontNumber=options.fontNumber) 200 ttf.saveXML(output, 201 quiet=options.quiet, 202 tables=options.onlyTables, 203 skipTables=options.skipTables, 204 splitTables=options.splitTables, 205 disassembleInstructions=options.disassembleInstructions, 206 bitmapGlyphDataFormat=options.bitmapGlyphDataFormat) 207 ttf.close() 208 209 210def ttCompile(input, output, options): 211 if not options.quiet: 212 print 'Compiling "%s" to "%s"...' % (input, output) 213 ttf = TTFont(options.mergeFile, 214 lazy=False, 215 recalcBBoxes=options.recalcBBoxes, 216 verbose=options.verbose, allowVID=options.allowVID) 217 ttf.importXML(input, quiet=options.quiet) 218 try: 219 ttf.save(output) 220 except OTLOffsetOverflowError, e: 221 # XXX This shouldn't be here at all, it should be as close to the 222 # OTL code as possible. 223 overflowRecord = e.value 224 print "Attempting to fix OTLOffsetOverflowError", e 225 lastItem = overflowRecord 226 while True: 227 ok = 0 228 if overflowRecord.itemName == None: 229 ok = fixLookupOverFlows(ttf, overflowRecord) 230 else: 231 ok = fixSubTableOverFlows(ttf, overflowRecord) 232 if not ok: 233 raise 234 235 try: 236 ttf.save(output) 237 break 238 except OTLOffsetOverflowError, e: 239 print "Attempting to fix OTLOffsetOverflowError", e 240 overflowRecord = e.value 241 if overflowRecord == lastItem: 242 raise 243 244 if options.verbose: 245 import time 246 print "finished at", time.strftime("%H:%M:%S", time.localtime(time.time())) 247 248 249def guessFileType(fileName): 250 base, ext = os.path.splitext(fileName) 251 try: 252 f = open(fileName, "rb") 253 except IOError: 254 return None 255 cr, tp = getMacCreatorAndType(fileName) 256 if tp in ("sfnt", "FFIL"): 257 return "TTF" 258 if ext == ".dfont": 259 return "TTF" 260 header = f.read(256) 261 head = header[:4] 262 if head == "OTTO": 263 return "OTF" 264 elif head == "ttcf": 265 return "TTC" 266 elif head in ("\0\1\0\0", "true"): 267 return "TTF" 268 elif head in ("wOFF", "true"): 269 return "WOFF" 270 elif head.lower() == "<?xm": 271 if opentypeheaderRE.search(header): 272 return "OTX" 273 else: 274 return "TTX" 275 return None 276 277 278def parseOptions(args): 279 try: 280 rawOptions, files = getopt.getopt(args, "ld:o:vqht:x:sim:z:baey:") 281 except getopt.GetoptError: 282 usage() 283 284 if not files: 285 usage() 286 287 options = Options(rawOptions, len(files)) 288 jobs = [] 289 290 for input in files: 291 tp = guessFileType(input) 292 if tp in ("OTF", "TTF", "TTC", "WOFF"): 293 extension = ".ttx" 294 if options.listTables: 295 action = ttList 296 else: 297 action = ttDump 298 elif tp == "TTX": 299 extension = ".ttf" 300 action = ttCompile 301 elif tp == "OTX": 302 extension = ".otf" 303 action = ttCompile 304 else: 305 print 'Unknown file type: "%s"' % input 306 continue 307 308 if options.outputFile: 309 output = options.outputFile 310 else: 311 output = makeOutputFileName(input, options.outputDir, extension) 312 jobs.append((action, input, output)) 313 return jobs, options 314 315 316def process(jobs, options): 317 for action, input, output in jobs: 318 action(input, output, options) 319 320 321def waitForKeyPress(): 322 """Force the DOS Prompt window to stay open so the user gets 323 a chance to see what's wrong.""" 324 import msvcrt 325 print '(Hit any key to exit)' 326 while not msvcrt.kbhit(): 327 pass 328 329 330def main(args): 331 jobs, options = parseOptions(args) 332 try: 333 process(jobs, options) 334 except KeyboardInterrupt: 335 print "(Cancelled.)" 336 except SystemExit: 337 if sys.platform == "win32": 338 waitForKeyPress() 339 else: 340 raise 341 except TTLibError, e: 342 print "Error:",e 343 except: 344 if sys.platform == "win32": 345 import traceback 346 traceback.print_exc() 347 waitForKeyPress() 348 else: 349 raise 350 351 352if __name__ == "__main__": 353 main(sys.argv[1:]) 354