ttx.py revision 70e2cb86e84b71de84a880d5c0ba1af2e24041d7
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	try:
177		import macfs
178	except ImportError:
179		pass
180	else:
181		cr, tp = macfs.FSSpec(fileName).GetCreatorType()
182		if tp == "FFIL":
183			return "TTF"
184	header = f.read(256)
185	head = header[:4]
186	if head == "OTTO":
187		return "OTF"
188	elif head in ("\0\1\0\0", "true"):
189		return "TTF"
190	elif head.lower() == "<?xm":
191		if header.find('sfntVersion="OTTO"') > 0:
192			return "OTX"
193		else:
194			return "TTX"
195	return None
196
197
198def parseOptions(args):
199	try:
200		rawOptions, files = getopt.getopt(args, "ld:vht:x:sim:b")
201	except getopt.GetoptError:
202		usage()
203
204	if not files:
205		usage()
206
207	options = Options(rawOptions, len(files))
208	jobs = []
209
210	for input in files:
211		tp = guessFileType(input)
212		if tp in ("OTF", "TTF"):
213			extension = ".ttx"
214			if options.listTables:
215				action = ttList
216			else:
217				action = ttDump
218		elif tp == "TTX":
219			extension = ".ttf"
220			action = ttCompile
221		elif tp == "OTX":
222			extension = ".otf"
223			action = ttCompile
224		else:
225			print 'Unknown file type: "%s"' % input
226			continue
227
228		output = makeOutputFileName(input, options.outputDir, extension)
229		jobs.append((action, input, output))
230	return jobs, options
231
232
233def process(jobs, options):
234	for action, input, output in jobs:
235		action(input, output, options)
236
237
238def waitForKeyPress():
239	"""Force the DOS Prompt window to stay open so the user gets
240	a chance to see what's wrong."""
241	import msvcrt
242	print '(Hit any key to exit)'
243	while not msvcrt.kbhit():
244		pass
245
246
247def main(args):
248	jobs, options = parseOptions(args)
249	try:
250		process(jobs, options)
251	except KeyboardInterrupt:
252		print "(Cancelled.)"
253	except SystemExit:
254		if sys.platform == "win32":
255			waitForKeyPress()
256		else:
257			raise
258	except:
259		if sys.platform == "win32":
260			import traceback
261			traceback.print_exc()
262			waitForKeyPress()
263		else:
264			raise
265
266
267if __name__ == "__main__":
268	main(sys.argv[1:])
269