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