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