• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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        inst = NamedInstance()
49        inst.nameID = AddName(font, name).nameID
50        inst.coordinates = {"wght": wght}
51        fvar.instances.append(inst)
52
53
54def AddName(font, name):
55    """(font, "Bold") --> NameRecord"""
56    nameTable = font.get("name")
57    namerec = NameRecord()
58    namerec.nameID = 1 + max([n.nameID for n in nameTable.names] + [256])
59    namerec.string = name.encode("mac_roman")
60    namerec.platformID, namerec.platEncID, namerec.langID = (1, 0, 0)
61    nameTable.names.append(namerec)
62    return namerec
63
64
65def AddGlyphVariations(font, thin, regular, black):
66    assert "gvar" not in font
67    gvar = font["gvar"] = table__g_v_a_r()
68    gvar.version = 1
69    gvar.reserved = 0
70    gvar.variations = {}
71    for glyphName in regular.getGlyphOrder():
72        regularCoord = GetCoordinates(regular, glyphName)
73        thinCoord = GetCoordinates(thin, glyphName)
74        blackCoord = GetCoordinates(black, glyphName)
75        if not regularCoord or not blackCoord or not thinCoord:
76            logging.warning("glyph %s not present in all input fonts",
77                            glyphName)
78            continue
79        if (len(regularCoord) != len(blackCoord) or
80            len(regularCoord) != len(thinCoord)):
81            logging.warning("glyph %s has not the same number of "
82                            "control points in all input fonts", glyphName)
83            continue
84        thinDelta = []
85        blackDelta = []
86        for ((regX, regY), (blackX, blackY), (thinX, thinY)) in \
87                zip(regularCoord, blackCoord, thinCoord):
88            thinDelta.append(((thinX - regX, thinY - regY)))
89            blackDelta.append((blackX - regX, blackY - regY))
90        thinVar = TupleVariation({"wght": (-1.0, -1.0, 0.0)}, thinDelta)
91        blackVar = TupleVariation({"wght": (0.0, 1.0, 1.0)}, blackDelta)
92        gvar.variations[glyphName] = [thinVar, blackVar]
93
94
95def GetCoordinates(font, glyphName):
96    """font, glyphName --> glyph coordinates as expected by "gvar" table
97
98    The result includes four "phantom points" for the glyph metrics,
99    as mandated by the "gvar" spec.
100    """
101    glyphTable = font["glyf"]
102    glyph = glyphTable.glyphs.get(glyphName)
103    if glyph is None:
104        return None
105    glyph.expand(glyphTable)
106    glyph.recalcBounds(glyphTable)
107    if glyph.isComposite():
108        coord = [c.getComponentInfo()[1][-2:] for c in glyph.components]
109    else:
110        coord = [c for c in glyph.getCoordinates(glyphTable)[0]]
111    # Add phantom points for (left, right, top, bottom) positions.
112    horizontalAdvanceWidth, leftSideBearing = font["hmtx"].metrics[glyphName]
113
114
115    leftSideX = glyph.xMin - leftSideBearing
116    rightSideX = leftSideX + horizontalAdvanceWidth
117
118    # XXX these are incorrect.  Load vmtx and fix.
119    topSideY = glyph.yMax
120    bottomSideY = -glyph.yMin
121
122    coord.extend([(leftSideX, 0),
123                  (rightSideX, 0),
124                  (0, topSideY),
125                  (0, bottomSideY)])
126    return coord
127
128
129def main():
130    logging.basicConfig(format="%(levelname)s: %(message)s")
131    thin = TTFont("/tmp/Roboto/Roboto-Thin.ttf")
132    regular = TTFont("/tmp/Roboto/Roboto-Regular.ttf")
133    black = TTFont("/tmp/Roboto/Roboto-Black.ttf")
134    out = regular
135    AddFontVariations(out)
136    AddGlyphVariations(out, thin, regular, black)
137    out.save("./Roboto.ttf")
138
139
140if __name__ == "__main__":
141    import sys
142    sys.exit(main())
143