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