1"""\ 2usage: ttx [options] inputfile1 [... inputfileN] 3 4 TTX -- From OpenType To XML And Back 5 6 If an input file is a TrueType or OpenType font file, it will be 7 decompiled to a TTX file (an XML-based text format). 8 If an input file is a TTX file, it will be compiled to whatever 9 format the data is in, a TrueType or OpenType/CFF 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 --version: show version and exit. 17 -d <outputfolder> Specify a directory where the output files are 18 to be created. 19 -o <outputfile> Specify a file to write the output to. A special 20 value of - would use the standard output. 21 -f Overwrite existing output file(s), ie. don't append numbers. 22 -v Verbose: more messages will be written to stdout about what 23 is being done. 24 -q Quiet: No messages will be written to stdout about what 25 is being done. 26 -a allow virtual glyphs ID's on compile or decompile. 27 28 Dump options: 29 -l List table info: instead of dumping to a TTX file, list some 30 minimal info about each table. 31 -t <table> Specify a table to dump. Multiple -t options 32 are allowed. When no -t option is specified, all tables 33 will be dumped. 34 -x <table> Specify a table to exclude from the dump. Multiple 35 -x options are allowed. -t and -x are mutually exclusive. 36 -s Split tables: save the TTX data into separate TTX files per 37 table and write one small TTX file that contains references 38 to the individual table dumps. This file can be used as 39 input to ttx, as long as the table files are in the 40 same directory. 41 -g Split glyf table: Save the glyf data into separate TTX files 42 per glyph and write a small TTX for the glyf table which 43 contains references to the individual TTGlyph elements. 44 NOTE: specifying -g implies -s (no need for -s together with -g) 45 -i Do NOT disassemble TT instructions: when this option is given, 46 all TrueType programs (glyph programs, the font program and the 47 pre-program) will be written to the TTX file as hex data 48 instead of assembly. This saves some time and makes the TTX 49 file smaller. 50 -z <format> Specify a bitmap data export option for EBDT: 51 {'raw', 'row', 'bitwise', 'extfile'} or for the CBDT: 52 {'raw', 'extfile'} Each option does one of the following: 53 -z raw 54 * export the bitmap data as a hex dump 55 -z row 56 * export each row as hex data 57 -z bitwise 58 * export each row as binary in an ASCII art style 59 -z extfile 60 * export the data as external files with XML references 61 If no export format is specified 'raw' format is used. 62 -e Don't ignore decompilation errors, but show a full traceback 63 and abort. 64 -y <number> Select font number for TrueType Collection (.ttc/.otc), 65 starting from 0. 66 --unicodedata <UnicodeData.txt> Use custom database file to write 67 character names in the comments of the cmap TTX output. 68 --newline <value> Control how line endings are written in the XML 69 file. It can be 'LF', 'CR', or 'CRLF'. If not specified, the 70 default platform-specific line endings are used. 71 72 Compile options: 73 -m Merge with TrueType-input-file: specify a TrueType or OpenType 74 font file to be merged with the TTX file. This option is only 75 valid when at most one TTX file is specified. 76 -b Don't recalc glyph bounding boxes: use the values in the TTX 77 file as-is. 78 --recalc-timestamp Set font 'modified' timestamp to current time. 79 By default, the modification time of the TTX file will be used. 80 --no-recalc-timestamp Keep the original font 'modified' timestamp. 81 --flavor <type> Specify flavor of output font file. May be 'woff' 82 or 'woff2'. Note that WOFF2 requires the Brotli Python extension, 83 available at https://github.com/google/brotli 84 --with-zopfli Use Zopfli instead of Zlib to compress WOFF. The Python 85 extension is available at https://pypi.python.org/pypi/zopfli 86""" 87 88 89from __future__ import print_function, division, absolute_import 90from fontTools.misc.py23 import * 91from fontTools.ttLib import TTFont, TTLibError 92from fontTools.misc.macCreatorType import getMacCreatorAndType 93from fontTools.unicode import setUnicodeData 94from fontTools.misc.timeTools import timestampSinceEpoch 95from fontTools.misc.loggingTools import Timer 96from fontTools.misc.cliTools import makeOutputFileName 97import os 98import sys 99import getopt 100import re 101import logging 102 103 104log = logging.getLogger("fontTools.ttx") 105 106opentypeheaderRE = re.compile('''sfntVersion=['"]OTTO["']''') 107 108 109class Options(object): 110 111 listTables = False 112 outputDir = None 113 outputFile = None 114 overWrite = False 115 verbose = False 116 quiet = False 117 splitTables = False 118 splitGlyphs = False 119 disassembleInstructions = True 120 mergeFile = None 121 recalcBBoxes = True 122 allowVID = False 123 ignoreDecompileErrors = True 124 bitmapGlyphDataFormat = 'raw' 125 unicodedata = None 126 newlinestr = None 127 recalcTimestamp = None 128 flavor = None 129 useZopfli = False 130 131 def __init__(self, rawOptions, numFiles): 132 self.onlyTables = [] 133 self.skipTables = [] 134 self.fontNumber = -1 135 for option, value in rawOptions: 136 # general options 137 if option == "-h": 138 print(__doc__) 139 sys.exit(0) 140 elif option == "--version": 141 from fontTools import version 142 print(version) 143 sys.exit(0) 144 elif option == "-d": 145 if not os.path.isdir(value): 146 raise getopt.GetoptError("The -d option value must be an existing directory") 147 self.outputDir = value 148 elif option == "-o": 149 self.outputFile = value 150 elif option == "-f": 151 self.overWrite = True 152 elif option == "-v": 153 self.verbose = True 154 elif option == "-q": 155 self.quiet = True 156 # dump options 157 elif option == "-l": 158 self.listTables = True 159 elif option == "-t": 160 # pad with space if table tag length is less than 4 161 value = value.ljust(4) 162 self.onlyTables.append(value) 163 elif option == "-x": 164 # pad with space if table tag length is less than 4 165 value = value.ljust(4) 166 self.skipTables.append(value) 167 elif option == "-s": 168 self.splitTables = True 169 elif option == "-g": 170 # -g implies (and forces) splitTables 171 self.splitGlyphs = True 172 self.splitTables = True 173 elif option == "-i": 174 self.disassembleInstructions = False 175 elif option == "-z": 176 validOptions = ('raw', 'row', 'bitwise', 'extfile') 177 if value not in validOptions: 178 raise getopt.GetoptError( 179 "-z does not allow %s as a format. Use %s" % (option, validOptions)) 180 self.bitmapGlyphDataFormat = value 181 elif option == "-y": 182 self.fontNumber = int(value) 183 # compile options 184 elif option == "-m": 185 self.mergeFile = value 186 elif option == "-b": 187 self.recalcBBoxes = False 188 elif option == "-a": 189 self.allowVID = True 190 elif option == "-e": 191 self.ignoreDecompileErrors = False 192 elif option == "--unicodedata": 193 self.unicodedata = value 194 elif option == "--newline": 195 validOptions = ('LF', 'CR', 'CRLF') 196 if value == "LF": 197 self.newlinestr = "\n" 198 elif value == "CR": 199 self.newlinestr = "\r" 200 elif value == "CRLF": 201 self.newlinestr = "\r\n" 202 else: 203 raise getopt.GetoptError( 204 "Invalid choice for --newline: %r (choose from %s)" 205 % (value, ", ".join(map(repr, validOptions)))) 206 elif option == "--recalc-timestamp": 207 self.recalcTimestamp = True 208 elif option == "--no-recalc-timestamp": 209 self.recalcTimestamp = False 210 elif option == "--flavor": 211 self.flavor = value 212 elif option == "--with-zopfli": 213 self.useZopfli = True 214 if self.verbose and self.quiet: 215 raise getopt.GetoptError("-q and -v options are mutually exclusive") 216 if self.verbose: 217 self.logLevel = logging.DEBUG 218 elif self.quiet: 219 self.logLevel = logging.WARNING 220 else: 221 self.logLevel = logging.INFO 222 if self.mergeFile and self.flavor: 223 raise getopt.GetoptError("-m and --flavor options are mutually exclusive") 224 if self.onlyTables and self.skipTables: 225 raise getopt.GetoptError("-t and -x options are mutually exclusive") 226 if self.mergeFile and numFiles > 1: 227 raise getopt.GetoptError("Must specify exactly one TTX source file when using -m") 228 if self.flavor != 'woff' and self.useZopfli: 229 raise getopt.GetoptError("--with-zopfli option requires --flavor 'woff'") 230 231 232def ttList(input, output, options): 233 ttf = TTFont(input, fontNumber=options.fontNumber, lazy=True) 234 reader = ttf.reader 235 tags = sorted(reader.keys()) 236 print('Listing table info for "%s":' % input) 237 format = " %4s %10s %8s %8s" 238 print(format % ("tag ", " checksum", " length", " offset")) 239 print(format % ("----", "----------", "--------", "--------")) 240 for tag in tags: 241 entry = reader.tables[tag] 242 if ttf.flavor == "woff2": 243 # WOFF2 doesn't store table checksums, so they must be calculated 244 from fontTools.ttLib.sfnt import calcChecksum 245 data = entry.loadData(reader.transformBuffer) 246 checkSum = calcChecksum(data) 247 else: 248 checkSum = int(entry.checkSum) 249 if checkSum < 0: 250 checkSum = checkSum + 0x100000000 251 checksum = "0x%08X" % checkSum 252 print(format % (tag, checksum, entry.length, entry.offset)) 253 print() 254 ttf.close() 255 256 257@Timer(log, 'Done dumping TTX in %(time).3f seconds') 258def ttDump(input, output, options): 259 log.info('Dumping "%s" to "%s"...', input, output) 260 if options.unicodedata: 261 setUnicodeData(options.unicodedata) 262 ttf = TTFont(input, 0, allowVID=options.allowVID, 263 ignoreDecompileErrors=options.ignoreDecompileErrors, 264 fontNumber=options.fontNumber) 265 ttf.saveXML(output, 266 tables=options.onlyTables, 267 skipTables=options.skipTables, 268 splitTables=options.splitTables, 269 splitGlyphs=options.splitGlyphs, 270 disassembleInstructions=options.disassembleInstructions, 271 bitmapGlyphDataFormat=options.bitmapGlyphDataFormat, 272 newlinestr=options.newlinestr) 273 ttf.close() 274 275 276@Timer(log, 'Done compiling TTX in %(time).3f seconds') 277def ttCompile(input, output, options): 278 log.info('Compiling "%s" to "%s"...' % (input, output)) 279 if options.useZopfli: 280 from fontTools.ttLib import sfnt 281 sfnt.USE_ZOPFLI = True 282 ttf = TTFont(options.mergeFile, flavor=options.flavor, 283 recalcBBoxes=options.recalcBBoxes, 284 recalcTimestamp=options.recalcTimestamp, 285 allowVID=options.allowVID) 286 ttf.importXML(input) 287 288 if options.recalcTimestamp is None and 'head' in ttf: 289 # use TTX file modification time for head "modified" timestamp 290 mtime = os.path.getmtime(input) 291 ttf['head'].modified = timestampSinceEpoch(mtime) 292 293 ttf.save(output) 294 295 296def guessFileType(fileName): 297 base, ext = os.path.splitext(fileName) 298 try: 299 with open(fileName, "rb") as f: 300 header = f.read(256) 301 except IOError: 302 return None 303 304 if header.startswith(b'\xef\xbb\xbf<?xml'): 305 header = header.lstrip(b'\xef\xbb\xbf') 306 cr, tp = getMacCreatorAndType(fileName) 307 if tp in ("sfnt", "FFIL"): 308 return "TTF" 309 if ext == ".dfont": 310 return "TTF" 311 head = Tag(header[:4]) 312 if head == "OTTO": 313 return "OTF" 314 elif head == "ttcf": 315 return "TTC" 316 elif head in ("\0\1\0\0", "true"): 317 return "TTF" 318 elif head == "wOFF": 319 return "WOFF" 320 elif head == "wOF2": 321 return "WOFF2" 322 elif head == "<?xm": 323 # Use 'latin1' because that can't fail. 324 header = tostr(header, 'latin1') 325 if opentypeheaderRE.search(header): 326 return "OTX" 327 else: 328 return "TTX" 329 return None 330 331 332def parseOptions(args): 333 rawOptions, files = getopt.getopt(args, "ld:o:fvqht:x:sgim:z:baey:", 334 ['unicodedata=', "recalc-timestamp", "no-recalc-timestamp", 335 'flavor=', 'version', 'with-zopfli', 'newline=']) 336 337 options = Options(rawOptions, len(files)) 338 jobs = [] 339 340 if not files: 341 raise getopt.GetoptError('Must specify at least one input file') 342 343 for input in files: 344 if not os.path.isfile(input): 345 raise getopt.GetoptError('File not found: "%s"' % input) 346 tp = guessFileType(input) 347 if tp in ("OTF", "TTF", "TTC", "WOFF", "WOFF2"): 348 extension = ".ttx" 349 if options.listTables: 350 action = ttList 351 else: 352 action = ttDump 353 elif tp == "TTX": 354 extension = "."+options.flavor if options.flavor else ".ttf" 355 action = ttCompile 356 elif tp == "OTX": 357 extension = "."+options.flavor if options.flavor else ".otf" 358 action = ttCompile 359 else: 360 raise getopt.GetoptError('Unknown file type: "%s"' % input) 361 362 if options.outputFile: 363 output = options.outputFile 364 else: 365 output = makeOutputFileName(input, options.outputDir, extension, options.overWrite) 366 # 'touch' output file to avoid race condition in choosing file names 367 if action != ttList: 368 open(output, 'a').close() 369 jobs.append((action, input, output)) 370 return jobs, options 371 372 373def process(jobs, options): 374 for action, input, output in jobs: 375 action(input, output, options) 376 377 378def waitForKeyPress(): 379 """Force the DOS Prompt window to stay open so the user gets 380 a chance to see what's wrong.""" 381 import msvcrt 382 print('(Hit any key to exit)', file=sys.stderr) 383 while not msvcrt.kbhit(): 384 pass 385 386 387def main(args=None): 388 from fontTools import configLogger 389 390 if args is None: 391 args = sys.argv[1:] 392 try: 393 jobs, options = parseOptions(args) 394 except getopt.GetoptError as e: 395 print("%s\nERROR: %s" % (__doc__, e), file=sys.stderr) 396 sys.exit(2) 397 398 configLogger(level=options.logLevel) 399 400 try: 401 process(jobs, options) 402 except KeyboardInterrupt: 403 log.error("(Cancelled.)") 404 sys.exit(1) 405 except SystemExit: 406 if sys.platform == "win32": 407 waitForKeyPress() 408 raise 409 except TTLibError as e: 410 log.error(e) 411 sys.exit(1) 412 except: 413 log.exception('Unhandled exception has occurred') 414 if sys.platform == "win32": 415 waitForKeyPress() 416 sys.exit(1) 417 418 419if __name__ == "__main__": 420 sys.exit(main()) 421