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