ttx.py revision b158597aea89fb178452da3787aacd8aa4e83d03
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 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 output = os.path.join(dir, file + extension) 93 m = numberAddedRE.match(file) 94 if m: 95 file = m.group(1) 96 n = 1 97 while os.path.exists(output): 98 output = os.path.join(dir, file + "#" + repr(n) + extension) 99 n = n + 1 100 return output 101 102 103class Options: 104 105 listTables = 0 106 outputDir = None 107 outputFile = None 108 verbose = 0 109 quiet = 0 110 splitTables = 0 111 disassembleInstructions = 1 112 mergeFile = None 113 recalcBBoxes = 1 114 allowVID = 0 115 ignoreDecompileErrors = True 116 bitmapGlyphDataFormat = 'raw' 117 118 def __init__(self, rawOptions, numFiles): 119 self.onlyTables = [] 120 self.skipTables = [] 121 self.fontNumber = -1 122 for option, value in rawOptions: 123 # general options 124 if option == "-h": 125 print __doc__ % version 126 sys.exit(0) 127 elif option == "-d": 128 if not os.path.isdir(value): 129 print "The -d option value must be an existing directory" 130 sys.exit(2) 131 self.outputDir = value 132 elif option == "-o": 133 self.outputFile = value 134 elif option == "-v": 135 self.verbose = 1 136 elif option == "-q": 137 self.quiet = 1 138 # dump options 139 elif option == "-l": 140 self.listTables = 1 141 elif option == "-t": 142 self.onlyTables.append(value) 143 elif option == "-x": 144 self.skipTables.append(value) 145 elif option == "-s": 146 self.splitTables = 1 147 elif option == "-i": 148 self.disassembleInstructions = 0 149 elif option == "-z": 150 validOptions = ('raw', 'row', 'bitwise', 'extfile') 151 if value not in validOptions: 152 print "-z does not allow %s as a format. Use %s" % (option, validOptions) 153 sys.exit(2) 154 self.bitmapGlyphDataFormat = value 155 elif option == "-y": 156 self.fontNumber = int(value) 157 # compile options 158 elif option == "-m": 159 self.mergeFile = value 160 elif option == "-b": 161 self.recalcBBoxes = 0 162 elif option == "-a": 163 self.allowVID = 1 164 elif option == "-e": 165 self.ignoreDecompileErrors = False 166 if self.onlyTables and self.skipTables: 167 print "-t and -x options are mutually exclusive" 168 sys.exit(2) 169 if self.mergeFile and numFiles > 1: 170 print "Must specify exactly one TTX source file when using -m" 171 sys.exit(2) 172 173 174def ttList(input, output, options): 175 import string 176 ttf = TTFont(input, fontNumber=options.fontNumber) 177 reader = ttf.reader 178 tags = reader.keys() 179 tags.sort() 180 print 'Listing table info for "%s":' % input 181 format = " %4s %10s %7s %7s" 182 print format % ("tag ", " checksum", " length", " offset") 183 print format % ("----", "----------", "-------", "-------") 184 for tag in tags: 185 entry = reader.tables[tag] 186 checkSum = long(entry.checkSum) 187 if checkSum < 0: 188 checkSum = checkSum + 0x100000000L 189 checksum = "0x" + string.zfill(hex(checkSum)[2:-1], 8) 190 print format % (tag, checksum, entry.length, entry.offset) 191 print 192 ttf.close() 193 194 195def ttDump(input, output, options): 196 if not options.quiet: 197 print 'Dumping "%s" to "%s"...' % (input, output) 198 ttf = TTFont(input, 0, verbose=options.verbose, allowVID=options.allowVID, 199 ignoreDecompileErrors=options.ignoreDecompileErrors, 200 fontNumber=options.fontNumber) 201 ttf.saveXML(output, 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 print 'Compiling "%s" to "%s"...' % (input, output) 212 ttf = TTFont(options.mergeFile, 213 recalcBBoxes=options.recalcBBoxes, 214 verbose=options.verbose, allowVID=options.allowVID) 215 ttf.importXML(input) 216 try: 217 ttf.save(output) 218 except OTLOffsetOverflowError, e: 219 # XXX This shouldn't be here at all, it should be as close to the 220 # OTL code as possible. 221 overflowRecord = e.value 222 print "Attempting to fix OTLOffsetOverflowError", e 223 lastItem = overflowRecord 224 while 1: 225 ok = 0 226 if overflowRecord.itemName == None: 227 ok = fixLookupOverFlows(ttf, overflowRecord) 228 else: 229 ok = fixSubTableOverFlows(ttf, overflowRecord) 230 if not ok: 231 raise 232 233 try: 234 ttf.save(output) 235 break 236 except OTLOffsetOverflowError, e: 237 print "Attempting to fix OTLOffsetOverflowError", e 238 overflowRecord = e.value 239 if overflowRecord == lastItem: 240 raise 241 242 if options.verbose: 243 import time 244 print "finished at", time.strftime("%H:%M:%S", time.localtime(time.time())) 245 246 247def guessFileType(fileName): 248 base, ext = os.path.splitext(fileName) 249 try: 250 f = open(fileName, "rb") 251 except IOError: 252 return None 253 cr, tp = getMacCreatorAndType(fileName) 254 if tp in ("sfnt", "FFIL"): 255 return "TTF" 256 if ext == ".dfont": 257 return "TTF" 258 header = f.read(256) 259 head = header[:4] 260 if head == "OTTO": 261 return "OTF" 262 elif head == "ttcf": 263 return "TTC" 264 elif head in ("\0\1\0\0", "true"): 265 return "TTF" 266 elif head.lower() == "<?xm": 267 if opentypeheaderRE.match(header): 268 return "OTX" 269 else: 270 return "TTX" 271 return None 272 273 274def parseOptions(args): 275 try: 276 rawOptions, files = getopt.getopt(args, "ld:o:vht:x:sim:z:baey:") 277 except getopt.GetoptError: 278 usage() 279 280 if not files: 281 usage() 282 283 options = Options(rawOptions, len(files)) 284 jobs = [] 285 286 for input in files: 287 tp = guessFileType(input) 288 if tp in ("OTF", "TTF", "TTC"): 289 extension = ".ttx" 290 if options.listTables: 291 action = ttList 292 else: 293 action = ttDump 294 elif tp == "TTX": 295 extension = ".ttf" 296 action = ttCompile 297 elif tp == "OTX": 298 extension = ".otf" 299 action = ttCompile 300 else: 301 print 'Unknown file type: "%s"' % input 302 continue 303 304 if options.outputFile: 305 output = options.outputFile 306 else: 307 output = makeOutputFileName(input, options.outputDir, extension) 308 jobs.append((action, input, output)) 309 return jobs, options 310 311 312def process(jobs, options): 313 for action, input, output in jobs: 314 action(input, output, options) 315 316 317def waitForKeyPress(): 318 """Force the DOS Prompt window to stay open so the user gets 319 a chance to see what's wrong.""" 320 import msvcrt 321 print '(Hit any key to exit)' 322 while not msvcrt.kbhit(): 323 pass 324 325 326def main(args): 327 jobs, options = parseOptions(args) 328 try: 329 process(jobs, options) 330 except KeyboardInterrupt: 331 print "(Cancelled.)" 332 except SystemExit: 333 if sys.platform == "win32": 334 waitForKeyPress() 335 else: 336 raise 337 except: 338 if sys.platform == "win32": 339 import traceback 340 traceback.print_exc() 341 waitForKeyPress() 342 else: 343 raise 344 345 346if __name__ == "__main__": 347 main(sys.argv[1:]) 348