Spaces:
Running
Running
"""\ | |
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: | |
# 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 | |
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() | |
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() | |
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: | |
# 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=", | |
], | |
) | |
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()) | |