• 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 fontTools.misc.py23 import Tag, tostr
90from fontTools.ttLib import TTFont, TTLibError
91from fontTools.misc.macCreatorType import getMacCreatorAndType
92from fontTools.unicode import setUnicodeData
93from fontTools.misc.timeTools import timestampSinceEpoch
94from fontTools.misc.loggingTools import Timer
95from fontTools.misc.cliTools import makeOutputFileName
96import os
97import sys
98import getopt
99import re
100import logging
101
102
103log = logging.getLogger("fontTools.ttx")
104
105opentypeheaderRE = re.compile('''sfntVersion=['"]OTTO["']''')
106
107
108class Options(object):
109
110	listTables = False
111	outputDir = None
112	outputFile = None
113	overWrite = False
114	verbose = False
115	quiet = False
116	splitTables = False
117	splitGlyphs = False
118	disassembleInstructions = True
119	mergeFile = None
120	recalcBBoxes = True
121	allowVID = False
122	ignoreDecompileErrors = True
123	bitmapGlyphDataFormat = 'raw'
124	unicodedata = None
125	newlinestr = None
126	recalcTimestamp = None
127	flavor = None
128	useZopfli = False
129
130	def __init__(self, rawOptions, numFiles):
131		self.onlyTables = []
132		self.skipTables = []
133		self.fontNumber = -1
134		for option, value in rawOptions:
135			# general options
136			if option == "-h":
137				print(__doc__)
138				sys.exit(0)
139			elif option == "--version":
140				from fontTools import version
141				print(version)
142				sys.exit(0)
143			elif option == "-d":
144				if not os.path.isdir(value):
145					raise getopt.GetoptError("The -d option value must be an existing directory")
146				self.outputDir = value
147			elif option == "-o":
148				self.outputFile = value
149			elif option == "-f":
150				self.overWrite = True
151			elif option == "-v":
152				self.verbose = True
153			elif option == "-q":
154				self.quiet = True
155			# dump options
156			elif option == "-l":
157				self.listTables = True
158			elif option == "-t":
159				# pad with space if table tag length is less than 4
160				value = value.ljust(4)
161				self.onlyTables.append(value)
162			elif option == "-x":
163				# pad with space if table tag length is less than 4
164				value = value.ljust(4)
165				self.skipTables.append(value)
166			elif option == "-s":
167				self.splitTables = True
168			elif option == "-g":
169				# -g implies (and forces) splitTables
170				self.splitGlyphs = True
171				self.splitTables = True
172			elif option == "-i":
173				self.disassembleInstructions = False
174			elif option == "-z":
175				validOptions = ('raw', 'row', 'bitwise', 'extfile')
176				if value not in validOptions:
177					raise getopt.GetoptError(
178						"-z does not allow %s as a format. Use %s" % (option, validOptions))
179				self.bitmapGlyphDataFormat = value
180			elif option == "-y":
181				self.fontNumber = int(value)
182			# compile options
183			elif option == "-m":
184				self.mergeFile = value
185			elif option == "-b":
186				self.recalcBBoxes = False
187			elif option == "-a":
188				self.allowVID = True
189			elif option == "-e":
190				self.ignoreDecompileErrors = False
191			elif option == "--unicodedata":
192				self.unicodedata = value
193			elif option == "--newline":
194				validOptions = ('LF', 'CR', 'CRLF')
195				if value == "LF":
196					self.newlinestr = "\n"
197				elif value == "CR":
198					self.newlinestr = "\r"
199				elif value == "CRLF":
200					self.newlinestr = "\r\n"
201				else:
202					raise getopt.GetoptError(
203						"Invalid choice for --newline: %r (choose from %s)"
204						% (value, ", ".join(map(repr, validOptions))))
205			elif option == "--recalc-timestamp":
206				self.recalcTimestamp = True
207			elif option == "--no-recalc-timestamp":
208				self.recalcTimestamp = False
209			elif option == "--flavor":
210				self.flavor = value
211			elif option == "--with-zopfli":
212				self.useZopfli = True
213		if self.verbose and self.quiet:
214			raise getopt.GetoptError("-q and -v options are mutually exclusive")
215		if self.verbose:
216			self.logLevel = logging.DEBUG
217		elif self.quiet:
218			self.logLevel = logging.WARNING
219		else:
220			self.logLevel = logging.INFO
221		if self.mergeFile and self.flavor:
222			raise getopt.GetoptError("-m and --flavor options are mutually exclusive")
223		if self.onlyTables and self.skipTables:
224			raise getopt.GetoptError("-t and -x options are mutually exclusive")
225		if self.mergeFile and numFiles > 1:
226			raise getopt.GetoptError("Must specify exactly one TTX source file when using -m")
227		if self.flavor != 'woff' and self.useZopfli:
228			raise getopt.GetoptError("--with-zopfli option requires --flavor 'woff'")
229
230
231def ttList(input, output, options):
232	ttf = TTFont(input, fontNumber=options.fontNumber, lazy=True)
233	reader = ttf.reader
234	tags = sorted(reader.keys())
235	print('Listing table info for "%s":' % input)
236	format = "    %4s  %10s  %8s  %8s"
237	print(format % ("tag ", "  checksum", "  length", "  offset"))
238	print(format % ("----", "----------", "--------", "--------"))
239	for tag in tags:
240		entry = reader.tables[tag]
241		if ttf.flavor == "woff2":
242			# WOFF2 doesn't store table checksums, so they must be calculated
243			from fontTools.ttLib.sfnt import calcChecksum
244			data = entry.loadData(reader.transformBuffer)
245			checkSum = calcChecksum(data)
246		else:
247			checkSum = int(entry.checkSum)
248		if checkSum < 0:
249			checkSum = checkSum + 0x100000000
250		checksum = "0x%08X" % checkSum
251		print(format % (tag, checksum, entry.length, entry.offset))
252	print()
253	ttf.close()
254
255
256@Timer(log, 'Done dumping TTX in %(time).3f seconds')
257def ttDump(input, output, options):
258	log.info('Dumping "%s" to "%s"...', input, output)
259	if options.unicodedata:
260		setUnicodeData(options.unicodedata)
261	ttf = TTFont(input, 0, allowVID=options.allowVID,
262			ignoreDecompileErrors=options.ignoreDecompileErrors,
263			fontNumber=options.fontNumber)
264	ttf.saveXML(output,
265			tables=options.onlyTables,
266			skipTables=options.skipTables,
267			splitTables=options.splitTables,
268			splitGlyphs=options.splitGlyphs,
269			disassembleInstructions=options.disassembleInstructions,
270			bitmapGlyphDataFormat=options.bitmapGlyphDataFormat,
271			newlinestr=options.newlinestr)
272	ttf.close()
273
274
275@Timer(log, 'Done compiling TTX in %(time).3f seconds')
276def ttCompile(input, output, options):
277	log.info('Compiling "%s" to "%s"...' % (input, output))
278	if options.useZopfli:
279		from fontTools.ttLib import sfnt
280		sfnt.USE_ZOPFLI = True
281	ttf = TTFont(options.mergeFile, flavor=options.flavor,
282			recalcBBoxes=options.recalcBBoxes,
283			recalcTimestamp=options.recalcTimestamp,
284			allowVID=options.allowVID)
285	ttf.importXML(input)
286
287	if options.recalcTimestamp is None and 'head' in ttf:
288		# use TTX file modification time for head "modified" timestamp
289		mtime = os.path.getmtime(input)
290		ttf['head'].modified = timestampSinceEpoch(mtime)
291
292	ttf.save(output)
293
294
295def guessFileType(fileName):
296	base, ext = os.path.splitext(fileName)
297	try:
298		with open(fileName, "rb") as f:
299			header = f.read(256)
300	except IOError:
301		return None
302
303	if header.startswith(b'\xef\xbb\xbf<?xml'):
304		header = header.lstrip(b'\xef\xbb\xbf')
305	cr, tp = getMacCreatorAndType(fileName)
306	if tp in ("sfnt", "FFIL"):
307		return "TTF"
308	if ext == ".dfont":
309		return "TTF"
310	head = Tag(header[:4])
311	if head == "OTTO":
312		return "OTF"
313	elif head == "ttcf":
314		return "TTC"
315	elif head in ("\0\1\0\0", "true"):
316		return "TTF"
317	elif head == "wOFF":
318		return "WOFF"
319	elif head == "wOF2":
320		return "WOFF2"
321	elif head == "<?xm":
322		# Use 'latin1' because that can't fail.
323		header = tostr(header, 'latin1')
324		if opentypeheaderRE.search(header):
325			return "OTX"
326		else:
327			return "TTX"
328	return None
329
330
331def parseOptions(args):
332	rawOptions, files = getopt.getopt(args, "ld:o:fvqht:x:sgim:z:baey:",
333			['unicodedata=', "recalc-timestamp", "no-recalc-timestamp",
334			 'flavor=', 'version', 'with-zopfli', 'newline='])
335
336	options = Options(rawOptions, len(files))
337	jobs = []
338
339	if not files:
340		raise getopt.GetoptError('Must specify at least one input file')
341
342	for input in files:
343		if not os.path.isfile(input):
344			raise getopt.GetoptError('File not found: "%s"' % input)
345		tp = guessFileType(input)
346		if tp in ("OTF", "TTF", "TTC", "WOFF", "WOFF2"):
347			extension = ".ttx"
348			if options.listTables:
349				action = ttList
350			else:
351				action = ttDump
352		elif tp == "TTX":
353			extension = "."+options.flavor if options.flavor else ".ttf"
354			action = ttCompile
355		elif tp == "OTX":
356			extension = "."+options.flavor if options.flavor else ".otf"
357			action = ttCompile
358		else:
359			raise getopt.GetoptError('Unknown file type: "%s"' % input)
360
361		if options.outputFile:
362			output = options.outputFile
363		else:
364			output = makeOutputFileName(input, options.outputDir, extension, options.overWrite)
365			# 'touch' output file to avoid race condition in choosing file names
366			if action != ttList:
367				open(output, 'a').close()
368		jobs.append((action, input, output))
369	return jobs, options
370
371
372def process(jobs, options):
373	for action, input, output in jobs:
374		action(input, output, options)
375
376
377def waitForKeyPress():
378	"""Force the DOS Prompt window to stay open so the user gets
379	a chance to see what's wrong."""
380	import msvcrt
381	print('(Hit any key to exit)', file=sys.stderr)
382	while not msvcrt.kbhit():
383		pass
384
385
386def main(args=None):
387	"""Convert OpenType fonts to XML and back"""
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