blob: 092e2ab2a4b6ddba78e6ad26baa24e607e3577e2 [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
Behdad Esfahbod7ed91ec2013-11-27 15:16:28 -050078from fontTools.misc.py23 import *
jvr1c803b62002-09-12 17:33:12 +000079
80def usage():
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -050081 print(__doc__ % version)
jvr1c803b62002-09-12 17:33:12 +000082 sys.exit(2)
83
jvr2e838ce2003-08-22 18:50:44 +000084
Behdad Esfahbodeac107f2013-11-01 00:43:06 +000085numberAddedRE = re.compile("#\d+$")
pabs3278d4d82013-06-22 08:16:33 +000086opentypeheaderRE = re.compile('''sfntVersion=['"]OTTO["']''')
jvr1c803b62002-09-12 17:33:12 +000087
88def makeOutputFileName(input, outputDir, extension):
89 dir, file = os.path.split(input)
90 file, ext = os.path.splitext(file)
91 if outputDir:
92 dir = outputDir
Behdad Esfahbodd1072dd2013-11-14 20:27:07 -050093 file = numberAddedRE.split(file)[0]
Behdad Esfahbod271f25f2013-11-14 21:57:25 -050094 output = os.path.join(dir, file + extension)
jvr1c803b62002-09-12 17:33:12 +000095 n = 1
96 while os.path.exists(output):
97 output = os.path.join(dir, file + "#" + repr(n) + extension)
98 n = n + 1
99 return output
100
101
102class Options:
103
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500104 listTables = False
jvr1c803b62002-09-12 17:33:12 +0000105 outputDir = None
pabs3fb37a242013-06-22 06:43:01 +0000106 outputFile = None
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500107 verbose = False
108 quiet = False
109 splitTables = False
110 disassembleInstructions = True
jvr1c803b62002-09-12 17:33:12 +0000111 mergeFile = None
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500112 recalcBBoxes = True
113 allowVID = False
jvr1bcc11d2008-03-01 09:42:58 +0000114 ignoreDecompileErrors = True
Matt Fontaine7baa1362013-08-09 13:25:15 -0700115 bitmapGlyphDataFormat = 'raw'
jvr1bcc11d2008-03-01 09:42:58 +0000116
jvr1c803b62002-09-12 17:33:12 +0000117 def __init__(self, rawOptions, numFiles):
118 self.onlyTables = []
119 self.skipTables = []
pabs37e91e772009-02-22 08:55:00 +0000120 self.fontNumber = -1
jvr1c803b62002-09-12 17:33:12 +0000121 for option, value in rawOptions:
122 # general options
123 if option == "-h":
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500124 print(__doc__ % version)
jvr1c803b62002-09-12 17:33:12 +0000125 sys.exit(0)
126 elif option == "-d":
127 if not os.path.isdir(value):
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500128 print("The -d option value must be an existing directory")
jvr1c803b62002-09-12 17:33:12 +0000129 sys.exit(2)
130 self.outputDir = value
pabs3fb37a242013-06-22 06:43:01 +0000131 elif option == "-o":
132 self.outputFile = value
jvr1c803b62002-09-12 17:33:12 +0000133 elif option == "-v":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500134 self.verbose = True
Dave Crosslandb1585972013-09-04 13:16:39 +0100135 elif option == "-q":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500136 self.quiet = True
jvr1c803b62002-09-12 17:33:12 +0000137 # dump options
138 elif option == "-l":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500139 self.listTables = True
jvr1c803b62002-09-12 17:33:12 +0000140 elif option == "-t":
141 self.onlyTables.append(value)
142 elif option == "-x":
143 self.skipTables.append(value)
144 elif option == "-s":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500145 self.splitTables = True
jvr1c803b62002-09-12 17:33:12 +0000146 elif option == "-i":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500147 self.disassembleInstructions = False
Matt Fontaine7baa1362013-08-09 13:25:15 -0700148 elif option == "-z":
149 validOptions = ('raw', 'row', 'bitwise', 'extfile')
150 if value not in validOptions:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500151 print("-z does not allow %s as a format. Use %s" % (option, validOptions))
Matt Fontaine7baa1362013-08-09 13:25:15 -0700152 sys.exit(2)
153 self.bitmapGlyphDataFormat = value
pabs37e91e772009-02-22 08:55:00 +0000154 elif option == "-y":
155 self.fontNumber = int(value)
jvr1c803b62002-09-12 17:33:12 +0000156 # compile options
157 elif option == "-m":
158 self.mergeFile = value
159 elif option == "-b":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500160 self.recalcBBoxes = False
jvr823f8cd2006-10-21 14:12:38 +0000161 elif option == "-a":
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500162 self.allowVID = True
jvr1bcc11d2008-03-01 09:42:58 +0000163 elif option == "-e":
164 self.ignoreDecompileErrors = False
jvr1c803b62002-09-12 17:33:12 +0000165 if self.onlyTables and self.skipTables:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500166 print("-t and -x options are mutually exclusive")
jvr1c803b62002-09-12 17:33:12 +0000167 sys.exit(2)
168 if self.mergeFile and numFiles > 1:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500169 print("Must specify exactly one TTX source file when using -m")
jvr1c803b62002-09-12 17:33:12 +0000170 sys.exit(2)
171
172
173def ttList(input, output, options):
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())
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500177 print('Listing table info for "%s":' % input)
jvr1c803b62002-09-12 17:33:12 +0000178 format = " %4s %10s %7s %7s"
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500179 print(format % ("tag ", " checksum", " length", " offset"))
180 print(format % ("----", "----------", "-------", "-------"))
jvr1c803b62002-09-12 17:33:12 +0000181 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
Behdad Esfahbod14fb0312013-11-27 05:47:34 -0500186 checksum = "0x%08X" % checkSum
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500187 print(format % (tag, checksum, entry.length, entry.offset))
188 print()
jvr1c803b62002-09-12 17:33:12 +0000189 ttf.close()
190
191
192def ttDump(input, output, options):
Dave Crosslandb1585972013-09-04 13:16:39 +0100193 if not options.quiet:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500194 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:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500212 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)
Behdad Esfahbod223273f2013-11-27 05:09:00 -0500220 except OTLOffsetOverflowError as 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
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500224 print("Attempting to fix OTLOffsetOverflowError", e)
jvr823f8cd2006-10-21 14:12:38 +0000225 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
Behdad Esfahbod223273f2013-11-27 05:09:00 -0500238 except OTLOffsetOverflowError as e:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500239 print("Attempting to fix OTLOffsetOverflowError", e)
jvr823f8cd2006-10-21 14:12:38 +0000240 overflowRecord = e.value
241 if overflowRecord == lastItem:
242 raise
jvr1c803b62002-09-12 17:33:12 +0000243
244 if options.verbose:
245 import time
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500246 print("finished at", time.strftime("%H:%M:%S", time.localtime(time.time())))
jvr1c803b62002-09-12 17:33:12 +0000247
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)
Behdad Esfahbodac4672e2013-11-27 16:44:53 -0500261 head = Tag(header[:4])
jvr1c803b62002-09-12 17:33:12 +0000262 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 Esfahbodac4672e2013-11-27 16:44:53 -0500268 elif head == "wOFF":
Behdad Esfahbod8c352392013-11-26 12:58:28 -0500269 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:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500305 print('Unknown file type: "%s"' % input)
jvr1c803b62002-09-12 17:33:12 +0000306 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
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500325 print('(Hit any key to exit)')
jvr1c803b62002-09-12 17:33:12 +0000326 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:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500335 print("(Cancelled.)")
jvr1c803b62002-09-12 17:33:12 +0000336 except SystemExit:
337 if sys.platform == "win32":
338 waitForKeyPress()
339 else:
340 raise
Behdad Esfahbod223273f2013-11-27 05:09:00 -0500341 except TTLibError as e:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500342 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:])