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