ttx.py revision 823f8cd15f16bb9dc3991c2672f16dd90579711b
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
43    Compile options:
44    -m Merge with TrueType-input-file: specify a TrueType or OpenType
45       font file to be merged with the TTX file. This option is only
46       valid when at most one TTX file is specified.
47    -b Don't recalc glyph boundig boxes: use the values in the TTX
48       file as-is.
49"""
50
51
52import sys
53import os
54import getopt
55import re
56from fontTools.ttLib import TTFont
57from fontTools.ttLib.tables.otBase import OTLOffsetOverflowError
58from fontTools.ttLib.tables.otTables import fixLookupOverFlows, fixSubTableOverFlows
59from fontTools import version
60
61def usage():
62	print __doc__ % version
63	sys.exit(2)
64
65
66if sys.platform == "darwin" and sys.version_info[:3] == (2, 2, 0):
67	# the Mac support of Jaguar's Python 2.2 is broken
68	have_broken_macsupport = 1
69else:
70	have_broken_macsupport = 0
71
72numberAddedRE = re.compile("(.*)#\d+$")
73
74def makeOutputFileName(input, outputDir, extension):
75	dir, file = os.path.split(input)
76	file, ext = os.path.splitext(file)
77	if outputDir:
78		dir = outputDir
79	output = os.path.join(dir, file + extension)
80	m = numberAddedRE.match(file)
81	if m:
82		file = m.group(1)
83	n = 1
84	while os.path.exists(output):
85		output = os.path.join(dir, file + "#" + repr(n) + extension)
86		n = n + 1
87	return output
88
89
90class Options:
91
92	listTables = 0
93	outputDir = None
94	verbose = 0
95	splitTables = 0
96	disassembleInstructions = 1
97	mergeFile = None
98	recalcBBoxes = 1
99	allowVID = 0
100
101	def __init__(self, rawOptions, numFiles):
102		self.onlyTables = []
103		self.skipTables = []
104		for option, value in rawOptions:
105			# general options
106			if option == "-h":
107				print __doc__ % version
108				sys.exit(0)
109			elif option == "-d":
110				if not os.path.isdir(value):
111					print "The -d option value must be an existing directory"
112					sys.exit(2)
113				self.outputDir = value
114			elif option == "-v":
115				self.verbose = 1
116			# dump options
117			elif option == "-l":
118				self.listTables = 1
119			elif option == "-t":
120				self.onlyTables.append(value)
121			elif option == "-x":
122				self.skipTables.append(value)
123			elif option == "-s":
124				self.splitTables = 1
125			elif option == "-i":
126				self.disassembleInstructions = 0
127			# compile options
128			elif option == "-m":
129				self.mergeFile = value
130			elif option == "-b":
131				self.recalcBBoxes = 0
132			elif option == "-a":
133				self.allowVID = 1
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	ttf.saveXML(output,
167			tables=options.onlyTables,
168			skipTables=options.skipTables,
169			splitTables=options.splitTables,
170			disassembleInstructions=options.disassembleInstructions)
171	ttf.close()
172
173
174def ttCompile(input, output, options):
175	print 'Compiling "%s" to "%s"...' % (input, output)
176	ttf = TTFont(options.mergeFile,
177			recalcBBoxes=options.recalcBBoxes,
178			verbose=options.verbose, allowVID=options.allowVID)
179	ttf.importXML(input)
180	try:
181		ttf.save(output)
182	except OTLOffsetOverflowError, e:
183		overflowRecord = e.value
184		print "Attempting to fix OTLOffsetOverflowError", e
185		lastItem = overflowRecord
186		while 1:
187			ok = 0
188			if overflowRecord.itemName == None:
189				ok = fixLookupOverFlows(ttf, overflowRecord)
190			else:
191				ok = fixSubTableOverFlows(ttf, overflowRecord)
192			if not ok:
193				raise
194
195			try:
196				ttf.save(output)
197				break
198			except OTLOffsetOverflowError, e:
199				print "Attempting to fix OTLOffsetOverflowError", e
200				overflowRecord = e.value
201				if overflowRecord == lastItem:
202					raise
203
204	if options.verbose:
205		import time
206		print "finished at", time.strftime("%H:%M:%S", time.localtime(time.time()))
207
208
209def guessFileType(fileName):
210	base, ext = os.path.splitext(fileName)
211	try:
212		f = open(fileName, "rb")
213	except IOError:
214		return None
215	if not have_broken_macsupport:
216		try:
217			import MacOS
218		except ImportError:
219			pass
220		else:
221			cr, tp = MacOS.GetCreatorAndType(fileName)
222			if tp in ("sfnt", "FFIL"):
223				return "TTF"
224			if ext == ".dfont":
225				return "TTF"
226	header = f.read(256)
227	head = header[:4]
228	if head == "OTTO":
229		return "OTF"
230	elif head in ("\0\1\0\0", "true"):
231		return "TTF"
232	elif head.lower() == "<?xm":
233		if header.find('sfntVersion="OTTO"') > 0:
234			return "OTX"
235		else:
236			return "TTX"
237	return None
238
239
240def parseOptions(args):
241	try:
242		rawOptions, files = getopt.getopt(args, "ld:vht:x:sim:ba")
243	except getopt.GetoptError:
244		usage()
245
246	if not files:
247		usage()
248
249	options = Options(rawOptions, len(files))
250	jobs = []
251
252	for input in files:
253		tp = guessFileType(input)
254		if tp in ("OTF", "TTF"):
255			extension = ".ttx"
256			if options.listTables:
257				action = ttList
258			else:
259				action = ttDump
260		elif tp == "TTX":
261			extension = ".ttf"
262			action = ttCompile
263		elif tp == "OTX":
264			extension = ".otf"
265			action = ttCompile
266		else:
267			print 'Unknown file type: "%s"' % input
268			continue
269
270		output = makeOutputFileName(input, options.outputDir, extension)
271		jobs.append((action, input, output))
272	return jobs, options
273
274
275def process(jobs, options):
276	for action, input, output in jobs:
277		action(input, output, options)
278
279
280def waitForKeyPress():
281	"""Force the DOS Prompt window to stay open so the user gets
282	a chance to see what's wrong."""
283	import msvcrt
284	print '(Hit any key to exit)'
285	while not msvcrt.kbhit():
286		pass
287
288
289def main(args):
290	jobs, options = parseOptions(args)
291	try:
292		process(jobs, options)
293	except KeyboardInterrupt:
294		print "(Cancelled.)"
295	except SystemExit:
296		if sys.platform == "win32":
297			waitForKeyPress()
298		else:
299			raise
300	except:
301		if sys.platform == "win32":
302			import traceback
303			traceback.print_exc()
304			waitForKeyPress()
305		else:
306			raise
307
308
309if __name__ == "__main__":
310	main(sys.argv[1:])
311