blob: cd42084814368cad7c76806886451c58f2ee3ea7 [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
Behdad Esfahbodac1b4352013-11-27 04:15:34 -0500176 tags = sorted(reader.keys())
jvr1c803b62002-09-12 17:33:12 +0000177 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]
Behdad Esfahbod7cc6d272013-11-27 04:00:15 -0500183 checkSum = int(entry.checkSum)
jvre0912bb2004-12-24 15:59:35 +0000184 if checkSum < 0:
Behdad Esfahbodecbe4c82013-11-27 03:37:29 -0500185 checkSum = checkSum + 0x100000000
jvre0912bb2004-12-24 15:59:35 +0000186 checksum = "0x" + string.zfill(hex(checkSum)[2:-1], 8)
jvr1c803b62002-09-12 17:33:12 +0000187 print format % (tag, checksum, entry.length, entry.offset)
188 print
189 ttf.close()
190
191
192def ttDump(input, output, options):
Dave Crosslandb1585972013-09-04 13:16:39 +0100193 if not options.quiet:
194 print 'Dumping "%s" to "%s"...' % (input, output)
jvr1bcc11d2008-03-01 09:42:58 +0000195 ttf = TTFont(input, 0, verbose=options.verbose, allowVID=options.allowVID,
Behdad Esfahbod188f2a32013-11-24 19:04:25 -0500196 lazy=False,
Dave Crosslandd7efd562013-09-04 14:51:16 +0100197 quiet=options.quiet,
pabs37e91e772009-02-22 08:55:00 +0000198 ignoreDecompileErrors=options.ignoreDecompileErrors,
199 fontNumber=options.fontNumber)
jvr1c803b62002-09-12 17:33:12 +0000200 ttf.saveXML(output,
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500201 quiet=options.quiet,
jvr1c803b62002-09-12 17:33:12 +0000202 tables=options.onlyTables,
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500203 skipTables=options.skipTables,
jvr1c803b62002-09-12 17:33:12 +0000204 splitTables=options.splitTables,
Matt Fontaine7baa1362013-08-09 13:25:15 -0700205 disassembleInstructions=options.disassembleInstructions,
206 bitmapGlyphDataFormat=options.bitmapGlyphDataFormat)
jvr1c803b62002-09-12 17:33:12 +0000207 ttf.close()
208
209
210def ttCompile(input, output, options):
Dave Crossland85af40e2013-09-04 13:30:21 +0100211 if not options.quiet:
212 print 'Compiling "%s" to "%s"...' % (input, output)
jvr1c803b62002-09-12 17:33:12 +0000213 ttf = TTFont(options.mergeFile,
Behdad Esfahbod188f2a32013-11-24 19:04:25 -0500214 lazy=False,
jvr1c803b62002-09-12 17:33:12 +0000215 recalcBBoxes=options.recalcBBoxes,
jvr823f8cd2006-10-21 14:12:38 +0000216 verbose=options.verbose, allowVID=options.allowVID)
Dave Crossland85af40e2013-09-04 13:30:21 +0100217 ttf.importXML(input, quiet=options.quiet)
jvr823f8cd2006-10-21 14:12:38 +0000218 try:
219 ttf.save(output)
220 except OTLOffsetOverflowError, e:
jvr142506b2008-03-09 20:39:38 +0000221 # XXX This shouldn't be here at all, it should be as close to the
222 # OTL code as possible.
jvr823f8cd2006-10-21 14:12:38 +0000223 overflowRecord = e.value
224 print "Attempting to fix OTLOffsetOverflowError", e
225 lastItem = overflowRecord
Behdad Esfahbodac1b4352013-11-27 04:15:34 -0500226 while True:
jvr823f8cd2006-10-21 14:12:38 +0000227 ok = 0
228 if overflowRecord.itemName == None:
229 ok = fixLookupOverFlows(ttf, overflowRecord)
230 else:
231 ok = fixSubTableOverFlows(ttf, overflowRecord)
232 if not ok:
233 raise
234
235 try:
236 ttf.save(output)
237 break
238 except OTLOffsetOverflowError, e:
239 print "Attempting to fix OTLOffsetOverflowError", e
240 overflowRecord = e.value
241 if overflowRecord == lastItem:
242 raise
jvr1c803b62002-09-12 17:33:12 +0000243
244 if options.verbose:
245 import time
246 print "finished at", time.strftime("%H:%M:%S", time.localtime(time.time()))
247
248
249def guessFileType(fileName):
jvr2e838ce2003-08-22 18:50:44 +0000250 base, ext = os.path.splitext(fileName)
jvr1c803b62002-09-12 17:33:12 +0000251 try:
252 f = open(fileName, "rb")
253 except IOError:
254 return None
jvr45d1f3b2008-03-01 11:34:54 +0000255 cr, tp = getMacCreatorAndType(fileName)
256 if tp in ("sfnt", "FFIL"):
257 return "TTF"
258 if ext == ".dfont":
259 return "TTF"
jvr1c803b62002-09-12 17:33:12 +0000260 header = f.read(256)
261 head = header[:4]
262 if head == "OTTO":
263 return "OTF"
pabs37e91e772009-02-22 08:55:00 +0000264 elif head == "ttcf":
265 return "TTC"
jvr1c803b62002-09-12 17:33:12 +0000266 elif head in ("\0\1\0\0", "true"):
267 return "TTF"
Behdad Esfahbod8c352392013-11-26 12:58:28 -0500268 elif head in ("wOFF", "true"):
269 return "WOFF"
jvr1c803b62002-09-12 17:33:12 +0000270 elif head.lower() == "<?xm":
Behdad Esfahbodeac107f2013-11-01 00:43:06 +0000271 if opentypeheaderRE.search(header):
jvr1c803b62002-09-12 17:33:12 +0000272 return "OTX"
273 else:
274 return "TTX"
jvr1c803b62002-09-12 17:33:12 +0000275 return None
276
277
278def parseOptions(args):
279 try:
Dave Crossland5ffb91e2013-09-04 14:49:37 +0100280 rawOptions, files = getopt.getopt(args, "ld:o:vqht:x:sim:z:baey:")
jvr1c803b62002-09-12 17:33:12 +0000281 except getopt.GetoptError:
282 usage()
283
284 if not files:
285 usage()
286
287 options = Options(rawOptions, len(files))
288 jobs = []
289
290 for input in files:
291 tp = guessFileType(input)
Behdad Esfahbod8c352392013-11-26 12:58:28 -0500292 if tp in ("OTF", "TTF", "TTC", "WOFF"):
jvr1c803b62002-09-12 17:33:12 +0000293 extension = ".ttx"
294 if options.listTables:
295 action = ttList
296 else:
297 action = ttDump
298 elif tp == "TTX":
299 extension = ".ttf"
300 action = ttCompile
301 elif tp == "OTX":
302 extension = ".otf"
303 action = ttCompile
304 else:
305 print 'Unknown file type: "%s"' % input
306 continue
307
pabs3fb37a242013-06-22 06:43:01 +0000308 if options.outputFile:
309 output = options.outputFile
310 else:
311 output = makeOutputFileName(input, options.outputDir, extension)
jvr1c803b62002-09-12 17:33:12 +0000312 jobs.append((action, input, output))
jvr2921bb22002-09-12 20:05:23 +0000313 return jobs, options
jvr1c803b62002-09-12 17:33:12 +0000314
315
316def process(jobs, options):
317 for action, input, output in jobs:
318 action(input, output, options)
319
320
321def waitForKeyPress():
322 """Force the DOS Prompt window to stay open so the user gets
323 a chance to see what's wrong."""
324 import msvcrt
325 print '(Hit any key to exit)'
326 while not msvcrt.kbhit():
327 pass
328
329
330def main(args):
331 jobs, options = parseOptions(args)
332 try:
333 process(jobs, options)
334 except KeyboardInterrupt:
335 print "(Cancelled.)"
336 except SystemExit:
337 if sys.platform == "win32":
338 waitForKeyPress()
339 else:
340 raise
Olivier Berten70343cc2013-11-19 10:32:09 +0100341 except TTLibError, e:
342 print "Error:",e
jvr1c803b62002-09-12 17:33:12 +0000343 except:
344 if sys.platform == "win32":
345 import traceback
346 traceback.print_exc()
347 waitForKeyPress()
348 else:
349 raise
350
351
352if __name__ == "__main__":
353 main(sys.argv[1:])