ttx.py revision 45d1f3b3b552297484bc2b8e9a2e999630bb5e50
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
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		for option, value in rawOptions:
103			# general options
104			if option == "-h":
105				print __doc__ % version
106				sys.exit(0)
107			elif option == "-d":
108				if not os.path.isdir(value):
109					print "The -d option value must be an existing directory"
110					sys.exit(2)
111				self.outputDir = value
112			elif option == "-v":
113				self.verbose = 1
114			# dump options
115			elif option == "-l":
116				self.listTables = 1
117			elif option == "-t":
118				self.onlyTables.append(value)
119			elif option == "-x":
120				self.skipTables.append(value)
121			elif option == "-s":
122				self.splitTables = 1
123			elif option == "-i":
124				self.disassembleInstructions = 0
125			# compile options
126			elif option == "-m":
127				self.mergeFile = value
128			elif option == "-b":
129				self.recalcBBoxes = 0
130			elif option == "-a":
131				self.allowVID = 1
132			elif option == "-e":
133				self.ignoreDecompileErrors = False
134		if self.onlyTables and self.skipTables:
135			print "-t and -x options are mutually exclusive"
136			sys.exit(2)
137		if self.mergeFile and numFiles > 1:
138			print "Must specify exactly one TTX source file when using -m"
139			sys.exit(2)
140
141
142def ttList(input, output, options):
143	import string
144	ttf = TTFont(input)
145	reader = ttf.reader
146	tags = reader.keys()
147	tags.sort()
148	print 'Listing table info for "%s":' % input
149	format = "    %4s  %10s  %7s  %7s"
150	print format % ("tag ", "  checksum", " length", " offset")
151	print format % ("----", "----------", "-------", "-------")
152	for tag in tags:
153		entry = reader.tables[tag]
154		checkSum = long(entry.checkSum)
155		if checkSum < 0:
156			checkSum = checkSum + 0x100000000L
157		checksum = "0x" + string.zfill(hex(checkSum)[2:-1], 8)
158		print format % (tag, checksum, entry.length, entry.offset)
159	print
160	ttf.close()
161
162
163def ttDump(input, output, options):
164	print 'Dumping "%s" to "%s"...' % (input, output)
165	ttf = TTFont(input, 0, verbose=options.verbose, allowVID=options.allowVID,
166			ignoreDecompileErrors=options.ignoreDecompileErrors)
167	ttf.saveXML(output,
168			tables=options.onlyTables,
169			skipTables=options.skipTables,
170			splitTables=options.splitTables,
171			disassembleInstructions=options.disassembleInstructions)
172	ttf.close()
173
174
175def ttCompile(input, output, options):
176	print 'Compiling "%s" to "%s"...' % (input, output)
177	ttf = TTFont(options.mergeFile,
178			recalcBBoxes=options.recalcBBoxes,
179			verbose=options.verbose, allowVID=options.allowVID)
180	ttf.importXML(input)
181	try:
182		ttf.save(output)
183	except OTLOffsetOverflowError, e:
184		overflowRecord = e.value
185		print "Attempting to fix OTLOffsetOverflowError", e
186		lastItem = overflowRecord
187		while 1:
188			ok = 0
189			if overflowRecord.itemName == None:
190				ok = fixLookupOverFlows(ttf, overflowRecord)
191			else:
192				ok = fixSubTableOverFlows(ttf, overflowRecord)
193			if not ok:
194				raise
195
196			try:
197				ttf.save(output)
198				break
199			except OTLOffsetOverflowError, e:
200				print "Attempting to fix OTLOffsetOverflowError", e
201				overflowRecord = e.value
202				if overflowRecord == lastItem:
203					raise
204
205	if options.verbose:
206		import time
207		print "finished at", time.strftime("%H:%M:%S", time.localtime(time.time()))
208
209
210def guessFileType(fileName):
211	base, ext = os.path.splitext(fileName)
212	try:
213		f = open(fileName, "rb")
214	except IOError:
215		return None
216	cr, tp = getMacCreatorAndType(fileName)
217	if tp in ("sfnt", "FFIL"):
218		return "TTF"
219	if ext == ".dfont":
220		return "TTF"
221	header = f.read(256)
222	head = header[:4]
223	if head == "OTTO":
224		return "OTF"
225	elif head in ("\0\1\0\0", "true"):
226		return "TTF"
227	elif head.lower() == "<?xm":
228		if header.find('sfntVersion="OTTO"') > 0:
229			return "OTX"
230		else:
231			return "TTX"
232	return None
233
234
235def parseOptions(args):
236	try:
237		rawOptions, files = getopt.getopt(args, "ld:vht:x:sim:bae")
238	except getopt.GetoptError:
239		usage()
240
241	if not files:
242		usage()
243
244	options = Options(rawOptions, len(files))
245	jobs = []
246
247	for input in files:
248		tp = guessFileType(input)
249		if tp in ("OTF", "TTF"):
250			extension = ".ttx"
251			if options.listTables:
252				action = ttList
253			else:
254				action = ttDump
255		elif tp == "TTX":
256			extension = ".ttf"
257			action = ttCompile
258		elif tp == "OTX":
259			extension = ".otf"
260			action = ttCompile
261		else:
262			print 'Unknown file type: "%s"' % input
263			continue
264
265		output = makeOutputFileName(input, options.outputDir, extension)
266		jobs.append((action, input, output))
267	return jobs, options
268
269
270def process(jobs, options):
271	for action, input, output in jobs:
272		action(input, output, options)
273
274
275def waitForKeyPress():
276	"""Force the DOS Prompt window to stay open so the user gets
277	a chance to see what's wrong."""
278	import msvcrt
279	print '(Hit any key to exit)'
280	while not msvcrt.kbhit():
281		pass
282
283
284def main(args):
285	jobs, options = parseOptions(args)
286	try:
287		process(jobs, options)
288	except KeyboardInterrupt:
289		print "(Cancelled.)"
290	except SystemExit:
291		if sys.platform == "win32":
292			waitForKeyPress()
293		else:
294			raise
295	except:
296		if sys.platform == "win32":
297			import traceback
298			traceback.print_exc()
299			waitForKeyPress()
300		else:
301			raise
302
303
304if __name__ == "__main__":
305	main(sys.argv[1:])
306