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