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