ttx.py revision 5f6418d9e1fa15a89dcec29cdc433ba2c99732c3
1"""\
2usage: ttx [options] inputfile1 [... inputfileN]
3
4    TTX %s -- From OpenType To XML And Back
5
6    If an input file is a TrueType or OpenType font file, it will be
7       dumped to an TTX file (an XML-based text format).
8    If an input file is a TTX file, it will be compiled to a TrueType
9       or OpenType 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    -d <outputfolder> Specify a directory where the output files are
17       to be created.
18    -o <outputfile> Specify a file to write the output to.
19    -v Verbose: more messages will be written to stdout about what
20       is being done.
21    -q Quiet: No messages will be written to stdout about what
22       is being done.
23    -a allow virtual glyphs ID's on compile or decompile.
24
25    Dump options:
26    -l List table info: instead of dumping to a TTX file, list some
27       minimal info about each table.
28    -t <table> Specify a table to dump. Multiple -t options
29       are allowed. When no -t option is specified, all tables
30       will be dumped.
31    -x <table> Specify a table to exclude from the dump. Multiple
32       -x options are allowed. -t and -x are mutually exclusive.
33    -s Split tables: save the TTX data into separate TTX files per
34       table and write one small TTX file that contains references
35       to the individual table dumps. This file can be used as
36       input to ttx, as long as the table files are in the
37       same directory.
38    -i Do NOT disassemble TT instructions: when this option is given,
39       all TrueType programs (glyph programs, the font program and the
40       pre-program) will be written to the TTX file as hex data
41       instead of assembly. This saves some time and makes the TTX
42       file smaller.
43    -z <format> Specify a bitmap data export option for EBDT:
44       {'raw', 'row', 'bitwise', 'extfile'} or for the CBDT:
45       {'raw', 'extfile'} Each option does one of the following:
46         -z raw
47            * export the bitmap data as a hex dump
48         -z row
49            * export each row as hex data
50         -z bitwise
51            * export each row as binary in an ASCII art style
52         -z extfile
53            * export the data as external files with XML refences
54       If no export format is specified 'raw' format is used.
55    -e Don't ignore decompilation errors, but show a full traceback
56       and abort.
57    -y <number> Select font number for TrueType Collection,
58       starting from 0.
59
60    Compile options:
61    -m Merge with TrueType-input-file: specify a TrueType or OpenType
62       font file to be merged with the TTX file. This option is only
63       valid when at most one TTX file is specified.
64    -b Don't recalc glyph bounding boxes: use the values in the TTX
65       file as-is.
66"""
67
68
69from __future__ import print_function, division
70from fontTools.misc.py23 import *
71from fontTools.ttLib import TTFont, TTLibError
72from fontTools.ttLib.tables.otBase import OTLOffsetOverflowError
73from fontTools.ttLib.tables.otTables import fixLookupOverFlows, fixSubTableOverFlows
74from fontTools.misc.macCreatorType import getMacCreatorAndType
75from fontTools import version
76import os
77import sys
78import getopt
79import re
80
81def usage():
82	print(__doc__ % version)
83	sys.exit(2)
84
85
86numberAddedRE = re.compile("#\d+$")
87opentypeheaderRE = re.compile('''sfntVersion=['"]OTTO["']''')
88
89def makeOutputFileName(input, outputDir, extension):
90	dir, file = os.path.split(input)
91	file, ext = os.path.splitext(file)
92	if outputDir:
93		dir = outputDir
94	file = numberAddedRE.split(file)[0]
95	output = os.path.join(dir, file + extension)
96	n = 1
97	while os.path.exists(output):
98		output = os.path.join(dir, file + "#" + repr(n) + extension)
99		n = n + 1
100	return output
101
102
103class Options:
104
105	listTables = False
106	outputDir = None
107	outputFile = None
108	verbose = False
109	quiet = False
110	splitTables = False
111	disassembleInstructions = True
112	mergeFile = None
113	recalcBBoxes = True
114	allowVID = False
115	ignoreDecompileErrors = True
116	bitmapGlyphDataFormat = 'raw'
117
118	def __init__(self, rawOptions, numFiles):
119		self.onlyTables = []
120		self.skipTables = []
121		self.fontNumber = -1
122		for option, value in rawOptions:
123			# general options
124			if option == "-h":
125				print(__doc__ % version)
126				sys.exit(0)
127			elif option == "-d":
128				if not os.path.isdir(value):
129					print("The -d option value must be an existing directory")
130					sys.exit(2)
131				self.outputDir = value
132			elif option == "-o":
133				self.outputFile = value
134			elif option == "-v":
135				self.verbose = True
136			elif option == "-q":
137				self.quiet = True
138			# dump options
139			elif option == "-l":
140				self.listTables = True
141			elif option == "-t":
142				self.onlyTables.append(value)
143			elif option == "-x":
144				self.skipTables.append(value)
145			elif option == "-s":
146				self.splitTables = True
147			elif option == "-i":
148				self.disassembleInstructions = False
149			elif option == "-z":
150				validOptions = ('raw', 'row', 'bitwise', 'extfile')
151				if value not in validOptions:
152					print("-z does not allow %s as a format. Use %s" % (option, validOptions))
153					sys.exit(2)
154				self.bitmapGlyphDataFormat = value
155			elif option == "-y":
156				self.fontNumber = int(value)
157			# compile options
158			elif option == "-m":
159				self.mergeFile = value
160			elif option == "-b":
161				self.recalcBBoxes = False
162			elif option == "-a":
163				self.allowVID = True
164			elif option == "-e":
165				self.ignoreDecompileErrors = False
166		if self.onlyTables and self.skipTables:
167			print("-t and -x options are mutually exclusive")
168			sys.exit(2)
169		if self.mergeFile and numFiles > 1:
170			print("Must specify exactly one TTX source file when using -m")
171			sys.exit(2)
172
173
174def ttList(input, output, options):
175	ttf = TTFont(input, fontNumber=options.fontNumber, lazy=True)
176	reader = ttf.reader
177	tags = sorted(reader.keys())
178	print('Listing table info for "%s":' % input)
179	format = "    %4s  %10s  %7s  %7s"
180	print(format % ("tag ", "  checksum", " length", " offset"))
181	print(format % ("----", "----------", "-------", "-------"))
182	for tag in tags:
183		entry = reader.tables[tag]
184		checkSum = int(entry.checkSum)
185		if checkSum < 0:
186			checkSum = checkSum + 0x100000000
187		checksum = "0x%08X" % checkSum
188		print(format % (tag, checksum, entry.length, entry.offset))
189	print()
190	ttf.close()
191
192
193def ttDump(input, output, options):
194	if not options.quiet:
195		print('Dumping "%s" to "%s"...' % (input, output))
196	ttf = TTFont(input, 0, verbose=options.verbose, allowVID=options.allowVID,
197			lazy=False,
198			quiet=options.quiet,
199			ignoreDecompileErrors=options.ignoreDecompileErrors,
200			fontNumber=options.fontNumber)
201	ttf.saveXML(output,
202			quiet=options.quiet,
203			tables=options.onlyTables,
204			skipTables=options.skipTables,
205			splitTables=options.splitTables,
206			disassembleInstructions=options.disassembleInstructions,
207			bitmapGlyphDataFormat=options.bitmapGlyphDataFormat)
208	ttf.close()
209
210
211def ttCompile(input, output, options):
212	if not options.quiet:
213		print('Compiling "%s" to "%s"...' % (input, output))
214	ttf = TTFont(options.mergeFile,
215			lazy=False,
216			recalcBBoxes=options.recalcBBoxes,
217			verbose=options.verbose, allowVID=options.allowVID)
218	ttf.importXML(input, quiet=options.quiet)
219	try:
220		ttf.save(output)
221	except OTLOffsetOverflowError as e:
222		# XXX This shouldn't be here at all, it should be as close to the
223		# OTL code as possible.
224		overflowRecord = e.value
225		print("Attempting to fix OTLOffsetOverflowError", e)
226		lastItem = overflowRecord
227		while True:
228			ok = 0
229			if overflowRecord.itemName == None:
230				ok = fixLookupOverFlows(ttf, overflowRecord)
231			else:
232				ok = fixSubTableOverFlows(ttf, overflowRecord)
233			if not ok:
234				raise
235
236			try:
237				ttf.save(output)
238				break
239			except OTLOffsetOverflowError as e:
240				print("Attempting to fix OTLOffsetOverflowError", e)
241				overflowRecord = e.value
242				if overflowRecord == lastItem:
243					raise
244
245	if options.verbose:
246		import time
247		print("finished at", time.strftime("%H:%M:%S", time.localtime(time.time())))
248
249
250def guessFileType(fileName):
251	base, ext = os.path.splitext(fileName)
252	try:
253		f = open(fileName, "rb")
254	except IOError:
255		return None
256	cr, tp = getMacCreatorAndType(fileName)
257	if tp in ("sfnt", "FFIL"):
258		return "TTF"
259	if ext == ".dfont":
260		return "TTF"
261	header = f.read(256)
262	head = Tag(header[:4])
263	if head == "OTTO":
264		return "OTF"
265	elif head == "ttcf":
266		return "TTC"
267	elif head in ("\0\1\0\0", "true"):
268		return "TTF"
269	elif head == "wOFF":
270		return "WOFF"
271	elif head.lower() == "<?xm":
272		# Use 'latin-1' because that can't fail.
273		header = tostr(header, 'latin-1')
274		if opentypeheaderRE.search(header):
275			return "OTX"
276		else:
277			return "TTX"
278	return None
279
280
281def parseOptions(args):
282	try:
283		rawOptions, files = getopt.getopt(args, "ld:o:vqht:x:sim:z:baey:")
284	except getopt.GetoptError:
285		usage()
286
287	if not files:
288		usage()
289
290	options = Options(rawOptions, len(files))
291	jobs = []
292
293	for input in files:
294		tp = guessFileType(input)
295		if tp in ("OTF", "TTF", "TTC", "WOFF"):
296			extension = ".ttx"
297			if options.listTables:
298				action = ttList
299			else:
300				action = ttDump
301		elif tp == "TTX":
302			extension = ".ttf"
303			action = ttCompile
304		elif tp == "OTX":
305			extension = ".otf"
306			action = ttCompile
307		else:
308			print('Unknown file type: "%s"' % input)
309			continue
310
311		if options.outputFile:
312			output = options.outputFile
313		else:
314			output = makeOutputFileName(input, options.outputDir, extension)
315		jobs.append((action, input, output))
316	return jobs, options
317
318
319def process(jobs, options):
320	for action, input, output in jobs:
321		action(input, output, options)
322
323
324def waitForKeyPress():
325	"""Force the DOS Prompt window to stay open so the user gets
326	a chance to see what's wrong."""
327	import msvcrt
328	print('(Hit any key to exit)')
329	while not msvcrt.kbhit():
330		pass
331
332
333def main(args):
334	jobs, options = parseOptions(args)
335	try:
336		process(jobs, options)
337	except KeyboardInterrupt:
338		print("(Cancelled.)")
339	except SystemExit:
340		if sys.platform == "win32":
341			waitForKeyPress()
342		else:
343			raise
344	except TTLibError as e:
345		print("Error:",e)
346	except:
347		if sys.platform == "win32":
348			import traceback
349			traceback.print_exc()
350			waitForKeyPress()
351		else:
352			raise
353
354
355if __name__ == "__main__":
356	main(sys.argv[1:])
357