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