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