blob: 2e477efb2b9e49e0894b15c0a8d59423f34fb192 [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.
Dave Crosslandb1585972013-09-04 13:16:39 +010021 -q Quiet: No messages will be written to stdout about what
22 is being done.
jvr823f8cd2006-10-21 14:12:38 +000023 -a allow virtual glyphs ID's on compile or decompile.
jvr1c803b62002-09-12 17:33:12 +000024
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.
Matt Fontaine7baa1362013-08-09 13:25:15 -070043 -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.
jvr1bcc11d2008-03-01 09:42:58 +000055 -e Don't ignore decompilation errors, but show a full traceback
56 and abort.
pabs30a6dea02009-11-08 15:53:24 +000057 -y <number> Select font number for TrueType Collection,
pabs37e91e772009-02-22 08:55:00 +000058 starting from 0.
jvr1c803b62002-09-12 17:33:12 +000059
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.
pabs3ca75e432011-10-30 12:26:09 +000064 -b Don't recalc glyph bounding boxes: use the values in the TTX
jvr1c803b62002-09-12 17:33:12 +000065 file as-is.
66"""
67
68
69import sys
70import os
71import getopt
72import re
Olivier Berten70343cc2013-11-19 10:32:09 +010073from fontTools.ttLib import TTFont, TTLibError
jvr823f8cd2006-10-21 14:12:38 +000074from fontTools.ttLib.tables.otBase import OTLOffsetOverflowError
75from fontTools.ttLib.tables.otTables import fixLookupOverFlows, fixSubTableOverFlows
jvr45d1f3b2008-03-01 11:34:54 +000076from fontTools.misc.macCreatorType import getMacCreatorAndType
jvr1c803b62002-09-12 17:33:12 +000077from fontTools import version
78
79def usage():
80 print __doc__ % version
81 sys.exit(2)
82
jvr2e838ce2003-08-22 18:50:44 +000083
Behdad Esfahbodeac107f2013-11-01 00:43:06 +000084numberAddedRE = re.compile("#\d+$")
pabs3278d4d82013-06-22 08:16:33 +000085opentypeheaderRE = re.compile('''sfntVersion=['"]OTTO["']''')
jvr1c803b62002-09-12 17:33:12 +000086
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
Behdad Esfahbodd1072dd2013-11-14 20:27:07 -050092 file = numberAddedRE.split(file)[0]
Behdad Esfahbod271f25f2013-11-14 21:57:25 -050093 output = os.path.join(dir, file + extension)
jvr1c803b62002-09-12 17:33:12 +000094 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
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500103 listTables = False
jvr1c803b62002-09-12 17:33:12 +0000104 outputDir = None
pabs3fb37a242013-06-22 06:43:01 +0000105 outputFile = None
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500106 verbose = False
107 quiet = False
108 splitTables = False
109 disassembleInstructions = True
jvr1c803b62002-09-12 17:33:12 +0000110 mergeFile = None
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500111 recalcBBoxes = True
112 allowVID = False
jvr1bcc11d2008-03-01 09:42:58 +0000113 ignoreDecompileErrors = True
Matt Fontaine7baa1362013-08-09 13:25:15 -0700114 bitmapGlyphDataFormat = 'raw'
jvr1bcc11d2008-03-01 09:42:58 +0000115
jvr1c803b62002-09-12 17:33:12 +0000116 def __init__(self, rawOptions, numFiles):
117 self.onlyTables = []
118 self.skipTables = []
pabs37e91e772009-02-22 08:55:00 +0000119 self.fontNumber = -1
jvr1c803b62002-09-12 17:33:12 +0000120 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
pabs3fb37a242013-06-22 06:43:01 +0000130 elif option == "-o":
131 self.outputFile = value
jvr1c803b62002-09-12 17:33:12 +0000132 elif option == "-v":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500133 self.verbose = True
Dave Crosslandb1585972013-09-04 13:16:39 +0100134 elif option == "-q":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500135 self.quiet = True
jvr1c803b62002-09-12 17:33:12 +0000136 # dump options
137 elif option == "-l":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500138 self.listTables = True
jvr1c803b62002-09-12 17:33:12 +0000139 elif option == "-t":
140 self.onlyTables.append(value)
141 elif option == "-x":
142 self.skipTables.append(value)
143 elif option == "-s":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500144 self.splitTables = True
jvr1c803b62002-09-12 17:33:12 +0000145 elif option == "-i":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500146 self.disassembleInstructions = False
Matt Fontaine7baa1362013-08-09 13:25:15 -0700147 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
pabs37e91e772009-02-22 08:55:00 +0000153 elif option == "-y":
154 self.fontNumber = int(value)
jvr1c803b62002-09-12 17:33:12 +0000155 # compile options
156 elif option == "-m":
157 self.mergeFile = value
158 elif option == "-b":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500159 self.recalcBBoxes = False
jvr823f8cd2006-10-21 14:12:38 +0000160 elif option == "-a":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500161 self.allowVID = True
jvr1bcc11d2008-03-01 09:42:58 +0000162 elif option == "-e":
163 self.ignoreDecompileErrors = False
jvr1c803b62002-09-12 17:33:12 +0000164 if self.onlyTables and self.skipTables:
jvr6588c4e2004-09-25 07:35:05 +0000165 print "-t and -x options are mutually exclusive"
jvr1c803b62002-09-12 17:33:12 +0000166 sys.exit(2)
167 if self.mergeFile and numFiles > 1:
jvr6588c4e2004-09-25 07:35:05 +0000168 print "Must specify exactly one TTX source file when using -m"
jvr1c803b62002-09-12 17:33:12 +0000169 sys.exit(2)
170
171
172def ttList(input, output, options):
jvrf7f0f742002-09-14 15:31:26 +0000173 import string
Behdad Esfahbod188f2a32013-11-24 19:04:25 -0500174 ttf = TTFont(input, fontNumber=options.fontNumber, lazy=True)
jvr1c803b62002-09-12 17:33:12 +0000175 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]
Behdad Esfahbod7cc6d272013-11-27 04:00:15 -0500184 checkSum = int(entry.checkSum)
jvre0912bb2004-12-24 15:59:35 +0000185 if checkSum < 0:
Behdad Esfahbodecbe4c82013-11-27 03:37:29 -0500186 checkSum = checkSum + 0x100000000
jvre0912bb2004-12-24 15:59:35 +0000187 checksum = "0x" + string.zfill(hex(checkSum)[2:-1], 8)
jvr1c803b62002-09-12 17:33:12 +0000188 print format % (tag, checksum, entry.length, entry.offset)
189 print
190 ttf.close()
191
192
193def ttDump(input, output, options):
Dave Crosslandb1585972013-09-04 13:16:39 +0100194 if not options.quiet:
195 print 'Dumping "%s" to "%s"...' % (input, output)
jvr1bcc11d2008-03-01 09:42:58 +0000196 ttf = TTFont(input, 0, verbose=options.verbose, allowVID=options.allowVID,
Behdad Esfahbod188f2a32013-11-24 19:04:25 -0500197 lazy=False,
Dave Crosslandd7efd562013-09-04 14:51:16 +0100198 quiet=options.quiet,
pabs37e91e772009-02-22 08:55:00 +0000199 ignoreDecompileErrors=options.ignoreDecompileErrors,
200 fontNumber=options.fontNumber)
jvr1c803b62002-09-12 17:33:12 +0000201 ttf.saveXML(output,
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500202 quiet=options.quiet,
jvr1c803b62002-09-12 17:33:12 +0000203 tables=options.onlyTables,
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500204 skipTables=options.skipTables,
jvr1c803b62002-09-12 17:33:12 +0000205 splitTables=options.splitTables,
Matt Fontaine7baa1362013-08-09 13:25:15 -0700206 disassembleInstructions=options.disassembleInstructions,
207 bitmapGlyphDataFormat=options.bitmapGlyphDataFormat)
jvr1c803b62002-09-12 17:33:12 +0000208 ttf.close()
209
210
211def ttCompile(input, output, options):
Dave Crossland85af40e2013-09-04 13:30:21 +0100212 if not options.quiet:
213 print 'Compiling "%s" to "%s"...' % (input, output)
jvr1c803b62002-09-12 17:33:12 +0000214 ttf = TTFont(options.mergeFile,
Behdad Esfahbod188f2a32013-11-24 19:04:25 -0500215 lazy=False,
jvr1c803b62002-09-12 17:33:12 +0000216 recalcBBoxes=options.recalcBBoxes,
jvr823f8cd2006-10-21 14:12:38 +0000217 verbose=options.verbose, allowVID=options.allowVID)
Dave Crossland85af40e2013-09-04 13:30:21 +0100218 ttf.importXML(input, quiet=options.quiet)
jvr823f8cd2006-10-21 14:12:38 +0000219 try:
220 ttf.save(output)
221 except OTLOffsetOverflowError, e:
jvr142506b2008-03-09 20:39:38 +0000222 # XXX This shouldn't be here at all, it should be as close to the
223 # OTL code as possible.
jvr823f8cd2006-10-21 14:12:38 +0000224 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
jvr1c803b62002-09-12 17:33:12 +0000244
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):
jvr2e838ce2003-08-22 18:50:44 +0000251 base, ext = os.path.splitext(fileName)
jvr1c803b62002-09-12 17:33:12 +0000252 try:
253 f = open(fileName, "rb")
254 except IOError:
255 return None
jvr45d1f3b2008-03-01 11:34:54 +0000256 cr, tp = getMacCreatorAndType(fileName)
257 if tp in ("sfnt", "FFIL"):
258 return "TTF"
259 if ext == ".dfont":
260 return "TTF"
jvr1c803b62002-09-12 17:33:12 +0000261 header = f.read(256)
262 head = header[:4]
263 if head == "OTTO":
264 return "OTF"
pabs37e91e772009-02-22 08:55:00 +0000265 elif head == "ttcf":
266 return "TTC"
jvr1c803b62002-09-12 17:33:12 +0000267 elif head in ("\0\1\0\0", "true"):
268 return "TTF"
Behdad Esfahbod8c352392013-11-26 12:58:28 -0500269 elif head in ("wOFF", "true"):
270 return "WOFF"
jvr1c803b62002-09-12 17:33:12 +0000271 elif head.lower() == "<?xm":
Behdad Esfahbodeac107f2013-11-01 00:43:06 +0000272 if opentypeheaderRE.search(header):
jvr1c803b62002-09-12 17:33:12 +0000273 return "OTX"
274 else:
275 return "TTX"
jvr1c803b62002-09-12 17:33:12 +0000276 return None
277
278
279def parseOptions(args):
280 try:
Dave Crossland5ffb91e2013-09-04 14:49:37 +0100281 rawOptions, files = getopt.getopt(args, "ld:o:vqht:x:sim:z:baey:")
jvr1c803b62002-09-12 17:33:12 +0000282 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)
Behdad Esfahbod8c352392013-11-26 12:58:28 -0500293 if tp in ("OTF", "TTF", "TTC", "WOFF"):
jvr1c803b62002-09-12 17:33:12 +0000294 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
pabs3fb37a242013-06-22 06:43:01 +0000309 if options.outputFile:
310 output = options.outputFile
311 else:
312 output = makeOutputFileName(input, options.outputDir, extension)
jvr1c803b62002-09-12 17:33:12 +0000313 jobs.append((action, input, output))
jvr2921bb22002-09-12 20:05:23 +0000314 return jobs, options
jvr1c803b62002-09-12 17:33:12 +0000315
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
Olivier Berten70343cc2013-11-19 10:32:09 +0100342 except TTLibError, e:
343 print "Error:",e
jvr1c803b62002-09-12 17:33:12 +0000344 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:])