blob: e0b5eddc8d5112cd711cd6063070198af1805d60 [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 Esfahbod1ae29592014-01-14 15:07:50 +080069from __future__ import print_function, division, absolute_import
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
jvr45d1f3b2008-03-01 11:34:54 +000072from fontTools.misc.macCreatorType import getMacCreatorAndType
Behdad Esfahbod30e691e2013-11-27 17:27:45 -050073import os
74import sys
75import getopt
76import re
jvr1c803b62002-09-12 17:33:12 +000077
78def usage():
Behdad Esfahbod153ec402013-12-04 01:15:46 -050079 from fontTools import version
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -050080 print(__doc__ % version)
jvr1c803b62002-09-12 17:33:12 +000081 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):
Behdad Esfahbod153ec402013-12-04 01:15:46 -050088 dirName, fileName = os.path.split(input)
89 fileName, ext = os.path.splitext(fileName)
jvr1c803b62002-09-12 17:33:12 +000090 if outputDir:
Behdad Esfahbod153ec402013-12-04 01:15:46 -050091 dirName = outputDir
92 fileName = numberAddedRE.split(fileName)[0]
93 output = os.path.join(dirName, fileName + extension)
jvr1c803b62002-09-12 17:33:12 +000094 n = 1
95 while os.path.exists(output):
Behdad Esfahbod153ec402013-12-04 01:15:46 -050096 output = os.path.join(dirName, fileName + "#" + repr(n) + extension)
jvr1c803b62002-09-12 17:33:12 +000097 n = n + 1
98 return output
99
100
Behdad Esfahbode388db52013-11-28 14:26:58 -0500101class Options(object):
jvr1c803b62002-09-12 17:33:12 +0000102
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":
Behdad Esfahbod153ec402013-12-04 01:15:46 -0500123 from fontTools import version
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,
Dave Crosslandd7efd562013-09-04 14:51:16 +0100196 quiet=options.quiet,
pabs37e91e772009-02-22 08:55:00 +0000197 ignoreDecompileErrors=options.ignoreDecompileErrors,
198 fontNumber=options.fontNumber)
jvr1c803b62002-09-12 17:33:12 +0000199 ttf.saveXML(output,
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500200 quiet=options.quiet,
jvr1c803b62002-09-12 17:33:12 +0000201 tables=options.onlyTables,
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500202 skipTables=options.skipTables,
jvr1c803b62002-09-12 17:33:12 +0000203 splitTables=options.splitTables,
Matt Fontaine7baa1362013-08-09 13:25:15 -0700204 disassembleInstructions=options.disassembleInstructions,
205 bitmapGlyphDataFormat=options.bitmapGlyphDataFormat)
jvr1c803b62002-09-12 17:33:12 +0000206 ttf.close()
207
208
209def ttCompile(input, output, options):
Dave Crossland85af40e2013-09-04 13:30:21 +0100210 if not options.quiet:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500211 print('Compiling "%s" to "%s"...' % (input, output))
jvr1c803b62002-09-12 17:33:12 +0000212 ttf = TTFont(options.mergeFile,
213 recalcBBoxes=options.recalcBBoxes,
jvr823f8cd2006-10-21 14:12:38 +0000214 verbose=options.verbose, allowVID=options.allowVID)
Dave Crossland85af40e2013-09-04 13:30:21 +0100215 ttf.importXML(input, quiet=options.quiet)
Behdad Esfahbod95f795f2013-12-20 21:52:28 -0500216 ttf.save(output)
jvr1c803b62002-09-12 17:33:12 +0000217
218 if options.verbose:
219 import time
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500220 print("finished at", time.strftime("%H:%M:%S", time.localtime(time.time())))
jvr1c803b62002-09-12 17:33:12 +0000221
222
223def guessFileType(fileName):
jvr2e838ce2003-08-22 18:50:44 +0000224 base, ext = os.path.splitext(fileName)
jvr1c803b62002-09-12 17:33:12 +0000225 try:
226 f = open(fileName, "rb")
227 except IOError:
228 return None
jvr45d1f3b2008-03-01 11:34:54 +0000229 cr, tp = getMacCreatorAndType(fileName)
230 if tp in ("sfnt", "FFIL"):
231 return "TTF"
232 if ext == ".dfont":
233 return "TTF"
jvr1c803b62002-09-12 17:33:12 +0000234 header = f.read(256)
Behdad Esfahbodac4672e2013-11-27 16:44:53 -0500235 head = Tag(header[:4])
jvr1c803b62002-09-12 17:33:12 +0000236 if head == "OTTO":
237 return "OTF"
pabs37e91e772009-02-22 08:55:00 +0000238 elif head == "ttcf":
239 return "TTC"
jvr1c803b62002-09-12 17:33:12 +0000240 elif head in ("\0\1\0\0", "true"):
241 return "TTF"
Behdad Esfahbodac4672e2013-11-27 16:44:53 -0500242 elif head == "wOFF":
Behdad Esfahbod8c352392013-11-26 12:58:28 -0500243 return "WOFF"
jvr1c803b62002-09-12 17:33:12 +0000244 elif head.lower() == "<?xm":
Behdad Esfahbodc0762612013-11-28 06:46:59 -0500245 # Use 'latin1' because that can't fail.
246 header = tostr(header, 'latin1')
Behdad Esfahbodeac107f2013-11-01 00:43:06 +0000247 if opentypeheaderRE.search(header):
jvr1c803b62002-09-12 17:33:12 +0000248 return "OTX"
249 else:
250 return "TTX"
jvr1c803b62002-09-12 17:33:12 +0000251 return None
252
253
254def parseOptions(args):
255 try:
Dave Crossland5ffb91e2013-09-04 14:49:37 +0100256 rawOptions, files = getopt.getopt(args, "ld:o:vqht:x:sim:z:baey:")
jvr1c803b62002-09-12 17:33:12 +0000257 except getopt.GetoptError:
258 usage()
259
260 if not files:
261 usage()
262
263 options = Options(rawOptions, len(files))
264 jobs = []
265
266 for input in files:
267 tp = guessFileType(input)
Behdad Esfahbod8c352392013-11-26 12:58:28 -0500268 if tp in ("OTF", "TTF", "TTC", "WOFF"):
jvr1c803b62002-09-12 17:33:12 +0000269 extension = ".ttx"
270 if options.listTables:
271 action = ttList
272 else:
273 action = ttDump
274 elif tp == "TTX":
275 extension = ".ttf"
276 action = ttCompile
277 elif tp == "OTX":
278 extension = ".otf"
279 action = ttCompile
280 else:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500281 print('Unknown file type: "%s"' % input)
jvr1c803b62002-09-12 17:33:12 +0000282 continue
283
pabs3fb37a242013-06-22 06:43:01 +0000284 if options.outputFile:
285 output = options.outputFile
286 else:
287 output = makeOutputFileName(input, options.outputDir, extension)
jvr1c803b62002-09-12 17:33:12 +0000288 jobs.append((action, input, output))
jvr2921bb22002-09-12 20:05:23 +0000289 return jobs, options
jvr1c803b62002-09-12 17:33:12 +0000290
291
292def process(jobs, options):
293 for action, input, output in jobs:
294 action(input, output, options)
295
296
297def waitForKeyPress():
298 """Force the DOS Prompt window to stay open so the user gets
299 a chance to see what's wrong."""
300 import msvcrt
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500301 print('(Hit any key to exit)')
jvr1c803b62002-09-12 17:33:12 +0000302 while not msvcrt.kbhit():
303 pass
304
305
306def main(args):
307 jobs, options = parseOptions(args)
308 try:
309 process(jobs, options)
310 except KeyboardInterrupt:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500311 print("(Cancelled.)")
jvr1c803b62002-09-12 17:33:12 +0000312 except SystemExit:
313 if sys.platform == "win32":
314 waitForKeyPress()
315 else:
316 raise
Behdad Esfahbod223273f2013-11-27 05:09:00 -0500317 except TTLibError as e:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500318 print("Error:",e)
jvr1c803b62002-09-12 17:33:12 +0000319 except:
320 if sys.platform == "win32":
321 import traceback
322 traceback.print_exc()
323 waitForKeyPress()
324 else:
325 raise
326
327
328if __name__ == "__main__":
329 main(sys.argv[1:])