You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
143 lines
5.0 KiB
143 lines
5.0 KiB
#! /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())
|