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