aboutsummaryrefslogtreecommitdiff
path: root/Snippets/interpolate.py
blob: 063046c9cf2f62105de26855949db10c957ef267 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#! /usr/bin/env python3

# Illustrates how a fonttools script can construct variable fonts.
#
# This script reads Roboto-Thin.ttf, Roboto-Regular.ttf, and
# Roboto-Black.ttf from /tmp/Roboto, and writes a Multiple Master GX
# font named "Roboto.ttf" into the current working directory.
# This output font supports interpolation along the Weight axis,
# and it contains named instances for "Thin", "Light", "Regular",
# "Bold", and "Black".
#
# All input fonts must contain the same set of glyphs, and these glyphs
# need to have the same control points in the same order. Note that this
# is *not* the case for the normal Roboto fonts that can be downloaded
# from Google. This demo script prints a warning for any problematic
# glyphs; in the resulting font, these glyphs will not be interpolated
# and get rendered in the "Regular" weight.
#
# Usage:
# $ mkdir /tmp/Roboto && cp Roboto-*.ttf /tmp/Roboto
# $ ./interpolate.py && open Roboto.ttf


from fontTools.ttLib import TTFont
from fontTools.ttLib.tables._n_a_m_e import NameRecord
from fontTools.ttLib.tables._f_v_a_r import table__f_v_a_r, Axis, NamedInstance
from fontTools.ttLib.tables._g_v_a_r import table__g_v_a_r, TupleVariation
import logging


def AddFontVariations(font):
    assert "fvar" not in font
    fvar = font["fvar"] = table__f_v_a_r()

    weight = Axis()
    weight.axisTag = "wght"
    weight.nameID = AddName(font, "Weight").nameID
    weight.minValue, weight.defaultValue, weight.maxValue = (100, 400, 900)
    fvar.axes.append(weight)

    # https://www.microsoft.com/typography/otspec/os2.htm#wtc
    for name, wght in (
            ("Thin", 100),
            ("Light", 300),
            ("Regular", 400),
            ("Bold", 700),
            ("Black", 900)):
        inst = NamedInstance()
        inst.nameID = AddName(font, name).nameID
        inst.coordinates = {"wght": wght}
        fvar.instances.append(inst)


def AddName(font, name):
    """(font, "Bold") --> NameRecord"""
    nameTable = font.get("name")
    namerec = NameRecord()
    namerec.nameID = 1 + max([n.nameID for n in nameTable.names] + [256])
    namerec.string = name.encode("mac_roman")
    namerec.platformID, namerec.platEncID, namerec.langID = (1, 0, 0)
    nameTable.names.append(namerec)
    return namerec


def AddGlyphVariations(font, thin, regular, black):
    assert "gvar" not in font
    gvar = font["gvar"] = table__g_v_a_r()
    gvar.version = 1
    gvar.reserved = 0
    gvar.variations = {}
    for glyphName in regular.getGlyphOrder():
        regularCoord = GetCoordinates(regular, glyphName)
        thinCoord = GetCoordinates(thin, glyphName)
        blackCoord = GetCoordinates(black, glyphName)
        if not regularCoord or not blackCoord or not thinCoord:            
            logging.warning("glyph %s not present in all input fonts",
                            glyphName)
            continue
        if (len(regularCoord) != len(blackCoord) or
            len(regularCoord) != len(thinCoord)):
            logging.warning("glyph %s has not the same number of "
                            "control points in all input fonts", glyphName)
            continue
        thinDelta = []
        blackDelta = []
        for ((regX, regY), (blackX, blackY), (thinX, thinY)) in \
                zip(regularCoord, blackCoord, thinCoord):
            thinDelta.append(((thinX - regX, thinY - regY)))
            blackDelta.append((blackX - regX, blackY - regY))
        thinVar = TupleVariation({"wght": (-1.0, -1.0, 0.0)}, thinDelta)
        blackVar = TupleVariation({"wght": (0.0, 1.0, 1.0)}, blackDelta)
        gvar.variations[glyphName] = [thinVar, blackVar]


def GetCoordinates(font, glyphName):
    """font, glyphName --> glyph coordinates as expected by "gvar" table

    The result includes four "phantom points" for the glyph metrics,
    as mandated by the "gvar" spec.
    """
    glyphTable = font["glyf"]
    glyph = glyphTable.glyphs.get(glyphName)
    if glyph is None:
        return None
    glyph.expand(glyphTable)
    glyph.recalcBounds(glyphTable)
    if glyph.isComposite():
        coord = [c.getComponentInfo()[1][-2:] for c in glyph.components]
    else:
        coord = [c for c in glyph.getCoordinates(glyphTable)[0]]
    # Add phantom points for (left, right, top, bottom) positions.
    horizontalAdvanceWidth, leftSideBearing = font["hmtx"].metrics[glyphName]


    leftSideX = glyph.xMin - leftSideBearing
    rightSideX = leftSideX + horizontalAdvanceWidth

    # XXX these are incorrect.  Load vmtx and fix.
    topSideY = glyph.yMax
    bottomSideY = -glyph.yMin

    coord.extend([(leftSideX, 0),
                  (rightSideX, 0),
                  (0, topSideY),
                  (0, bottomSideY)])
    return coord


def main():
    logging.basicConfig(format="%(levelname)s: %(message)s")
    thin = TTFont("/tmp/Roboto/Roboto-Thin.ttf")
    regular = TTFont("/tmp/Roboto/Roboto-Regular.ttf")
    black = TTFont("/tmp/Roboto/Roboto-Black.ttf")
    out = regular
    AddFontVariations(out)
    AddGlyphVariations(out, thin, regular, black)
    out.save("./Roboto.ttf")


if __name__ == "__main__":
    import sys
    sys.exit(main())