xref: /aosp_15_r20/external/fonttools/Lib/fontTools/ttx.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1"""\
2usage: ttx [options] inputfile1 [... inputfileN]
3
4TTX -- From OpenType To XML And Back
5
6If an input file is a TrueType or OpenType font file, it will be
7decompiled to a TTX file (an XML-based text format).
8If an input file is a TTX file, it will be compiled to whatever
9format the data is in, a TrueType or OpenType/CFF font file.
10A special input value of - means read from the standard input.
11
12Output files are created so they are unique: an existing file is
13never overwritten.
14
15General options
16===============
17
18-h Help            print this message.
19--version          show version and exit.
20-d <outputfolder>  Specify a directory where the output files are
21                   to be created.
22-o <outputfile>    Specify a file to write the output to. A special
23                   value of - would use the standard output.
24-f                 Overwrite existing output file(s), ie. don't append
25                   numbers.
26-v                 Verbose: more messages will be written to stdout
27                   about what is being done.
28-q                 Quiet: No messages will be written to stdout about
29                   what is being done.
30-a                 allow virtual glyphs ID's on compile or decompile.
31
32Dump options
33============
34
35-l           List table info: instead of dumping to a TTX file, list
36             some minimal info about each table.
37-t <table>   Specify a table to dump. Multiple -t options
38             are allowed. When no -t option is specified, all tables
39             will be dumped.
40-x <table>   Specify a table to exclude from the dump. Multiple
41             -x options are allowed. -t and -x are mutually exclusive.
42-s           Split tables: save the TTX data into separate TTX files per
43             table and write one small TTX file that contains references
44             to the individual table dumps. This file can be used as
45             input to ttx, as long as the table files are in the
46             same directory.
47-g           Split glyf table: Save the glyf data into separate TTX files
48             per glyph and write a small TTX for the glyf table which
49             contains references to the individual TTGlyph elements.
50             NOTE: specifying -g implies -s (no need for -s together
51             with -g)
52-i           Do NOT disassemble TT instructions: when this option is
53             given, all TrueType programs (glyph programs, the font
54             program and the pre-program) will be written to the TTX
55             file as hex data instead of assembly. This saves some time
56             and makes the TTX file smaller.
57-z <format>  Specify a bitmap data export option for EBDT:
58             {'raw', 'row', 'bitwise', 'extfile'} or for the CBDT:
59             {'raw', 'extfile'} Each option does one of the following:
60
61             -z raw
62               export the bitmap data as a hex dump
63             -z row
64               export each row as hex data
65             -z bitwise
66               export each row as binary in an ASCII art style
67             -z extfile
68               export the data as external files with XML references
69
70             If no export format is specified 'raw' format is used.
71-e           Don't ignore decompilation errors, but show a full traceback
72             and abort.
73-y <number>  Select font number for TrueType Collection (.ttc/.otc),
74             starting from 0.
75--unicodedata <UnicodeData.txt>
76             Use custom database file to write character names in the
77             comments of the cmap TTX output.
78--newline <value>
79             Control how line endings are written in the XML file. It
80             can be 'LF', 'CR', or 'CRLF'. If not specified, the
81             default platform-specific line endings are used.
82
83Compile options
84===============
85
86-m           Merge with TrueType-input-file: specify a TrueType or
87             OpenType font file to be merged with the TTX file. This
88             option is only valid when at most one TTX file is specified.
89-b           Don't recalc glyph bounding boxes: use the values in the
90             TTX file as-is.
91--recalc-timestamp
92             Set font 'modified' timestamp to current time.
93             By default, the modification time of the TTX file will be
94             used.
95--no-recalc-timestamp
96             Keep the original font 'modified' timestamp.
97--flavor <type>
98             Specify flavor of output font file. May be 'woff' or 'woff2'.
99             Note that WOFF2 requires the Brotli Python extension,
100             available at https://github.com/google/brotli
101--with-zopfli
102             Use Zopfli instead of Zlib to compress WOFF. The Python
103             extension is available at https://pypi.python.org/pypi/zopfli
104"""
105
106from fontTools.ttLib import TTFont, TTLibError
107from fontTools.misc.macCreatorType import getMacCreatorAndType
108from fontTools.unicode import setUnicodeData
109from fontTools.misc.textTools import Tag, tostr
110from fontTools.misc.timeTools import timestampSinceEpoch
111from fontTools.misc.loggingTools import Timer
112from fontTools.misc.cliTools import makeOutputFileName
113import os
114import sys
115import getopt
116import re
117import logging
118
119
120log = logging.getLogger("fontTools.ttx")
121
122opentypeheaderRE = re.compile("""sfntVersion=['"]OTTO["']""")
123
124
125class Options(object):
126    listTables = False
127    outputDir = None
128    outputFile = None
129    overWrite = False
130    verbose = False
131    quiet = False
132    splitTables = False
133    splitGlyphs = False
134    disassembleInstructions = True
135    mergeFile = None
136    recalcBBoxes = True
137    ignoreDecompileErrors = True
138    bitmapGlyphDataFormat = "raw"
139    unicodedata = None
140    newlinestr = "\n"
141    recalcTimestamp = None
142    flavor = None
143    useZopfli = False
144
145    def __init__(self, rawOptions, numFiles):
146        self.onlyTables = []
147        self.skipTables = []
148        self.fontNumber = -1
149        for option, value in rawOptions:
150            # general options
151            if option == "-h":
152                print(__doc__)
153                sys.exit(0)
154            elif option == "--version":
155                from fontTools import version
156
157                print(version)
158                sys.exit(0)
159            elif option == "-d":
160                if not os.path.isdir(value):
161                    raise getopt.GetoptError(
162                        "The -d option value must be an existing directory"
163                    )
164                self.outputDir = value
165            elif option == "-o":
166                self.outputFile = value
167            elif option == "-f":
168                self.overWrite = True
169            elif option == "-v":
170                self.verbose = True
171            elif option == "-q":
172                self.quiet = True
173            # dump options
174            elif option == "-l":
175                self.listTables = True
176            elif option == "-t":
177                # pad with space if table tag length is less than 4
178                value = value.ljust(4)
179                self.onlyTables.append(value)
180            elif option == "-x":
181                # pad with space if table tag length is less than 4
182                value = value.ljust(4)
183                self.skipTables.append(value)
184            elif option == "-s":
185                self.splitTables = True
186            elif option == "-g":
187                # -g implies (and forces) splitTables
188                self.splitGlyphs = True
189                self.splitTables = True
190            elif option == "-i":
191                self.disassembleInstructions = False
192            elif option == "-z":
193                validOptions = ("raw", "row", "bitwise", "extfile")
194                if value not in validOptions:
195                    raise getopt.GetoptError(
196                        "-z does not allow %s as a format. Use %s"
197                        % (option, validOptions)
198                    )
199                self.bitmapGlyphDataFormat = value
200            elif option == "-y":
201                self.fontNumber = int(value)
202            # compile options
203            elif option == "-m":
204                self.mergeFile = value
205            elif option == "-b":
206                self.recalcBBoxes = False
207            elif option == "-e":
208                self.ignoreDecompileErrors = False
209            elif option == "--unicodedata":
210                self.unicodedata = value
211            elif option == "--newline":
212                validOptions = ("LF", "CR", "CRLF")
213                if value == "LF":
214                    self.newlinestr = "\n"
215                elif value == "CR":
216                    self.newlinestr = "\r"
217                elif value == "CRLF":
218                    self.newlinestr = "\r\n"
219                else:
220                    raise getopt.GetoptError(
221                        "Invalid choice for --newline: %r (choose from %s)"
222                        % (value, ", ".join(map(repr, validOptions)))
223                    )
224            elif option == "--recalc-timestamp":
225                self.recalcTimestamp = True
226            elif option == "--no-recalc-timestamp":
227                self.recalcTimestamp = False
228            elif option == "--flavor":
229                self.flavor = value
230            elif option == "--with-zopfli":
231                self.useZopfli = True
232        if self.verbose and self.quiet:
233            raise getopt.GetoptError("-q and -v options are mutually exclusive")
234        if self.verbose:
235            self.logLevel = logging.DEBUG
236        elif self.quiet:
237            self.logLevel = logging.WARNING
238        else:
239            self.logLevel = logging.INFO
240        if self.mergeFile and self.flavor:
241            raise getopt.GetoptError("-m and --flavor options are mutually exclusive")
242        if self.onlyTables and self.skipTables:
243            raise getopt.GetoptError("-t and -x options are mutually exclusive")
244        if self.mergeFile and numFiles > 1:
245            raise getopt.GetoptError(
246                "Must specify exactly one TTX source file when using -m"
247            )
248        if self.flavor != "woff" and self.useZopfli:
249            raise getopt.GetoptError("--with-zopfli option requires --flavor 'woff'")
250
251
252def ttList(input, output, options):
253    ttf = TTFont(input, fontNumber=options.fontNumber, lazy=True)
254    reader = ttf.reader
255    tags = sorted(reader.keys())
256    print('Listing table info for "%s":' % input)
257    format = "    %4s  %10s  %8s  %8s"
258    print(format % ("tag ", "  checksum", "  length", "  offset"))
259    print(format % ("----", "----------", "--------", "--------"))
260    for tag in tags:
261        entry = reader.tables[tag]
262        if ttf.flavor == "woff2":
263            # WOFF2 doesn't store table checksums, so they must be calculated
264            from fontTools.ttLib.sfnt import calcChecksum
265
266            data = entry.loadData(reader.transformBuffer)
267            checkSum = calcChecksum(data)
268        else:
269            checkSum = int(entry.checkSum)
270        if checkSum < 0:
271            checkSum = checkSum + 0x100000000
272        checksum = "0x%08X" % checkSum
273        print(format % (tag, checksum, entry.length, entry.offset))
274    print()
275    ttf.close()
276
277
278@Timer(log, "Done dumping TTX in %(time).3f seconds")
279def ttDump(input, output, options):
280    input_name = input
281    if input == "-":
282        input, input_name = sys.stdin.buffer, sys.stdin.name
283    output_name = output
284    if output == "-":
285        output, output_name = sys.stdout, sys.stdout.name
286    log.info('Dumping "%s" to "%s"...', input_name, output_name)
287    if options.unicodedata:
288        setUnicodeData(options.unicodedata)
289    ttf = TTFont(
290        input,
291        0,
292        ignoreDecompileErrors=options.ignoreDecompileErrors,
293        fontNumber=options.fontNumber,
294    )
295    ttf.saveXML(
296        output,
297        tables=options.onlyTables,
298        skipTables=options.skipTables,
299        splitTables=options.splitTables,
300        splitGlyphs=options.splitGlyphs,
301        disassembleInstructions=options.disassembleInstructions,
302        bitmapGlyphDataFormat=options.bitmapGlyphDataFormat,
303        newlinestr=options.newlinestr,
304    )
305    ttf.close()
306
307
308@Timer(log, "Done compiling TTX in %(time).3f seconds")
309def ttCompile(input, output, options):
310    input_name = input
311    if input == "-":
312        input, input_name = sys.stdin, sys.stdin.name
313    output_name = output
314    if output == "-":
315        output, output_name = sys.stdout.buffer, sys.stdout.name
316    log.info('Compiling "%s" to "%s"...' % (input_name, output))
317    if options.useZopfli:
318        from fontTools.ttLib import sfnt
319
320        sfnt.USE_ZOPFLI = True
321    ttf = TTFont(
322        options.mergeFile,
323        flavor=options.flavor,
324        recalcBBoxes=options.recalcBBoxes,
325        recalcTimestamp=options.recalcTimestamp,
326    )
327    ttf.importXML(input)
328
329    if options.recalcTimestamp is None and "head" in ttf and input is not sys.stdin:
330        # use TTX file modification time for head "modified" timestamp
331        mtime = os.path.getmtime(input)
332        ttf["head"].modified = timestampSinceEpoch(mtime)
333
334    ttf.save(output)
335
336
337def guessFileType(fileName):
338    if fileName == "-":
339        header = sys.stdin.buffer.peek(256)
340        ext = ""
341    else:
342        base, ext = os.path.splitext(fileName)
343        try:
344            with open(fileName, "rb") as f:
345                header = f.read(256)
346        except IOError:
347            return None
348
349    if header.startswith(b"\xef\xbb\xbf<?xml"):
350        header = header.lstrip(b"\xef\xbb\xbf")
351    cr, tp = getMacCreatorAndType(fileName)
352    if tp in ("sfnt", "FFIL"):
353        return "TTF"
354    if ext == ".dfont":
355        return "TTF"
356    head = Tag(header[:4])
357    if head == "OTTO":
358        return "OTF"
359    elif head == "ttcf":
360        return "TTC"
361    elif head in ("\0\1\0\0", "true"):
362        return "TTF"
363    elif head == "wOFF":
364        return "WOFF"
365    elif head == "wOF2":
366        return "WOFF2"
367    elif head == "<?xm":
368        # Use 'latin1' because that can't fail.
369        header = tostr(header, "latin1")
370        if opentypeheaderRE.search(header):
371            return "OTX"
372        else:
373            return "TTX"
374    return None
375
376
377def parseOptions(args):
378    rawOptions, files = getopt.getopt(
379        args,
380        "ld:o:fvqht:x:sgim:z:baey:",
381        [
382            "unicodedata=",
383            "recalc-timestamp",
384            "no-recalc-timestamp",
385            "flavor=",
386            "version",
387            "with-zopfli",
388            "newline=",
389        ],
390    )
391
392    options = Options(rawOptions, len(files))
393    jobs = []
394
395    if not files:
396        raise getopt.GetoptError("Must specify at least one input file")
397
398    for input in files:
399        if input != "-" and not os.path.isfile(input):
400            raise getopt.GetoptError('File not found: "%s"' % input)
401        tp = guessFileType(input)
402        if tp in ("OTF", "TTF", "TTC", "WOFF", "WOFF2"):
403            extension = ".ttx"
404            if options.listTables:
405                action = ttList
406            else:
407                action = ttDump
408        elif tp == "TTX":
409            extension = "." + options.flavor if options.flavor else ".ttf"
410            action = ttCompile
411        elif tp == "OTX":
412            extension = "." + options.flavor if options.flavor else ".otf"
413            action = ttCompile
414        else:
415            raise getopt.GetoptError('Unknown file type: "%s"' % input)
416
417        if options.outputFile:
418            output = options.outputFile
419        else:
420            if input == "-":
421                raise getopt.GetoptError("Must provide -o when reading from stdin")
422            output = makeOutputFileName(
423                input, options.outputDir, extension, options.overWrite
424            )
425            # 'touch' output file to avoid race condition in choosing file names
426            if action != ttList:
427                open(output, "a").close()
428        jobs.append((action, input, output))
429    return jobs, options
430
431
432def process(jobs, options):
433    for action, input, output in jobs:
434        action(input, output, options)
435
436
437def main(args=None):
438    """Convert OpenType fonts to XML and back"""
439    from fontTools import configLogger
440
441    if args is None:
442        args = sys.argv[1:]
443    try:
444        jobs, options = parseOptions(args)
445    except getopt.GetoptError as e:
446        print("%s\nERROR: %s" % (__doc__, e), file=sys.stderr)
447        sys.exit(2)
448
449    configLogger(level=options.logLevel)
450
451    try:
452        process(jobs, options)
453    except KeyboardInterrupt:
454        log.error("(Cancelled.)")
455        sys.exit(1)
456    except SystemExit:
457        raise
458    except TTLibError as e:
459        log.error(e)
460        sys.exit(1)
461    except:
462        log.exception("Unhandled exception has occurred")
463        sys.exit(1)
464
465
466if __name__ == "__main__":
467    sys.exit(main())
468