ttx.py revision 2921bb25cd89f665bafb166b38b83baab5c1bd38
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
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
42    Compile options:
43    -m Merge with TrueType-input-file: specify a TrueType or OpenType
44       font file to be merged with the TTX file. This option is only
45       valid when at most one TTX file is specified.
46    -b Don't recalc glyph boundig boxes: use the values in the TTX
47       file as-is.
48"""
49
50
51import sys
52import os
53import getopt
54import re
55from fontTools.ttLib import TTFont
56from fontTools import version
57
58def usage():
59	print __doc__ % version
60	sys.exit(2)
61
62
63numberAddedRE = re.compile("(.*)#\d+$")
64
65def makeOutputFileName(input, outputDir, extension):
66	dir, file = os.path.split(input)
67	file, ext = os.path.splitext(file)
68	if outputDir:
69		dir = outputDir
70	output = os.path.join(dir, file + extension)
71	m = numberAddedRE.match(file)
72	if m:
73		file = m.group(1)
74	n = 1
75	while os.path.exists(output):
76		output = os.path.join(dir, file + "#" + repr(n) + extension)
77		n = n + 1
78	return output
79
80
81class Options:
82
83	listTables = 0
84	outputDir = None
85	verbose = 0
86	splitTables = 0
87	disassembleInstructions = 1
88	mergeFile = None
89	recalcBBoxes = 1
90
91	def __init__(self, rawOptions, numFiles):
92		self.onlyTables = []
93		self.skipTables = []
94		for option, value in rawOptions:
95			# general options
96			if option == "-h":
97				print __doc__ % version
98				sys.exit(0)
99			elif option == "-d":
100				if not os.path.isdir(value):
101					print "The -d option value must be an existing directory"
102					sys.exit(2)
103				self.outputDir = value
104			elif option == "-v":
105				self.verbose = 1
106			# dump options
107			elif option == "-l":
108				self.listTables = 1
109			elif option == "-t":
110				self.onlyTables.append(value)
111			elif option == "-x":
112				self.skipTables.append(value)
113			elif option == "-s":
114				self.splitTables = 1
115			elif option == "-i":
116				self.disassembleInstructions = 0
117			# compile options
118			elif option == "-m":
119				self.mergeFile = value
120			elif option == "-b":
121				self.recalcBBoxes = 0
122		if self.onlyTables and self.skipTables:
123			print "-t and -x options are mutually exlusive"
124			sys.exit(2)
125		if self.mergeFile and numFiles > 1:
126			print "Must specify exactly one TTX source file when using -i"
127			sys.exit(2)
128
129
130def ttList(input, output, options):
131	ttf = TTFont(input)
132	reader = ttf.reader
133	tags = reader.keys()
134	tags.sort()
135	print 'Listing table info for "%s":' % input
136	format = "    %4s  %10s  %7s  %7s"
137	print format % ("tag ", "  checksum", " length", " offset")
138	print format % ("----", "----------", "-------", "-------")
139	for tag in tags:
140		entry = reader.tables[tag]
141		checksum = "0x" + hex(entry.checkSum)[2:].zfill(8)
142		print format % (tag, checksum, entry.length, entry.offset)
143	print
144	ttf.close()
145
146
147def ttDump(input, output, options):
148	print 'Dumping "%s" to "%s"...' % (input, output)
149	ttf = TTFont(input, 0, verbose=options.verbose)
150	ttf.saveXML(output,
151			tables=options.onlyTables,
152			skipTables=options.skipTables,
153			splitTables=options.splitTables,
154			disassembleInstructions=options.disassembleInstructions)
155	ttf.close()
156
157
158def ttCompile(input, output, options):
159	print 'Compiling "%s" to "%s"...' % (input, output)
160	ttf = TTFont(options.mergeFile,
161			recalcBBoxes=options.recalcBBoxes,
162			verbose=options.verbose)
163	ttf.importXML(input)
164	ttf.save(output)
165
166	if options.verbose:
167		import time
168		print "finished at", time.strftime("%H:%M:%S", time.localtime(time.time()))
169
170
171def guessFileType(fileName):
172	try:
173		f = open(fileName, "rb")
174	except IOError:
175		return None
176	header = f.read(256)
177	head = header[:4]
178	if head == "OTTO":
179		return "OTF"
180	elif head in ("\0\1\0\0", "true"):
181		return "TTF"
182	elif head.lower() == "<?xm":
183		if header.find('sfntVersion="OTTO"') > 0:
184			return "OTX"
185		else:
186			return "TTX"
187	# XXX Mac suitcase!
188	return None
189
190
191def parseOptions(args):
192	try:
193		rawOptions, files = getopt.getopt(args, "ld:vht:x:sim:b")
194	except getopt.GetoptError:
195		usage()
196
197	if not files:
198		usage()
199
200	options = Options(rawOptions, len(files))
201	jobs = []
202
203	for input in files:
204		tp = guessFileType(input)
205		if tp in ("OTF", "TTF"):
206			extension = ".ttx"
207			if options.listTables:
208				action = ttList
209			else:
210				action = ttDump
211		elif tp == "TTX":
212			extension = ".ttf"
213			action = ttCompile
214		elif tp == "OTX":
215			extension = ".otf"
216			action = ttCompile
217		else:
218			print 'Unknown file type: "%s"' % input
219			continue
220
221		output = makeOutputFileName(input, options.outputDir, extension)
222		jobs.append((action, input, output))
223	return jobs, options
224
225
226def process(jobs, options):
227	for action, input, output in jobs:
228		action(input, output, options)
229
230
231def waitForKeyPress():
232	"""Force the DOS Prompt window to stay open so the user gets
233	a chance to see what's wrong."""
234	import msvcrt
235	print '(Hit any key to exit)'
236	while not msvcrt.kbhit():
237		pass
238
239
240def main(args):
241	jobs, options = parseOptions(args)
242	try:
243		process(jobs, options)
244	except KeyboardInterrupt:
245		print "(Cancelled.)"
246	except SystemExit:
247		if sys.platform == "win32":
248			waitForKeyPress()
249		else:
250			raise
251	except:
252		if sys.platform == "win32":
253			import traceback
254			traceback.print_exc()
255			waitForKeyPress()
256		else:
257			raise
258
259
260if __name__ == "__main__":
261	main(sys.argv[1:])
262