ttx.py revision 142506b07df7382f7a599cb661273bdaaef0f67c
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		# XXX This shouldn't be here at all, it should be as close to the
185		# OTL code as possible.
186		overflowRecord = e.value
187		print "Attempting to fix OTLOffsetOverflowError", e
188		lastItem = overflowRecord
189		while 1:
190			ok = 0
191			if overflowRecord.itemName == None:
192				ok = fixLookupOverFlows(ttf, overflowRecord)
193			else:
194				ok = fixSubTableOverFlows(ttf, overflowRecord)
195			if not ok:
196				raise
197
198			try:
199				ttf.save(output)
200				break
201			except OTLOffsetOverflowError, e:
202				print "Attempting to fix OTLOffsetOverflowError", e
203				overflowRecord = e.value
204				if overflowRecord == lastItem:
205					raise
206
207	if options.verbose:
208		import time
209		print "finished at", time.strftime("%H:%M:%S", time.localtime(time.time()))
210
211
212def guessFileType(fileName):
213	base, ext = os.path.splitext(fileName)
214	try:
215		f = open(fileName, "rb")
216	except IOError:
217		return None
218	cr, tp = getMacCreatorAndType(fileName)
219	if tp in ("sfnt", "FFIL"):
220		return "TTF"
221	if ext == ".dfont":
222		return "TTF"
223	header = f.read(256)
224	head = header[:4]
225	if head == "OTTO":
226		return "OTF"
227	elif head in ("\0\1\0\0", "true"):
228		return "TTF"
229	elif head.lower() == "<?xm":
230		if header.find('sfntVersion="OTTO"') > 0:
231			return "OTX"
232		else:
233			return "TTX"
234	return None
235
236
237def parseOptions(args):
238	try:
239		rawOptions, files = getopt.getopt(args, "ld:vht:x:sim:bae")
240	except getopt.GetoptError:
241		usage()
242
243	if not files:
244		usage()
245
246	options = Options(rawOptions, len(files))
247	jobs = []
248
249	for input in files:
250		tp = guessFileType(input)
251		if tp in ("OTF", "TTF"):
252			extension = ".ttx"
253			if options.listTables:
254				action = ttList
255			else:
256				action = ttDump
257		elif tp == "TTX":
258			extension = ".ttf"
259			action = ttCompile
260		elif tp == "OTX":
261			extension = ".otf"
262			action = ttCompile
263		else:
264			print 'Unknown file type: "%s"' % input
265			continue
266
267		output = makeOutputFileName(input, options.outputDir, extension)
268		jobs.append((action, input, output))
269	return jobs, options
270
271
272def process(jobs, options):
273	for action, input, output in jobs:
274		action(input, output, options)
275
276
277def waitForKeyPress():
278	"""Force the DOS Prompt window to stay open so the user gets
279	a chance to see what's wrong."""
280	import msvcrt
281	print '(Hit any key to exit)'
282	while not msvcrt.kbhit():
283		pass
284
285
286def main(args):
287	jobs, options = parseOptions(args)
288	try:
289		process(jobs, options)
290	except KeyboardInterrupt:
291		print "(Cancelled.)"
292	except SystemExit:
293		if sys.platform == "win32":
294			waitForKeyPress()
295		else:
296			raise
297	except:
298		if sys.platform == "win32":
299			import traceback
300			traceback.print_exc()
301			waitForKeyPress()
302		else:
303			raise
304
305
306if __name__ == "__main__":
307	main(sys.argv[1:])
308