• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""\
2usage: 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
89from __future__ import print_function, division, absolute_import
90from fontTools.misc.py23 import *
91from fontTools.ttLib import TTFont, TTLibError
92from fontTools.misc.macCreatorType import getMacCreatorAndType
93from fontTools.unicode import setUnicodeData
94from fontTools.misc.timeTools import timestampSinceEpoch
95from fontTools.misc.loggingTools import Timer
96from fontTools.misc.cliTools import makeOutputFileName
97import os
98import sys
99import getopt
100import re
101import logging
102
103
104log = logging.getLogger("fontTools.ttx")
105
106opentypeheaderRE = re.compile('''sfntVersion=['"]OTTO["']''')
107
108
109class 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
232def 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')
258def 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')
277def 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
296def 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
332def 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
373def process(jobs, options):
374	for action, input, output in jobs:
375		action(input, output, options)
376
377
378def 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
387def 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
419if __name__ == "__main__":
420	sys.exit(main())
421