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