ttx.py revision 7cc6d271ac955782d730161b27e728001fb5f347
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, TTLibError
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	file = numberAddedRE.split(file)[0]
93	output = os.path.join(dir, file + extension)
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 = False
104	outputDir = None
105	outputFile = None
106	verbose = False
107	quiet = False
108	splitTables = False
109	disassembleInstructions = True
110	mergeFile = None
111	recalcBBoxes = True
112	allowVID = False
113	ignoreDecompileErrors = True
114	bitmapGlyphDataFormat = 'raw'
115
116	def __init__(self, rawOptions, numFiles):
117		self.onlyTables = []
118		self.skipTables = []
119		self.fontNumber = -1
120		for option, value in rawOptions:
121			# general options
122			if option == "-h":
123				print __doc__ % version
124				sys.exit(0)
125			elif option == "-d":
126				if not os.path.isdir(value):
127					print "The -d option value must be an existing directory"
128					sys.exit(2)
129				self.outputDir = value
130			elif option == "-o":
131				self.outputFile = value
132			elif option == "-v":
133				self.verbose = True
134			elif option == "-q":
135				self.quiet = True
136			# dump options
137			elif option == "-l":
138				self.listTables = True
139			elif option == "-t":
140				self.onlyTables.append(value)
141			elif option == "-x":
142				self.skipTables.append(value)
143			elif option == "-s":
144				self.splitTables = True
145			elif option == "-i":
146				self.disassembleInstructions = False
147			elif option == "-z":
148				validOptions = ('raw', 'row', 'bitwise', 'extfile')
149				if value not in validOptions:
150					print "-z does not allow %s as a format. Use %s" % (option, validOptions)
151					sys.exit(2)
152				self.bitmapGlyphDataFormat = value
153			elif option == "-y":
154				self.fontNumber = int(value)
155			# compile options
156			elif option == "-m":
157				self.mergeFile = value
158			elif option == "-b":
159				self.recalcBBoxes = False
160			elif option == "-a":
161				self.allowVID = True
162			elif option == "-e":
163				self.ignoreDecompileErrors = False
164		if self.onlyTables and self.skipTables:
165			print "-t and -x options are mutually exclusive"
166			sys.exit(2)
167		if self.mergeFile and numFiles > 1:
168			print "Must specify exactly one TTX source file when using -m"
169			sys.exit(2)
170
171
172def ttList(input, output, options):
173	import string
174	ttf = TTFont(input, fontNumber=options.fontNumber, lazy=True)
175	reader = ttf.reader
176	tags = reader.keys()
177	tags.sort()
178	print 'Listing table info for "%s":' % input
179	format = "    %4s  %10s  %7s  %7s"
180	print format % ("tag ", "  checksum", " length", " offset")
181	print format % ("----", "----------", "-------", "-------")
182	for tag in tags:
183		entry = reader.tables[tag]
184		checkSum = int(entry.checkSum)
185		if checkSum < 0:
186			checkSum = checkSum + 0x100000000
187		checksum = "0x" + string.zfill(hex(checkSum)[2:-1], 8)
188		print format % (tag, checksum, entry.length, entry.offset)
189	print
190	ttf.close()
191
192
193def ttDump(input, output, options):
194	if not options.quiet:
195		print 'Dumping "%s" to "%s"...' % (input, output)
196	ttf = TTFont(input, 0, verbose=options.verbose, allowVID=options.allowVID,
197			lazy=False,
198			quiet=options.quiet,
199			ignoreDecompileErrors=options.ignoreDecompileErrors,
200			fontNumber=options.fontNumber)
201	ttf.saveXML(output,
202			quiet=options.quiet,
203			tables=options.onlyTables,
204			skipTables=options.skipTables,
205			splitTables=options.splitTables,
206			disassembleInstructions=options.disassembleInstructions,
207			bitmapGlyphDataFormat=options.bitmapGlyphDataFormat)
208	ttf.close()
209
210
211def ttCompile(input, output, options):
212	if not options.quiet:
213		print 'Compiling "%s" to "%s"...' % (input, output)
214	ttf = TTFont(options.mergeFile,
215			lazy=False,
216			recalcBBoxes=options.recalcBBoxes,
217			verbose=options.verbose, allowVID=options.allowVID)
218	ttf.importXML(input, quiet=options.quiet)
219	try:
220		ttf.save(output)
221	except OTLOffsetOverflowError, e:
222		# XXX This shouldn't be here at all, it should be as close to the
223		# OTL code as possible.
224		overflowRecord = e.value
225		print "Attempting to fix OTLOffsetOverflowError", e
226		lastItem = overflowRecord
227		while 1:
228			ok = 0
229			if overflowRecord.itemName == None:
230				ok = fixLookupOverFlows(ttf, overflowRecord)
231			else:
232				ok = fixSubTableOverFlows(ttf, overflowRecord)
233			if not ok:
234				raise
235
236			try:
237				ttf.save(output)
238				break
239			except OTLOffsetOverflowError, e:
240				print "Attempting to fix OTLOffsetOverflowError", e
241				overflowRecord = e.value
242				if overflowRecord == lastItem:
243					raise
244
245	if options.verbose:
246		import time
247		print "finished at", time.strftime("%H:%M:%S", time.localtime(time.time()))
248
249
250def guessFileType(fileName):
251	base, ext = os.path.splitext(fileName)
252	try:
253		f = open(fileName, "rb")
254	except IOError:
255		return None
256	cr, tp = getMacCreatorAndType(fileName)
257	if tp in ("sfnt", "FFIL"):
258		return "TTF"
259	if ext == ".dfont":
260		return "TTF"
261	header = f.read(256)
262	head = header[:4]
263	if head == "OTTO":
264		return "OTF"
265	elif head == "ttcf":
266		return "TTC"
267	elif head in ("\0\1\0\0", "true"):
268		return "TTF"
269	elif head in ("wOFF", "true"):
270		return "WOFF"
271	elif head.lower() == "<?xm":
272		if opentypeheaderRE.search(header):
273			return "OTX"
274		else:
275			return "TTX"
276	return None
277
278
279def parseOptions(args):
280	try:
281		rawOptions, files = getopt.getopt(args, "ld:o:vqht:x:sim:z:baey:")
282	except getopt.GetoptError:
283		usage()
284
285	if not files:
286		usage()
287
288	options = Options(rawOptions, len(files))
289	jobs = []
290
291	for input in files:
292		tp = guessFileType(input)
293		if tp in ("OTF", "TTF", "TTC", "WOFF"):
294			extension = ".ttx"
295			if options.listTables:
296				action = ttList
297			else:
298				action = ttDump
299		elif tp == "TTX":
300			extension = ".ttf"
301			action = ttCompile
302		elif tp == "OTX":
303			extension = ".otf"
304			action = ttCompile
305		else:
306			print 'Unknown file type: "%s"' % input
307			continue
308
309		if options.outputFile:
310			output = options.outputFile
311		else:
312			output = makeOutputFileName(input, options.outputDir, extension)
313		jobs.append((action, input, output))
314	return jobs, options
315
316
317def process(jobs, options):
318	for action, input, output in jobs:
319		action(input, output, options)
320
321
322def waitForKeyPress():
323	"""Force the DOS Prompt window to stay open so the user gets
324	a chance to see what's wrong."""
325	import msvcrt
326	print '(Hit any key to exit)'
327	while not msvcrt.kbhit():
328		pass
329
330
331def main(args):
332	jobs, options = parseOptions(args)
333	try:
334		process(jobs, options)
335	except KeyboardInterrupt:
336		print "(Cancelled.)"
337	except SystemExit:
338		if sys.platform == "win32":
339			waitForKeyPress()
340		else:
341			raise
342	except TTLibError, e:
343		print "Error:",e
344	except:
345		if sys.platform == "win32":
346			import traceback
347			traceback.print_exc()
348			waitForKeyPress()
349		else:
350			raise
351
352
353if __name__ == "__main__":
354	main(sys.argv[1:])
355