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