aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/varLib/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/varLib/__init__.py')
-rw-r--r--Lib/fontTools/varLib/__init__.py2346
1 files changed, 1310 insertions, 1036 deletions
diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py
index f1ca99ff..b130d5b2 100644
--- a/Lib/fontTools/varLib/__init__.py
+++ b/Lib/fontTools/varLib/__init__.py
@@ -21,10 +21,11 @@ API *will* change in near future.
from typing import List
from fontTools.misc.vector import Vector
from fontTools.misc.roundTools import noRound, otRound
+from fontTools.misc.fixedTools import floatToFixed as fl2fi
from fontTools.misc.textTools import Tag, tostr
from fontTools.ttLib import TTFont, newTable
from fontTools.ttLib.tables._f_v_a_r import Axis, NamedInstance
-from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
+from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates, dropImpliedOnCurvePoints
from fontTools.ttLib.tables.ttProgram import Program
from fontTools.ttLib.tables.TupleVariation import TupleVariation
from fontTools.ttLib.tables import otTables as ot
@@ -40,11 +41,12 @@ from fontTools.varLib.stat import buildVFStatTable
from fontTools.colorLib.builder import buildColrV1
from fontTools.colorLib.unbuilder import unbuildColrV1
from functools import partial
-from collections import OrderedDict, namedtuple
+from collections import OrderedDict, defaultdict, namedtuple
import os.path
import logging
from copy import deepcopy
from pprint import pformat
+from re import fullmatch
from .errors import VarLibError, VarLibValidationError
log = logging.getLogger("fontTools.varLib")
@@ -58,1121 +60,1393 @@ FEAVAR_FEATURETAG_LIB_KEY = "com.github.fonttools.varLib.featureVarsFeatureTag"
# Creation routines
#
+
def _add_fvar(font, axes, instances: List[InstanceDescriptor]):
- """
- Add 'fvar' table to font.
-
- axes is an ordered dictionary of DesignspaceAxis objects.
-
- instances is list of dictionary objects with 'location', 'stylename',
- and possibly 'postscriptfontname' entries.
- """
-
- assert axes
- assert isinstance(axes, OrderedDict)
-
- log.info("Generating fvar")
-
- fvar = newTable('fvar')
- nameTable = font['name']
-
- for a in axes.values():
- axis = Axis()
- axis.axisTag = Tag(a.tag)
- # TODO Skip axes that have no variation.
- axis.minValue, axis.defaultValue, axis.maxValue = a.minimum, a.default, a.maximum
- axis.axisNameID = nameTable.addMultilingualName(a.labelNames, font, minNameID=256)
- axis.flags = int(a.hidden)
- fvar.axes.append(axis)
-
- for instance in instances:
- # Filter out discrete axis locations
- coordinates = {name: value for name, value in instance.location.items() if name in axes}
-
- if "en" not in instance.localisedStyleName:
- if not instance.styleName:
- raise VarLibValidationError(
- f"Instance at location '{coordinates}' must have a default English "
- "style name ('stylename' attribute on the instance element or a "
- "stylename element with an 'xml:lang=\"en\"' attribute)."
- )
- localisedStyleName = dict(instance.localisedStyleName)
- localisedStyleName["en"] = tostr(instance.styleName)
- else:
- localisedStyleName = instance.localisedStyleName
-
- psname = instance.postScriptFontName
-
- inst = NamedInstance()
- inst.subfamilyNameID = nameTable.addMultilingualName(localisedStyleName)
- if psname is not None:
- psname = tostr(psname)
- inst.postscriptNameID = nameTable.addName(psname)
- inst.coordinates = {axes[k].tag:axes[k].map_backward(v) for k,v in coordinates.items()}
- #inst.coordinates = {axes[k].tag:v for k,v in coordinates.items()}
- fvar.instances.append(inst)
-
- assert "fvar" not in font
- font['fvar'] = fvar
-
- return fvar
-
-def _add_avar(font, axes):
- """
- Add 'avar' table to font.
-
- axes is an ordered dictionary of AxisDescriptor objects.
- """
-
- assert axes
- assert isinstance(axes, OrderedDict)
-
- log.info("Generating avar")
-
- avar = newTable('avar')
-
- interesting = False
- for axis in axes.values():
- # Currently, some rasterizers require that the default value maps
- # (-1 to -1, 0 to 0, and 1 to 1) be present for all the segment
- # maps, even when the default normalization mapping for the axis
- # was not modified.
- # https://github.com/googlei18n/fontmake/issues/295
- # https://github.com/fonttools/fonttools/issues/1011
- # TODO(anthrotype) revert this (and 19c4b37) when issue is fixed
- curve = avar.segments[axis.tag] = {-1.0: -1.0, 0.0: 0.0, 1.0: 1.0}
- if not axis.map:
- continue
-
- items = sorted(axis.map)
- keys = [item[0] for item in items]
- vals = [item[1] for item in items]
-
- # Current avar requirements. We don't have to enforce
- # these on the designer and can deduce some ourselves,
- # but for now just enforce them.
- if axis.minimum != min(keys):
- raise VarLibValidationError(
- f"Axis '{axis.name}': there must be a mapping for the axis minimum "
- f"value {axis.minimum} and it must be the lowest input mapping value."
- )
- if axis.maximum != max(keys):
- raise VarLibValidationError(
- f"Axis '{axis.name}': there must be a mapping for the axis maximum "
- f"value {axis.maximum} and it must be the highest input mapping value."
- )
- if axis.default not in keys:
- raise VarLibValidationError(
- f"Axis '{axis.name}': there must be a mapping for the axis default "
- f"value {axis.default}."
- )
- # No duplicate input values (output values can be >= their preceeding value).
- if len(set(keys)) != len(keys):
- raise VarLibValidationError(
- f"Axis '{axis.name}': All axis mapping input='...' values must be "
- "unique, but we found duplicates."
- )
- # Ascending values
- if sorted(vals) != vals:
- raise VarLibValidationError(
- f"Axis '{axis.name}': mapping output values must be in ascending order."
- )
-
- keys_triple = (axis.minimum, axis.default, axis.maximum)
- vals_triple = tuple(axis.map_forward(v) for v in keys_triple)
-
- keys = [models.normalizeValue(v, keys_triple) for v in keys]
- vals = [models.normalizeValue(v, vals_triple) for v in vals]
-
- if all(k == v for k, v in zip(keys, vals)):
- continue
- interesting = True
-
- curve.update(zip(keys, vals))
-
- assert 0.0 in curve and curve[0.0] == 0.0
- assert -1.0 not in curve or curve[-1.0] == -1.0
- assert +1.0 not in curve or curve[+1.0] == +1.0
- # curve.update({-1.0: -1.0, 0.0: 0.0, 1.0: 1.0})
-
- assert "avar" not in font
- if not interesting:
- log.info("No need for avar")
- avar = None
- else:
- font['avar'] = avar
-
- return avar
+ """
+ Add 'fvar' table to font.
+
+ axes is an ordered dictionary of DesignspaceAxis objects.
+
+ instances is list of dictionary objects with 'location', 'stylename',
+ and possibly 'postscriptfontname' entries.
+ """
+
+ assert axes
+ assert isinstance(axes, OrderedDict)
+
+ log.info("Generating fvar")
+
+ fvar = newTable("fvar")
+ nameTable = font["name"]
+
+ for a in axes.values():
+ axis = Axis()
+ axis.axisTag = Tag(a.tag)
+ # TODO Skip axes that have no variation.
+ axis.minValue, axis.defaultValue, axis.maxValue = (
+ a.minimum,
+ a.default,
+ a.maximum,
+ )
+ axis.axisNameID = nameTable.addMultilingualName(
+ a.labelNames, font, minNameID=256
+ )
+ axis.flags = int(a.hidden)
+ fvar.axes.append(axis)
+
+ for instance in instances:
+ # Filter out discrete axis locations
+ coordinates = {
+ name: value for name, value in instance.location.items() if name in axes
+ }
+
+ if "en" not in instance.localisedStyleName:
+ if not instance.styleName:
+ raise VarLibValidationError(
+ f"Instance at location '{coordinates}' must have a default English "
+ "style name ('stylename' attribute on the instance element or a "
+ "stylename element with an 'xml:lang=\"en\"' attribute)."
+ )
+ localisedStyleName = dict(instance.localisedStyleName)
+ localisedStyleName["en"] = tostr(instance.styleName)
+ else:
+ localisedStyleName = instance.localisedStyleName
+
+ psname = instance.postScriptFontName
+
+ inst = NamedInstance()
+ inst.subfamilyNameID = nameTable.addMultilingualName(localisedStyleName)
+ if psname is not None:
+ psname = tostr(psname)
+ inst.postscriptNameID = nameTable.addName(psname)
+ inst.coordinates = {
+ axes[k].tag: axes[k].map_backward(v) for k, v in coordinates.items()
+ }
+ # inst.coordinates = {axes[k].tag:v for k,v in coordinates.items()}
+ fvar.instances.append(inst)
+
+ assert "fvar" not in font
+ font["fvar"] = fvar
+
+ return fvar
+
+
+def _add_avar(font, axes, mappings, axisTags):
+ """
+ Add 'avar' table to font.
+
+ axes is an ordered dictionary of AxisDescriptor objects.
+ """
+
+ assert axes
+ assert isinstance(axes, OrderedDict)
+
+ log.info("Generating avar")
+
+ avar = newTable("avar")
+
+ interesting = False
+ vals_triples = {}
+ for axis in axes.values():
+ # Currently, some rasterizers require that the default value maps
+ # (-1 to -1, 0 to 0, and 1 to 1) be present for all the segment
+ # maps, even when the default normalization mapping for the axis
+ # was not modified.
+ # https://github.com/googlei18n/fontmake/issues/295
+ # https://github.com/fonttools/fonttools/issues/1011
+ # TODO(anthrotype) revert this (and 19c4b37) when issue is fixed
+ curve = avar.segments[axis.tag] = {-1.0: -1.0, 0.0: 0.0, 1.0: 1.0}
+
+ keys_triple = (axis.minimum, axis.default, axis.maximum)
+ vals_triple = tuple(axis.map_forward(v) for v in keys_triple)
+ vals_triples[axis.tag] = vals_triple
+
+ if not axis.map:
+ continue
+
+ items = sorted(axis.map)
+ keys = [item[0] for item in items]
+ vals = [item[1] for item in items]
+
+ # Current avar requirements. We don't have to enforce
+ # these on the designer and can deduce some ourselves,
+ # but for now just enforce them.
+ if axis.minimum != min(keys):
+ raise VarLibValidationError(
+ f"Axis '{axis.name}': there must be a mapping for the axis minimum "
+ f"value {axis.minimum} and it must be the lowest input mapping value."
+ )
+ if axis.maximum != max(keys):
+ raise VarLibValidationError(
+ f"Axis '{axis.name}': there must be a mapping for the axis maximum "
+ f"value {axis.maximum} and it must be the highest input mapping value."
+ )
+ if axis.default not in keys:
+ raise VarLibValidationError(
+ f"Axis '{axis.name}': there must be a mapping for the axis default "
+ f"value {axis.default}."
+ )
+ # No duplicate input values (output values can be >= their preceeding value).
+ if len(set(keys)) != len(keys):
+ raise VarLibValidationError(
+ f"Axis '{axis.name}': All axis mapping input='...' values must be "
+ "unique, but we found duplicates."
+ )
+ # Ascending values
+ if sorted(vals) != vals:
+ raise VarLibValidationError(
+ f"Axis '{axis.name}': mapping output values must be in ascending order."
+ )
+
+ keys = [models.normalizeValue(v, keys_triple) for v in keys]
+ vals = [models.normalizeValue(v, vals_triple) for v in vals]
+
+ if all(k == v for k, v in zip(keys, vals)):
+ continue
+ interesting = True
+
+ curve.update(zip(keys, vals))
+
+ assert 0.0 in curve and curve[0.0] == 0.0
+ assert -1.0 not in curve or curve[-1.0] == -1.0
+ assert +1.0 not in curve or curve[+1.0] == +1.0
+ # curve.update({-1.0: -1.0, 0.0: 0.0, 1.0: 1.0})
+
+ if mappings:
+ interesting = True
+
+ hiddenAxes = [axis for axis in axes.values() if axis.hidden]
+
+ inputLocations = [
+ {
+ axes[name].tag: models.normalizeValue(v, vals_triples[axes[name].tag])
+ for name, v in mapping.inputLocation.items()
+ }
+ for mapping in mappings
+ ]
+ outputLocations = [
+ {
+ axes[name].tag: models.normalizeValue(v, vals_triples[axes[name].tag])
+ for name, v in mapping.outputLocation.items()
+ }
+ for mapping in mappings
+ ]
+ assert len(inputLocations) == len(outputLocations)
+
+ # If base-master is missing, insert it at zero location.
+ if not any(all(v == 0 for k, v in loc.items()) for loc in inputLocations):
+ inputLocations.insert(0, {})
+ outputLocations.insert(0, {})
+
+ model = models.VariationModel(inputLocations, axisTags)
+ storeBuilder = varStore.OnlineVarStoreBuilder(axisTags)
+ storeBuilder.setModel(model)
+ varIdxes = {}
+ for tag in axisTags:
+ masterValues = []
+ for vo, vi in zip(outputLocations, inputLocations):
+ if tag not in vo:
+ masterValues.append(0)
+ continue
+ v = vo[tag] - vi.get(tag, 0)
+ masterValues.append(fl2fi(v, 14))
+ varIdxes[tag] = storeBuilder.storeMasters(masterValues)[1]
+
+ store = storeBuilder.finish()
+ optimized = store.optimize()
+ varIdxes = {axis: optimized[value] for axis, value in varIdxes.items()}
+
+ varIdxMap = builder.buildDeltaSetIndexMap(varIdxes[t] for t in axisTags)
+
+ avar.majorVersion = 2
+ avar.table = ot.avar()
+ avar.table.VarIdxMap = varIdxMap
+ avar.table.VarStore = store
+
+ assert "avar" not in font
+ if not interesting:
+ log.info("No need for avar")
+ avar = None
+ else:
+ font["avar"] = avar
+
+ return avar
+
def _add_stat(font):
- # Note: this function only gets called by old code that calls `build()`
- # directly. Newer code that wants to benefit from STAT data from the
- # designspace should call `build_many()`
+ # Note: this function only gets called by old code that calls `build()`
+ # directly. Newer code that wants to benefit from STAT data from the
+ # designspace should call `build_many()`
+
+ if "STAT" in font:
+ return
+
+ from ..otlLib.builder import buildStatTable
- if "STAT" in font:
- return
+ fvarTable = font["fvar"]
+ axes = [dict(tag=a.axisTag, name=a.axisNameID) for a in fvarTable.axes]
+ buildStatTable(font, axes)
- from ..otlLib.builder import buildStatTable
- fvarTable = font['fvar']
- axes = [dict(tag=a.axisTag, name=a.axisNameID) for a in fvarTable.axes]
- buildStatTable(font, axes)
-_MasterData = namedtuple('_MasterData', ['glyf', 'hMetrics', 'vMetrics'])
+_MasterData = namedtuple("_MasterData", ["glyf", "hMetrics", "vMetrics"])
+
def _add_gvar(font, masterModel, master_ttfs, tolerance=0.5, optimize=True):
- if tolerance < 0:
- raise ValueError("`tolerance` must be a positive number.")
-
- log.info("Generating gvar")
- assert "gvar" not in font
- gvar = font["gvar"] = newTable('gvar')
- glyf = font['glyf']
- defaultMasterIndex = masterModel.reverseMapping[0]
-
- master_datas = [_MasterData(m['glyf'],
- m['hmtx'].metrics,
- getattr(m.get('vmtx'), 'metrics', None))
- for m in master_ttfs]
-
- for glyph in font.getGlyphOrder():
- log.debug("building gvar for glyph '%s'", glyph)
- isComposite = glyf[glyph].isComposite()
-
- allData = [
- m.glyf._getCoordinatesAndControls(glyph, m.hMetrics, m.vMetrics)
- for m in master_datas
- ]
-
- if allData[defaultMasterIndex][1].numberOfContours != 0:
- # If the default master is not empty, interpret empty non-default masters
- # as missing glyphs from a sparse master
- allData = [
- d if d is not None and d[1].numberOfContours != 0 else None
- for d in allData
- ]
-
- model, allData = masterModel.getSubModel(allData)
-
- allCoords = [d[0] for d in allData]
- allControls = [d[1] for d in allData]
- control = allControls[0]
- if not models.allEqual(allControls):
- log.warning("glyph %s has incompatible masters; skipping" % glyph)
- continue
- del allControls
-
- # Update gvar
- gvar.variations[glyph] = []
- deltas = model.getDeltas(allCoords, round=partial(GlyphCoordinates.__round__, round=round))
- supports = model.supports
- assert len(deltas) == len(supports)
-
- # Prepare for IUP optimization
- origCoords = deltas[0]
- endPts = control.endPts
-
- for i,(delta,support) in enumerate(zip(deltas[1:], supports[1:])):
- if all(v == 0 for v in delta.array) and not isComposite:
- continue
- var = TupleVariation(support, delta)
- if optimize:
- delta_opt = iup_delta_optimize(delta, origCoords, endPts, tolerance=tolerance)
-
- if None in delta_opt:
- """In composite glyphs, there should be one 0 entry
- to make sure the gvar entry is written to the font.
-
- This is to work around an issue with macOS 10.14 and can be
- removed once the behaviour of macOS is changed.
-
- https://github.com/fonttools/fonttools/issues/1381
- """
- if all(d is None for d in delta_opt):
- delta_opt = [(0, 0)] + [None] * (len(delta_opt) - 1)
- # Use "optimized" version only if smaller...
- var_opt = TupleVariation(support, delta_opt)
-
- axis_tags = sorted(support.keys()) # Shouldn't matter that this is different from fvar...?
- tupleData, auxData = var.compile(axis_tags)
- unoptimized_len = len(tupleData) + len(auxData)
- tupleData, auxData = var_opt.compile(axis_tags)
- optimized_len = len(tupleData) + len(auxData)
-
- if optimized_len < unoptimized_len:
- var = var_opt
-
- gvar.variations[glyph].append(var)
+ if tolerance < 0:
+ raise ValueError("`tolerance` must be a positive number.")
+
+ log.info("Generating gvar")
+ assert "gvar" not in font
+ gvar = font["gvar"] = newTable("gvar")
+ glyf = font["glyf"]
+ defaultMasterIndex = masterModel.reverseMapping[0]
+
+ master_datas = [
+ _MasterData(
+ m["glyf"], m["hmtx"].metrics, getattr(m.get("vmtx"), "metrics", None)
+ )
+ for m in master_ttfs
+ ]
+
+ for glyph in font.getGlyphOrder():
+ log.debug("building gvar for glyph '%s'", glyph)
+ isComposite = glyf[glyph].isComposite()
+
+ allData = [
+ m.glyf._getCoordinatesAndControls(glyph, m.hMetrics, m.vMetrics)
+ for m in master_datas
+ ]
+
+ if allData[defaultMasterIndex][1].numberOfContours != 0:
+ # If the default master is not empty, interpret empty non-default masters
+ # as missing glyphs from a sparse master
+ allData = [
+ d if d is not None and d[1].numberOfContours != 0 else None
+ for d in allData
+ ]
+
+ model, allData = masterModel.getSubModel(allData)
+
+ allCoords = [d[0] for d in allData]
+ allControls = [d[1] for d in allData]
+ control = allControls[0]
+ if not models.allEqual(allControls):
+ log.warning("glyph %s has incompatible masters; skipping" % glyph)
+ continue
+ del allControls
+
+ # Update gvar
+ gvar.variations[glyph] = []
+ deltas = model.getDeltas(
+ allCoords, round=partial(GlyphCoordinates.__round__, round=round)
+ )
+ supports = model.supports
+ assert len(deltas) == len(supports)
+
+ # Prepare for IUP optimization
+ origCoords = deltas[0]
+ endPts = control.endPts
+
+ for i, (delta, support) in enumerate(zip(deltas[1:], supports[1:])):
+ if all(v == 0 for v in delta.array) and not isComposite:
+ continue
+ var = TupleVariation(support, delta)
+ if optimize:
+ delta_opt = iup_delta_optimize(
+ delta, origCoords, endPts, tolerance=tolerance
+ )
+
+ if None in delta_opt:
+ """In composite glyphs, there should be one 0 entry
+ to make sure the gvar entry is written to the font.
+
+ This is to work around an issue with macOS 10.14 and can be
+ removed once the behaviour of macOS is changed.
+
+ https://github.com/fonttools/fonttools/issues/1381
+ """
+ if all(d is None for d in delta_opt):
+ delta_opt = [(0, 0)] + [None] * (len(delta_opt) - 1)
+ # Use "optimized" version only if smaller...
+ var_opt = TupleVariation(support, delta_opt)
+
+ axis_tags = sorted(
+ support.keys()
+ ) # Shouldn't matter that this is different from fvar...?
+ tupleData, auxData = var.compile(axis_tags)
+ unoptimized_len = len(tupleData) + len(auxData)
+ tupleData, auxData = var_opt.compile(axis_tags)
+ optimized_len = len(tupleData) + len(auxData)
+
+ if optimized_len < unoptimized_len:
+ var = var_opt
+
+ gvar.variations[glyph].append(var)
def _remove_TTHinting(font):
- for tag in ("cvar", "cvt ", "fpgm", "prep"):
- if tag in font:
- del font[tag]
- maxp = font['maxp']
- for attr in ("maxTwilightPoints", "maxStorage", "maxFunctionDefs", "maxInstructionDefs", "maxStackElements", "maxSizeOfInstructions"):
- setattr(maxp, attr, 0)
- maxp.maxZones = 1
- font["glyf"].removeHinting()
- # TODO: Modify gasp table to deactivate gridfitting for all ranges?
+ for tag in ("cvar", "cvt ", "fpgm", "prep"):
+ if tag in font:
+ del font[tag]
+ maxp = font["maxp"]
+ for attr in (
+ "maxTwilightPoints",
+ "maxStorage",
+ "maxFunctionDefs",
+ "maxInstructionDefs",
+ "maxStackElements",
+ "maxSizeOfInstructions",
+ ):
+ setattr(maxp, attr, 0)
+ maxp.maxZones = 1
+ font["glyf"].removeHinting()
+ # TODO: Modify gasp table to deactivate gridfitting for all ranges?
+
def _merge_TTHinting(font, masterModel, master_ttfs):
+ log.info("Merging TT hinting")
+ assert "cvar" not in font
+
+ # Check that the existing hinting is compatible
+
+ # fpgm and prep table
+
+ for tag in ("fpgm", "prep"):
+ all_pgms = [m[tag].program for m in master_ttfs if tag in m]
+ if not all_pgms:
+ continue
+ font_pgm = getattr(font.get(tag), "program", None)
+ if any(pgm != font_pgm for pgm in all_pgms):
+ log.warning(
+ "Masters have incompatible %s tables, hinting is discarded." % tag
+ )
+ _remove_TTHinting(font)
+ return
+
+ # glyf table
+
+ font_glyf = font["glyf"]
+ master_glyfs = [m["glyf"] for m in master_ttfs]
+ for name, glyph in font_glyf.glyphs.items():
+ all_pgms = [getattr(glyf.get(name), "program", None) for glyf in master_glyfs]
+ if not any(all_pgms):
+ continue
+ glyph.expand(font_glyf)
+ font_pgm = getattr(glyph, "program", None)
+ if any(pgm != font_pgm for pgm in all_pgms if pgm):
+ log.warning(
+ "Masters have incompatible glyph programs in glyph '%s', hinting is discarded."
+ % name
+ )
+ # TODO Only drop hinting from this glyph.
+ _remove_TTHinting(font)
+ return
+
+ # cvt table
+
+ all_cvs = [Vector(m["cvt "].values) if "cvt " in m else None for m in master_ttfs]
+
+ nonNone_cvs = models.nonNone(all_cvs)
+ if not nonNone_cvs:
+ # There is no cvt table to make a cvar table from, we're done here.
+ return
+
+ if not models.allEqual(len(c) for c in nonNone_cvs):
+ log.warning("Masters have incompatible cvt tables, hinting is discarded.")
+ _remove_TTHinting(font)
+ return
+
+ variations = []
+ deltas, supports = masterModel.getDeltasAndSupports(
+ all_cvs, round=round
+ ) # builtin round calls into Vector.__round__, which uses builtin round as we like
+ for i, (delta, support) in enumerate(zip(deltas[1:], supports[1:])):
+ if all(v == 0 for v in delta):
+ continue
+ var = TupleVariation(support, delta)
+ variations.append(var)
+
+ # We can build the cvar table now.
+ if variations:
+ cvar = font["cvar"] = newTable("cvar")
+ cvar.version = 1
+ cvar.variations = variations
+
+
+_MetricsFields = namedtuple(
+ "_MetricsFields",
+ ["tableTag", "metricsTag", "sb1", "sb2", "advMapping", "vOrigMapping"],
+)
+
+HVAR_FIELDS = _MetricsFields(
+ tableTag="HVAR",
+ metricsTag="hmtx",
+ sb1="LsbMap",
+ sb2="RsbMap",
+ advMapping="AdvWidthMap",
+ vOrigMapping=None,
+)
+
+VVAR_FIELDS = _MetricsFields(
+ tableTag="VVAR",
+ metricsTag="vmtx",
+ sb1="TsbMap",
+ sb2="BsbMap",
+ advMapping="AdvHeightMap",
+ vOrigMapping="VOrgMap",
+)
- log.info("Merging TT hinting")
- assert "cvar" not in font
-
- # Check that the existing hinting is compatible
-
- # fpgm and prep table
-
- for tag in ("fpgm", "prep"):
- all_pgms = [m[tag].program for m in master_ttfs if tag in m]
- if not all_pgms:
- continue
- font_pgm = getattr(font.get(tag), 'program', None)
- if any(pgm != font_pgm for pgm in all_pgms):
- log.warning("Masters have incompatible %s tables, hinting is discarded." % tag)
- _remove_TTHinting(font)
- return
-
- # glyf table
-
- font_glyf = font['glyf']
- master_glyfs = [m['glyf'] for m in master_ttfs]
- for name, glyph in font_glyf.glyphs.items():
- all_pgms = [
- getattr(glyf.get(name), 'program', None)
- for glyf in master_glyfs
- ]
- if not any(all_pgms):
- continue
- glyph.expand(font_glyf)
- font_pgm = getattr(glyph, 'program', None)
- if any(pgm != font_pgm for pgm in all_pgms if pgm):
- log.warning("Masters have incompatible glyph programs in glyph '%s', hinting is discarded." % name)
- # TODO Only drop hinting from this glyph.
- _remove_TTHinting(font)
- return
-
- # cvt table
-
- all_cvs = [Vector(m["cvt "].values) if 'cvt ' in m else None
- for m in master_ttfs]
-
- nonNone_cvs = models.nonNone(all_cvs)
- if not nonNone_cvs:
- # There is no cvt table to make a cvar table from, we're done here.
- return
-
- if not models.allEqual(len(c) for c in nonNone_cvs):
- log.warning("Masters have incompatible cvt tables, hinting is discarded.")
- _remove_TTHinting(font)
- return
-
- variations = []
- deltas, supports = masterModel.getDeltasAndSupports(all_cvs, round=round) # builtin round calls into Vector.__round__, which uses builtin round as we like
- for i,(delta,support) in enumerate(zip(deltas[1:], supports[1:])):
- if all(v == 0 for v in delta):
- continue
- var = TupleVariation(support, delta)
- variations.append(var)
-
- # We can build the cvar table now.
- if variations:
- cvar = font["cvar"] = newTable('cvar')
- cvar.version = 1
- cvar.variations = variations
-
-
-_MetricsFields = namedtuple('_MetricsFields',
- ['tableTag', 'metricsTag', 'sb1', 'sb2', 'advMapping', 'vOrigMapping'])
-
-HVAR_FIELDS = _MetricsFields(tableTag='HVAR', metricsTag='hmtx', sb1='LsbMap',
- sb2='RsbMap', advMapping='AdvWidthMap', vOrigMapping=None)
-
-VVAR_FIELDS = _MetricsFields(tableTag='VVAR', metricsTag='vmtx', sb1='TsbMap',
- sb2='BsbMap', advMapping='AdvHeightMap', vOrigMapping='VOrgMap')
def _add_HVAR(font, masterModel, master_ttfs, axisTags):
- _add_VHVAR(font, masterModel, master_ttfs, axisTags, HVAR_FIELDS)
+ _add_VHVAR(font, masterModel, master_ttfs, axisTags, HVAR_FIELDS)
+
def _add_VVAR(font, masterModel, master_ttfs, axisTags):
- _add_VHVAR(font, masterModel, master_ttfs, axisTags, VVAR_FIELDS)
+ _add_VHVAR(font, masterModel, master_ttfs, axisTags, VVAR_FIELDS)
+
def _add_VHVAR(font, masterModel, master_ttfs, axisTags, tableFields):
+ tableTag = tableFields.tableTag
+ assert tableTag not in font
+ log.info("Generating " + tableTag)
+ VHVAR = newTable(tableTag)
+ tableClass = getattr(ot, tableTag)
+ vhvar = VHVAR.table = tableClass()
+ vhvar.Version = 0x00010000
+
+ glyphOrder = font.getGlyphOrder()
+
+ # Build list of source font advance widths for each glyph
+ metricsTag = tableFields.metricsTag
+ advMetricses = [m[metricsTag].metrics for m in master_ttfs]
+
+ # Build list of source font vertical origin coords for each glyph
+ if tableTag == "VVAR" and "VORG" in master_ttfs[0]:
+ vOrigMetricses = [m["VORG"].VOriginRecords for m in master_ttfs]
+ defaultYOrigs = [m["VORG"].defaultVertOriginY for m in master_ttfs]
+ vOrigMetricses = list(zip(vOrigMetricses, defaultYOrigs))
+ else:
+ vOrigMetricses = None
+
+ metricsStore, advanceMapping, vOrigMapping = _get_advance_metrics(
+ font,
+ masterModel,
+ master_ttfs,
+ axisTags,
+ glyphOrder,
+ advMetricses,
+ vOrigMetricses,
+ )
+
+ vhvar.VarStore = metricsStore
+ if advanceMapping is None:
+ setattr(vhvar, tableFields.advMapping, None)
+ else:
+ setattr(vhvar, tableFields.advMapping, advanceMapping)
+ if vOrigMapping is not None:
+ setattr(vhvar, tableFields.vOrigMapping, vOrigMapping)
+ setattr(vhvar, tableFields.sb1, None)
+ setattr(vhvar, tableFields.sb2, None)
+
+ font[tableTag] = VHVAR
+ return
+
+
+def _get_advance_metrics(
+ font,
+ masterModel,
+ master_ttfs,
+ axisTags,
+ glyphOrder,
+ advMetricses,
+ vOrigMetricses=None,
+):
+ vhAdvanceDeltasAndSupports = {}
+ vOrigDeltasAndSupports = {}
+ # HACK: we treat width 65535 as a sentinel value to signal that a glyph
+ # from a non-default master should not participate in computing {H,V}VAR,
+ # as if it were missing. Allows to variate other glyph-related data independently
+ # from glyph metrics
+ sparse_advance = 0xFFFF
+ for glyph in glyphOrder:
+ vhAdvances = [
+ metrics[glyph][0]
+ if glyph in metrics and metrics[glyph][0] != sparse_advance
+ else None
+ for metrics in advMetricses
+ ]
+ vhAdvanceDeltasAndSupports[glyph] = masterModel.getDeltasAndSupports(
+ vhAdvances, round=round
+ )
+
+ singleModel = models.allEqual(id(v[1]) for v in vhAdvanceDeltasAndSupports.values())
+
+ if vOrigMetricses:
+ singleModel = False
+ for glyph in glyphOrder:
+ # We need to supply a vOrigs tuple with non-None default values
+ # for each glyph. vOrigMetricses contains values only for those
+ # glyphs which have a non-default vOrig.
+ vOrigs = [
+ metrics[glyph] if glyph in metrics else defaultVOrig
+ for metrics, defaultVOrig in vOrigMetricses
+ ]
+ vOrigDeltasAndSupports[glyph] = masterModel.getDeltasAndSupports(
+ vOrigs, round=round
+ )
+
+ directStore = None
+ if singleModel:
+ # Build direct mapping
+ supports = next(iter(vhAdvanceDeltasAndSupports.values()))[1][1:]
+ varTupleList = builder.buildVarRegionList(supports, axisTags)
+ varTupleIndexes = list(range(len(supports)))
+ varData = builder.buildVarData(varTupleIndexes, [], optimize=False)
+ for glyphName in glyphOrder:
+ varData.addItem(vhAdvanceDeltasAndSupports[glyphName][0], round=noRound)
+ varData.optimize()
+ directStore = builder.buildVarStore(varTupleList, [varData])
+
+ # Build optimized indirect mapping
+ storeBuilder = varStore.OnlineVarStoreBuilder(axisTags)
+ advMapping = {}
+ for glyphName in glyphOrder:
+ deltas, supports = vhAdvanceDeltasAndSupports[glyphName]
+ storeBuilder.setSupports(supports)
+ advMapping[glyphName] = storeBuilder.storeDeltas(deltas, round=noRound)
+
+ if vOrigMetricses:
+ vOrigMap = {}
+ for glyphName in glyphOrder:
+ deltas, supports = vOrigDeltasAndSupports[glyphName]
+ storeBuilder.setSupports(supports)
+ vOrigMap[glyphName] = storeBuilder.storeDeltas(deltas, round=noRound)
+
+ indirectStore = storeBuilder.finish()
+ mapping2 = indirectStore.optimize(use_NO_VARIATION_INDEX=False)
+ advMapping = [mapping2[advMapping[g]] for g in glyphOrder]
+ advanceMapping = builder.buildVarIdxMap(advMapping, glyphOrder)
+
+ if vOrigMetricses:
+ vOrigMap = [mapping2[vOrigMap[g]] for g in glyphOrder]
+
+ useDirect = False
+ vOrigMapping = None
+ if directStore:
+ # Compile both, see which is more compact
+
+ writer = OTTableWriter()
+ directStore.compile(writer, font)
+ directSize = len(writer.getAllData())
+
+ writer = OTTableWriter()
+ indirectStore.compile(writer, font)
+ advanceMapping.compile(writer, font)
+ indirectSize = len(writer.getAllData())
+
+ useDirect = directSize < indirectSize
+
+ if useDirect:
+ metricsStore = directStore
+ advanceMapping = None
+ else:
+ metricsStore = indirectStore
+ if vOrigMetricses:
+ vOrigMapping = builder.buildVarIdxMap(vOrigMap, glyphOrder)
+
+ return metricsStore, advanceMapping, vOrigMapping
- tableTag = tableFields.tableTag
- assert tableTag not in font
- log.info("Generating " + tableTag)
- VHVAR = newTable(tableTag)
- tableClass = getattr(ot, tableTag)
- vhvar = VHVAR.table = tableClass()
- vhvar.Version = 0x00010000
-
- glyphOrder = font.getGlyphOrder()
-
- # Build list of source font advance widths for each glyph
- metricsTag = tableFields.metricsTag
- advMetricses = [m[metricsTag].metrics for m in master_ttfs]
-
- # Build list of source font vertical origin coords for each glyph
- if tableTag == 'VVAR' and 'VORG' in master_ttfs[0]:
- vOrigMetricses = [m['VORG'].VOriginRecords for m in master_ttfs]
- defaultYOrigs = [m['VORG'].defaultVertOriginY for m in master_ttfs]
- vOrigMetricses = list(zip(vOrigMetricses, defaultYOrigs))
- else:
- vOrigMetricses = None
-
- metricsStore, advanceMapping, vOrigMapping = _get_advance_metrics(font,
- masterModel, master_ttfs, axisTags, glyphOrder, advMetricses,
- vOrigMetricses)
-
- vhvar.VarStore = metricsStore
- if advanceMapping is None:
- setattr(vhvar, tableFields.advMapping, None)
- else:
- setattr(vhvar, tableFields.advMapping, advanceMapping)
- if vOrigMapping is not None:
- setattr(vhvar, tableFields.vOrigMapping, vOrigMapping)
- setattr(vhvar, tableFields.sb1, None)
- setattr(vhvar, tableFields.sb2, None)
-
- font[tableTag] = VHVAR
- return
-
-def _get_advance_metrics(font, masterModel, master_ttfs,
- axisTags, glyphOrder, advMetricses, vOrigMetricses=None):
-
- vhAdvanceDeltasAndSupports = {}
- vOrigDeltasAndSupports = {}
- for glyph in glyphOrder:
- vhAdvances = [metrics[glyph][0] if glyph in metrics else None for metrics in advMetricses]
- vhAdvanceDeltasAndSupports[glyph] = masterModel.getDeltasAndSupports(vhAdvances, round=round)
-
- singleModel = models.allEqual(id(v[1]) for v in vhAdvanceDeltasAndSupports.values())
-
- if vOrigMetricses:
- singleModel = False
- for glyph in glyphOrder:
- # We need to supply a vOrigs tuple with non-None default values
- # for each glyph. vOrigMetricses contains values only for those
- # glyphs which have a non-default vOrig.
- vOrigs = [metrics[glyph] if glyph in metrics else defaultVOrig
- for metrics, defaultVOrig in vOrigMetricses]
- vOrigDeltasAndSupports[glyph] = masterModel.getDeltasAndSupports(vOrigs, round=round)
-
- directStore = None
- if singleModel:
- # Build direct mapping
- supports = next(iter(vhAdvanceDeltasAndSupports.values()))[1][1:]
- varTupleList = builder.buildVarRegionList(supports, axisTags)
- varTupleIndexes = list(range(len(supports)))
- varData = builder.buildVarData(varTupleIndexes, [], optimize=False)
- for glyphName in glyphOrder:
- varData.addItem(vhAdvanceDeltasAndSupports[glyphName][0], round=noRound)
- varData.optimize()
- directStore = builder.buildVarStore(varTupleList, [varData])
-
- # Build optimized indirect mapping
- storeBuilder = varStore.OnlineVarStoreBuilder(axisTags)
- advMapping = {}
- for glyphName in glyphOrder:
- deltas, supports = vhAdvanceDeltasAndSupports[glyphName]
- storeBuilder.setSupports(supports)
- advMapping[glyphName] = storeBuilder.storeDeltas(deltas, round=noRound)
-
- if vOrigMetricses:
- vOrigMap = {}
- for glyphName in glyphOrder:
- deltas, supports = vOrigDeltasAndSupports[glyphName]
- storeBuilder.setSupports(supports)
- vOrigMap[glyphName] = storeBuilder.storeDeltas(deltas, round=noRound)
-
- indirectStore = storeBuilder.finish()
- mapping2 = indirectStore.optimize(use_NO_VARIATION_INDEX=False)
- advMapping = [mapping2[advMapping[g]] for g in glyphOrder]
- advanceMapping = builder.buildVarIdxMap(advMapping, glyphOrder)
-
- if vOrigMetricses:
- vOrigMap = [mapping2[vOrigMap[g]] for g in glyphOrder]
-
- useDirect = False
- vOrigMapping = None
- if directStore:
- # Compile both, see which is more compact
-
- writer = OTTableWriter()
- directStore.compile(writer, font)
- directSize = len(writer.getAllData())
-
- writer = OTTableWriter()
- indirectStore.compile(writer, font)
- advanceMapping.compile(writer, font)
- indirectSize = len(writer.getAllData())
-
- useDirect = directSize < indirectSize
-
- if useDirect:
- metricsStore = directStore
- advanceMapping = None
- else:
- metricsStore = indirectStore
- if vOrigMetricses:
- vOrigMapping = builder.buildVarIdxMap(vOrigMap, glyphOrder)
-
- return metricsStore, advanceMapping, vOrigMapping
def _add_MVAR(font, masterModel, master_ttfs, axisTags):
-
- log.info("Generating MVAR")
-
- store_builder = varStore.OnlineVarStoreBuilder(axisTags)
-
- records = []
- lastTableTag = None
- fontTable = None
- tables = None
- # HACK: we need to special-case post.underlineThickness and .underlinePosition
- # and unilaterally/arbitrarily define a sentinel value to distinguish the case
- # when a post table is present in a given master simply because that's where
- # the glyph names in TrueType must be stored, but the underline values are not
- # meant to be used for building MVAR's deltas. The value of -0x8000 (-36768)
- # the minimum FWord (int16) value, was chosen for its unlikelyhood to appear
- # in real-world underline position/thickness values.
- specialTags = {"unds": -0x8000, "undo": -0x8000}
-
- for tag, (tableTag, itemName) in sorted(MVAR_ENTRIES.items(), key=lambda kv: kv[1]):
- # For each tag, fetch the associated table from all fonts (or not when we are
- # still looking at a tag from the same tables) and set up the variation model
- # for them.
- if tableTag != lastTableTag:
- tables = fontTable = None
- if tableTag in font:
- fontTable = font[tableTag]
- tables = []
- for master in master_ttfs:
- if tableTag not in master or (
- tag in specialTags
- and getattr(master[tableTag], itemName) == specialTags[tag]
- ):
- tables.append(None)
- else:
- tables.append(master[tableTag])
- model, tables = masterModel.getSubModel(tables)
- store_builder.setModel(model)
- lastTableTag = tableTag
-
- if tables is None: # Tag not applicable to the master font.
- continue
-
- # TODO support gasp entries
-
- master_values = [getattr(table, itemName) for table in tables]
- if models.allEqual(master_values):
- base, varIdx = master_values[0], None
- else:
- base, varIdx = store_builder.storeMasters(master_values)
- setattr(fontTable, itemName, base)
-
- if varIdx is None:
- continue
- log.info(' %s: %s.%s %s', tag, tableTag, itemName, master_values)
- rec = ot.MetricsValueRecord()
- rec.ValueTag = tag
- rec.VarIdx = varIdx
- records.append(rec)
-
- assert "MVAR" not in font
- if records:
- store = store_builder.finish()
- # Optimize
- mapping = store.optimize()
- for rec in records:
- rec.VarIdx = mapping[rec.VarIdx]
-
- MVAR = font["MVAR"] = newTable('MVAR')
- mvar = MVAR.table = ot.MVAR()
- mvar.Version = 0x00010000
- mvar.Reserved = 0
- mvar.VarStore = store
- # XXX these should not be hard-coded but computed automatically
- mvar.ValueRecordSize = 8
- mvar.ValueRecordCount = len(records)
- mvar.ValueRecord = sorted(records, key=lambda r: r.ValueTag)
+ log.info("Generating MVAR")
+
+ store_builder = varStore.OnlineVarStoreBuilder(axisTags)
+
+ records = []
+ lastTableTag = None
+ fontTable = None
+ tables = None
+ # HACK: we need to special-case post.underlineThickness and .underlinePosition
+ # and unilaterally/arbitrarily define a sentinel value to distinguish the case
+ # when a post table is present in a given master simply because that's where
+ # the glyph names in TrueType must be stored, but the underline values are not
+ # meant to be used for building MVAR's deltas. The value of -0x8000 (-36768)
+ # the minimum FWord (int16) value, was chosen for its unlikelyhood to appear
+ # in real-world underline position/thickness values.
+ specialTags = {"unds": -0x8000, "undo": -0x8000}
+
+ for tag, (tableTag, itemName) in sorted(MVAR_ENTRIES.items(), key=lambda kv: kv[1]):
+ # For each tag, fetch the associated table from all fonts (or not when we are
+ # still looking at a tag from the same tables) and set up the variation model
+ # for them.
+ if tableTag != lastTableTag:
+ tables = fontTable = None
+ if tableTag in font:
+ fontTable = font[tableTag]
+ tables = []
+ for master in master_ttfs:
+ if tableTag not in master or (
+ tag in specialTags
+ and getattr(master[tableTag], itemName) == specialTags[tag]
+ ):
+ tables.append(None)
+ else:
+ tables.append(master[tableTag])
+ model, tables = masterModel.getSubModel(tables)
+ store_builder.setModel(model)
+ lastTableTag = tableTag
+
+ if tables is None: # Tag not applicable to the master font.
+ continue
+
+ # TODO support gasp entries
+
+ master_values = [getattr(table, itemName) for table in tables]
+ if models.allEqual(master_values):
+ base, varIdx = master_values[0], None
+ else:
+ base, varIdx = store_builder.storeMasters(master_values)
+ setattr(fontTable, itemName, base)
+
+ if varIdx is None:
+ continue
+ log.info(" %s: %s.%s %s", tag, tableTag, itemName, master_values)
+ rec = ot.MetricsValueRecord()
+ rec.ValueTag = tag
+ rec.VarIdx = varIdx
+ records.append(rec)
+
+ assert "MVAR" not in font
+ if records:
+ store = store_builder.finish()
+ # Optimize
+ mapping = store.optimize()
+ for rec in records:
+ rec.VarIdx = mapping[rec.VarIdx]
+
+ MVAR = font["MVAR"] = newTable("MVAR")
+ mvar = MVAR.table = ot.MVAR()
+ mvar.Version = 0x00010000
+ mvar.Reserved = 0
+ mvar.VarStore = store
+ # XXX these should not be hard-coded but computed automatically
+ mvar.ValueRecordSize = 8
+ mvar.ValueRecordCount = len(records)
+ mvar.ValueRecord = sorted(records, key=lambda r: r.ValueTag)
def _add_BASE(font, masterModel, master_ttfs, axisTags):
+ log.info("Generating BASE")
- log.info("Generating BASE")
+ merger = VariationMerger(masterModel, axisTags, font)
+ merger.mergeTables(font, master_ttfs, ["BASE"])
+ store = merger.store_builder.finish()
- merger = VariationMerger(masterModel, axisTags, font)
- merger.mergeTables(font, master_ttfs, ['BASE'])
- store = merger.store_builder.finish()
-
- if not store:
- return
- base = font['BASE'].table
- assert base.Version == 0x00010000
- base.Version = 0x00010001
- base.VarStore = store
+ if not store:
+ return
+ base = font["BASE"].table
+ assert base.Version == 0x00010000
+ base.Version = 0x00010001
+ base.VarStore = store
def _merge_OTL(font, model, master_fonts, axisTags):
-
- log.info("Merging OpenType Layout tables")
- merger = VariationMerger(model, axisTags, font)
-
- merger.mergeTables(font, master_fonts, ['GSUB', 'GDEF', 'GPOS'])
- store = merger.store_builder.finish()
- if not store:
- return
- try:
- GDEF = font['GDEF'].table
- assert GDEF.Version <= 0x00010002
- except KeyError:
- font['GDEF'] = newTable('GDEF')
- GDEFTable = font["GDEF"] = newTable('GDEF')
- GDEF = GDEFTable.table = ot.GDEF()
- GDEF.GlyphClassDef = None
- GDEF.AttachList = None
- GDEF.LigCaretList = None
- GDEF.MarkAttachClassDef = None
- GDEF.MarkGlyphSetsDef = None
-
- GDEF.Version = 0x00010003
- GDEF.VarStore = store
-
- # Optimize
- varidx_map = store.optimize()
- GDEF.remap_device_varidxes(varidx_map)
- if 'GPOS' in font:
- font['GPOS'].table.remap_device_varidxes(varidx_map)
+ log.info("Merging OpenType Layout tables")
+ merger = VariationMerger(model, axisTags, font)
+
+ merger.mergeTables(font, master_fonts, ["GSUB", "GDEF", "GPOS"])
+ store = merger.store_builder.finish()
+ if not store:
+ return
+ try:
+ GDEF = font["GDEF"].table
+ assert GDEF.Version <= 0x00010002
+ except KeyError:
+ font["GDEF"] = newTable("GDEF")
+ GDEFTable = font["GDEF"] = newTable("GDEF")
+ GDEF = GDEFTable.table = ot.GDEF()
+ GDEF.GlyphClassDef = None
+ GDEF.AttachList = None
+ GDEF.LigCaretList = None
+ GDEF.MarkAttachClassDef = None
+ GDEF.MarkGlyphSetsDef = None
+
+ GDEF.Version = 0x00010003
+ GDEF.VarStore = store
+
+ # Optimize
+ varidx_map = store.optimize()
+ GDEF.remap_device_varidxes(varidx_map)
+ if "GPOS" in font:
+ font["GPOS"].table.remap_device_varidxes(varidx_map)
def _add_GSUB_feature_variations(font, axes, internal_axis_supports, rules, featureTag):
+ def normalize(name, value):
+ return models.normalizeLocation({name: value}, internal_axis_supports)[name]
- def normalize(name, value):
- return models.normalizeLocation(
- {name: value}, internal_axis_supports
- )[name]
+ log.info("Generating GSUB FeatureVariations")
- log.info("Generating GSUB FeatureVariations")
+ axis_tags = {name: axis.tag for name, axis in axes.items()}
- axis_tags = {name: axis.tag for name, axis in axes.items()}
+ conditional_subs = []
+ for rule in rules:
+ region = []
+ for conditions in rule.conditionSets:
+ space = {}
+ for condition in conditions:
+ axis_name = condition["name"]
+ if condition["minimum"] is not None:
+ minimum = normalize(axis_name, condition["minimum"])
+ else:
+ minimum = -1.0
+ if condition["maximum"] is not None:
+ maximum = normalize(axis_name, condition["maximum"])
+ else:
+ maximum = 1.0
+ tag = axis_tags[axis_name]
+ space[tag] = (minimum, maximum)
+ region.append(space)
- conditional_subs = []
- for rule in rules:
+ subs = {k: v for k, v in rule.subs}
- region = []
- for conditions in rule.conditionSets:
- space = {}
- for condition in conditions:
- axis_name = condition["name"]
- if condition["minimum"] is not None:
- minimum = normalize(axis_name, condition["minimum"])
- else:
- minimum = -1.0
- if condition["maximum"] is not None:
- maximum = normalize(axis_name, condition["maximum"])
- else:
- maximum = 1.0
- tag = axis_tags[axis_name]
- space[tag] = (minimum, maximum)
- region.append(space)
+ conditional_subs.append((region, subs))
- subs = {k: v for k, v in rule.subs}
-
- conditional_subs.append((region, subs))
-
- addFeatureVariations(font, conditional_subs, featureTag)
+ addFeatureVariations(font, conditional_subs, featureTag)
_DesignSpaceData = namedtuple(
- "_DesignSpaceData",
- [
- "axes",
- "internal_axis_supports",
- "base_idx",
- "normalized_master_locs",
- "masters",
- "instances",
- "rules",
- "rulesProcessingLast",
- "lib",
- ],
+ "_DesignSpaceData",
+ [
+ "axes",
+ "axisMappings",
+ "internal_axis_supports",
+ "base_idx",
+ "normalized_master_locs",
+ "masters",
+ "instances",
+ "rules",
+ "rulesProcessingLast",
+ "lib",
+ ],
)
def _add_CFF2(varFont, model, master_fonts):
- from .cff import merge_region_fonts
- glyphOrder = varFont.getGlyphOrder()
- if "CFF2" not in varFont:
- from .cff import convertCFFtoCFF2
- convertCFFtoCFF2(varFont)
- ordered_fonts_list = model.reorderMasters(master_fonts, model.reverseMapping)
- # re-ordering the master list simplifies building the CFF2 data item lists.
- merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder)
+ from .cff import merge_region_fonts
+
+ glyphOrder = varFont.getGlyphOrder()
+ if "CFF2" not in varFont:
+ from .cff import convertCFFtoCFF2
+
+ convertCFFtoCFF2(varFont)
+ ordered_fonts_list = model.reorderMasters(master_fonts, model.reverseMapping)
+ # re-ordering the master list simplifies building the CFF2 data item lists.
+ merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder)
def _add_COLR(font, model, master_fonts, axisTags, colr_layer_reuse=True):
- merger = COLRVariationMerger(model, axisTags, font, allowLayerReuse=colr_layer_reuse)
- merger.mergeTables(font, master_fonts)
- store = merger.store_builder.finish()
+ merger = COLRVariationMerger(
+ model, axisTags, font, allowLayerReuse=colr_layer_reuse
+ )
+ merger.mergeTables(font, master_fonts)
+ store = merger.store_builder.finish()
- colr = font["COLR"].table
- if store:
- mapping = store.optimize()
- colr.VarStore = store
- varIdxes = [mapping[v] for v in merger.varIdxes]
- colr.VarIndexMap = builder.buildDeltaSetIndexMap(varIdxes)
+ colr = font["COLR"].table
+ if store:
+ mapping = store.optimize()
+ colr.VarStore = store
+ varIdxes = [mapping[v] for v in merger.varIdxes]
+ colr.VarIndexMap = builder.buildDeltaSetIndexMap(varIdxes)
def load_designspace(designspace):
- # TODO: remove this and always assume 'designspace' is a DesignSpaceDocument,
- # never a file path, as that's already handled by caller
- if hasattr(designspace, "sources"): # Assume a DesignspaceDocument
- ds = designspace
- else: # Assume a file path
- ds = DesignSpaceDocument.fromfile(designspace)
-
- masters = ds.sources
- if not masters:
- raise VarLibValidationError("Designspace must have at least one source.")
- instances = ds.instances
-
- # TODO: Use fontTools.designspaceLib.tagForAxisName instead.
- standard_axis_map = OrderedDict([
- ('weight', ('wght', {'en': u'Weight'})),
- ('width', ('wdth', {'en': u'Width'})),
- ('slant', ('slnt', {'en': u'Slant'})),
- ('optical', ('opsz', {'en': u'Optical Size'})),
- ('italic', ('ital', {'en': u'Italic'})),
- ])
-
- # Setup axes
- if not ds.axes:
- raise VarLibValidationError(f"Designspace must have at least one axis.")
-
- axes = OrderedDict()
- for axis_index, axis in enumerate(ds.axes):
- axis_name = axis.name
- if not axis_name:
- if not axis.tag:
- raise VarLibValidationError(f"Axis at index {axis_index} needs a tag.")
- axis_name = axis.name = axis.tag
-
- if axis_name in standard_axis_map:
- if axis.tag is None:
- axis.tag = standard_axis_map[axis_name][0]
- if not axis.labelNames:
- axis.labelNames.update(standard_axis_map[axis_name][1])
- else:
- if not axis.tag:
- raise VarLibValidationError(f"Axis at index {axis_index} needs a tag.")
- if not axis.labelNames:
- axis.labelNames["en"] = tostr(axis_name)
-
- axes[axis_name] = axis
- log.info("Axes:\n%s", pformat([axis.asdict() for axis in axes.values()]))
-
- # Check all master and instance locations are valid and fill in defaults
- for obj in masters+instances:
- obj_name = obj.name or obj.styleName or ''
- loc = obj.getFullDesignLocation(ds)
- obj.designLocation = loc
- if loc is None:
- raise VarLibValidationError(
- f"Source or instance '{obj_name}' has no location."
- )
- for axis_name in loc.keys():
- if axis_name not in axes:
- raise VarLibValidationError(
- f"Location axis '{axis_name}' unknown for '{obj_name}'."
- )
- for axis_name,axis in axes.items():
- v = axis.map_backward(loc[axis_name])
- if not (axis.minimum <= v <= axis.maximum):
- raise VarLibValidationError(
- f"Source or instance '{obj_name}' has out-of-range location "
- f"for axis '{axis_name}': is mapped to {v} but must be in "
- f"mapped range [{axis.minimum}..{axis.maximum}] (NOTE: all "
- "values are in user-space)."
- )
-
- # Normalize master locations
-
- internal_master_locs = [o.getFullDesignLocation(ds) for o in masters]
- log.info("Internal master locations:\n%s", pformat(internal_master_locs))
-
- # TODO This mapping should ideally be moved closer to logic in _add_fvar/avar
- internal_axis_supports = {}
- for axis in axes.values():
- triple = (axis.minimum, axis.default, axis.maximum)
- internal_axis_supports[axis.name] = [axis.map_forward(v) for v in triple]
- log.info("Internal axis supports:\n%s", pformat(internal_axis_supports))
-
- normalized_master_locs = [models.normalizeLocation(m, internal_axis_supports) for m in internal_master_locs]
- log.info("Normalized master locations:\n%s", pformat(normalized_master_locs))
-
- # Find base master
- base_idx = None
- for i,m in enumerate(normalized_master_locs):
- if all(v == 0 for v in m.values()):
- if base_idx is not None:
- raise VarLibValidationError(
- "More than one base master found in Designspace."
- )
- base_idx = i
- if base_idx is None:
- raise VarLibValidationError(
- "Base master not found; no master at default location?"
- )
- log.info("Index of base master: %s", base_idx)
-
- return _DesignSpaceData(
- axes,
- internal_axis_supports,
- base_idx,
- normalized_master_locs,
- masters,
- instances,
- ds.rules,
- ds.rulesProcessingLast,
- ds.lib,
- )
+ # TODO: remove this and always assume 'designspace' is a DesignSpaceDocument,
+ # never a file path, as that's already handled by caller
+ if hasattr(designspace, "sources"): # Assume a DesignspaceDocument
+ ds = designspace
+ else: # Assume a file path
+ ds = DesignSpaceDocument.fromfile(designspace)
+
+ masters = ds.sources
+ if not masters:
+ raise VarLibValidationError("Designspace must have at least one source.")
+ instances = ds.instances
+
+ # TODO: Use fontTools.designspaceLib.tagForAxisName instead.
+ standard_axis_map = OrderedDict(
+ [
+ ("weight", ("wght", {"en": "Weight"})),
+ ("width", ("wdth", {"en": "Width"})),
+ ("slant", ("slnt", {"en": "Slant"})),
+ ("optical", ("opsz", {"en": "Optical Size"})),
+ ("italic", ("ital", {"en": "Italic"})),
+ ]
+ )
+
+ # Setup axes
+ if not ds.axes:
+ raise VarLibValidationError(f"Designspace must have at least one axis.")
+
+ axes = OrderedDict()
+ for axis_index, axis in enumerate(ds.axes):
+ axis_name = axis.name
+ if not axis_name:
+ if not axis.tag:
+ raise VarLibValidationError(f"Axis at index {axis_index} needs a tag.")
+ axis_name = axis.name = axis.tag
+
+ if axis_name in standard_axis_map:
+ if axis.tag is None:
+ axis.tag = standard_axis_map[axis_name][0]
+ if not axis.labelNames:
+ axis.labelNames.update(standard_axis_map[axis_name][1])
+ else:
+ if not axis.tag:
+ raise VarLibValidationError(f"Axis at index {axis_index} needs a tag.")
+ if not axis.labelNames:
+ axis.labelNames["en"] = tostr(axis_name)
+
+ axes[axis_name] = axis
+ log.info("Axes:\n%s", pformat([axis.asdict() for axis in axes.values()]))
+
+ axisMappings = ds.axisMappings
+ if axisMappings:
+ log.info("Mappings:\n%s", pformat(axisMappings))
+
+ # Check all master and instance locations are valid and fill in defaults
+ for obj in masters + instances:
+ obj_name = obj.name or obj.styleName or ""
+ loc = obj.getFullDesignLocation(ds)
+ obj.designLocation = loc
+ if loc is None:
+ raise VarLibValidationError(
+ f"Source or instance '{obj_name}' has no location."
+ )
+ for axis_name in loc.keys():
+ if axis_name not in axes:
+ raise VarLibValidationError(
+ f"Location axis '{axis_name}' unknown for '{obj_name}'."
+ )
+ for axis_name, axis in axes.items():
+ v = axis.map_backward(loc[axis_name])
+ if not (axis.minimum <= v <= axis.maximum):
+ raise VarLibValidationError(
+ f"Source or instance '{obj_name}' has out-of-range location "
+ f"for axis '{axis_name}': is mapped to {v} but must be in "
+ f"mapped range [{axis.minimum}..{axis.maximum}] (NOTE: all "
+ "values are in user-space)."
+ )
+
+ # Normalize master locations
+
+ internal_master_locs = [o.getFullDesignLocation(ds) for o in masters]
+ log.info("Internal master locations:\n%s", pformat(internal_master_locs))
+
+ # TODO This mapping should ideally be moved closer to logic in _add_fvar/avar
+ internal_axis_supports = {}
+ for axis in axes.values():
+ triple = (axis.minimum, axis.default, axis.maximum)
+ internal_axis_supports[axis.name] = [axis.map_forward(v) for v in triple]
+ log.info("Internal axis supports:\n%s", pformat(internal_axis_supports))
+
+ normalized_master_locs = [
+ models.normalizeLocation(m, internal_axis_supports)
+ for m in internal_master_locs
+ ]
+ log.info("Normalized master locations:\n%s", pformat(normalized_master_locs))
+
+ # Find base master
+ base_idx = None
+ for i, m in enumerate(normalized_master_locs):
+ if all(v == 0 for v in m.values()):
+ if base_idx is not None:
+ raise VarLibValidationError(
+ "More than one base master found in Designspace."
+ )
+ base_idx = i
+ if base_idx is None:
+ raise VarLibValidationError(
+ "Base master not found; no master at default location?"
+ )
+ log.info("Index of base master: %s", base_idx)
+
+ return _DesignSpaceData(
+ axes,
+ axisMappings,
+ internal_axis_supports,
+ base_idx,
+ normalized_master_locs,
+ masters,
+ instances,
+ ds.rules,
+ ds.rulesProcessingLast,
+ ds.lib,
+ )
# https://docs.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass
WDTH_VALUE_TO_OS2_WIDTH_CLASS = {
- 50: 1,
- 62.5: 2,
- 75: 3,
- 87.5: 4,
- 100: 5,
- 112.5: 6,
- 125: 7,
- 150: 8,
- 200: 9,
+ 50: 1,
+ 62.5: 2,
+ 75: 3,
+ 87.5: 4,
+ 100: 5,
+ 112.5: 6,
+ 125: 7,
+ 150: 8,
+ 200: 9,
}
def set_default_weight_width_slant(font, location):
- if "OS/2" in font:
- if "wght" in location:
- weight_class = otRound(max(1, min(location["wght"], 1000)))
- if font["OS/2"].usWeightClass != weight_class:
- log.info("Setting OS/2.usWeightClass = %s", weight_class)
- font["OS/2"].usWeightClass = weight_class
-
- if "wdth" in location:
- # map 'wdth' axis (50..200) to OS/2.usWidthClass (1..9), rounding to closest
- widthValue = min(max(location["wdth"], 50), 200)
- widthClass = otRound(
- models.piecewiseLinearMap(widthValue, WDTH_VALUE_TO_OS2_WIDTH_CLASS)
- )
- if font["OS/2"].usWidthClass != widthClass:
- log.info("Setting OS/2.usWidthClass = %s", widthClass)
- font["OS/2"].usWidthClass = widthClass
-
- if "slnt" in location and "post" in font:
- italicAngle = max(-90, min(location["slnt"], 90))
- if font["post"].italicAngle != italicAngle:
- log.info("Setting post.italicAngle = %s", italicAngle)
- font["post"].italicAngle = italicAngle
+ if "OS/2" in font:
+ if "wght" in location:
+ weight_class = otRound(max(1, min(location["wght"], 1000)))
+ if font["OS/2"].usWeightClass != weight_class:
+ log.info("Setting OS/2.usWeightClass = %s", weight_class)
+ font["OS/2"].usWeightClass = weight_class
+
+ if "wdth" in location:
+ # map 'wdth' axis (50..200) to OS/2.usWidthClass (1..9), rounding to closest
+ widthValue = min(max(location["wdth"], 50), 200)
+ widthClass = otRound(
+ models.piecewiseLinearMap(widthValue, WDTH_VALUE_TO_OS2_WIDTH_CLASS)
+ )
+ if font["OS/2"].usWidthClass != widthClass:
+ log.info("Setting OS/2.usWidthClass = %s", widthClass)
+ font["OS/2"].usWidthClass = widthClass
+
+ if "slnt" in location and "post" in font:
+ italicAngle = max(-90, min(location["slnt"], 90))
+ if font["post"].italicAngle != italicAngle:
+ log.info("Setting post.italicAngle = %s", italicAngle)
+ font["post"].italicAngle = italicAngle
+
+
+def drop_implied_oncurve_points(*masters: TTFont) -> int:
+ """Drop impliable on-curve points from all the simple glyphs in masters.
+
+ In TrueType glyf outlines, on-curve points can be implied when they are located
+ exactly at the midpoint of the line connecting two consecutive off-curve points.
+
+ The input masters' glyf tables are assumed to contain same-named glyphs that are
+ interpolatable. Oncurve points are only dropped if they can be implied for all
+ the masters. The fonts are modified in-place.
+
+ Args:
+ masters: The TTFont(s) to modify
+
+ Returns:
+ The total number of points that were dropped if any.
+
+ Reference:
+ https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html
+ """
+
+ count = 0
+ glyph_masters = defaultdict(list)
+ # multiple DS source may point to the same TTFont object and we want to
+ # avoid processing the same glyph twice as they are modified in-place
+ for font in {id(m): m for m in masters}.values():
+ glyf = font["glyf"]
+ for glyphName in glyf.keys():
+ glyph_masters[glyphName].append(glyf[glyphName])
+ count = 0
+ for glyphName, glyphs in glyph_masters.items():
+ try:
+ dropped = dropImpliedOnCurvePoints(*glyphs)
+ except ValueError as e:
+ # we don't fail for incompatible glyphs in _add_gvar so we shouldn't here
+ log.warning("Failed to drop implied oncurves for %r: %s", glyphName, e)
+ else:
+ count += len(dropped)
+ return count
def build_many(
- designspace: DesignSpaceDocument,
- master_finder=lambda s:s,
- exclude=[],
- optimize=True,
- skip_vf=lambda vf_name: False,
- colr_layer_reuse=True,
+ designspace: DesignSpaceDocument,
+ master_finder=lambda s: s,
+ exclude=[],
+ optimize=True,
+ skip_vf=lambda vf_name: False,
+ colr_layer_reuse=True,
+ drop_implied_oncurves=False,
):
- """
- Build variable fonts from a designspace file, version 5 which can define
- several VFs, or version 4 which has implicitly one VF covering the whole doc.
-
- If master_finder is set, it should be a callable that takes master
- filename as found in designspace file and map it to master font
- binary as to be opened (eg. .ttf or .otf).
-
- skip_vf can be used to skip building some of the variable fonts defined in
- the input designspace. It's a predicate that takes as argument the name
- of the variable font and returns `bool`.
-
- Always returns a Dict[str, TTFont] keyed by VariableFontDescriptor.name
- """
- res = {}
- for _location, subDoc in splitInterpolable(designspace):
- for name, vfDoc in splitVariableFonts(subDoc):
- if skip_vf(name):
- log.debug(f"Skipping variable TTF font: {name}")
- continue
- vf = build(
- vfDoc,
- master_finder,
- exclude=list(exclude) + ["STAT"],
- optimize=optimize,
- colr_layer_reuse=colr_layer_reuse,
- )[0]
- if "STAT" not in exclude:
- buildVFStatTable(vf, designspace, name)
- res[name] = vf
- return res
+ """
+ Build variable fonts from a designspace file, version 5 which can define
+ several VFs, or version 4 which has implicitly one VF covering the whole doc.
+
+ If master_finder is set, it should be a callable that takes master
+ filename as found in designspace file and map it to master font
+ binary as to be opened (eg. .ttf or .otf).
+
+ skip_vf can be used to skip building some of the variable fonts defined in
+ the input designspace. It's a predicate that takes as argument the name
+ of the variable font and returns `bool`.
+
+ Always returns a Dict[str, TTFont] keyed by VariableFontDescriptor.name
+ """
+ res = {}
+ # varLib.build (used further below) by default only builds an incomplete 'STAT'
+ # with an empty AxisValueArray--unless the VF inherited 'STAT' from its base master.
+ # Designspace version 5 can also be used to define 'STAT' labels or customize
+ # axes ordering, etc. To avoid overwriting a pre-existing 'STAT' or redoing the
+ # same work twice, here we check if designspace contains any 'STAT' info before
+ # proceeding to call buildVFStatTable for each VF.
+ # https://github.com/fonttools/fonttools/pull/3024
+ # https://github.com/fonttools/fonttools/issues/3045
+ doBuildStatFromDSv5 = (
+ "STAT" not in exclude
+ and designspace.formatTuple >= (5, 0)
+ and (
+ any(a.axisLabels or a.axisOrdering is not None for a in designspace.axes)
+ or designspace.locationLabels
+ )
+ )
+ for _location, subDoc in splitInterpolable(designspace):
+ for name, vfDoc in splitVariableFonts(subDoc):
+ if skip_vf(name):
+ log.debug(f"Skipping variable TTF font: {name}")
+ continue
+ vf = build(
+ vfDoc,
+ master_finder,
+ exclude=exclude,
+ optimize=optimize,
+ colr_layer_reuse=colr_layer_reuse,
+ drop_implied_oncurves=drop_implied_oncurves,
+ )[0]
+ if doBuildStatFromDSv5:
+ buildVFStatTable(vf, designspace, name)
+ res[name] = vf
+ return res
+
def build(
- designspace,
- master_finder=lambda s:s,
- exclude=[],
- optimize=True,
- colr_layer_reuse=True,
+ designspace,
+ master_finder=lambda s: s,
+ exclude=[],
+ optimize=True,
+ colr_layer_reuse=True,
+ drop_implied_oncurves=False,
):
- """
- Build variation font from a designspace file.
-
- If master_finder is set, it should be a callable that takes master
- filename as found in designspace file and map it to master font
- binary as to be opened (eg. .ttf or .otf).
- """
- if hasattr(designspace, "sources"): # Assume a DesignspaceDocument
- pass
- else: # Assume a file path
- designspace = DesignSpaceDocument.fromfile(designspace)
-
- ds = load_designspace(designspace)
- log.info("Building variable font")
-
- log.info("Loading master fonts")
- master_fonts = load_masters(designspace, master_finder)
-
- # TODO: 'master_ttfs' is unused except for return value, remove later
- master_ttfs = []
- for master in master_fonts:
- try:
- master_ttfs.append(master.reader.file.name)
- except AttributeError:
- master_ttfs.append(None) # in-memory fonts have no path
-
- # Copy the base master to work from it
- vf = deepcopy(master_fonts[ds.base_idx])
-
- # TODO append masters as named-instances as well; needs .designspace change.
- fvar = _add_fvar(vf, ds.axes, ds.instances)
- if 'STAT' not in exclude:
- _add_stat(vf)
- if 'avar' not in exclude:
- _add_avar(vf, ds.axes)
-
- # Map from axis names to axis tags...
- normalized_master_locs = [
- {ds.axes[k].tag: v for k,v in loc.items()} for loc in ds.normalized_master_locs
- ]
- # From here on, we use fvar axes only
- axisTags = [axis.axisTag for axis in fvar.axes]
-
- # Assume single-model for now.
- model = models.VariationModel(normalized_master_locs, axisOrder=axisTags)
- assert 0 == model.mapping[ds.base_idx]
-
- log.info("Building variations tables")
- if 'BASE' not in exclude and 'BASE' in vf:
- _add_BASE(vf, model, master_fonts, axisTags)
- if 'MVAR' not in exclude:
- _add_MVAR(vf, model, master_fonts, axisTags)
- if 'HVAR' not in exclude:
- _add_HVAR(vf, model, master_fonts, axisTags)
- if 'VVAR' not in exclude and 'vmtx' in vf:
- _add_VVAR(vf, model, master_fonts, axisTags)
- if 'GDEF' not in exclude or 'GPOS' not in exclude:
- _merge_OTL(vf, model, master_fonts, axisTags)
- if 'gvar' not in exclude and 'glyf' in vf:
- _add_gvar(vf, model, master_fonts, optimize=optimize)
- if 'cvar' not in exclude and 'glyf' in vf:
- _merge_TTHinting(vf, model, master_fonts)
- if 'GSUB' not in exclude and ds.rules:
- featureTag = ds.lib.get(
- FEAVAR_FEATURETAG_LIB_KEY,
- "rclt" if ds.rulesProcessingLast else "rvrn"
- )
- _add_GSUB_feature_variations(vf, ds.axes, ds.internal_axis_supports, ds.rules, featureTag)
- if 'CFF2' not in exclude and ('CFF ' in vf or 'CFF2' in vf):
- _add_CFF2(vf, model, master_fonts)
- if "post" in vf:
- # set 'post' to format 2 to keep the glyph names dropped from CFF2
- post = vf["post"]
- if post.formatType != 2.0:
- post.formatType = 2.0
- post.extraNames = []
- post.mapping = {}
- if 'COLR' not in exclude and 'COLR' in vf and vf['COLR'].version > 0:
- _add_COLR(vf, model, master_fonts, axisTags, colr_layer_reuse)
-
- set_default_weight_width_slant(
- vf, location={axis.axisTag: axis.defaultValue for axis in vf["fvar"].axes}
- )
-
- for tag in exclude:
- if tag in vf:
- del vf[tag]
-
- # TODO: Only return vf for 4.0+, the rest is unused.
- return vf, model, master_ttfs
+ """
+ Build variation font from a designspace file.
+
+ If master_finder is set, it should be a callable that takes master
+ filename as found in designspace file and map it to master font
+ binary as to be opened (eg. .ttf or .otf).
+ """
+ if hasattr(designspace, "sources"): # Assume a DesignspaceDocument
+ pass
+ else: # Assume a file path
+ designspace = DesignSpaceDocument.fromfile(designspace)
+
+ ds = load_designspace(designspace)
+ log.info("Building variable font")
+
+ log.info("Loading master fonts")
+ master_fonts = load_masters(designspace, master_finder)
+
+ # TODO: 'master_ttfs' is unused except for return value, remove later
+ master_ttfs = []
+ for master in master_fonts:
+ try:
+ master_ttfs.append(master.reader.file.name)
+ except AttributeError:
+ master_ttfs.append(None) # in-memory fonts have no path
+
+ if drop_implied_oncurves and "glyf" in master_fonts[ds.base_idx]:
+ drop_count = drop_implied_oncurve_points(*master_fonts)
+ log.info(
+ "Dropped %s on-curve points from simple glyphs in the 'glyf' table",
+ drop_count,
+ )
+
+ # Copy the base master to work from it
+ vf = deepcopy(master_fonts[ds.base_idx])
+
+ if "DSIG" in vf:
+ del vf["DSIG"]
+
+ # TODO append masters as named-instances as well; needs .designspace change.
+ fvar = _add_fvar(vf, ds.axes, ds.instances)
+ if "STAT" not in exclude:
+ _add_stat(vf)
+
+ # Map from axis names to axis tags...
+ normalized_master_locs = [
+ {ds.axes[k].tag: v for k, v in loc.items()} for loc in ds.normalized_master_locs
+ ]
+ # From here on, we use fvar axes only
+ axisTags = [axis.axisTag for axis in fvar.axes]
+
+ # Assume single-model for now.
+ model = models.VariationModel(normalized_master_locs, axisOrder=axisTags)
+ assert 0 == model.mapping[ds.base_idx]
+
+ log.info("Building variations tables")
+ if "avar" not in exclude:
+ _add_avar(vf, ds.axes, ds.axisMappings, axisTags)
+ if "BASE" not in exclude and "BASE" in vf:
+ _add_BASE(vf, model, master_fonts, axisTags)
+ if "MVAR" not in exclude:
+ _add_MVAR(vf, model, master_fonts, axisTags)
+ if "HVAR" not in exclude:
+ _add_HVAR(vf, model, master_fonts, axisTags)
+ if "VVAR" not in exclude and "vmtx" in vf:
+ _add_VVAR(vf, model, master_fonts, axisTags)
+ if "GDEF" not in exclude or "GPOS" not in exclude:
+ _merge_OTL(vf, model, master_fonts, axisTags)
+ if "gvar" not in exclude and "glyf" in vf:
+ _add_gvar(vf, model, master_fonts, optimize=optimize)
+ if "cvar" not in exclude and "glyf" in vf:
+ _merge_TTHinting(vf, model, master_fonts)
+ if "GSUB" not in exclude and ds.rules:
+ featureTag = ds.lib.get(
+ FEAVAR_FEATURETAG_LIB_KEY, "rclt" if ds.rulesProcessingLast else "rvrn"
+ )
+ _add_GSUB_feature_variations(
+ vf, ds.axes, ds.internal_axis_supports, ds.rules, featureTag
+ )
+ if "CFF2" not in exclude and ("CFF " in vf or "CFF2" in vf):
+ _add_CFF2(vf, model, master_fonts)
+ if "post" in vf:
+ # set 'post' to format 2 to keep the glyph names dropped from CFF2
+ post = vf["post"]
+ if post.formatType != 2.0:
+ post.formatType = 2.0
+ post.extraNames = []
+ post.mapping = {}
+ if "COLR" not in exclude and "COLR" in vf and vf["COLR"].version > 0:
+ _add_COLR(vf, model, master_fonts, axisTags, colr_layer_reuse)
+
+ set_default_weight_width_slant(
+ vf, location={axis.axisTag: axis.defaultValue for axis in vf["fvar"].axes}
+ )
+
+ for tag in exclude:
+ if tag in vf:
+ del vf[tag]
+
+ # TODO: Only return vf for 4.0+, the rest is unused.
+ return vf, model, master_ttfs
def _open_font(path, master_finder=lambda s: s):
- # load TTFont masters from given 'path': this can be either a .TTX or an
- # OpenType binary font; or if neither of these, try use the 'master_finder'
- # callable to resolve the path to a valid .TTX or OpenType font binary.
- from fontTools.ttx import guessFileType
-
- master_path = os.path.normpath(path)
- tp = guessFileType(master_path)
- if tp is None:
- # not an OpenType binary/ttx, fall back to the master finder.
- master_path = master_finder(master_path)
- tp = guessFileType(master_path)
- if tp in ("TTX", "OTX"):
- font = TTFont()
- font.importXML(master_path)
- elif tp in ("TTF", "OTF", "WOFF", "WOFF2"):
- font = TTFont(master_path)
- else:
- raise VarLibValidationError("Invalid master path: %r" % master_path)
- return font
+ # load TTFont masters from given 'path': this can be either a .TTX or an
+ # OpenType binary font; or if neither of these, try use the 'master_finder'
+ # callable to resolve the path to a valid .TTX or OpenType font binary.
+ from fontTools.ttx import guessFileType
+
+ master_path = os.path.normpath(path)
+ tp = guessFileType(master_path)
+ if tp is None:
+ # not an OpenType binary/ttx, fall back to the master finder.
+ master_path = master_finder(master_path)
+ tp = guessFileType(master_path)
+ if tp in ("TTX", "OTX"):
+ font = TTFont()
+ font.importXML(master_path)
+ elif tp in ("TTF", "OTF", "WOFF", "WOFF2"):
+ font = TTFont(master_path)
+ else:
+ raise VarLibValidationError("Invalid master path: %r" % master_path)
+ return font
def load_masters(designspace, master_finder=lambda s: s):
- """Ensure that all SourceDescriptor.font attributes have an appropriate TTFont
- object loaded, or else open TTFont objects from the SourceDescriptor.path
- attributes.
-
- The paths can point to either an OpenType font, a TTX file, or a UFO. In the
- latter case, use the provided master_finder callable to map from UFO paths to
- the respective master font binaries (e.g. .ttf, .otf or .ttx).
-
- Return list of master TTFont objects in the same order they are listed in the
- DesignSpaceDocument.
- """
- for master in designspace.sources:
- # If a SourceDescriptor has a layer name, demand that the compiled TTFont
- # be supplied by the caller. This spares us from modifying MasterFinder.
- if master.layerName and master.font is None:
- raise VarLibValidationError(
- f"Designspace source '{master.name or '<Unknown>'}' specified a "
- "layer name but lacks the required TTFont object in the 'font' "
- "attribute."
- )
-
- return designspace.loadSourceFonts(_open_font, master_finder=master_finder)
+ """Ensure that all SourceDescriptor.font attributes have an appropriate TTFont
+ object loaded, or else open TTFont objects from the SourceDescriptor.path
+ attributes.
+
+ The paths can point to either an OpenType font, a TTX file, or a UFO. In the
+ latter case, use the provided master_finder callable to map from UFO paths to
+ the respective master font binaries (e.g. .ttf, .otf or .ttx).
+
+ Return list of master TTFont objects in the same order they are listed in the
+ DesignSpaceDocument.
+ """
+ for master in designspace.sources:
+ # If a SourceDescriptor has a layer name, demand that the compiled TTFont
+ # be supplied by the caller. This spares us from modifying MasterFinder.
+ if master.layerName and master.font is None:
+ raise VarLibValidationError(
+ f"Designspace source '{master.name or '<Unknown>'}' specified a "
+ "layer name but lacks the required TTFont object in the 'font' "
+ "attribute."
+ )
+
+ return designspace.loadSourceFonts(_open_font, master_finder=master_finder)
class MasterFinder(object):
-
- def __init__(self, template):
- self.template = template
-
- def __call__(self, src_path):
- fullname = os.path.abspath(src_path)
- dirname, basename = os.path.split(fullname)
- stem, ext = os.path.splitext(basename)
- path = self.template.format(
- fullname=fullname,
- dirname=dirname,
- basename=basename,
- stem=stem,
- ext=ext,
- )
- return os.path.normpath(path)
+ def __init__(self, template):
+ self.template = template
+
+ def __call__(self, src_path):
+ fullname = os.path.abspath(src_path)
+ dirname, basename = os.path.split(fullname)
+ stem, ext = os.path.splitext(basename)
+ path = self.template.format(
+ fullname=fullname,
+ dirname=dirname,
+ basename=basename,
+ stem=stem,
+ ext=ext,
+ )
+ return os.path.normpath(path)
def main(args=None):
- """Build a variable font from a designspace file and masters"""
- from argparse import ArgumentParser
- from fontTools import configLogger
-
- parser = ArgumentParser(prog='varLib', description = main.__doc__)
- parser.add_argument('designspace')
- parser.add_argument(
- '-o',
- metavar='OUTPUTFILE',
- dest='outfile',
- default=None,
- help='output file'
- )
- parser.add_argument(
- '-x',
- metavar='TAG',
- dest='exclude',
- action='append',
- default=[],
- help='exclude table'
- )
- parser.add_argument(
- '--disable-iup',
- dest='optimize',
- action='store_false',
- help='do not perform IUP optimization'
- )
- parser.add_argument(
- '--no-colr-layer-reuse',
- dest='colr_layer_reuse',
- action='store_false',
- help='do not rebuild variable COLR table to optimize COLR layer reuse',
- )
- parser.add_argument(
- '--master-finder',
- default='master_ttf_interpolatable/{stem}.ttf',
- help=(
- 'templated string used for finding binary font '
- 'files given the source file names defined in the '
- 'designspace document. The following special strings '
- 'are defined: {fullname} is the absolute source file '
- 'name; {basename} is the file name without its '
- 'directory; {stem} is the basename without the file '
- 'extension; {ext} is the source file extension; '
- '{dirname} is the directory of the absolute file '
- 'name. The default value is "%(default)s".'
- )
- )
- logging_group = parser.add_mutually_exclusive_group(required=False)
- logging_group.add_argument(
- "-v", "--verbose",
- action="store_true",
- help="Run more verbosely.")
- logging_group.add_argument(
- "-q", "--quiet",
- action="store_true",
- help="Turn verbosity off.")
- options = parser.parse_args(args)
-
- configLogger(level=(
- "DEBUG" if options.verbose else
- "ERROR" if options.quiet else
- "INFO"))
-
- designspace_filename = options.designspace
- finder = MasterFinder(options.master_finder)
-
- vf, _, _ = build(
- designspace_filename,
- finder,
- exclude=options.exclude,
- optimize=options.optimize,
- colr_layer_reuse=options.colr_layer_reuse,
- )
-
- outfile = options.outfile
- if outfile is None:
- ext = "otf" if vf.sfntVersion == "OTTO" else "ttf"
- outfile = os.path.splitext(designspace_filename)[0] + '-VF.' + ext
-
- log.info("Saving variation font %s", outfile)
- vf.save(outfile)
+ """Build variable fonts from a designspace file and masters"""
+ from argparse import ArgumentParser
+ from fontTools import configLogger
+
+ parser = ArgumentParser(prog="varLib", description=main.__doc__)
+ parser.add_argument("designspace")
+ output_group = parser.add_mutually_exclusive_group()
+ output_group.add_argument(
+ "-o", metavar="OUTPUTFILE", dest="outfile", default=None, help="output file"
+ )
+ output_group.add_argument(
+ "-d",
+ "--output-dir",
+ metavar="OUTPUTDIR",
+ default=None,
+ help="output dir (default: same as input designspace file)",
+ )
+ parser.add_argument(
+ "-x",
+ metavar="TAG",
+ dest="exclude",
+ action="append",
+ default=[],
+ help="exclude table",
+ )
+ parser.add_argument(
+ "--disable-iup",
+ dest="optimize",
+ action="store_false",
+ help="do not perform IUP optimization",
+ )
+ parser.add_argument(
+ "--no-colr-layer-reuse",
+ dest="colr_layer_reuse",
+ action="store_false",
+ help="do not rebuild variable COLR table to optimize COLR layer reuse",
+ )
+ parser.add_argument(
+ "--drop-implied-oncurves",
+ action="store_true",
+ help=(
+ "drop on-curve points that can be implied when exactly in the middle of "
+ "two off-curve points (only applies to TrueType fonts)"
+ ),
+ )
+ parser.add_argument(
+ "--master-finder",
+ default="master_ttf_interpolatable/{stem}.ttf",
+ help=(
+ "templated string used for finding binary font "
+ "files given the source file names defined in the "
+ "designspace document. The following special strings "
+ "are defined: {fullname} is the absolute source file "
+ "name; {basename} is the file name without its "
+ "directory; {stem} is the basename without the file "
+ "extension; {ext} is the source file extension; "
+ "{dirname} is the directory of the absolute file "
+ 'name. The default value is "%(default)s".'
+ ),
+ )
+ parser.add_argument(
+ "--variable-fonts",
+ default=".*",
+ metavar="VF_NAME",
+ help=(
+ "Filter the list of variable fonts produced from the input "
+ "Designspace v5 file. By default all listed variable fonts are "
+ "generated. To generate a specific variable font (or variable fonts) "
+ 'that match a given "name" attribute, you can pass as argument '
+ "the full name or a regular expression. E.g.: --variable-fonts "
+ '"MyFontVF_WeightOnly"; or --variable-fonts "MyFontVFItalic_.*".'
+ ),
+ )
+ logging_group = parser.add_mutually_exclusive_group(required=False)
+ logging_group.add_argument(
+ "-v", "--verbose", action="store_true", help="Run more verbosely."
+ )
+ logging_group.add_argument(
+ "-q", "--quiet", action="store_true", help="Turn verbosity off."
+ )
+ options = parser.parse_args(args)
+
+ configLogger(
+ level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO")
+ )
+
+ designspace_filename = options.designspace
+ designspace = DesignSpaceDocument.fromfile(designspace_filename)
+
+ vf_descriptors = designspace.getVariableFonts()
+ if not vf_descriptors:
+ parser.error(f"No variable fonts in given designspace {designspace.path!r}")
+
+ vfs_to_build = []
+ for vf in vf_descriptors:
+ # Skip variable fonts that do not match the user's inclusion regex if given.
+ if not fullmatch(options.variable_fonts, vf.name):
+ continue
+ vfs_to_build.append(vf)
+
+ if not vfs_to_build:
+ parser.error(f"No variable fonts matching {options.variable_fonts!r}")
+
+ if options.outfile is not None and len(vfs_to_build) > 1:
+ parser.error(
+ "can't specify -o because there are multiple VFs to build; "
+ "use --output-dir, or select a single VF with --variable-fonts"
+ )
+
+ output_dir = options.output_dir
+ if output_dir is None:
+ output_dir = os.path.dirname(designspace_filename)
+
+ vf_name_to_output_path = {}
+ if len(vfs_to_build) == 1 and options.outfile is not None:
+ vf_name_to_output_path[vfs_to_build[0].name] = options.outfile
+ else:
+ for vf in vfs_to_build:
+ filename = vf.filename if vf.filename is not None else vf.name + ".{ext}"
+ vf_name_to_output_path[vf.name] = os.path.join(output_dir, filename)
+
+ finder = MasterFinder(options.master_finder)
+
+ vfs = build_many(
+ designspace,
+ finder,
+ exclude=options.exclude,
+ optimize=options.optimize,
+ colr_layer_reuse=options.colr_layer_reuse,
+ drop_implied_oncurves=options.drop_implied_oncurves,
+ )
+
+ for vf_name, vf in vfs.items():
+ ext = "otf" if vf.sfntVersion == "OTTO" else "ttf"
+ output_path = vf_name_to_output_path[vf_name].format(ext=ext)
+ output_dir = os.path.dirname(output_path)
+ if output_dir:
+ os.makedirs(output_dir, exist_ok=True)
+ log.info("Saving variation font %s", output_path)
+ vf.save(output_path)
if __name__ == "__main__":
- import sys
- if len(sys.argv) > 1:
- sys.exit(main())
- import doctest
- sys.exit(doctest.testmod().failed)
+ import sys
+
+ if len(sys.argv) > 1:
+ sys.exit(main())
+ import doctest
+
+ sys.exit(doctest.testmod().failed)