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