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