[instancer] expand singles/tuples to triples upfront and use triples throughout

Also renamed AxisTent => AxisTriple because I think "tent" is more appropriate to visualize master supports (i.e. a triangle with a peak in the middle), here it's simply the new desired min, default, max values of an axis, or the axis limits.

The public facing instantiateVariableFont continues to accept a hybrid map of axis tags to single values or 2- (and now also 3-) tuples, these are normalized to an AxisLimits mapping that contains only AxisTriples and passed around to the internal methods.
This commit is contained in:
Cosimo Lupo 2022-10-21 17:49:35 +01:00
parent 40b2473a48
commit 86e1202129
No known key found for this signature in database
GPG Key ID: DF65A8A5A119C9A8
2 changed files with 279 additions and 187 deletions

View File

@ -51,19 +51,30 @@ Note that, unlike varLib.mutator, when an axis is not mentioned in the input
location, the varLib.instancer will keep the axis and the corresponding deltas,
whereas mutator implicitly drops the axis at its default coordinate.
The module currently supports only the first three "levels" of partial instancing,
with the rest planned to be implemented in the future, namely:
The module supports all the following "levels" of instancing, which can of
course be combined:
L1
dropping one or more axes while leaving the default tables unmodified;
| >>> font = instancer.instantiateVariableFont(varfont, {"wght": None})
L2
dropping one or more axes while pinning them at non-default locations;
| >>> font = instancer.instantiateVariableFont(varfont, {"wght": 700})
L3
restricting the range of variation of one or more axes, by setting either
a new minimum or maximum, potentially -- though not necessarily -- dropping
entire regions of variations that fall completely outside this new range.
| >>> font = instancer.instantiateVariableFont(varfont, {"wght": (100, 300)})
L4
moving the default location of an axis.
moving the default location of an axis, by specifying (min,defalt,max) values:
| >>> font = instancer.instantiateVariableFont(varfont, {"wght": (100, 300, 700)})
Currently only TrueType-flavored variable fonts (i.e. containing 'glyf' table)
are supported, but support for CFF2 variable fonts will be added soon.
@ -92,65 +103,246 @@ from fontTools.varLib.instancer import names
from fontTools.misc.cliTools import makeOutputFileName
from fontTools.varLib.instancer import solver
import collections
import dataclasses
from copy import deepcopy
from enum import IntEnum
import logging
import os
import re
from typing import Dict, Iterable, Mapping, Optional, Sequence, Tuple, Union
import warnings
log = logging.getLogger("fontTools.varLib.instancer")
def _expand(v):
if not isinstance(v, tuple):
return (v, v, v)
else:
if len(v) == 2:
return (v[0], None, v[1])
return v
def AxisRange(minimum, maximum):
warnings.warn(
"AxisRange is deprecated; use AxisTriple instead",
DeprecationWarning,
stacklevel=2,
)
return AxisTriple(minimum, None, maximum)
class AxisRange(collections.namedtuple("AxisRange", "minimum maximum")):
def __new__(cls, *args, **kwargs):
self = super().__new__(cls, *args, **kwargs)
if self.minimum > self.maximum:
def NormalizedAxisRange(minimum, maximum):
warnings.warn(
"NormalizedAxisRange is deprecated; use AxisTriple instead",
DeprecationWarning,
stacklevel=2,
)
return NormalizedAxisTriple(minimum, None, maximum)
@dataclasses.dataclass(frozen=True, order=True)
class AxisTriple(Sequence):
"""A triple of (min, default, max) axis values.
The default value can be None, in which case the populateDefault() method can be
used to fill in the missing default value based on the fvar axis default.
"""
minimum: float
default: Optional[float] # if None, filled with fvar default by populateDefault
maximum: float
def __post_init__(self):
if self.default is None and self.minimum == self.maximum:
object.__setattr__(self, "default", self.minimum)
if not (
(self.minimum <= self.default <= self.maximum)
if self.default is not None
else (self.minimum <= self.maximum)
):
raise ValueError(
f"Range minimum ({self.minimum:g}) must be <= maximum ({self.maximum:g})"
f"{type(self).__name__} minimum ({self.minimum}) must be <= default "
f"({self.default}) which must be <= maximum ({self.maximum})"
)
return self
def __repr__(self):
return f"{type(self).__name__}({self.minimum:g}, {self.maximum:g})"
def __getitem__(self, i):
fields = dataclasses.fields(self)
return getattr(self, fields[i].name)
def __len__(self):
return len(dataclasses.fields(self))
class NormalizedAxisRange(AxisRange):
def __new__(cls, *args, **kwargs):
self = super().__new__(cls, *args, **kwargs)
if self.minimum < -1.0 or self.maximum > 1.0:
raise ValueError("Axis range values must be normalized to -1..+1 range")
return self
def _replace(self, **kwargs):
return dataclasses.replace(self, **kwargs)
@classmethod
def expand(
cls,
v: Union[
"AxisTriple",
float, # pin axis at single value, same as min==default==max
Tuple[float, float], # (min, max), restrict axis and keep default
Tuple[float, float, float], # (min, default, max)
],
) -> "AxisTriple":
"""Convert a single value or a tuple into an AxisTriple.
class AxisTent(collections.namedtuple("AxisTent", "minimum default maximum")):
def __new__(cls, *args, **kwargs):
self = super().__new__(cls, *args, **kwargs)
if not (self.minimum <= self.default <= self.maximum):
If the input is a single value, it is interpreted as a pin at that value.
If the input is a tuple, it is interpreted as (min, max) or (min, default, max).
"""
if isinstance(v, cls):
return v
if isinstance(v, (int, float)):
return cls(v, v, v)
try:
n = len(v)
except TypeError as e:
raise ValueError(
f"Tent minimum ({self.minimum:g}) must be <= default ({self.default:g}) which must be <= maximum ({self.maximum:g})"
f"expected float, 2- or 3-tuple of floats; got {type(v)}: {v!r}"
) from e
default = None
if n == 2:
minimum, maximum = v
elif n == 3:
minimum, default, maximum = v
else:
raise ValueError(f"expected sequence of 2 or 3; got {n}: {v!r}")
return cls(minimum, default, maximum)
def populateDefault(self, fvarAxisDefault) -> "AxisTriple":
"""Return a new AxisTriple with the default value filled in.
Set default to fvar axis default if the latter is within the min/max range,
otherwise set default to the min or max value, whichever is closer to the
fvar axis default.
If the default value is already set, return self.
"""
if self.default is not None:
return self
default = max(self.minimum, min(self.maximum, fvarAxisDefault))
return dataclasses.replace(self, default=default)
@dataclasses.dataclass(frozen=True, order=True)
class NormalizedAxisTriple(AxisTriple):
"""A triple of (min, default, max) normalized axis values."""
minimum: float
default: float
maximum: float
def __post_init__(self):
if self.default is None:
object.__setattr__(self, "default", max(self.minimum, min(self.maximum, 0)))
if not (-1.0 <= self.minimum <= self.default <= self.maximum <= 1.0):
raise ValueError(
"Normalized axis values not in -1..+1 range; got "
f"minimum={self.minimum:g}, default={self.default:g}, maximum={self.maximum:g})"
)
return self
def __repr__(self):
return f"{type(self).__name__}({self.minimum:g}, {self.default:g}, {self.maximum:g})"
class NormalizedAxisTent(AxisTent):
def __new__(cls, *args, **kwargs):
self = super().__new__(cls, *args, **kwargs)
if self.minimum < -1.0 or self.maximum > 1.0:
raise ValueError("Axis tent values must be normalized to -1..+1 range")
return self
class _BaseAxisLimits(Mapping[str, AxisTriple]):
def __getitem__(self, key: str) -> AxisTriple:
return self._data[key]
def __iter__(self) -> Iterable[str]:
return iter(self._data)
def __len__(self) -> int:
return len(self._data)
def __repr__(self) -> str:
return f"{type(self).__name__}({self._data!r})"
def pinnedLocation(self) -> Dict[str, float]:
"""Return a location dict with only the pinned axes."""
return {k: v.default for k, v in self.items() if v.minimum == v.maximum}
class AxisLimits(_BaseAxisLimits):
"""Maps axis tags (str) to AxisTriple values."""
def __init__(self, *args, **kwargs):
self.have_defaults = True
self._data = data = {}
for k, v in dict(*args, **kwargs).items():
if v is None:
# will be filled in by populateDefaults
self.have_defaults = False
data[k] = v
else:
try:
triple = AxisTriple.expand(v)
except ValueError as e:
raise ValueError(f"Invalid axis limits for {k!r}: {v!r}") from e
if triple.default is None:
# also filled in by populateDefaults
self.have_defaults = False
data[k] = triple
def populateDefaults(self, varfont) -> "AxisLimits":
"""Return a new AxisLimits with defaults filled in from fvar table.
If all axis limits already have defaults, return self.
"""
if self.have_defaults:
return self
fvar = varfont["fvar"]
defaultValues = {a.axisTag: a.defaultValue for a in fvar.axes}
newLimits = {}
for axisTag, triple in self.items():
default = defaultValues[axisTag]
if triple is None:
newLimits[axisTag] = AxisTriple(default, default, default)
else:
newLimits[axisTag] = triple.populateDefault(default)
return type(self)(newLimits)
def normalize(self, varfont, usingAvar=True) -> "NormalizedAxisLimits":
"""Return a new NormalizedAxisLimits with normalized -1..0..+1 values.
If usingAvar is True, the avar table is used to warp the default normalization.
"""
fvar = varfont["fvar"]
badLimits = set(self.keys()).difference(a.axisTag for a in fvar.axes)
if badLimits:
raise ValueError("Cannot limit: {} not present in fvar".format(badLimits))
axes = {
a.axisTag: (a.minValue, a.defaultValue, a.maxValue)
for a in fvar.axes
if a.axisTag in self
}
avarSegments = {}
if usingAvar and "avar" in varfont:
avarSegments = varfont["avar"].segments
normalizedLimits = {}
for axis_tag, triple in axes.items():
if self[axis_tag] is None:
normalizedLimits[axis_tag] = NormalizedAxisTriple(0, 0, 0)
continue
minV, defaultV, maxV = self[axis_tag]
if defaultV is None:
defaultV = triple[1]
avarMapping = avarSegments.get(axis_tag, None)
normalizedLimits[axis_tag] = NormalizedAxisTriple(
*(normalize(v, triple, avarMapping) for v in (minV, defaultV, maxV))
)
return NormalizedAxisLimits(normalizedLimits)
class NormalizedAxisLimits(_BaseAxisLimits):
"""Maps axis tags (str) to NormalizedAxisTriple values."""
def __init__(self, *args, **kwargs):
self._data = data = {}
for k, v in dict(*args, **kwargs).items():
try:
triple = NormalizedAxisTriple.expand(v)
except ValueError as e:
raise ValueError(f"Invalid axis limits for {k!r}: {v!r}") from e
data[k] = triple
class OverlapMode(IntEnum):
@ -166,8 +358,9 @@ def instantiateTupleVariationStore(
"""Instantiate TupleVariation list at the given location, or limit axes' min/max.
The 'variations' list of TupleVariation objects is modified in-place.
The 'axisLimits' (dict) maps axis tags (str) to either a single coordinate along the
axis (float), or to minimum/maximum coordinates (NormalizedAxisRange).
The 'axisLimits' (dict) maps axis tags (str) to NormalizedAxisTriple namedtuples
specifying (minimum, default, maximum) in the -1,0,+1 normalized space. Pinned axes
have minimum == default == maximum.
A 'full' instance (i.e. static font) is produced when all the axes are pinned to
single coordinates; a 'partial' instance (i.e. a less variable font) is produced
@ -184,8 +377,8 @@ def instantiateTupleVariationStore(
Args:
variations: List[TupleVariation] from either 'gvar' or 'cvar'.
axisLimits: Dict[str, Union[float, NormalizedAxisRange]]: axes' coordinates for
the full or partial instance, or ranges for restricting an axis' min/max.
axisLimits: NormalizedAxisLimits: map from axis tags to (min, default, max)
normalized coordinates for the full or partial instance.
origCoords: GlyphCoordinates: default instance's coordinates for computing 'gvar'
inferred points (cf. table__g_l_y_f._getCoordinatesAndControls).
endPts: List[int]: indices of contour end points, for inferring 'gvar' deltas.
@ -230,11 +423,7 @@ def changeTupleVariationsAxisLimits(variations, axisLimits):
def changeTupleVariationAxisLimit(var, axisTag, axisLimit):
if not isinstance(axisLimit, tuple):
axisLimit = NormalizedAxisTent(axisLimit, axisLimit, axisLimit)
if isinstance(axisLimit, NormalizedAxisRange):
axisLimit = NormalizedAxisTent(axisLimit.minimum, 0, axisLimit.maximum)
assert isinstance(axisLimit, NormalizedAxisTent)
assert isinstance(axisLimit, NormalizedAxisTriple)
# Skip when current axis is missing (i.e. doesn't participate),
lower, peak, upper = var.axes.get(axisTag, (-1, 0, 1))
@ -409,9 +598,7 @@ def _remapVarIdxMap(table, attrName, varIndexMapping, glyphOrder):
# TODO(anthrotype) Add support for HVAR/VVAR in CFF2
def _instantiateVHVAR(varfont, axisLimits, tableFields):
location, axisRanges = splitAxisLocationAndRanges(
axisLimits, rangeType=NormalizedAxisRange
)
location = axisLimits.pinnedLocation()
tableTag = tableFields.tableTag
fvarAxes = varfont["fvar"].axes
# Deltas from gvar table have already been applied to the hmtx/vmtx. For full
@ -510,11 +697,7 @@ class _TupleVarStoreAdapter(object):
# rebuild regions whose axes were dropped or limited
self.rebuildRegions()
pinnedAxes = {
axisTag
for axisTag, v in axisLimits.items()
if (v.minimum == v.maximum if isinstance(v, tuple) else True)
}
pinnedAxes = set(axisLimits.pinnedLocation())
self.axisOrder = [
axisTag for axisTag in self.axisOrder if axisTag not in pinnedAxes
]
@ -559,9 +742,9 @@ def instantiateItemVariationStore(itemVarStore, fvarAxes, axisLimits):
Args:
varStore: An otTables.VarStore object (Item Variation Store)
fvarAxes: list of fvar's Axis objects
axisLimits: Dict[str, float] mapping axis tags to normalized axis coordinates
(float) or ranges for restricting an axis' min/max (NormalizedAxisRange).
May not specify coordinates/ranges for all the fvar axes.
axisLimits: NormalizedAxisLimits: mapping axis tags to normalized
min/default/max axis coordinates. May not specify coordinates/ranges for
all the fvar axes.
Returns:
defaultDeltas: to be added to the default instance, of type dict of floats
@ -691,11 +874,7 @@ def _limitFeatureVariationConditionRange(condition, axisLimit):
# condition invalid or out of range
return
values = [minValue, maxValue]
for i, value in enumerate(values):
values[i] = normalizeValue(value, axisLimit)
return AxisRange(*values)
return tuple(normalizeValue(v, axisLimit) for v in (minValue, maxValue))
def _instantiateFeatureVariationRecord(
@ -703,6 +882,7 @@ def _instantiateFeatureVariationRecord(
):
applies = True
newConditions = []
default_triple = NormalizedAxisTriple(-1, 0, +1)
for i, condition in enumerate(record.ConditionSet.ConditionTable):
if condition.Format == 1:
axisIdx = condition.AxisIndex
@ -710,12 +890,11 @@ def _instantiateFeatureVariationRecord(
minValue = condition.FilterRangeMinValue
maxValue = condition.FilterRangeMaxValue
triple = _expand(axisLimits.get(axisTag, (-1, 0, +1)))
v = triple[1]
if not (minValue <= v <= maxValue):
triple = axisLimits.get(axisTag, default_triple)
if not (minValue <= triple.default <= maxValue):
applies = False
# condition not met so remove entire record
if triple[0] > maxValue or triple[2] < minValue:
if triple.minimum > maxValue or triple.maximum < minValue:
newConditions = None
break
@ -742,7 +921,7 @@ def _instantiateFeatureVariationRecord(
def _limitFeatureVariationRecord(record, axisLimits, axisOrder):
newConditions = []
for i, condition in enumerate(record.ConditionSet.ConditionTable):
for condition in record.ConditionSet.ConditionTable:
if condition.Format == 1:
axisIdx = condition.AxisIndex
axisTag = axisOrder[axisIdx]
@ -750,10 +929,11 @@ def _limitFeatureVariationRecord(record, axisLimits, axisOrder):
axisLimit = axisLimits[axisTag]
newRange = _limitFeatureVariationConditionRange(condition, axisLimit)
if newRange:
# keep condition with updated limits and remapped axis index
condition.FilterRangeMinValue = newRange.minimum
condition.FilterRangeMaxValue = newRange.maximum
if newRange.minimum != -1 or newRange.maximum != +1:
# keep condition with updated limits
minimum, maximum = newRange
condition.FilterRangeMinValue = minimum
condition.FilterRangeMaxValue = maximum
if minimum != -1 or maximum != +1:
newConditions.append(condition)
else:
# condition out of range, remove entire record
@ -769,10 +949,7 @@ def _limitFeatureVariationRecord(record, axisLimits, axisOrder):
def _instantiateFeatureVariations(table, fvarAxes, axisLimits):
location, axisRanges = splitAxisLocationAndRanges(
axisLimits, rangeType=NormalizedAxisRange
)
pinnedAxes = set(location.keys())
pinnedAxes = set(axisLimits.pinnedLocation())
axisOrder = [axis.axisTag for axis in fvarAxes if axis.axisTag not in pinnedAxes]
axisIndexMap = {axisTag: axisOrder.index(axisTag) for axisTag in axisOrder}
@ -833,12 +1010,10 @@ def _isValidAvarSegmentMap(axisTag, segmentMap):
def instantiateAvar(varfont, axisLimits):
# 'axisLimits' dict must contain user-space (non-normalized) coordinates.
location, axisRanges = splitAxisLocationAndRanges(axisLimits)
segments = varfont["avar"].segments
# drop table if we instantiate all the axes
pinnedAxes = set(location.keys())
pinnedAxes = set(axisLimits.pinnedLocation())
if pinnedAxes.issuperset(segments):
log.info("Dropping avar table")
del varfont["avar"]
@ -849,7 +1024,7 @@ def instantiateAvar(varfont, axisLimits):
if axis in segments:
del segments[axis]
# First compute the default normalization for axisRanges coordinates: i.e.
# First compute the default normalization for axisLimits coordinates: i.e.
# min = -1.0, default = 0, max = +1.0, and in between values interpolated linearly,
# without using the avar table's mappings.
# Then, for each SegmentMap, if we are restricting its axis, compute the new
@ -857,7 +1032,7 @@ def instantiateAvar(varfont, axisLimits):
# dropping any mappings that fall outside the restricted range.
# The keys ('fromCoord') are specified in default normalized coordinate space,
# whereas the values ('toCoord') are "mapped forward" using the SegmentMap.
normalizedRanges = normalizeAxisLimits(varfont, axisLimits, usingAvar=False)
normalizedRanges = axisLimits.normalize(varfont, usingAvar=False)
newSegments = {}
for axisTag, mapping in segments.items():
if not _isValidAvarSegmentMap(axisTag, mapping):
@ -905,7 +1080,7 @@ def isInstanceWithinAxisRanges(location, axisRanges):
def instantiateFvar(varfont, axisLimits):
# 'axisLimits' dict must contain user-space (non-normalized) coordinates
location, axisRanges = splitAxisLocationAndRanges(axisLimits, rangeType=AxisRange)
location = axisLimits.pinnedLocation()
fvar = varfont["fvar"]
@ -937,7 +1112,7 @@ def instantiateFvar(varfont, axisLimits):
continue
for axisTag in location:
del instance.coordinates[axisTag]
if not isInstanceWithinAxisRanges(instance.coordinates, axisRanges):
if not isInstanceWithinAxisRanges(instance.coordinates, axisLimits):
continue
instances.append(instance)
fvar.instances = instances
@ -962,14 +1137,10 @@ def instantiateSTAT(varfont, axisLimits):
def axisValuesFromAxisLimits(stat, axisLimits):
location, axisRanges = splitAxisLocationAndRanges(axisLimits, rangeType=AxisRange)
def isAxisValueOutsideLimits(axisTag, axisValue):
if axisTag in location and axisValue != location[axisTag]:
return True
elif axisTag in axisRanges:
axisRange = axisRanges[axisTag]
if axisValue < axisRange.minimum or axisValue > axisRange.maximum:
if axisTag in axisLimits:
triple = axisLimits[axisTag]
if axisValue < triple.minimum or axisValue > triple.maximum:
return True
return False
@ -1026,43 +1197,6 @@ def normalize(value, triple, avarMapping):
return floatToFixedToFloat(value, 14)
def normalizeAxisLimits(varfont, axisLimits, usingAvar=True):
fvar = varfont["fvar"]
badLimits = set(axisLimits.keys()).difference(a.axisTag for a in fvar.axes)
if badLimits:
raise ValueError("Cannot limit: {} not present in fvar".format(badLimits))
axes = {
a.axisTag: (a.minValue, a.defaultValue, a.maxValue)
for a in fvar.axes
if a.axisTag in axisLimits
}
avarSegments = {}
if usingAvar and "avar" in varfont:
avarSegments = varfont["avar"].segments
normalizedLimits = {}
for axis_tag, triple in axes.items():
default = triple[1]
value = axisLimits[axis_tag]
minV, defaultV, maxV = _expand(value)
if defaultV is None:
defaultV = default
value = (minV, defaultV, maxV)
avarMapping = avarSegments.get(axis_tag, None)
normalizedLimits[axis_tag] = NormalizedAxisTent(
*(normalize(v, triple, avarMapping) for v in value)
)
return normalizedLimits
def sanityCheckVariableTables(varfont):
if "fvar" not in varfont:
raise ValueError("Missing required table fvar")
@ -1074,19 +1208,6 @@ def sanityCheckVariableTables(varfont):
raise NotImplementedError("Instancing CFF2 variable fonts is not supported yet")
def populateAxisDefaults(varfont, axisLimits):
if any(None in _expand(value) for value in axisLimits.values()):
fvar = varfont["fvar"]
defaultValues = {a.axisTag: a.defaultValue for a in fvar.axes}
return {
axisTag: tuple(
defaultValues[axisTag] if v is None else v for v in _expand(value)
)
for axisTag, value in axisLimits.items()
}
return axisLimits
def instantiateVariableFont(
varfont,
axisLimits,
@ -1139,9 +1260,9 @@ def instantiateVariableFont(
sanityCheckVariableTables(varfont)
axisLimits = populateAxisDefaults(varfont, axisLimits)
axisLimits = AxisLimits(axisLimits).populateDefaults(varfont)
normalizedLimits = normalizeAxisLimits(varfont, axisLimits)
normalizedLimits = axisLimits.normalize(varfont)
log.info("Normalized limits: %s", normalizedLimits)
@ -1193,14 +1314,7 @@ def instantiateVariableFont(
ignoreErrors=(overlap == OverlapMode.REMOVE_AND_IGNORE_ERRORS),
)
varLib.set_default_weight_width_slant(
varfont,
location={
axisTag: _expand(limit)[1]
for axisTag, limit in axisLimits.items()
if _expand(limit)[0] == _expand(limit)[2]
},
)
varLib.set_default_weight_width_slant(varfont, location=axisLimits.pinnedLocation())
if updateFontNames:
# Set Regular/Italic/Bold/Bold Italic bits as appropriate, after the
@ -1247,18 +1361,7 @@ def setRibbiBits(font):
font["OS/2"].fsSelection = selection
def splitAxisLocationAndRanges(axisLimits, rangeType=AxisRange):
location, axisRanges = {}, {}
for axisTag, value in axisLimits.items():
(minimum, default, maximum) = _expand(value)
if minimum == default == maximum:
location[axisTag] = default
else:
axisRanges[axisTag] = rangeType(minimum, maximum)
return location, axisRanges
def parseLimits(limits):
def parseLimits(limits: Iterable[str]) -> Dict[str, Optional[AxisTriple]]:
result = {}
for limitString in limits:
match = re.match(
@ -1280,20 +1383,11 @@ def parseLimits(limits):
default = ubound
ubound = strToFixedToFloat(match.group(5), precisionBits=16)
if lbound is not None:
if default is None:
assert lbound <= ubound
else:
assert lbound <= default <= ubound
if lbound == default == ubound:
result[tag] = lbound
continue
elif default is None:
result[tag] = AxisRange(lbound, ubound)
if all(v is None for v in (lbound, default, ubound)):
result[tag] = None
continue
result[tag] = AxisTent(lbound, default, ubound)
result[tag] = AxisTriple(lbound, default, ubound)
return result

View File

@ -86,7 +86,7 @@ def updateNameTable(varfont, axisLimits):
Example: Updating a partial variable font:
| >>> ttFont = TTFont("OpenSans[wdth,wght].ttf")
| >>> updateNameTable(ttFont, {"wght": AxisRange(400, 900), "wdth": 75})
| >>> updateNameTable(ttFont, {"wght": (400, 900), "wdth": 75})
The name table records will be updated in the following manner:
NameID 1 familyName: "Open Sans" --> "Open Sans Condensed"
@ -102,7 +102,7 @@ def updateNameTable(varfont, axisLimits):
https://docs.microsoft.com/en-us/typography/opentype/spec/stat
https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids
"""
from . import AxisRange, axisValuesFromAxisLimits
from . import AxisLimits, axisValuesFromAxisLimits
if "STAT" not in varfont:
raise ValueError("Cannot update name table since there is no STAT table.")
@ -113,17 +113,15 @@ def updateNameTable(varfont, axisLimits):
# The updated name table will reflect the new 'zero origin' of the font.
# If we're instantiating a partial font, we will populate the unpinned
# axes with their default axis values.
# axes with their default axis values from fvar.
axisLimits = AxisLimits(axisLimits).populateDefaults(varfont)
partialDefaults = {k: v.default for k, v in axisLimits.items()}
fvarDefaults = {a.axisTag: a.defaultValue for a in fvar.axes}
defaultAxisCoords = deepcopy(axisLimits)
for axisTag, val in fvarDefaults.items():
if axisTag not in defaultAxisCoords or isinstance(
defaultAxisCoords[axisTag], AxisRange
):
defaultAxisCoords[axisTag] = val
defaultAxisCoords = AxisLimits({**fvarDefaults, **partialDefaults})
assert all(v.minimum == v.maximum for v in defaultAxisCoords.values())
axisValueTables = axisValuesFromAxisLimits(stat, defaultAxisCoords)
checkAxisValuesExist(stat, axisValueTables, defaultAxisCoords)
checkAxisValuesExist(stat, axisValueTables, defaultAxisCoords.pinnedLocation())
# ignore "elidable" axis values, should be omitted in application font menus.
axisValueTables = [
@ -154,8 +152,8 @@ def checkAxisValuesExist(stat, axisValues, axisCoords):
missingAxes = set(axisCoords) - seen
if missingAxes:
missing = ", ".join(f"'{i}={axisCoords[i]}'" for i in missingAxes)
raise ValueError(f"Cannot find Axis Values [{missing}]")
missing = ", ".join(f"'{i}': {axisCoords[i]}" for i in missingAxes)
raise ValueError(f"Cannot find Axis Values {{{missing}}}")
def _sortAxisValues(axisValues):