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