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