| | """\ |
| | usage: ttx [options] inputfile1 [... inputfileN] |
| | |
| | TTX -- From OpenType To XML And Back |
| | |
| | If an input file is a TrueType or OpenType font file, it will be |
| | decompiled to a TTX file (an XML-based text format). |
| | If an input file is a TTX file, it will be compiled to whatever |
| | format the data is in, a TrueType or OpenType/CFF font file. |
| | A special input value of - means read from the standard input. |
| | |
| | Output files are created so they are unique: an existing file is |
| | never overwritten. |
| | |
| | General options |
| | =============== |
| | |
| | -h Help print this message. |
| | --version show version and exit. |
| | -d <outputfolder> Specify a directory where the output files are |
| | to be created. |
| | -o <outputfile> Specify a file to write the output to. A special |
| | value of - would use the standard output. |
| | -f Overwrite existing output file(s), ie. don't append |
| | numbers. |
| | -v Verbose: more messages will be written to stdout |
| | about what is being done. |
| | -q Quiet: No messages will be written to stdout about |
| | what is being done. |
| | -a allow virtual glyphs ID's on compile or decompile. |
| | |
| | Dump options |
| | ============ |
| | |
| | -l List table info: instead of dumping to a TTX file, list |
| | some minimal info about each table. |
| | -t <table> Specify a table to dump. Multiple -t options |
| | are allowed. When no -t option is specified, all tables |
| | will be dumped. |
| | -x <table> Specify a table to exclude from the dump. Multiple |
| | -x options are allowed. -t and -x are mutually exclusive. |
| | -s Split tables: save the TTX data into separate TTX files per |
| | table and write one small TTX file that contains references |
| | to the individual table dumps. This file can be used as |
| | input to ttx, as long as the table files are in the |
| | same directory. |
| | -g Split glyf table: Save the glyf data into separate TTX files |
| | per glyph and write a small TTX for the glyf table which |
| | contains references to the individual TTGlyph elements. |
| | NOTE: specifying -g implies -s (no need for -s together |
| | with -g) |
| | -i Do NOT disassemble TT instructions: when this option is |
| | given, all TrueType programs (glyph programs, the font |
| | program and the pre-program) will be written to the TTX |
| | file as hex data instead of assembly. This saves some time |
| | and makes the TTX file smaller. |
| | -z <format> Specify a bitmap data export option for EBDT: |
| | {'raw', 'row', 'bitwise', 'extfile'} or for the CBDT: |
| | {'raw', 'extfile'} Each option does one of the following: |
| | |
| | -z raw |
| | export the bitmap data as a hex dump |
| | -z row |
| | export each row as hex data |
| | -z bitwise |
| | export each row as binary in an ASCII art style |
| | -z extfile |
| | export the data as external files with XML references |
| | |
| | If no export format is specified 'raw' format is used. |
| | -e Don't ignore decompilation errors, but show a full traceback |
| | and abort. |
| | -y <number> Select font number for TrueType Collection (.ttc/.otc), |
| | starting from 0. |
| | --unicodedata <UnicodeData.txt> |
| | Use custom database file to write character names in the |
| | comments of the cmap TTX output. |
| | --newline <value> |
| | Control how line endings are written in the XML file. It |
| | can be 'LF', 'CR', or 'CRLF'. If not specified, the |
| | default platform-specific line endings are used. |
| | |
| | Compile options |
| | =============== |
| | |
| | -m Merge with TrueType-input-file: specify a TrueType or |
| | OpenType font file to be merged with the TTX file. This |
| | option is only valid when at most one TTX file is specified. |
| | -b Don't recalc glyph bounding boxes: use the values in the |
| | TTX file as-is. |
| | --recalc-timestamp |
| | Set font 'modified' timestamp to current time. |
| | By default, the modification time of the TTX file will be |
| | used. |
| | --no-recalc-timestamp |
| | Keep the original font 'modified' timestamp. |
| | --flavor <type> |
| | Specify flavor of output font file. May be 'woff' or 'woff2'. |
| | Note that WOFF2 requires the Brotli Python extension, |
| | available at https://github.com/google/brotli |
| | --with-zopfli |
| | Use Zopfli instead of Zlib to compress WOFF. The Python |
| | extension is available at https://pypi.python.org/pypi/zopfli |
| | """ |
| |
|
| | from fontTools.ttLib import TTFont, TTLibError |
| | from fontTools.misc.macCreatorType import getMacCreatorAndType |
| | from fontTools.unicode import setUnicodeData |
| | from fontTools.misc.textTools import Tag, tostr |
| | from fontTools.misc.timeTools import timestampSinceEpoch |
| | from fontTools.misc.loggingTools import Timer |
| | from fontTools.misc.cliTools import makeOutputFileName |
| | import os |
| | import sys |
| | import getopt |
| | import re |
| | import logging |
| |
|
| |
|
| | log = logging.getLogger("fontTools.ttx") |
| |
|
| | opentypeheaderRE = re.compile("""sfntVersion=['"]OTTO["']""") |
| |
|
| |
|
| | class Options(object): |
| | listTables = False |
| | outputDir = None |
| | outputFile = None |
| | overWrite = False |
| | verbose = False |
| | quiet = False |
| | splitTables = False |
| | splitGlyphs = False |
| | disassembleInstructions = True |
| | mergeFile = None |
| | recalcBBoxes = True |
| | ignoreDecompileErrors = True |
| | bitmapGlyphDataFormat = "raw" |
| | unicodedata = None |
| | newlinestr = "\n" |
| | recalcTimestamp = None |
| | flavor = None |
| | useZopfli = False |
| |
|
| | def __init__(self, rawOptions, numFiles): |
| | self.onlyTables = [] |
| | self.skipTables = [] |
| | self.fontNumber = -1 |
| | for option, value in rawOptions: |
| | |
| | if option == "-h": |
| | print(__doc__) |
| | sys.exit(0) |
| | elif option == "--version": |
| | from fontTools import version |
| |
|
| | print(version) |
| | sys.exit(0) |
| | elif option == "-d": |
| | if not os.path.isdir(value): |
| | raise getopt.GetoptError( |
| | "The -d option value must be an existing directory" |
| | ) |
| | self.outputDir = value |
| | elif option == "-o": |
| | self.outputFile = value |
| | elif option == "-f": |
| | self.overWrite = True |
| | elif option == "-v": |
| | self.verbose = True |
| | elif option == "-q": |
| | self.quiet = True |
| | |
| | elif option == "-l": |
| | self.listTables = True |
| | elif option == "-t": |
| | |
| | value = value.ljust(4) |
| | self.onlyTables.append(value) |
| | elif option == "-x": |
| | |
| | value = value.ljust(4) |
| | self.skipTables.append(value) |
| | elif option == "-s": |
| | self.splitTables = True |
| | elif option == "-g": |
| | |
| | self.splitGlyphs = True |
| | self.splitTables = True |
| | elif option == "-i": |
| | self.disassembleInstructions = False |
| | elif option == "-z": |
| | validOptions = ("raw", "row", "bitwise", "extfile") |
| | if value not in validOptions: |
| | raise getopt.GetoptError( |
| | "-z does not allow %s as a format. Use %s" |
| | % (option, validOptions) |
| | ) |
| | self.bitmapGlyphDataFormat = value |
| | elif option == "-y": |
| | self.fontNumber = int(value) |
| | |
| | elif option == "-m": |
| | self.mergeFile = value |
| | elif option == "-b": |
| | self.recalcBBoxes = False |
| | elif option == "-e": |
| | self.ignoreDecompileErrors = False |
| | elif option == "--unicodedata": |
| | self.unicodedata = value |
| | elif option == "--newline": |
| | validOptions = ("LF", "CR", "CRLF") |
| | if value == "LF": |
| | self.newlinestr = "\n" |
| | elif value == "CR": |
| | self.newlinestr = "\r" |
| | elif value == "CRLF": |
| | self.newlinestr = "\r\n" |
| | else: |
| | raise getopt.GetoptError( |
| | "Invalid choice for --newline: %r (choose from %s)" |
| | % (value, ", ".join(map(repr, validOptions))) |
| | ) |
| | elif option == "--recalc-timestamp": |
| | self.recalcTimestamp = True |
| | elif option == "--no-recalc-timestamp": |
| | self.recalcTimestamp = False |
| | elif option == "--flavor": |
| | self.flavor = value |
| | elif option == "--with-zopfli": |
| | self.useZopfli = True |
| | if self.verbose and self.quiet: |
| | raise getopt.GetoptError("-q and -v options are mutually exclusive") |
| | if self.verbose: |
| | self.logLevel = logging.DEBUG |
| | elif self.quiet: |
| | self.logLevel = logging.WARNING |
| | else: |
| | self.logLevel = logging.INFO |
| | if self.mergeFile and self.flavor: |
| | raise getopt.GetoptError("-m and --flavor options are mutually exclusive") |
| | if self.onlyTables and self.skipTables: |
| | raise getopt.GetoptError("-t and -x options are mutually exclusive") |
| | if self.mergeFile and numFiles > 1: |
| | raise getopt.GetoptError( |
| | "Must specify exactly one TTX source file when using -m" |
| | ) |
| | if self.flavor != "woff" and self.useZopfli: |
| | raise getopt.GetoptError("--with-zopfli option requires --flavor 'woff'") |
| |
|
| |
|
| | def ttList(input, output, options): |
| | ttf = TTFont(input, fontNumber=options.fontNumber, lazy=True) |
| | reader = ttf.reader |
| | tags = sorted(reader.keys()) |
| | print('Listing table info for "%s":' % input) |
| | format = " %4s %10s %8s %8s" |
| | print(format % ("tag ", " checksum", " length", " offset")) |
| | print(format % ("----", "----------", "--------", "--------")) |
| | for tag in tags: |
| | entry = reader.tables[tag] |
| | if ttf.flavor == "woff2": |
| | |
| | from fontTools.ttLib.sfnt import calcChecksum |
| |
|
| | data = entry.loadData(reader.transformBuffer) |
| | checkSum = calcChecksum(data) |
| | else: |
| | checkSum = int(entry.checkSum) |
| | if checkSum < 0: |
| | checkSum = checkSum + 0x100000000 |
| | checksum = "0x%08X" % checkSum |
| | print(format % (tag, checksum, entry.length, entry.offset)) |
| | print() |
| | ttf.close() |
| |
|
| |
|
| | @Timer(log, "Done dumping TTX in %(time).3f seconds") |
| | def ttDump(input, output, options): |
| | input_name = input |
| | if input == "-": |
| | input, input_name = sys.stdin.buffer, sys.stdin.name |
| | output_name = output |
| | if output == "-": |
| | output, output_name = sys.stdout, sys.stdout.name |
| | log.info('Dumping "%s" to "%s"...', input_name, output_name) |
| | if options.unicodedata: |
| | setUnicodeData(options.unicodedata) |
| | ttf = TTFont( |
| | input, |
| | 0, |
| | ignoreDecompileErrors=options.ignoreDecompileErrors, |
| | fontNumber=options.fontNumber, |
| | ) |
| | ttf.saveXML( |
| | output, |
| | tables=options.onlyTables, |
| | skipTables=options.skipTables, |
| | splitTables=options.splitTables, |
| | splitGlyphs=options.splitGlyphs, |
| | disassembleInstructions=options.disassembleInstructions, |
| | bitmapGlyphDataFormat=options.bitmapGlyphDataFormat, |
| | newlinestr=options.newlinestr, |
| | ) |
| | ttf.close() |
| |
|
| |
|
| | @Timer(log, "Done compiling TTX in %(time).3f seconds") |
| | def ttCompile(input, output, options): |
| | input_name = input |
| | if input == "-": |
| | input, input_name = sys.stdin, sys.stdin.name |
| | output_name = output |
| | if output == "-": |
| | output, output_name = sys.stdout.buffer, sys.stdout.name |
| | log.info('Compiling "%s" to "%s"...' % (input_name, output)) |
| | if options.useZopfli: |
| | from fontTools.ttLib import sfnt |
| |
|
| | sfnt.USE_ZOPFLI = True |
| | ttf = TTFont( |
| | options.mergeFile, |
| | flavor=options.flavor, |
| | recalcBBoxes=options.recalcBBoxes, |
| | recalcTimestamp=options.recalcTimestamp, |
| | ) |
| | ttf.importXML(input) |
| |
|
| | if options.recalcTimestamp is None and "head" in ttf and input is not sys.stdin: |
| | |
| | mtime = os.path.getmtime(input) |
| | ttf["head"].modified = timestampSinceEpoch(mtime) |
| |
|
| | ttf.save(output) |
| |
|
| |
|
| | def guessFileType(fileName): |
| | if fileName == "-": |
| | header = sys.stdin.buffer.peek(256) |
| | ext = "" |
| | else: |
| | base, ext = os.path.splitext(fileName) |
| | try: |
| | with open(fileName, "rb") as f: |
| | header = f.read(256) |
| | except IOError: |
| | return None |
| |
|
| | if header.startswith(b"\xef\xbb\xbf<?xml"): |
| | header = header.lstrip(b"\xef\xbb\xbf") |
| | cr, tp = getMacCreatorAndType(fileName) |
| | if tp in ("sfnt", "FFIL"): |
| | return "TTF" |
| | if ext == ".dfont": |
| | return "TTF" |
| | head = Tag(header[:4]) |
| | if head == "OTTO": |
| | return "OTF" |
| | elif head == "ttcf": |
| | return "TTC" |
| | elif head in ("\0\1\0\0", "true"): |
| | return "TTF" |
| | elif head == "wOFF": |
| | return "WOFF" |
| | elif head == "wOF2": |
| | return "WOFF2" |
| | elif head == "<?xm": |
| | |
| | header = tostr(header, "latin1") |
| | if opentypeheaderRE.search(header): |
| | return "OTX" |
| | else: |
| | return "TTX" |
| | return None |
| |
|
| |
|
| | def parseOptions(args): |
| | rawOptions, files = getopt.getopt( |
| | args, |
| | "ld:o:fvqht:x:sgim:z:baey:", |
| | [ |
| | "unicodedata=", |
| | "recalc-timestamp", |
| | "no-recalc-timestamp", |
| | "flavor=", |
| | "version", |
| | "with-zopfli", |
| | "newline=", |
| | ], |
| | ) |
| |
|
| | options = Options(rawOptions, len(files)) |
| | jobs = [] |
| |
|
| | if not files: |
| | raise getopt.GetoptError("Must specify at least one input file") |
| |
|
| | for input in files: |
| | if input != "-" and not os.path.isfile(input): |
| | raise getopt.GetoptError('File not found: "%s"' % input) |
| | tp = guessFileType(input) |
| | if tp in ("OTF", "TTF", "TTC", "WOFF", "WOFF2"): |
| | extension = ".ttx" |
| | if options.listTables: |
| | action = ttList |
| | else: |
| | action = ttDump |
| | elif tp == "TTX": |
| | extension = "." + options.flavor if options.flavor else ".ttf" |
| | action = ttCompile |
| | elif tp == "OTX": |
| | extension = "." + options.flavor if options.flavor else ".otf" |
| | action = ttCompile |
| | else: |
| | raise getopt.GetoptError('Unknown file type: "%s"' % input) |
| |
|
| | if options.outputFile: |
| | output = options.outputFile |
| | else: |
| | if input == "-": |
| | raise getopt.GetoptError("Must provide -o when reading from stdin") |
| | output = makeOutputFileName( |
| | input, options.outputDir, extension, options.overWrite |
| | ) |
| | |
| | if action != ttList: |
| | open(output, "a").close() |
| | jobs.append((action, input, output)) |
| | return jobs, options |
| |
|
| |
|
| | def process(jobs, options): |
| | for action, input, output in jobs: |
| | action(input, output, options) |
| |
|
| |
|
| | def main(args=None): |
| | """Convert OpenType fonts to XML and back""" |
| | from fontTools import configLogger |
| |
|
| | if args is None: |
| | args = sys.argv[1:] |
| | try: |
| | jobs, options = parseOptions(args) |
| | except getopt.GetoptError as e: |
| | print("%s\nERROR: %s" % (__doc__, e), file=sys.stderr) |
| | sys.exit(2) |
| |
|
| | configLogger(level=options.logLevel) |
| |
|
| | try: |
| | process(jobs, options) |
| | except KeyboardInterrupt: |
| | log.error("(Cancelled.)") |
| | sys.exit(1) |
| | except SystemExit: |
| | raise |
| | except TTLibError as e: |
| | log.error(e) |
| | sys.exit(1) |
| | except: |
| | log.exception("Unhandled exception has occurred") |
| | sys.exit(1) |
| |
|
| |
|
| | if __name__ == "__main__": |
| | sys.exit(main()) |
| |
|