ttx.py revision ac4672e4510a79a56c4983ad28b42724c30ea9d1
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
69import sys
70import os
71import getopt
72import re
73from fontTools.ttLib import TTFont, TTLibError
74from fontTools.ttLib.tables.otBase import OTLOffsetOverflowError
75from fontTools.ttLib.tables.otTables import fixLookupOverFlows, fixSubTableOverFlows
76from fontTools.misc.macCreatorType import getMacCreatorAndType
77from fontTools import version
78from fontTools.misc.py23 import *
79
80def usage():
81	print(__doc__ % version)
82	sys.exit(2)
83
84
85numberAddedRE = re.compile("#\d+$")
86opentypeheaderRE = re.compile('''sfntVersion=['"]OTTO["']''')
87
88def makeOutputFileName(input, outputDir, extension):
89	dir, file = os.path.split(input)
90	file, ext = os.path.splitext(file)
91	if outputDir:
92		dir = outputDir
93	file = numberAddedRE.split(file)[0]
94	output = os.path.join(dir, file + extension)
95	n = 1
96	while os.path.exists(output):
97		output = os.path.join(dir, file + "#" + repr(n) + extension)
98		n = n + 1
99	return output
100
101
102class Options:
103
104	listTables = False
105	outputDir = None
106	outputFile = None
107	verbose = False
108	quiet = False
109	splitTables = False
110	disassembleInstructions = True
111	mergeFile = None
112	recalcBBoxes = True
113	allowVID = False
114	ignoreDecompileErrors = True
115	bitmapGlyphDataFormat = 'raw'
116
117	def __init__(self, rawOptions, numFiles):
118		self.onlyTables = []
119		self.skipTables = []
120		self.fontNumber = -1
121		for option, value in rawOptions:
122			# general options
123			if option == "-h":
124				print(__doc__ % version)
125				sys.exit(0)
126			elif option == "-d":
127				if not os.path.isdir(value):
128					print("The -d option value must be an existing directory")
129					sys.exit(2)
130				self.outputDir = value
131			elif option == "-o":
132				self.outputFile = value
133			elif option == "-v":
134				self.verbose = True
135			elif option == "-q":
136				self.quiet = True
137			# dump options
138			elif option == "-l":
139				self.listTables = True
140			elif option == "-t":
141				self.onlyTables.append(value)
142			elif option == "-x":
143				self.skipTables.append(value)
144			elif option == "-s":
145				self.splitTables = True
146			elif option == "-i":
147				self.disassembleInstructions = False
148			elif option == "-z":
149				validOptions = ('raw', 'row', 'bitwise', 'extfile')
150				if value not in validOptions:
151					print("-z does not allow %s as a format. Use %s" % (option, validOptions))
152					sys.exit(2)
153				self.bitmapGlyphDataFormat = value
154			elif option == "-y":
155				self.fontNumber = int(value)
156			# compile options
157			elif option == "-m":
158				self.mergeFile = value
159			elif option == "-b":
160				self.recalcBBoxes = False
161			elif option == "-a":
162				self.allowVID = True
163			elif option == "-e":
164				self.ignoreDecompileErrors = False
165		if self.onlyTables and self.skipTables:
166			print("-t and -x options are mutually exclusive")
167			sys.exit(2)
168		if self.mergeFile and numFiles > 1:
169			print("Must specify exactly one TTX source file when using -m")
170			sys.exit(2)
171
172
173def ttList(input, output, options):
174	ttf = TTFont(input, fontNumber=options.fontNumber, lazy=True)
175	reader = ttf.reader
176	tags = sorted(reader.keys())
177	print('Listing table info for "%s":' % input)
178	format = "    %4s  %10s  %7s  %7s"
179	print(format % ("tag ", "  checksum", " length", " offset"))
180	print(format % ("----", "----------", "-------", "-------"))
181	for tag in tags:
182		entry = reader.tables[tag]
183		checkSum = int(entry.checkSum)
184		if checkSum < 0:
185			checkSum = checkSum + 0x100000000
186		checksum = "0x%08X" % checkSum
187		print(format % (tag, checksum, entry.length, entry.offset))
188	print()
189	ttf.close()
190
191
192def ttDump(input, output, options):
193	if not options.quiet:
194		print('Dumping "%s" to "%s"...' % (input, output))
195	ttf = TTFont(input, 0, verbose=options.verbose, allowVID=options.allowVID,
196			lazy=False,
197			quiet=options.quiet,
198			ignoreDecompileErrors=options.ignoreDecompileErrors,
199			fontNumber=options.fontNumber)
200	ttf.saveXML(output,
201			quiet=options.quiet,
202			tables=options.onlyTables,
203			skipTables=options.skipTables,
204			splitTables=options.splitTables,
205			disassembleInstructions=options.disassembleInstructions,
206			bitmapGlyphDataFormat=options.bitmapGlyphDataFormat)
207	ttf.close()
208
209
210def ttCompile(input, output, options):
211	if not options.quiet:
212		print('Compiling "%s" to "%s"...' % (input, output))
213	ttf = TTFont(options.mergeFile,
214			lazy=False,
215			recalcBBoxes=options.recalcBBoxes,
216			verbose=options.verbose, allowVID=options.allowVID)
217	ttf.importXML(input, quiet=options.quiet)
218	try:
219		ttf.save(output)
220	except OTLOffsetOverflowError as e:
221		# XXX This shouldn't be here at all, it should be as close to the
222		# OTL code as possible.
223		overflowRecord = e.value
224		print("Attempting to fix OTLOffsetOverflowError", e)
225		lastItem = overflowRecord
226		while True:
227			ok = 0
228			if overflowRecord.itemName == None:
229				ok = fixLookupOverFlows(ttf, overflowRecord)
230			else:
231				ok = fixSubTableOverFlows(ttf, overflowRecord)
232			if not ok:
233				raise
234
235			try:
236				ttf.save(output)
237				break
238			except OTLOffsetOverflowError as e:
239				print("Attempting to fix OTLOffsetOverflowError", e)
240				overflowRecord = e.value
241				if overflowRecord == lastItem:
242					raise
243
244	if options.verbose:
245		import time
246		print("finished at", time.strftime("%H:%M:%S", time.localtime(time.time())))
247
248
249def guessFileType(fileName):
250	base, ext = os.path.splitext(fileName)
251	try:
252		f = open(fileName, "rb")
253	except IOError:
254		return None
255	cr, tp = getMacCreatorAndType(fileName)
256	if tp in ("sfnt", "FFIL"):
257		return "TTF"
258	if ext == ".dfont":
259		return "TTF"
260	header = f.read(256)
261	head = Tag(header[:4])
262	if head == "OTTO":
263		return "OTF"
264	elif head == "ttcf":
265		return "TTC"
266	elif head in ("\0\1\0\0", "true"):
267		return "TTF"
268	elif head == "wOFF":
269		return "WOFF"
270	elif head.lower() == "<?xm":
271		if opentypeheaderRE.search(header):
272			return "OTX"
273		else:
274			return "TTX"
275	return None
276
277
278def parseOptions(args):
279	try:
280		rawOptions, files = getopt.getopt(args, "ld:o:vqht:x:sim:z:baey:")
281	except getopt.GetoptError:
282		usage()
283
284	if not files:
285		usage()
286
287	options = Options(rawOptions, len(files))
288	jobs = []
289
290	for input in files:
291		tp = guessFileType(input)
292		if tp in ("OTF", "TTF", "TTC", "WOFF"):
293			extension = ".ttx"
294			if options.listTables:
295				action = ttList
296			else:
297				action = ttDump
298		elif tp == "TTX":
299			extension = ".ttf"
300			action = ttCompile
301		elif tp == "OTX":
302			extension = ".otf"
303			action = ttCompile
304		else:
305			print('Unknown file type: "%s"' % input)
306			continue
307
308		if options.outputFile:
309			output = options.outputFile
310		else:
311			output = makeOutputFileName(input, options.outputDir, extension)
312		jobs.append((action, input, output))
313	return jobs, options
314
315
316def process(jobs, options):
317	for action, input, output in jobs:
318		action(input, output, options)
319
320
321def waitForKeyPress():
322	"""Force the DOS Prompt window to stay open so the user gets
323	a chance to see what's wrong."""
324	import msvcrt
325	print('(Hit any key to exit)')
326	while not msvcrt.kbhit():
327		pass
328
329
330def main(args):
331	jobs, options = parseOptions(args)
332	try:
333		process(jobs, options)
334	except KeyboardInterrupt:
335		print("(Cancelled.)")
336	except SystemExit:
337		if sys.platform == "win32":
338			waitForKeyPress()
339		else:
340			raise
341	except TTLibError as e:
342		print("Error:",e)
343	except:
344		if sys.platform == "win32":
345			import traceback
346			traceback.print_exc()
347			waitForKeyPress()
348		else:
349			raise
350
351
352if __name__ == "__main__":
353	main(sys.argv[1:])
354