ttx.py revision 5f6418d9e1fa15a89dcec29cdc433ba2c99732c3
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 69from __future__ import print_function, division 70from fontTools.misc.py23 import * 71from fontTools.ttLib import TTFont, TTLibError 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 76import os 77import sys 78import getopt 79import re 80 81def usage(): 82 print(__doc__ % version) 83 sys.exit(2) 84 85 86numberAddedRE = re.compile("#\d+$") 87opentypeheaderRE = re.compile('''sfntVersion=['"]OTTO["']''') 88 89def makeOutputFileName(input, outputDir, extension): 90 dir, file = os.path.split(input) 91 file, ext = os.path.splitext(file) 92 if outputDir: 93 dir = outputDir 94 file = numberAddedRE.split(file)[0] 95 output = os.path.join(dir, file + extension) 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 = False 106 outputDir = None 107 outputFile = None 108 verbose = False 109 quiet = False 110 splitTables = False 111 disassembleInstructions = True 112 mergeFile = None 113 recalcBBoxes = True 114 allowVID = False 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 = True 136 elif option == "-q": 137 self.quiet = True 138 # dump options 139 elif option == "-l": 140 self.listTables = True 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 = True 147 elif option == "-i": 148 self.disassembleInstructions = False 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 = False 162 elif option == "-a": 163 self.allowVID = True 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 ttf = TTFont(input, fontNumber=options.fontNumber, lazy=True) 176 reader = ttf.reader 177 tags = sorted(reader.keys()) 178 print('Listing table info for "%s":' % input) 179 format = " %4s %10s %7s %7s" 180 print(format % ("tag ", " checksum", " length", " offset")) 181 print(format % ("----", "----------", "-------", "-------")) 182 for tag in tags: 183 entry = reader.tables[tag] 184 checkSum = int(entry.checkSum) 185 if checkSum < 0: 186 checkSum = checkSum + 0x100000000 187 checksum = "0x%08X" % checkSum 188 print(format % (tag, checksum, entry.length, entry.offset)) 189 print() 190 ttf.close() 191 192 193def ttDump(input, output, options): 194 if not options.quiet: 195 print('Dumping "%s" to "%s"...' % (input, output)) 196 ttf = TTFont(input, 0, verbose=options.verbose, allowVID=options.allowVID, 197 lazy=False, 198 quiet=options.quiet, 199 ignoreDecompileErrors=options.ignoreDecompileErrors, 200 fontNumber=options.fontNumber) 201 ttf.saveXML(output, 202 quiet=options.quiet, 203 tables=options.onlyTables, 204 skipTables=options.skipTables, 205 splitTables=options.splitTables, 206 disassembleInstructions=options.disassembleInstructions, 207 bitmapGlyphDataFormat=options.bitmapGlyphDataFormat) 208 ttf.close() 209 210 211def ttCompile(input, output, options): 212 if not options.quiet: 213 print('Compiling "%s" to "%s"...' % (input, output)) 214 ttf = TTFont(options.mergeFile, 215 lazy=False, 216 recalcBBoxes=options.recalcBBoxes, 217 verbose=options.verbose, allowVID=options.allowVID) 218 ttf.importXML(input, quiet=options.quiet) 219 try: 220 ttf.save(output) 221 except OTLOffsetOverflowError as e: 222 # XXX This shouldn't be here at all, it should be as close to the 223 # OTL code as possible. 224 overflowRecord = e.value 225 print("Attempting to fix OTLOffsetOverflowError", e) 226 lastItem = overflowRecord 227 while True: 228 ok = 0 229 if overflowRecord.itemName == None: 230 ok = fixLookupOverFlows(ttf, overflowRecord) 231 else: 232 ok = fixSubTableOverFlows(ttf, overflowRecord) 233 if not ok: 234 raise 235 236 try: 237 ttf.save(output) 238 break 239 except OTLOffsetOverflowError as e: 240 print("Attempting to fix OTLOffsetOverflowError", e) 241 overflowRecord = e.value 242 if overflowRecord == lastItem: 243 raise 244 245 if options.verbose: 246 import time 247 print("finished at", time.strftime("%H:%M:%S", time.localtime(time.time()))) 248 249 250def guessFileType(fileName): 251 base, ext = os.path.splitext(fileName) 252 try: 253 f = open(fileName, "rb") 254 except IOError: 255 return None 256 cr, tp = getMacCreatorAndType(fileName) 257 if tp in ("sfnt", "FFIL"): 258 return "TTF" 259 if ext == ".dfont": 260 return "TTF" 261 header = f.read(256) 262 head = Tag(header[:4]) 263 if head == "OTTO": 264 return "OTF" 265 elif head == "ttcf": 266 return "TTC" 267 elif head in ("\0\1\0\0", "true"): 268 return "TTF" 269 elif head == "wOFF": 270 return "WOFF" 271 elif head.lower() == "<?xm": 272 # Use 'latin-1' because that can't fail. 273 header = tostr(header, 'latin-1') 274 if opentypeheaderRE.search(header): 275 return "OTX" 276 else: 277 return "TTX" 278 return None 279 280 281def parseOptions(args): 282 try: 283 rawOptions, files = getopt.getopt(args, "ld:o:vqht:x:sim:z:baey:") 284 except getopt.GetoptError: 285 usage() 286 287 if not files: 288 usage() 289 290 options = Options(rawOptions, len(files)) 291 jobs = [] 292 293 for input in files: 294 tp = guessFileType(input) 295 if tp in ("OTF", "TTF", "TTC", "WOFF"): 296 extension = ".ttx" 297 if options.listTables: 298 action = ttList 299 else: 300 action = ttDump 301 elif tp == "TTX": 302 extension = ".ttf" 303 action = ttCompile 304 elif tp == "OTX": 305 extension = ".otf" 306 action = ttCompile 307 else: 308 print('Unknown file type: "%s"' % input) 309 continue 310 311 if options.outputFile: 312 output = options.outputFile 313 else: 314 output = makeOutputFileName(input, options.outputDir, extension) 315 jobs.append((action, input, output)) 316 return jobs, options 317 318 319def process(jobs, options): 320 for action, input, output in jobs: 321 action(input, output, options) 322 323 324def waitForKeyPress(): 325 """Force the DOS Prompt window to stay open so the user gets 326 a chance to see what's wrong.""" 327 import msvcrt 328 print('(Hit any key to exit)') 329 while not msvcrt.kbhit(): 330 pass 331 332 333def main(args): 334 jobs, options = parseOptions(args) 335 try: 336 process(jobs, options) 337 except KeyboardInterrupt: 338 print("(Cancelled.)") 339 except SystemExit: 340 if sys.platform == "win32": 341 waitForKeyPress() 342 else: 343 raise 344 except TTLibError as e: 345 print("Error:",e) 346 except: 347 if sys.platform == "win32": 348 import traceback 349 traceback.print_exc() 350 waitForKeyPress() 351 else: 352 raise 353 354 355if __name__ == "__main__": 356 main(sys.argv[1:]) 357