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