xref: /aosp_15_r20/external/fonttools/Snippets/interpolate.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughes#! /usr/bin/env python3
2*e1fe3e4aSElliott Hughes
3*e1fe3e4aSElliott Hughes# Illustrates how a fonttools script can construct variable fonts.
4*e1fe3e4aSElliott Hughes#
5*e1fe3e4aSElliott Hughes# This script reads Roboto-Thin.ttf, Roboto-Regular.ttf, and
6*e1fe3e4aSElliott Hughes# Roboto-Black.ttf from /tmp/Roboto, and writes a Multiple Master GX
7*e1fe3e4aSElliott Hughes# font named "Roboto.ttf" into the current working directory.
8*e1fe3e4aSElliott Hughes# This output font supports interpolation along the Weight axis,
9*e1fe3e4aSElliott Hughes# and it contains named instances for "Thin", "Light", "Regular",
10*e1fe3e4aSElliott Hughes# "Bold", and "Black".
11*e1fe3e4aSElliott Hughes#
12*e1fe3e4aSElliott Hughes# All input fonts must contain the same set of glyphs, and these glyphs
13*e1fe3e4aSElliott Hughes# need to have the same control points in the same order. Note that this
14*e1fe3e4aSElliott Hughes# is *not* the case for the normal Roboto fonts that can be downloaded
15*e1fe3e4aSElliott Hughes# from Google. This demo script prints a warning for any problematic
16*e1fe3e4aSElliott Hughes# glyphs; in the resulting font, these glyphs will not be interpolated
17*e1fe3e4aSElliott Hughes# and get rendered in the "Regular" weight.
18*e1fe3e4aSElliott Hughes#
19*e1fe3e4aSElliott Hughes# Usage:
20*e1fe3e4aSElliott Hughes# $ mkdir /tmp/Roboto && cp Roboto-*.ttf /tmp/Roboto
21*e1fe3e4aSElliott Hughes# $ ./interpolate.py && open Roboto.ttf
22*e1fe3e4aSElliott Hughes
23*e1fe3e4aSElliott Hughes
24*e1fe3e4aSElliott Hughesfrom fontTools.ttLib import TTFont
25*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables._n_a_m_e import NameRecord
26*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables._f_v_a_r import table__f_v_a_r, Axis, NamedInstance
27*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables._g_v_a_r import table__g_v_a_r, TupleVariation
28*e1fe3e4aSElliott Hughesimport logging
29*e1fe3e4aSElliott Hughes
30*e1fe3e4aSElliott Hughes
31*e1fe3e4aSElliott Hughesdef AddFontVariations(font):
32*e1fe3e4aSElliott Hughes    assert "fvar" not in font
33*e1fe3e4aSElliott Hughes    fvar = font["fvar"] = table__f_v_a_r()
34*e1fe3e4aSElliott Hughes
35*e1fe3e4aSElliott Hughes    weight = Axis()
36*e1fe3e4aSElliott Hughes    weight.axisTag = "wght"
37*e1fe3e4aSElliott Hughes    weight.nameID = AddName(font, "Weight").nameID
38*e1fe3e4aSElliott Hughes    weight.minValue, weight.defaultValue, weight.maxValue = (100, 400, 900)
39*e1fe3e4aSElliott Hughes    fvar.axes.append(weight)
40*e1fe3e4aSElliott Hughes
41*e1fe3e4aSElliott Hughes    # https://www.microsoft.com/typography/otspec/os2.htm#wtc
42*e1fe3e4aSElliott Hughes    for name, wght in (
43*e1fe3e4aSElliott Hughes        ("Thin", 100),
44*e1fe3e4aSElliott Hughes        ("Light", 300),
45*e1fe3e4aSElliott Hughes        ("Regular", 400),
46*e1fe3e4aSElliott Hughes        ("Bold", 700),
47*e1fe3e4aSElliott Hughes        ("Black", 900),
48*e1fe3e4aSElliott Hughes    ):
49*e1fe3e4aSElliott Hughes        inst = NamedInstance()
50*e1fe3e4aSElliott Hughes        inst.nameID = AddName(font, name).nameID
51*e1fe3e4aSElliott Hughes        inst.coordinates = {"wght": wght}
52*e1fe3e4aSElliott Hughes        fvar.instances.append(inst)
53*e1fe3e4aSElliott Hughes
54*e1fe3e4aSElliott Hughes
55*e1fe3e4aSElliott Hughesdef AddName(font, name):
56*e1fe3e4aSElliott Hughes    """(font, "Bold") --> NameRecord"""
57*e1fe3e4aSElliott Hughes    nameTable = font.get("name")
58*e1fe3e4aSElliott Hughes    namerec = NameRecord()
59*e1fe3e4aSElliott Hughes    namerec.nameID = 1 + max([n.nameID for n in nameTable.names] + [256])
60*e1fe3e4aSElliott Hughes    namerec.string = name.encode("mac_roman")
61*e1fe3e4aSElliott Hughes    namerec.platformID, namerec.platEncID, namerec.langID = (1, 0, 0)
62*e1fe3e4aSElliott Hughes    nameTable.names.append(namerec)
63*e1fe3e4aSElliott Hughes    return namerec
64*e1fe3e4aSElliott Hughes
65*e1fe3e4aSElliott Hughes
66*e1fe3e4aSElliott Hughesdef AddGlyphVariations(font, thin, regular, black):
67*e1fe3e4aSElliott Hughes    assert "gvar" not in font
68*e1fe3e4aSElliott Hughes    gvar = font["gvar"] = table__g_v_a_r()
69*e1fe3e4aSElliott Hughes    gvar.version = 1
70*e1fe3e4aSElliott Hughes    gvar.reserved = 0
71*e1fe3e4aSElliott Hughes    gvar.variations = {}
72*e1fe3e4aSElliott Hughes    for glyphName in regular.getGlyphOrder():
73*e1fe3e4aSElliott Hughes        regularCoord = GetCoordinates(regular, glyphName)
74*e1fe3e4aSElliott Hughes        thinCoord = GetCoordinates(thin, glyphName)
75*e1fe3e4aSElliott Hughes        blackCoord = GetCoordinates(black, glyphName)
76*e1fe3e4aSElliott Hughes        if not regularCoord or not blackCoord or not thinCoord:
77*e1fe3e4aSElliott Hughes            logging.warning("glyph %s not present in all input fonts", glyphName)
78*e1fe3e4aSElliott Hughes            continue
79*e1fe3e4aSElliott Hughes        if len(regularCoord) != len(blackCoord) or len(regularCoord) != len(thinCoord):
80*e1fe3e4aSElliott Hughes            logging.warning(
81*e1fe3e4aSElliott Hughes                "glyph %s has not the same number of "
82*e1fe3e4aSElliott Hughes                "control points in all input fonts",
83*e1fe3e4aSElliott Hughes                glyphName,
84*e1fe3e4aSElliott Hughes            )
85*e1fe3e4aSElliott Hughes            continue
86*e1fe3e4aSElliott Hughes        thinDelta = []
87*e1fe3e4aSElliott Hughes        blackDelta = []
88*e1fe3e4aSElliott Hughes        for (regX, regY), (blackX, blackY), (thinX, thinY) in zip(
89*e1fe3e4aSElliott Hughes            regularCoord, blackCoord, thinCoord
90*e1fe3e4aSElliott Hughes        ):
91*e1fe3e4aSElliott Hughes            thinDelta.append(((thinX - regX, thinY - regY)))
92*e1fe3e4aSElliott Hughes            blackDelta.append((blackX - regX, blackY - regY))
93*e1fe3e4aSElliott Hughes        thinVar = TupleVariation({"wght": (-1.0, -1.0, 0.0)}, thinDelta)
94*e1fe3e4aSElliott Hughes        blackVar = TupleVariation({"wght": (0.0, 1.0, 1.0)}, blackDelta)
95*e1fe3e4aSElliott Hughes        gvar.variations[glyphName] = [thinVar, blackVar]
96*e1fe3e4aSElliott Hughes
97*e1fe3e4aSElliott Hughes
98*e1fe3e4aSElliott Hughesdef GetCoordinates(font, glyphName):
99*e1fe3e4aSElliott Hughes    """font, glyphName --> glyph coordinates as expected by "gvar" table
100*e1fe3e4aSElliott Hughes
101*e1fe3e4aSElliott Hughes    The result includes four "phantom points" for the glyph metrics,
102*e1fe3e4aSElliott Hughes    as mandated by the "gvar" spec.
103*e1fe3e4aSElliott Hughes    """
104*e1fe3e4aSElliott Hughes    glyphTable = font["glyf"]
105*e1fe3e4aSElliott Hughes    glyph = glyphTable.glyphs.get(glyphName)
106*e1fe3e4aSElliott Hughes    if glyph is None:
107*e1fe3e4aSElliott Hughes        return None
108*e1fe3e4aSElliott Hughes    glyph.expand(glyphTable)
109*e1fe3e4aSElliott Hughes    glyph.recalcBounds(glyphTable)
110*e1fe3e4aSElliott Hughes    if glyph.isComposite():
111*e1fe3e4aSElliott Hughes        coord = [c.getComponentInfo()[1][-2:] for c in glyph.components]
112*e1fe3e4aSElliott Hughes    else:
113*e1fe3e4aSElliott Hughes        coord = [c for c in glyph.getCoordinates(glyphTable)[0]]
114*e1fe3e4aSElliott Hughes    # Add phantom points for (left, right, top, bottom) positions.
115*e1fe3e4aSElliott Hughes    horizontalAdvanceWidth, leftSideBearing = font["hmtx"].metrics[glyphName]
116*e1fe3e4aSElliott Hughes
117*e1fe3e4aSElliott Hughes    leftSideX = glyph.xMin - leftSideBearing
118*e1fe3e4aSElliott Hughes    rightSideX = leftSideX + horizontalAdvanceWidth
119*e1fe3e4aSElliott Hughes
120*e1fe3e4aSElliott Hughes    # XXX these are incorrect.  Load vmtx and fix.
121*e1fe3e4aSElliott Hughes    topSideY = glyph.yMax
122*e1fe3e4aSElliott Hughes    bottomSideY = -glyph.yMin
123*e1fe3e4aSElliott Hughes
124*e1fe3e4aSElliott Hughes    coord.extend([(leftSideX, 0), (rightSideX, 0), (0, topSideY), (0, bottomSideY)])
125*e1fe3e4aSElliott Hughes    return coord
126*e1fe3e4aSElliott Hughes
127*e1fe3e4aSElliott Hughes
128*e1fe3e4aSElliott Hughesdef main():
129*e1fe3e4aSElliott Hughes    logging.basicConfig(format="%(levelname)s: %(message)s")
130*e1fe3e4aSElliott Hughes    thin = TTFont("/tmp/Roboto/Roboto-Thin.ttf")
131*e1fe3e4aSElliott Hughes    regular = TTFont("/tmp/Roboto/Roboto-Regular.ttf")
132*e1fe3e4aSElliott Hughes    black = TTFont("/tmp/Roboto/Roboto-Black.ttf")
133*e1fe3e4aSElliott Hughes    out = regular
134*e1fe3e4aSElliott Hughes    AddFontVariations(out)
135*e1fe3e4aSElliott Hughes    AddGlyphVariations(out, thin, regular, black)
136*e1fe3e4aSElliott Hughes    out.save("./Roboto.ttf")
137*e1fe3e4aSElliott Hughes
138*e1fe3e4aSElliott Hughes
139*e1fe3e4aSElliott Hughesif __name__ == "__main__":
140*e1fe3e4aSElliott Hughes    import sys
141*e1fe3e4aSElliott Hughes
142*e1fe3e4aSElliott Hughes    sys.exit(main())
143