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())
|