From 97c7168749f14bc1774b51cc81b6d191f0c9fba3 Mon Sep 17 00:00:00 2001 From: Sascha Brawer Date: Fri, 26 Jun 2015 13:16:32 +0200 Subject: [PATCH 1/2] [GX] Code snippet for building interpolated fonts --- Snippets/interpolate.py | 140 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100755 Snippets/interpolate.py diff --git a/Snippets/interpolate.py b/Snippets/interpolate.py new file mode 100755 index 000000000..1190770e7 --- /dev/null +++ b/Snippets/interpolate.py @@ -0,0 +1,140 @@ +#! /usr/bin/env python + +# 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 +# at all (so they appear always in the "Regular" weight). +# +# TODO: Currently, the resulting font is structurally valid, and it +# looks fine for "Thin", "Regular", "Bold" and "Black". However, the +# "Light" instance appears identical to "Thin". Still need to figure out +# the reason for this. +# +# Usage: +# $ mkdir /tmp/Roboto && cp Roboto-*.ttf /tmp/Roboto +# $ ./interpolate.py && open Roboto.ttf + + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +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, GlyphVariation +import warnings + + +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 = (-0.3, 1.0, 1.7) + fvar.axes.append(weight) + + for name, wght in ( + ("Thin", -0.3), + ("Light", -0.7), + ("Regular", 1.0), + ("Bold", 1.3), + ("Black", 1.7)): + 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: + warnings.warn("glyph %s not present in all input fonts" % + glyphName) + continue + if (len(regularCoord) != len(blackCoord) or + len(regularCoord) != len(thinCoord)): + warnings.warn("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 = GlyphVariation({"wght": (-1.0, -1.0, 0.0)}, thinDelta) + blackVar = GlyphVariation({"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) side bearing. + horizontalAdvanceWidth, leftSideBearing = font["hmtx"].metrics[glyphName] + rightSideBearing = ( + horizontalAdvanceWidth - leftSideBearing - (glyph.xMax - glyph.xMin)) + # TODO: Not sure if top and bottom side bearing are correct. + topSideBearing = glyph.yMax + bottomSideBearing = -glyph.yMin + coord.extend([(leftSideBearing, 0), (rightSideBearing, 0), + (0, topSideBearing), (0, bottomSideBearing)]) + return coord + + +def main(): + 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__": + main() From ef98f8ac1292a58126ff1c43748937f021cd8f07 Mon Sep 17 00:00:00 2001 From: Sascha Brawer Date: Fri, 26 Jun 2015 21:14:12 +0200 Subject: [PATCH 2/2] Use the same range as Skia for the weight axis With this change, the "Thin" instance now renders fine, too. Apparently, the axis values need to be positive; this was not clear to me from reading the specification. --- Snippets/interpolate.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/Snippets/interpolate.py b/Snippets/interpolate.py index 1190770e7..53204c378 100755 --- a/Snippets/interpolate.py +++ b/Snippets/interpolate.py @@ -14,12 +14,7 @@ # 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 -# at all (so they appear always in the "Regular" weight). -# -# TODO: Currently, the resulting font is structurally valid, and it -# looks fine for "Thin", "Regular", "Bold" and "Black". However, the -# "Light" instance appears identical to "Thin". Still need to figure out -# the reason for this. +# and get rendered in the "Regular" weight. # # Usage: # $ mkdir /tmp/Roboto && cp Roboto-*.ttf /tmp/Roboto @@ -42,15 +37,15 @@ def AddFontVariations(font): weight = Axis() weight.axisTag = "wght" weight.nameID = AddName(font, "Weight").nameID - weight.minValue, weight.defaultValue, weight.maxValue = (-0.3, 1.0, 1.7) + weight.minValue, weight.defaultValue, weight.maxValue = (0.48, 1.0, 3.2) fvar.axes.append(weight) for name, wght in ( - ("Thin", -0.3), - ("Light", -0.7), + ("Thin", 0.48), + ("Light", 0.8), ("Regular", 1.0), - ("Bold", 1.3), - ("Black", 1.7)): + ("Bold", 1.95), + ("Black", 3.2)): inst = NamedInstance() inst.nameID = AddName(font, name).nameID inst.coordinates = {"wght": wght}