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