blob: 0ed719d4c667ca79332fce18fdcfabac6c51b156 [file] [log] [blame]
jvr1c803b62002-09-12 17:33:12 +00001"""\
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
pabs3ca75e432011-10-30 12:26:09 +000012 never overwritten.
jvr1c803b62002-09-12 17:33:12 +000013
14 General options:
15 -h Help: print this message
16 -d <outputfolder> Specify a directory where the output files are
17 to be created.
pabs35f419332013-06-22 06:47:34 +000018 -o <outputfile> Specify a file to write the output to.
jvr1c803b62002-09-12 17:33:12 +000019 -v Verbose: more messages will be written to stdout about what
20 is being done.
jvr823f8cd2006-10-21 14:12:38 +000021 -a allow virtual glyphs ID's on compile or decompile.
jvr1c803b62002-09-12 17:33:12 +000022
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.
Matt Fontaine7baa1362013-08-09 13:25:15 -070041 -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.
jvr1bcc11d2008-03-01 09:42:58 +000053 -e Don't ignore decompilation errors, but show a full traceback
54 and abort.
pabs30a6dea02009-11-08 15:53:24 +000055 -y <number> Select font number for TrueType Collection,
pabs37e91e772009-02-22 08:55:00 +000056 starting from 0.
jvr1c803b62002-09-12 17:33:12 +000057
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.
pabs3ca75e432011-10-30 12:26:09 +000062 -b Don't recalc glyph bounding boxes: use the values in the TTX
jvr1c803b62002-09-12 17:33:12 +000063 file as-is.
64"""
65
66
67import sys
68import os
69import getopt
70import re
71from fontTools.ttLib import TTFont
jvr823f8cd2006-10-21 14:12:38 +000072from fontTools.ttLib.tables.otBase import OTLOffsetOverflowError
73from fontTools.ttLib.tables.otTables import fixLookupOverFlows, fixSubTableOverFlows
jvr45d1f3b2008-03-01 11:34:54 +000074from fontTools.misc.macCreatorType import getMacCreatorAndType
jvr1c803b62002-09-12 17:33:12 +000075from fontTools import version
76
77def usage():
78 print __doc__ % version
79 sys.exit(2)
80
jvr2e838ce2003-08-22 18:50:44 +000081
jvr1c803b62002-09-12 17:33:12 +000082numberAddedRE = re.compile("(.*)#\d+$")
pabs3278d4d82013-06-22 08:16:33 +000083opentypeheaderRE = re.compile('''sfntVersion=['"]OTTO["']''')
jvr1c803b62002-09-12 17:33:12 +000084
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
pabs3fb37a242013-06-22 06:43:01 +0000105 outputFile = None
jvr1c803b62002-09-12 17:33:12 +0000106 verbose = 0
107 splitTables = 0
108 disassembleInstructions = 1
109 mergeFile = None
110 recalcBBoxes = 1
jvr823f8cd2006-10-21 14:12:38 +0000111 allowVID = 0
jvr1bcc11d2008-03-01 09:42:58 +0000112 ignoreDecompileErrors = True
Matt Fontaine7baa1362013-08-09 13:25:15 -0700113 bitmapGlyphDataFormat = 'raw'
jvr1bcc11d2008-03-01 09:42:58 +0000114
jvr1c803b62002-09-12 17:33:12 +0000115 def __init__(self, rawOptions, numFiles):
116 self.onlyTables = []
117 self.skipTables = []
pabs37e91e772009-02-22 08:55:00 +0000118 self.fontNumber = -1
jvr1c803b62002-09-12 17:33:12 +0000119 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
pabs3fb37a242013-06-22 06:43:01 +0000129 elif option == "-o":
130 self.outputFile = value
jvr1c803b62002-09-12 17:33:12 +0000131 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
Matt Fontaine7baa1362013-08-09 13:25:15 -0700144 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
pabs37e91e772009-02-22 08:55:00 +0000150 elif option == "-y":
151 self.fontNumber = int(value)
jvr1c803b62002-09-12 17:33:12 +0000152 # compile options
153 elif option == "-m":
154 self.mergeFile = value
155 elif option == "-b":
156 self.recalcBBoxes = 0
jvr823f8cd2006-10-21 14:12:38 +0000157 elif option == "-a":
158 self.allowVID = 1
jvr1bcc11d2008-03-01 09:42:58 +0000159 elif option == "-e":
160 self.ignoreDecompileErrors = False
jvr1c803b62002-09-12 17:33:12 +0000161 if self.onlyTables and self.skipTables:
jvr6588c4e2004-09-25 07:35:05 +0000162 print "-t and -x options are mutually exclusive"
jvr1c803b62002-09-12 17:33:12 +0000163 sys.exit(2)
164 if self.mergeFile and numFiles > 1:
jvr6588c4e2004-09-25 07:35:05 +0000165 print "Must specify exactly one TTX source file when using -m"
jvr1c803b62002-09-12 17:33:12 +0000166 sys.exit(2)
167
168
169def ttList(input, output, options):
jvrf7f0f742002-09-14 15:31:26 +0000170 import string
pabs37e91e772009-02-22 08:55:00 +0000171 ttf = TTFont(input, fontNumber=options.fontNumber)
jvr1c803b62002-09-12 17:33:12 +0000172 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]
jvre0912bb2004-12-24 15:59:35 +0000181 checkSum = long(entry.checkSum)
182 if checkSum < 0:
183 checkSum = checkSum + 0x100000000L
184 checksum = "0x" + string.zfill(hex(checkSum)[2:-1], 8)
jvr1c803b62002-09-12 17:33:12 +0000185 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)
jvr1bcc11d2008-03-01 09:42:58 +0000192 ttf = TTFont(input, 0, verbose=options.verbose, allowVID=options.allowVID,
pabs37e91e772009-02-22 08:55:00 +0000193 ignoreDecompileErrors=options.ignoreDecompileErrors,
194 fontNumber=options.fontNumber)
jvr1c803b62002-09-12 17:33:12 +0000195 ttf.saveXML(output,
196 tables=options.onlyTables,
197 skipTables=options.skipTables,
198 splitTables=options.splitTables,
Matt Fontaine7baa1362013-08-09 13:25:15 -0700199 disassembleInstructions=options.disassembleInstructions,
200 bitmapGlyphDataFormat=options.bitmapGlyphDataFormat)
jvr1c803b62002-09-12 17:33:12 +0000201 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,
jvr823f8cd2006-10-21 14:12:38 +0000208 verbose=options.verbose, allowVID=options.allowVID)
jvr1c803b62002-09-12 17:33:12 +0000209 ttf.importXML(input)
jvr823f8cd2006-10-21 14:12:38 +0000210 try:
211 ttf.save(output)
212 except OTLOffsetOverflowError, e:
jvr142506b2008-03-09 20:39:38 +0000213 # XXX This shouldn't be here at all, it should be as close to the
214 # OTL code as possible.
jvr823f8cd2006-10-21 14:12:38 +0000215 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
jvr1c803b62002-09-12 17:33:12 +0000235
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):
jvr2e838ce2003-08-22 18:50:44 +0000242 base, ext = os.path.splitext(fileName)
jvr1c803b62002-09-12 17:33:12 +0000243 try:
244 f = open(fileName, "rb")
245 except IOError:
246 return None
jvr45d1f3b2008-03-01 11:34:54 +0000247 cr, tp = getMacCreatorAndType(fileName)
248 if tp in ("sfnt", "FFIL"):
249 return "TTF"
250 if ext == ".dfont":
251 return "TTF"
jvr1c803b62002-09-12 17:33:12 +0000252 header = f.read(256)
253 head = header[:4]
254 if head == "OTTO":
255 return "OTF"
pabs37e91e772009-02-22 08:55:00 +0000256 elif head == "ttcf":
257 return "TTC"
jvr1c803b62002-09-12 17:33:12 +0000258 elif head in ("\0\1\0\0", "true"):
259 return "TTF"
260 elif head.lower() == "<?xm":
pabs3e83b4c42013-06-22 08:13:22 +0000261 if opentypeheaderRE.match(header):
jvr1c803b62002-09-12 17:33:12 +0000262 return "OTX"
263 else:
264 return "TTX"
jvr1c803b62002-09-12 17:33:12 +0000265 return None
266
267
268def parseOptions(args):
269 try:
Matt Fontaine7baa1362013-08-09 13:25:15 -0700270 rawOptions, files = getopt.getopt(args, "ld:o:vht:x:sim:z:baey:")
jvr1c803b62002-09-12 17:33:12 +0000271 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)
pabs37e91e772009-02-22 08:55:00 +0000282 if tp in ("OTF", "TTF", "TTC"):
jvr1c803b62002-09-12 17:33:12 +0000283 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
pabs3fb37a242013-06-22 06:43:01 +0000298 if options.outputFile:
299 output = options.outputFile
300 else:
301 output = makeOutputFileName(input, options.outputDir, extension)
jvr1c803b62002-09-12 17:33:12 +0000302 jobs.append((action, input, output))
jvr2921bb22002-09-12 20:05:23 +0000303 return jobs, options
jvr1c803b62002-09-12 17:33:12 +0000304
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:])