ttx.py revision f7f0f74419f44adf001426feef12a282243f13aa
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	import string
132	ttf = TTFont(input)
133	reader = ttf.reader
134	tags = reader.keys()
135	tags.sort()
136	print 'Listing table info for "%s":' % input
137	format = "    %4s  %10s  %7s  %7s"
138	print format % ("tag ", "  checksum", " length", " offset")
139	print format % ("----", "----------", "-------", "-------")
140	for tag in tags:
141		entry = reader.tables[tag]
142		checksum = "0x" + string.zfill(hex(entry.checkSum)[2:], 8)
143		print format % (tag, checksum, entry.length, entry.offset)
144	print
145	ttf.close()
146
147
148def ttDump(input, output, options):
149	print 'Dumping "%s" to "%s"...' % (input, output)
150	ttf = TTFont(input, 0, verbose=options.verbose)
151	ttf.saveXML(output,
152			tables=options.onlyTables,
153			skipTables=options.skipTables,
154			splitTables=options.splitTables,
155			disassembleInstructions=options.disassembleInstructions)
156	ttf.close()
157
158
159def ttCompile(input, output, options):
160	print 'Compiling "%s" to "%s"...' % (input, output)
161	ttf = TTFont(options.mergeFile,
162			recalcBBoxes=options.recalcBBoxes,
163			verbose=options.verbose)
164	ttf.importXML(input)
165	ttf.save(output)
166
167	if options.verbose:
168		import time
169		print "finished at", time.strftime("%H:%M:%S", time.localtime(time.time()))
170
171
172def guessFileType(fileName):
173	try:
174		f = open(fileName, "rb")
175	except IOError:
176		return None
177	try:
178		import macfs
179	except ImportError:
180		pass
181	else:
182		cr, tp = macfs.FSSpec(fileName).GetCreatorType()
183		if tp == "FFIL":
184			return "TTF"
185	header = f.read(256)
186	head = header[:4]
187	if head == "OTTO":
188		return "OTF"
189	elif head in ("\0\1\0\0", "true"):
190		return "TTF"
191	elif head.lower() == "<?xm":
192		if header.find('sfntVersion="OTTO"') > 0:
193			return "OTX"
194		else:
195			return "TTX"
196	return None
197
198
199def parseOptions(args):
200	try:
201		rawOptions, files = getopt.getopt(args, "ld:vht:x:sim:b")
202	except getopt.GetoptError:
203		usage()
204
205	if not files:
206		usage()
207
208	options = Options(rawOptions, len(files))
209	jobs = []
210
211	for input in files:
212		tp = guessFileType(input)
213		if tp in ("OTF", "TTF"):
214			extension = ".ttx"
215			if options.listTables:
216				action = ttList
217			else:
218				action = ttDump
219		elif tp == "TTX":
220			extension = ".ttf"
221			action = ttCompile
222		elif tp == "OTX":
223			extension = ".otf"
224			action = ttCompile
225		else:
226			print 'Unknown file type: "%s"' % input
227			continue
228
229		output = makeOutputFileName(input, options.outputDir, extension)
230		jobs.append((action, input, output))
231	return jobs, options
232
233
234def process(jobs, options):
235	for action, input, output in jobs:
236		action(input, output, options)
237
238
239def waitForKeyPress():
240	"""Force the DOS Prompt window to stay open so the user gets
241	a chance to see what's wrong."""
242	import msvcrt
243	print '(Hit any key to exit)'
244	while not msvcrt.kbhit():
245		pass
246
247
248def main(args):
249	jobs, options = parseOptions(args)
250	try:
251		process(jobs, options)
252	except KeyboardInterrupt:
253		print "(Cancelled.)"
254	except SystemExit:
255		if sys.platform == "win32":
256			waitForKeyPress()
257		else:
258			raise
259	except:
260		if sys.platform == "win32":
261			import traceback
262			traceback.print_exc()
263			waitForKeyPress()
264		else:
265			raise
266
267
268if __name__ == "__main__":
269	main(sys.argv[1:])
270