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