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