• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#! /usr/bin/env python
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 __future__ import print_function, division, absolute_import
25from fontTools.misc.py23 import *
26from fontTools.ttLib import TTFont
27from fontTools.ttLib.tables._n_a_m_e import NameRecord
28from fontTools.ttLib.tables._f_v_a_r import table__f_v_a_r, Axis, NamedInstance
29from fontTools.ttLib.tables._g_v_a_r import table__g_v_a_r, TupleVariation
30import logging
31
32
33def AddFontVariations(font):
34    assert "fvar" not in font
35    fvar = font["fvar"] = table__f_v_a_r()
36
37    weight = Axis()
38    weight.axisTag = "wght"
39    weight.nameID = AddName(font, "Weight").nameID
40    weight.minValue, weight.defaultValue, weight.maxValue = (100, 400, 900)
41    fvar.axes.append(weight)
42
43    # https://www.microsoft.com/typography/otspec/os2.htm#wtc
44    for name, wght in (
45            ("Thin", 100),
46            ("Light", 300),
47            ("Regular", 400),
48            ("Bold", 700),
49            ("Black", 900)):
50        inst = NamedInstance()
51        inst.nameID = AddName(font, name).nameID
52        inst.coordinates = {"wght": wght}
53        fvar.instances.append(inst)
54
55
56def AddName(font, name):
57    """(font, "Bold") --> NameRecord"""
58    nameTable = font.get("name")
59    namerec = NameRecord()
60    namerec.nameID = 1 + max([n.nameID for n in nameTable.names] + [256])
61    namerec.string = name.encode("mac_roman")
62    namerec.platformID, namerec.platEncID, namerec.langID = (1, 0, 0)
63    nameTable.names.append(namerec)
64    return namerec
65
66
67def AddGlyphVariations(font, thin, regular, black):
68    assert "gvar" not in font
69    gvar = font["gvar"] = table__g_v_a_r()
70    gvar.version = 1
71    gvar.reserved = 0
72    gvar.variations = {}
73    for glyphName in regular.getGlyphOrder():
74        regularCoord = GetCoordinates(regular, glyphName)
75        thinCoord = GetCoordinates(thin, glyphName)
76        blackCoord = GetCoordinates(black, glyphName)
77        if not regularCoord or not blackCoord or not thinCoord:
78            logging.warning("glyph %s not present in all input fonts",
79                            glyphName)
80            continue
81        if (len(regularCoord) != len(blackCoord) or
82            len(regularCoord) != len(thinCoord)):
83            logging.warning("glyph %s has not the same number of "
84                            "control points in all input fonts", glyphName)
85            continue
86        thinDelta = []
87        blackDelta = []
88        for ((regX, regY), (blackX, blackY), (thinX, thinY)) in \
89                zip(regularCoord, blackCoord, thinCoord):
90            thinDelta.append(((thinX - regX, thinY - regY)))
91            blackDelta.append((blackX - regX, blackY - regY))
92        thinVar = TupleVariation({"wght": (-1.0, -1.0, 0.0)}, thinDelta)
93        blackVar = TupleVariation({"wght": (0.0, 1.0, 1.0)}, blackDelta)
94        gvar.variations[glyphName] = [thinVar, blackVar]
95
96
97def GetCoordinates(font, glyphName):
98    """font, glyphName --> glyph coordinates as expected by "gvar" table
99
100    The result includes four "phantom points" for the glyph metrics,
101    as mandated by the "gvar" spec.
102    """
103    glyphTable = font["glyf"]
104    glyph = glyphTable.glyphs.get(glyphName)
105    if glyph is None:
106        return None
107    glyph.expand(glyphTable)
108    glyph.recalcBounds(glyphTable)
109    if glyph.isComposite():
110        coord = [c.getComponentInfo()[1][-2:] for c in glyph.components]
111    else:
112        coord = [c for c in glyph.getCoordinates(glyphTable)[0]]
113    # Add phantom points for (left, right, top, bottom) positions.
114    horizontalAdvanceWidth, leftSideBearing = font["hmtx"].metrics[glyphName]
115
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),
125                  (rightSideX, 0),
126                  (0, topSideY),
127                  (0, bottomSideY)])
128    return coord
129
130
131def main():
132    logging.basicConfig(format="%(levelname)s: %(message)s")
133    thin = TTFont("/tmp/Roboto/Roboto-Thin.ttf")
134    regular = TTFont("/tmp/Roboto/Roboto-Regular.ttf")
135    black = TTFont("/tmp/Roboto/Roboto-Black.ttf")
136    out = regular
137    AddFontVariations(out)
138    AddGlyphVariations(out, thin, regular, black)
139    out.save("./Roboto.ttf")
140
141
142if __name__ == "__main__":
143    import sys
144    sys.exit(main())
145