You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			480 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			480 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
"""\
 | 
						|
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
 | 
						|
--optimize-font-speed
 | 
						|
             Enable optimizations that prioritize speed over file size.
 | 
						|
             This mainly affects how glyf t able and gvar / VARC tables are
 | 
						|
             compiled. The produced fonts will be larger, but rendering
 | 
						|
             performance will be improved with HarfBuzz and other text
 | 
						|
             layout engines.
 | 
						|
"""
 | 
						|
 | 
						|
from fontTools.ttLib import OPTIMIZE_FONT_SPEED, 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
 | 
						|
    optimizeFontSpeed = False
 | 
						|
 | 
						|
    def __init__(self, rawOptions, numFiles):
 | 
						|
        self.onlyTables = []
 | 
						|
        self.skipTables = []
 | 
						|
        self.fontNumber = -1
 | 
						|
        for option, value in rawOptions:
 | 
						|
            # general options
 | 
						|
            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
 | 
						|
            # dump options
 | 
						|
            elif option == "-l":
 | 
						|
                self.listTables = True
 | 
						|
            elif option == "-t":
 | 
						|
                # pad with space if table tag length is less than 4
 | 
						|
                value = value.ljust(4)
 | 
						|
                self.onlyTables.append(value)
 | 
						|
            elif option == "-x":
 | 
						|
                # pad with space if table tag length is less than 4
 | 
						|
                value = value.ljust(4)
 | 
						|
                self.skipTables.append(value)
 | 
						|
            elif option == "-s":
 | 
						|
                self.splitTables = True
 | 
						|
            elif option == "-g":
 | 
						|
                # -g implies (and forces) splitTables
 | 
						|
                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)
 | 
						|
            # compile options
 | 
						|
            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
 | 
						|
            elif option == "--optimize-font-speed":
 | 
						|
                self.optimizeFontSpeed = 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":
 | 
						|
            # WOFF2 doesn't store table checksums, so they must be calculated
 | 
						|
            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,
 | 
						|
    )
 | 
						|
    if options.optimizeFontSpeed:
 | 
						|
        ttf.cfg[OPTIMIZE_FONT_SPEED] = options.optimizeFontSpeed
 | 
						|
    ttf.importXML(input)
 | 
						|
 | 
						|
    if options.recalcTimestamp is None and "head" in ttf and input is not sys.stdin:
 | 
						|
        # use TTX file modification time for head "modified" timestamp
 | 
						|
        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":
 | 
						|
        # Use 'latin1' because that can't fail.
 | 
						|
        header = tostr(header, "latin1")
 | 
						|
        if opentypeheaderRE.search(header):
 | 
						|
            return "OTX"
 | 
						|
        else:
 | 
						|
            return "TTX"
 | 
						|
    return None
 | 
						|
 | 
						|
 | 
						|
def parseOptions(args):
 | 
						|
    rawOptions, files = getopt.gnu_getopt(
 | 
						|
        args,
 | 
						|
        "ld:o:fvqht:x:sgim:z:baey:",
 | 
						|
        [
 | 
						|
            "unicodedata=",
 | 
						|
            "recalc-timestamp",
 | 
						|
            "no-recalc-timestamp",
 | 
						|
            "flavor=",
 | 
						|
            "version",
 | 
						|
            "with-zopfli",
 | 
						|
            "newline=",
 | 
						|
            "optimize-font-speed",
 | 
						|
        ],
 | 
						|
    )
 | 
						|
 | 
						|
    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
 | 
						|
            )
 | 
						|
            # 'touch' output file to avoid race condition in choosing file names
 | 
						|
            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())
 |