blob: ae3c115e72e558c3e148ec6267a8a71ef9635cd4 [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
Behdad Esfahbod32c10ee2013-11-27 17:46:17 -050069from __future__ import print_function, division
Behdad Esfahbod30e691e2013-11-27 17:27:45 -050070from fontTools.misc.py23 import *
Olivier Berten70343cc2013-11-19 10:32:09 +010071from fontTools.ttLib import TTFont, TTLibError
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
Behdad Esfahbod30e691e2013-11-27 17:27:45 -050076import os
77import sys
78import getopt
79import re
jvr1c803b62002-09-12 17:33:12 +000080
81def usage():
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -050082 print(__doc__ % version)
jvr1c803b62002-09-12 17:33:12 +000083 sys.exit(2)
84
jvr2e838ce2003-08-22 18:50:44 +000085
Behdad Esfahbodeac107f2013-11-01 00:43:06 +000086numberAddedRE = re.compile("#\d+$")
pabs3278d4d82013-06-22 08:16:33 +000087opentypeheaderRE = re.compile('''sfntVersion=['"]OTTO["']''')
jvr1c803b62002-09-12 17:33:12 +000088
89def makeOutputFileName(input, outputDir, extension):
90 dir, file = os.path.split(input)
91 file, ext = os.path.splitext(file)
92 if outputDir:
93 dir = outputDir
Behdad Esfahbodd1072dd2013-11-14 20:27:07 -050094 file = numberAddedRE.split(file)[0]
Behdad Esfahbod271f25f2013-11-14 21:57:25 -050095 output = os.path.join(dir, file + extension)
jvr1c803b62002-09-12 17:33:12 +000096 n = 1
97 while os.path.exists(output):
98 output = os.path.join(dir, file + "#" + repr(n) + extension)
99 n = n + 1
100 return output
101
102
Behdad Esfahbode388db52013-11-28 14:26:58 -0500103class Options(object):
jvr1c803b62002-09-12 17:33:12 +0000104
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500105 listTables = False
jvr1c803b62002-09-12 17:33:12 +0000106 outputDir = None
pabs3fb37a242013-06-22 06:43:01 +0000107 outputFile = None
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500108 verbose = False
109 quiet = False
110 splitTables = False
111 disassembleInstructions = True
jvr1c803b62002-09-12 17:33:12 +0000112 mergeFile = None
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500113 recalcBBoxes = True
114 allowVID = False
jvr1bcc11d2008-03-01 09:42:58 +0000115 ignoreDecompileErrors = True
Matt Fontaine7baa1362013-08-09 13:25:15 -0700116 bitmapGlyphDataFormat = 'raw'
jvr1bcc11d2008-03-01 09:42:58 +0000117
jvr1c803b62002-09-12 17:33:12 +0000118 def __init__(self, rawOptions, numFiles):
119 self.onlyTables = []
120 self.skipTables = []
pabs37e91e772009-02-22 08:55:00 +0000121 self.fontNumber = -1
jvr1c803b62002-09-12 17:33:12 +0000122 for option, value in rawOptions:
123 # general options
124 if option == "-h":
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500125 print(__doc__ % version)
jvr1c803b62002-09-12 17:33:12 +0000126 sys.exit(0)
127 elif option == "-d":
128 if not os.path.isdir(value):
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500129 print("The -d option value must be an existing directory")
jvr1c803b62002-09-12 17:33:12 +0000130 sys.exit(2)
131 self.outputDir = value
pabs3fb37a242013-06-22 06:43:01 +0000132 elif option == "-o":
133 self.outputFile = value
jvr1c803b62002-09-12 17:33:12 +0000134 elif option == "-v":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500135 self.verbose = True
Dave Crosslandb1585972013-09-04 13:16:39 +0100136 elif option == "-q":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500137 self.quiet = True
jvr1c803b62002-09-12 17:33:12 +0000138 # dump options
139 elif option == "-l":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500140 self.listTables = True
jvr1c803b62002-09-12 17:33:12 +0000141 elif option == "-t":
142 self.onlyTables.append(value)
143 elif option == "-x":
144 self.skipTables.append(value)
145 elif option == "-s":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500146 self.splitTables = True
jvr1c803b62002-09-12 17:33:12 +0000147 elif option == "-i":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500148 self.disassembleInstructions = False
Matt Fontaine7baa1362013-08-09 13:25:15 -0700149 elif option == "-z":
150 validOptions = ('raw', 'row', 'bitwise', 'extfile')
151 if value not in validOptions:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500152 print("-z does not allow %s as a format. Use %s" % (option, validOptions))
Matt Fontaine7baa1362013-08-09 13:25:15 -0700153 sys.exit(2)
154 self.bitmapGlyphDataFormat = value
pabs37e91e772009-02-22 08:55:00 +0000155 elif option == "-y":
156 self.fontNumber = int(value)
jvr1c803b62002-09-12 17:33:12 +0000157 # compile options
158 elif option == "-m":
159 self.mergeFile = value
160 elif option == "-b":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500161 self.recalcBBoxes = False
jvr823f8cd2006-10-21 14:12:38 +0000162 elif option == "-a":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500163 self.allowVID = True
jvr1bcc11d2008-03-01 09:42:58 +0000164 elif option == "-e":
165 self.ignoreDecompileErrors = False
jvr1c803b62002-09-12 17:33:12 +0000166 if self.onlyTables and self.skipTables:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500167 print("-t and -x options are mutually exclusive")
jvr1c803b62002-09-12 17:33:12 +0000168 sys.exit(2)
169 if self.mergeFile and numFiles > 1:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500170 print("Must specify exactly one TTX source file when using -m")
jvr1c803b62002-09-12 17:33:12 +0000171 sys.exit(2)
172
173
174def ttList(input, output, options):
Behdad Esfahbod188f2a32013-11-24 19:04:25 -0500175 ttf = TTFont(input, fontNumber=options.fontNumber, lazy=True)
jvr1c803b62002-09-12 17:33:12 +0000176 reader = ttf.reader
Behdad Esfahbodac1b4352013-11-27 04:15:34 -0500177 tags = sorted(reader.keys())
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500178 print('Listing table info for "%s":' % input)
jvr1c803b62002-09-12 17:33:12 +0000179 format = " %4s %10s %7s %7s"
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500180 print(format % ("tag ", " checksum", " length", " offset"))
181 print(format % ("----", "----------", "-------", "-------"))
jvr1c803b62002-09-12 17:33:12 +0000182 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
Behdad Esfahbod14fb0312013-11-27 05:47:34 -0500187 checksum = "0x%08X" % checkSum
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500188 print(format % (tag, checksum, entry.length, entry.offset))
189 print()
jvr1c803b62002-09-12 17:33:12 +0000190 ttf.close()
191
192
193def ttDump(input, output, options):
Dave Crosslandb1585972013-09-04 13:16:39 +0100194 if not options.quiet:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500195 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:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500213 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)
Behdad Esfahbod223273f2013-11-27 05:09:00 -0500221 except OTLOffsetOverflowError as 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
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500225 print("Attempting to fix OTLOffsetOverflowError", e)
jvr823f8cd2006-10-21 14:12:38 +0000226 lastItem = overflowRecord
Behdad Esfahbodac1b4352013-11-27 04:15:34 -0500227 while True:
jvr823f8cd2006-10-21 14:12:38 +0000228 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
Behdad Esfahbod223273f2013-11-27 05:09:00 -0500239 except OTLOffsetOverflowError as e:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500240 print("Attempting to fix OTLOffsetOverflowError", e)
jvr823f8cd2006-10-21 14:12:38 +0000241 overflowRecord = e.value
242 if overflowRecord == lastItem:
243 raise
jvr1c803b62002-09-12 17:33:12 +0000244
245 if options.verbose:
246 import time
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500247 print("finished at", time.strftime("%H:%M:%S", time.localtime(time.time())))
jvr1c803b62002-09-12 17:33:12 +0000248
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)
Behdad Esfahbodac4672e2013-11-27 16:44:53 -0500262 head = Tag(header[:4])
jvr1c803b62002-09-12 17:33:12 +0000263 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 Esfahbodac4672e2013-11-27 16:44:53 -0500269 elif head == "wOFF":
Behdad Esfahbod8c352392013-11-26 12:58:28 -0500270 return "WOFF"
jvr1c803b62002-09-12 17:33:12 +0000271 elif head.lower() == "<?xm":
Behdad Esfahbodc0762612013-11-28 06:46:59 -0500272 # Use 'latin1' because that can't fail.
273 header = tostr(header, 'latin1')
Behdad Esfahbodeac107f2013-11-01 00:43:06 +0000274 if opentypeheaderRE.search(header):
jvr1c803b62002-09-12 17:33:12 +0000275 return "OTX"
276 else:
277 return "TTX"
jvr1c803b62002-09-12 17:33:12 +0000278 return None
279
280
281def parseOptions(args):
282 try:
Dave Crossland5ffb91e2013-09-04 14:49:37 +0100283 rawOptions, files = getopt.getopt(args, "ld:o:vqht:x:sim:z:baey:")
jvr1c803b62002-09-12 17:33:12 +0000284 except getopt.GetoptError:
285 usage()
286
287 if not files:
288 usage()
289
290 options = Options(rawOptions, len(files))
291 jobs = []
292
293 for input in files:
294 tp = guessFileType(input)
Behdad Esfahbod8c352392013-11-26 12:58:28 -0500295 if tp in ("OTF", "TTF", "TTC", "WOFF"):
jvr1c803b62002-09-12 17:33:12 +0000296 extension = ".ttx"
297 if options.listTables:
298 action = ttList
299 else:
300 action = ttDump
301 elif tp == "TTX":
302 extension = ".ttf"
303 action = ttCompile
304 elif tp == "OTX":
305 extension = ".otf"
306 action = ttCompile
307 else:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500308 print('Unknown file type: "%s"' % input)
jvr1c803b62002-09-12 17:33:12 +0000309 continue
310
pabs3fb37a242013-06-22 06:43:01 +0000311 if options.outputFile:
312 output = options.outputFile
313 else:
314 output = makeOutputFileName(input, options.outputDir, extension)
jvr1c803b62002-09-12 17:33:12 +0000315 jobs.append((action, input, output))
jvr2921bb22002-09-12 20:05:23 +0000316 return jobs, options
jvr1c803b62002-09-12 17:33:12 +0000317
318
319def process(jobs, options):
320 for action, input, output in jobs:
321 action(input, output, options)
322
323
324def waitForKeyPress():
325 """Force the DOS Prompt window to stay open so the user gets
326 a chance to see what's wrong."""
327 import msvcrt
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500328 print('(Hit any key to exit)')
jvr1c803b62002-09-12 17:33:12 +0000329 while not msvcrt.kbhit():
330 pass
331
332
333def main(args):
334 jobs, options = parseOptions(args)
335 try:
336 process(jobs, options)
337 except KeyboardInterrupt:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500338 print("(Cancelled.)")
jvr1c803b62002-09-12 17:33:12 +0000339 except SystemExit:
340 if sys.platform == "win32":
341 waitForKeyPress()
342 else:
343 raise
Behdad Esfahbod223273f2013-11-27 05:09:00 -0500344 except TTLibError as e:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500345 print("Error:",e)
jvr1c803b62002-09-12 17:33:12 +0000346 except:
347 if sys.platform == "win32":
348 import traceback
349 traceback.print_exc()
350 waitForKeyPress()
351 else:
352 raise
353
354
355if __name__ == "__main__":
356 main(sys.argv[1:])