xref: /aosp_15_r20/external/fonttools/Snippets/otf2ttf.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1#!/usr/bin/env python3
2
3import argparse
4import logging
5import os
6import sys
7
8from fontTools.pens.cu2quPen import Cu2QuPen
9from fontTools import configLogger
10from fontTools.misc.cliTools import makeOutputFileName
11from fontTools.pens.ttGlyphPen import TTGlyphPen
12from fontTools.ttLib import TTFont, newTable
13
14
15log = logging.getLogger()
16
17# default approximation error, measured in UPEM
18MAX_ERR = 1.0
19
20# default 'post' table format
21POST_FORMAT = 2.0
22
23# assuming the input contours' direction is correctly set (counter-clockwise),
24# we just flip it to clockwise
25REVERSE_DIRECTION = True
26
27
28def glyphs_to_quadratic(glyphs, max_err=MAX_ERR, reverse_direction=REVERSE_DIRECTION):
29    quadGlyphs = {}
30    for gname in glyphs.keys():
31        glyph = glyphs[gname]
32        ttPen = TTGlyphPen(glyphs)
33        cu2quPen = Cu2QuPen(ttPen, max_err, reverse_direction=reverse_direction)
34        glyph.draw(cu2quPen)
35        quadGlyphs[gname] = ttPen.glyph()
36    return quadGlyphs
37
38
39def update_hmtx(ttFont, glyf):
40    hmtx = ttFont["hmtx"]
41    for glyphName, glyph in glyf.glyphs.items():
42        if hasattr(glyph, "xMin"):
43            hmtx[glyphName] = (hmtx[glyphName][0], glyph.xMin)
44
45
46def otf_to_ttf(ttFont, post_format=POST_FORMAT, **kwargs):
47    assert ttFont.sfntVersion == "OTTO"
48    assert "CFF " in ttFont
49
50    glyphOrder = ttFont.getGlyphOrder()
51
52    ttFont["loca"] = newTable("loca")
53    ttFont["glyf"] = glyf = newTable("glyf")
54    glyf.glyphOrder = glyphOrder
55    glyf.glyphs = glyphs_to_quadratic(ttFont.getGlyphSet(), **kwargs)
56    del ttFont["CFF "]
57    glyf.compile(ttFont)
58    update_hmtx(ttFont, glyf)
59
60    ttFont["maxp"] = maxp = newTable("maxp")
61    maxp.tableVersion = 0x00010000
62    maxp.maxZones = 1
63    maxp.maxTwilightPoints = 0
64    maxp.maxStorage = 0
65    maxp.maxFunctionDefs = 0
66    maxp.maxInstructionDefs = 0
67    maxp.maxStackElements = 0
68    maxp.maxSizeOfInstructions = 0
69    maxp.maxComponentElements = max(
70        len(g.components if hasattr(g, "components") else [])
71        for g in glyf.glyphs.values()
72    )
73    maxp.compile(ttFont)
74
75    post = ttFont["post"]
76    post.formatType = post_format
77    post.extraNames = []
78    post.mapping = {}
79    post.glyphOrder = glyphOrder
80    try:
81        post.compile(ttFont)
82    except OverflowError:
83        post.formatType = 3
84        log.warning("Dropping glyph names, they do not fit in 'post' table.")
85
86    ttFont.sfntVersion = "\000\001\000\000"
87
88
89def main(args=None):
90    configLogger(logger=log)
91
92    parser = argparse.ArgumentParser()
93    parser.add_argument("input", nargs="+", metavar="INPUT")
94    parser.add_argument("-o", "--output")
95    parser.add_argument("-e", "--max-error", type=float, default=MAX_ERR)
96    parser.add_argument("--post-format", type=float, default=POST_FORMAT)
97    parser.add_argument(
98        "--keep-direction", dest="reverse_direction", action="store_false"
99    )
100    parser.add_argument("--face-index", type=int, default=0)
101    parser.add_argument("--overwrite", action="store_true")
102    options = parser.parse_args(args)
103
104    if options.output and len(options.input) > 1:
105        if not os.path.isdir(options.output):
106            parser.error(
107                "-o/--output option must be a directory when "
108                "processing multiple fonts"
109            )
110
111    for path in options.input:
112        if options.output and not os.path.isdir(options.output):
113            output = options.output
114        else:
115            output = makeOutputFileName(
116                path,
117                outputDir=options.output,
118                extension=".ttf",
119                overWrite=options.overwrite,
120            )
121
122        font = TTFont(path, fontNumber=options.face_index)
123        otf_to_ttf(
124            font,
125            post_format=options.post_format,
126            max_err=options.max_error,
127            reverse_direction=options.reverse_direction,
128        )
129        font.save(output)
130
131
132if __name__ == "__main__":
133    sys.exit(main())
134