ttx.py revision 2921bb25cd89f665bafb166b38b83baab5c1bd38
1#! /usr/bin/env python 2 3"""\ 4usage: ttx [options] inputfile1 [... inputfileN] 5 6 TTX %s -- From OpenType To XML And Back 7 8 If an input file is a TrueType or OpenType font file, it will be 9 dumped to an TTX file (an XML-based text format). 10 If an input file is a TTX file, it will be compiled to a TrueType 11 or OpenType font file. 12 13 Output files are created so they are unique: an existing file is 14 never overwrritten. 15 16 General options: 17 -h Help: print this message 18 -d <outputfolder> Specify a directory where the output files are 19 to be created. 20 -v Verbose: more messages will be written to stdout about what 21 is being done. 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 42 Compile options: 43 -m Merge with TrueType-input-file: specify a TrueType or OpenType 44 font file to be merged with the TTX file. This option is only 45 valid when at most one TTX file is specified. 46 -b Don't recalc glyph boundig boxes: use the values in the TTX 47 file as-is. 48""" 49 50 51import sys 52import os 53import getopt 54import re 55from fontTools.ttLib import TTFont 56from fontTools import version 57 58def usage(): 59 print __doc__ % version 60 sys.exit(2) 61 62 63numberAddedRE = re.compile("(.*)#\d+$") 64 65def makeOutputFileName(input, outputDir, extension): 66 dir, file = os.path.split(input) 67 file, ext = os.path.splitext(file) 68 if outputDir: 69 dir = outputDir 70 output = os.path.join(dir, file + extension) 71 m = numberAddedRE.match(file) 72 if m: 73 file = m.group(1) 74 n = 1 75 while os.path.exists(output): 76 output = os.path.join(dir, file + "#" + repr(n) + extension) 77 n = n + 1 78 return output 79 80 81class Options: 82 83 listTables = 0 84 outputDir = None 85 verbose = 0 86 splitTables = 0 87 disassembleInstructions = 1 88 mergeFile = None 89 recalcBBoxes = 1 90 91 def __init__(self, rawOptions, numFiles): 92 self.onlyTables = [] 93 self.skipTables = [] 94 for option, value in rawOptions: 95 # general options 96 if option == "-h": 97 print __doc__ % version 98 sys.exit(0) 99 elif option == "-d": 100 if not os.path.isdir(value): 101 print "The -d option value must be an existing directory" 102 sys.exit(2) 103 self.outputDir = value 104 elif option == "-v": 105 self.verbose = 1 106 # dump options 107 elif option == "-l": 108 self.listTables = 1 109 elif option == "-t": 110 self.onlyTables.append(value) 111 elif option == "-x": 112 self.skipTables.append(value) 113 elif option == "-s": 114 self.splitTables = 1 115 elif option == "-i": 116 self.disassembleInstructions = 0 117 # compile options 118 elif option == "-m": 119 self.mergeFile = value 120 elif option == "-b": 121 self.recalcBBoxes = 0 122 if self.onlyTables and self.skipTables: 123 print "-t and -x options are mutually exlusive" 124 sys.exit(2) 125 if self.mergeFile and numFiles > 1: 126 print "Must specify exactly one TTX source file when using -i" 127 sys.exit(2) 128 129 130def ttList(input, output, options): 131 ttf = TTFont(input) 132 reader = ttf.reader 133 tags = reader.keys() 134 tags.sort() 135 print 'Listing table info for "%s":' % input 136 format = " %4s %10s %7s %7s" 137 print format % ("tag ", " checksum", " length", " offset") 138 print format % ("----", "----------", "-------", "-------") 139 for tag in tags: 140 entry = reader.tables[tag] 141 checksum = "0x" + hex(entry.checkSum)[2:].zfill(8) 142 print format % (tag, checksum, entry.length, entry.offset) 143 print 144 ttf.close() 145 146 147def ttDump(input, output, options): 148 print 'Dumping "%s" to "%s"...' % (input, output) 149 ttf = TTFont(input, 0, verbose=options.verbose) 150 ttf.saveXML(output, 151 tables=options.onlyTables, 152 skipTables=options.skipTables, 153 splitTables=options.splitTables, 154 disassembleInstructions=options.disassembleInstructions) 155 ttf.close() 156 157 158def ttCompile(input, output, options): 159 print 'Compiling "%s" to "%s"...' % (input, output) 160 ttf = TTFont(options.mergeFile, 161 recalcBBoxes=options.recalcBBoxes, 162 verbose=options.verbose) 163 ttf.importXML(input) 164 ttf.save(output) 165 166 if options.verbose: 167 import time 168 print "finished at", time.strftime("%H:%M:%S", time.localtime(time.time())) 169 170 171def guessFileType(fileName): 172 try: 173 f = open(fileName, "rb") 174 except IOError: 175 return None 176 header = f.read(256) 177 head = header[:4] 178 if head == "OTTO": 179 return "OTF" 180 elif head in ("\0\1\0\0", "true"): 181 return "TTF" 182 elif head.lower() == "<?xm": 183 if header.find('sfntVersion="OTTO"') > 0: 184 return "OTX" 185 else: 186 return "TTX" 187 # XXX Mac suitcase! 188 return None 189 190 191def parseOptions(args): 192 try: 193 rawOptions, files = getopt.getopt(args, "ld:vht:x:sim:b") 194 except getopt.GetoptError: 195 usage() 196 197 if not files: 198 usage() 199 200 options = Options(rawOptions, len(files)) 201 jobs = [] 202 203 for input in files: 204 tp = guessFileType(input) 205 if tp in ("OTF", "TTF"): 206 extension = ".ttx" 207 if options.listTables: 208 action = ttList 209 else: 210 action = ttDump 211 elif tp == "TTX": 212 extension = ".ttf" 213 action = ttCompile 214 elif tp == "OTX": 215 extension = ".otf" 216 action = ttCompile 217 else: 218 print 'Unknown file type: "%s"' % input 219 continue 220 221 output = makeOutputFileName(input, options.outputDir, extension) 222 jobs.append((action, input, output)) 223 return jobs, options 224 225 226def process(jobs, options): 227 for action, input, output in jobs: 228 action(input, output, options) 229 230 231def waitForKeyPress(): 232 """Force the DOS Prompt window to stay open so the user gets 233 a chance to see what's wrong.""" 234 import msvcrt 235 print '(Hit any key to exit)' 236 while not msvcrt.kbhit(): 237 pass 238 239 240def main(args): 241 jobs, options = parseOptions(args) 242 try: 243 process(jobs, options) 244 except KeyboardInterrupt: 245 print "(Cancelled.)" 246 except SystemExit: 247 if sys.platform == "win32": 248 waitForKeyPress() 249 else: 250 raise 251 except: 252 if sys.platform == "win32": 253 import traceback 254 traceback.print_exc() 255 waitForKeyPress() 256 else: 257 raise 258 259 260if __name__ == "__main__": 261 main(sys.argv[1:]) 262