ttx.py revision 7baa13689c6263f3ab152f77ab2c9f9c83398dc4
1"""\
2usage: ttx [options] inputfile1 [... inputfileN]
3
4    TTX %s -- From OpenType To XML And Back
5
6    If an input file is a TrueType or OpenType font file, it will be
7       dumped to an TTX file (an XML-based text format).
8    If an input file is a TTX file, it will be compiled to a TrueType
9       or OpenType font file.
10
11    Output files are created so they are unique: an existing file is
12       never overwritten.
13
14    General options:
15    -h Help: print this message
16    -d <outputfolder> Specify a directory where the output files are
17       to be created.
18    -o <outputfile> Specify a file to write the output to.
19    -v Verbose: more messages will be written to stdout about what
20       is being done.
21    -a allow virtual glyphs ID's on compile or decompile.
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    -z <format> Specify a bitmap data export option for EBDT:
42       {'raw', 'row', 'bitwise', 'extfile'} or for the CBDT:
43       {'raw', 'extfile'} Each option does one of the following:
44         -z raw
45            * export the bitmap data as a hex dump
46         -z row
47            * export each row as hex data
48         -z bitwise
49            * export each row as binary in an ASCII art style
50         -z extfile
51            * export the data as external files with XML refences
52       If no export format is specified 'raw' format is used.
53    -e Don't ignore decompilation errors, but show a full traceback
54       and abort.
55    -y <number> Select font number for TrueType Collection,
56       starting from 0.
57
58    Compile options:
59    -m Merge with TrueType-input-file: specify a TrueType or OpenType
60       font file to be merged with the TTX file. This option is only
61       valid when at most one TTX file is specified.
62    -b Don't recalc glyph bounding boxes: use the values in the TTX
63       file as-is.
64"""
65
66
67import sys
68import os
69import getopt
70import re
71from fontTools.ttLib import TTFont
72from fontTools.ttLib.tables.otBase import OTLOffsetOverflowError
73from fontTools.ttLib.tables.otTables import fixLookupOverFlows, fixSubTableOverFlows
74from fontTools.misc.macCreatorType import getMacCreatorAndType
75from fontTools import version
76
77def usage():
78	print __doc__ % version
79	sys.exit(2)
80
81
82numberAddedRE = re.compile("(.*)#\d+$")
83opentypeheaderRE = re.compile('''sfntVersion=['"]OTTO["']''')
84
85def makeOutputFileName(input, outputDir, extension):
86	dir, file = os.path.split(input)
87	file, ext = os.path.splitext(file)
88	if outputDir:
89		dir = outputDir
90	output = os.path.join(dir, file + extension)
91	m = numberAddedRE.match(file)
92	if m:
93		file = m.group(1)
94	n = 1
95	while os.path.exists(output):
96		output = os.path.join(dir, file + "#" + repr(n) + extension)
97		n = n + 1
98	return output
99
100
101class Options:
102
103	listTables = 0
104	outputDir = None
105	outputFile = None
106	verbose = 0
107	splitTables = 0
108	disassembleInstructions = 1
109	mergeFile = None
110	recalcBBoxes = 1
111	allowVID = 0
112	ignoreDecompileErrors = True
113	bitmapGlyphDataFormat = 'raw'
114
115	def __init__(self, rawOptions, numFiles):
116		self.onlyTables = []
117		self.skipTables = []
118		self.fontNumber = -1
119		for option, value in rawOptions:
120			# general options
121			if option == "-h":
122				print __doc__ % version
123				sys.exit(0)
124			elif option == "-d":
125				if not os.path.isdir(value):
126					print "The -d option value must be an existing directory"
127					sys.exit(2)
128				self.outputDir = value
129			elif option == "-o":
130				self.outputFile = value
131			elif option == "-v":
132				self.verbose = 1
133			# dump options
134			elif option == "-l":
135				self.listTables = 1
136			elif option == "-t":
137				self.onlyTables.append(value)
138			elif option == "-x":
139				self.skipTables.append(value)
140			elif option == "-s":
141				self.splitTables = 1
142			elif option == "-i":
143				self.disassembleInstructions = 0
144			elif option == "-z":
145				validOptions = ('raw', 'row', 'bitwise', 'extfile')
146				if value not in validOptions:
147					print "-z does not allow %s as a format. Use %s" % (option, validOptions)
148					sys.exit(2)
149				self.bitmapGlyphDataFormat = value
150			elif option == "-y":
151				self.fontNumber = int(value)
152			# compile options
153			elif option == "-m":
154				self.mergeFile = value
155			elif option == "-b":
156				self.recalcBBoxes = 0
157			elif option == "-a":
158				self.allowVID = 1
159			elif option == "-e":
160				self.ignoreDecompileErrors = False
161		if self.onlyTables and self.skipTables:
162			print "-t and -x options are mutually exclusive"
163			sys.exit(2)
164		if self.mergeFile and numFiles > 1:
165			print "Must specify exactly one TTX source file when using -m"
166			sys.exit(2)
167
168
169def ttList(input, output, options):
170	import string
171	ttf = TTFont(input, fontNumber=options.fontNumber)
172	reader = ttf.reader
173	tags = reader.keys()
174	tags.sort()
175	print 'Listing table info for "%s":' % input
176	format = "    %4s  %10s  %7s  %7s"
177	print format % ("tag ", "  checksum", " length", " offset")
178	print format % ("----", "----------", "-------", "-------")
179	for tag in tags:
180		entry = reader.tables[tag]
181		checkSum = long(entry.checkSum)
182		if checkSum < 0:
183			checkSum = checkSum + 0x100000000L
184		checksum = "0x" + string.zfill(hex(checkSum)[2:-1], 8)
185		print format % (tag, checksum, entry.length, entry.offset)
186	print
187	ttf.close()
188
189
190def ttDump(input, output, options):
191	print 'Dumping "%s" to "%s"...' % (input, output)
192	ttf = TTFont(input, 0, verbose=options.verbose, allowVID=options.allowVID,
193			ignoreDecompileErrors=options.ignoreDecompileErrors,
194			fontNumber=options.fontNumber)
195	ttf.saveXML(output,
196			tables=options.onlyTables,
197			skipTables=options.skipTables,
198			splitTables=options.splitTables,
199			disassembleInstructions=options.disassembleInstructions,
200			bitmapGlyphDataFormat=options.bitmapGlyphDataFormat)
201	ttf.close()
202
203
204def ttCompile(input, output, options):
205	print 'Compiling "%s" to "%s"...' % (input, output)
206	ttf = TTFont(options.mergeFile,
207			recalcBBoxes=options.recalcBBoxes,
208			verbose=options.verbose, allowVID=options.allowVID)
209	ttf.importXML(input)
210	try:
211		ttf.save(output)
212	except OTLOffsetOverflowError, e:
213		# XXX This shouldn't be here at all, it should be as close to the
214		# OTL code as possible.
215		overflowRecord = e.value
216		print "Attempting to fix OTLOffsetOverflowError", e
217		lastItem = overflowRecord
218		while 1:
219			ok = 0
220			if overflowRecord.itemName == None:
221				ok = fixLookupOverFlows(ttf, overflowRecord)
222			else:
223				ok = fixSubTableOverFlows(ttf, overflowRecord)
224			if not ok:
225				raise
226
227			try:
228				ttf.save(output)
229				break
230			except OTLOffsetOverflowError, e:
231				print "Attempting to fix OTLOffsetOverflowError", e
232				overflowRecord = e.value
233				if overflowRecord == lastItem:
234					raise
235
236	if options.verbose:
237		import time
238		print "finished at", time.strftime("%H:%M:%S", time.localtime(time.time()))
239
240
241def guessFileType(fileName):
242	base, ext = os.path.splitext(fileName)
243	try:
244		f = open(fileName, "rb")
245	except IOError:
246		return None
247	cr, tp = getMacCreatorAndType(fileName)
248	if tp in ("sfnt", "FFIL"):
249		return "TTF"
250	if ext == ".dfont":
251		return "TTF"
252	header = f.read(256)
253	head = header[:4]
254	if head == "OTTO":
255		return "OTF"
256	elif head == "ttcf":
257		return "TTC"
258	elif head in ("\0\1\0\0", "true"):
259		return "TTF"
260	elif head.lower() == "<?xm":
261		if opentypeheaderRE.match(header):
262			return "OTX"
263		else:
264			return "TTX"
265	return None
266
267
268def parseOptions(args):
269	try:
270		rawOptions, files = getopt.getopt(args, "ld:o:vht:x:sim:z:baey:")
271	except getopt.GetoptError:
272		usage()
273
274	if not files:
275		usage()
276
277	options = Options(rawOptions, len(files))
278	jobs = []
279
280	for input in files:
281		tp = guessFileType(input)
282		if tp in ("OTF", "TTF", "TTC"):
283			extension = ".ttx"
284			if options.listTables:
285				action = ttList
286			else:
287				action = ttDump
288		elif tp == "TTX":
289			extension = ".ttf"
290			action = ttCompile
291		elif tp == "OTX":
292			extension = ".otf"
293			action = ttCompile
294		else:
295			print 'Unknown file type: "%s"' % input
296			continue
297
298		if options.outputFile:
299			output = options.outputFile
300		else:
301			output = makeOutputFileName(input, options.outputDir, extension)
302		jobs.append((action, input, output))
303	return jobs, options
304
305
306def process(jobs, options):
307	for action, input, output in jobs:
308		action(input, output, options)
309
310
311def waitForKeyPress():
312	"""Force the DOS Prompt window to stay open so the user gets
313	a chance to see what's wrong."""
314	import msvcrt
315	print '(Hit any key to exit)'
316	while not msvcrt.kbhit():
317		pass
318
319
320def main(args):
321	jobs, options = parseOptions(args)
322	try:
323		process(jobs, options)
324	except KeyboardInterrupt:
325		print "(Cancelled.)"
326	except SystemExit:
327		if sys.platform == "win32":
328			waitForKeyPress()
329		else:
330			raise
331	except:
332		if sys.platform == "win32":
333			import traceback
334			traceback.print_exc()
335			waitForKeyPress()
336		else:
337			raise
338
339
340if __name__ == "__main__":
341	main(sys.argv[1:])
342