aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/ttx.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/ttx.py')
-rw-r--r--Lib/fontTools/ttx.py602
1 files changed, 325 insertions, 277 deletions
diff --git a/Lib/fontTools/ttx.py b/Lib/fontTools/ttx.py
index 3f06c58b..d8c2a3a7 100644
--- a/Lib/fontTools/ttx.py
+++ b/Lib/fontTools/ttx.py
@@ -5,8 +5,9 @@ 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
+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.
@@ -119,302 +120,349 @@ import logging
log = logging.getLogger("fontTools.ttx")
-opentypeheaderRE = re.compile('''sfntVersion=['"]OTTO["']''')
+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'")
+ 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()
-
-
-@Timer(log, 'Done dumping TTX in %(time).3f seconds')
+ 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):
- log.info('Dumping "%s" to "%s"...', input, output)
- 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')
+ 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):
- log.info('Compiling "%s" to "%s"...' % (input, 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:
- # use TTX file modification time for head "modified" timestamp
- mtime = os.path.getmtime(input)
- ttf['head'].modified = timestampSinceEpoch(mtime)
-
- ttf.save(output)
+ 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):
- 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
+ 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.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 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:
- 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
+ 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
+ )
+ # '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)
+ 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)
+ """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())
+ sys.exit(main())