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 fontTools.misc.py23 import Tag, tostr 90from fontTools.ttLib import TTFont, TTLibError 91from fontTools.misc.macCreatorType import getMacCreatorAndType 92from fontTools.unicode import setUnicodeData 93from fontTools.misc.timeTools import timestampSinceEpoch 94from fontTools.misc.loggingTools import Timer 95from fontTools.misc.cliTools import makeOutputFileName 96import os 97import sys 98import getopt 99import re 100import logging 101 102 103log = logging.getLogger("fontTools.ttx") 104 105opentypeheaderRE = re.compile('''sfntVersion=['"]OTTO["']''') 106 107 108class Options(object): 109 110 listTables = False 111 outputDir = None 112 outputFile = None 113 overWrite = False 114 verbose = False 115 quiet = False 116 splitTables = False 117 splitGlyphs = False 118 disassembleInstructions = True 119 mergeFile = None 120 recalcBBoxes = True 121 allowVID = False 122 ignoreDecompileErrors = True 123 bitmapGlyphDataFormat = 'raw' 124 unicodedata = None 125 newlinestr = None 126 recalcTimestamp = None 127 flavor = None 128 useZopfli = False 129 130 def __init__(self, rawOptions, numFiles): 131 self.onlyTables = [] 132 self.skipTables = [] 133 self.fontNumber = -1 134 for option, value in rawOptions: 135 # general options 136 if option == "-h": 137 print(__doc__) 138 sys.exit(0) 139 elif option == "--version": 140 from fontTools import version 141 print(version) 142 sys.exit(0) 143 elif option == "-d": 144 if not os.path.isdir(value): 145 raise getopt.GetoptError("The -d option value must be an existing directory") 146 self.outputDir = value 147 elif option == "-o": 148 self.outputFile = value 149 elif option == "-f": 150 self.overWrite = True 151 elif option == "-v": 152 self.verbose = True 153 elif option == "-q": 154 self.quiet = True 155 # dump options 156 elif option == "-l": 157 self.listTables = True 158 elif option == "-t": 159 # pad with space if table tag length is less than 4 160 value = value.ljust(4) 161 self.onlyTables.append(value) 162 elif option == "-x": 163 # pad with space if table tag length is less than 4 164 value = value.ljust(4) 165 self.skipTables.append(value) 166 elif option == "-s": 167 self.splitTables = True 168 elif option == "-g": 169 # -g implies (and forces) splitTables 170 self.splitGlyphs = True 171 self.splitTables = True 172 elif option == "-i": 173 self.disassembleInstructions = False 174 elif option == "-z": 175 validOptions = ('raw', 'row', 'bitwise', 'extfile') 176 if value not in validOptions: 177 raise getopt.GetoptError( 178 "-z does not allow %s as a format. Use %s" % (option, validOptions)) 179 self.bitmapGlyphDataFormat = value 180 elif option == "-y": 181 self.fontNumber = int(value) 182 # compile options 183 elif option == "-m": 184 self.mergeFile = value 185 elif option == "-b": 186 self.recalcBBoxes = False 187 elif option == "-a": 188 self.allowVID = True 189 elif option == "-e": 190 self.ignoreDecompileErrors = False 191 elif option == "--unicodedata": 192 self.unicodedata = value 193 elif option == "--newline": 194 validOptions = ('LF', 'CR', 'CRLF') 195 if value == "LF": 196 self.newlinestr = "\n" 197 elif value == "CR": 198 self.newlinestr = "\r" 199 elif value == "CRLF": 200 self.newlinestr = "\r\n" 201 else: 202 raise getopt.GetoptError( 203 "Invalid choice for --newline: %r (choose from %s)" 204 % (value, ", ".join(map(repr, validOptions)))) 205 elif option == "--recalc-timestamp": 206 self.recalcTimestamp = True 207 elif option == "--no-recalc-timestamp": 208 self.recalcTimestamp = False 209 elif option == "--flavor": 210 self.flavor = value 211 elif option == "--with-zopfli": 212 self.useZopfli = True 213 if self.verbose and self.quiet: 214 raise getopt.GetoptError("-q and -v options are mutually exclusive") 215 if self.verbose: 216 self.logLevel = logging.DEBUG 217 elif self.quiet: 218 self.logLevel = logging.WARNING 219 else: 220 self.logLevel = logging.INFO 221 if self.mergeFile and self.flavor: 222 raise getopt.GetoptError("-m and --flavor options are mutually exclusive") 223 if self.onlyTables and self.skipTables: 224 raise getopt.GetoptError("-t and -x options are mutually exclusive") 225 if self.mergeFile and numFiles > 1: 226 raise getopt.GetoptError("Must specify exactly one TTX source file when using -m") 227 if self.flavor != 'woff' and self.useZopfli: 228 raise getopt.GetoptError("--with-zopfli option requires --flavor 'woff'") 229 230 231def ttList(input, output, options): 232 ttf = TTFont(input, fontNumber=options.fontNumber, lazy=True) 233 reader = ttf.reader 234 tags = sorted(reader.keys()) 235 print('Listing table info for "%s":' % input) 236 format = " %4s %10s %8s %8s" 237 print(format % ("tag ", " checksum", " length", " offset")) 238 print(format % ("----", "----------", "--------", "--------")) 239 for tag in tags: 240 entry = reader.tables[tag] 241 if ttf.flavor == "woff2": 242 # WOFF2 doesn't store table checksums, so they must be calculated 243 from fontTools.ttLib.sfnt import calcChecksum 244 data = entry.loadData(reader.transformBuffer) 245 checkSum = calcChecksum(data) 246 else: 247 checkSum = int(entry.checkSum) 248 if checkSum < 0: 249 checkSum = checkSum + 0x100000000 250 checksum = "0x%08X" % checkSum 251 print(format % (tag, checksum, entry.length, entry.offset)) 252 print() 253 ttf.close() 254 255 256@Timer(log, 'Done dumping TTX in %(time).3f seconds') 257def ttDump(input, output, options): 258 log.info('Dumping "%s" to "%s"...', input, output) 259 if options.unicodedata: 260 setUnicodeData(options.unicodedata) 261 ttf = TTFont(input, 0, allowVID=options.allowVID, 262 ignoreDecompileErrors=options.ignoreDecompileErrors, 263 fontNumber=options.fontNumber) 264 ttf.saveXML(output, 265 tables=options.onlyTables, 266 skipTables=options.skipTables, 267 splitTables=options.splitTables, 268 splitGlyphs=options.splitGlyphs, 269 disassembleInstructions=options.disassembleInstructions, 270 bitmapGlyphDataFormat=options.bitmapGlyphDataFormat, 271 newlinestr=options.newlinestr) 272 ttf.close() 273 274 275@Timer(log, 'Done compiling TTX in %(time).3f seconds') 276def ttCompile(input, output, options): 277 log.info('Compiling "%s" to "%s"...' % (input, output)) 278 if options.useZopfli: 279 from fontTools.ttLib import sfnt 280 sfnt.USE_ZOPFLI = True 281 ttf = TTFont(options.mergeFile, flavor=options.flavor, 282 recalcBBoxes=options.recalcBBoxes, 283 recalcTimestamp=options.recalcTimestamp, 284 allowVID=options.allowVID) 285 ttf.importXML(input) 286 287 if options.recalcTimestamp is None and 'head' in ttf: 288 # use TTX file modification time for head "modified" timestamp 289 mtime = os.path.getmtime(input) 290 ttf['head'].modified = timestampSinceEpoch(mtime) 291 292 ttf.save(output) 293 294 295def guessFileType(fileName): 296 base, ext = os.path.splitext(fileName) 297 try: 298 with open(fileName, "rb") as f: 299 header = f.read(256) 300 except IOError: 301 return None 302 303 if header.startswith(b'\xef\xbb\xbf<?xml'): 304 header = header.lstrip(b'\xef\xbb\xbf') 305 cr, tp = getMacCreatorAndType(fileName) 306 if tp in ("sfnt", "FFIL"): 307 return "TTF" 308 if ext == ".dfont": 309 return "TTF" 310 head = Tag(header[:4]) 311 if head == "OTTO": 312 return "OTF" 313 elif head == "ttcf": 314 return "TTC" 315 elif head in ("\0\1\0\0", "true"): 316 return "TTF" 317 elif head == "wOFF": 318 return "WOFF" 319 elif head == "wOF2": 320 return "WOFF2" 321 elif head == "<?xm": 322 # Use 'latin1' because that can't fail. 323 header = tostr(header, 'latin1') 324 if opentypeheaderRE.search(header): 325 return "OTX" 326 else: 327 return "TTX" 328 return None 329 330 331def parseOptions(args): 332 rawOptions, files = getopt.getopt(args, "ld:o:fvqht:x:sgim:z:baey:", 333 ['unicodedata=', "recalc-timestamp", "no-recalc-timestamp", 334 'flavor=', 'version', 'with-zopfli', 'newline=']) 335 336 options = Options(rawOptions, len(files)) 337 jobs = [] 338 339 if not files: 340 raise getopt.GetoptError('Must specify at least one input file') 341 342 for input in files: 343 if not os.path.isfile(input): 344 raise getopt.GetoptError('File not found: "%s"' % input) 345 tp = guessFileType(input) 346 if tp in ("OTF", "TTF", "TTC", "WOFF", "WOFF2"): 347 extension = ".ttx" 348 if options.listTables: 349 action = ttList 350 else: 351 action = ttDump 352 elif tp == "TTX": 353 extension = "."+options.flavor if options.flavor else ".ttf" 354 action = ttCompile 355 elif tp == "OTX": 356 extension = "."+options.flavor if options.flavor else ".otf" 357 action = ttCompile 358 else: 359 raise getopt.GetoptError('Unknown file type: "%s"' % input) 360 361 if options.outputFile: 362 output = options.outputFile 363 else: 364 output = makeOutputFileName(input, options.outputDir, extension, options.overWrite) 365 # 'touch' output file to avoid race condition in choosing file names 366 if action != ttList: 367 open(output, 'a').close() 368 jobs.append((action, input, output)) 369 return jobs, options 370 371 372def process(jobs, options): 373 for action, input, output in jobs: 374 action(input, output, options) 375 376 377def waitForKeyPress(): 378 """Force the DOS Prompt window to stay open so the user gets 379 a chance to see what's wrong.""" 380 import msvcrt 381 print('(Hit any key to exit)', file=sys.stderr) 382 while not msvcrt.kbhit(): 383 pass 384 385 386def main(args=None): 387 """Convert OpenType fonts to XML and back""" 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