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