Merge pull request #1753 from anthrotype/l3-instancer

[varLib.instancer] implement restricting axis ranges (aka L3)
This commit is contained in:
Cosimo Lupo 2019-11-18 10:55:56 +00:00 committed by GitHub
commit 6725b34566
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1550 additions and 215 deletions

View File

@ -21,6 +21,11 @@ __all__ = [
]
# the max value that can still fit in an F2Dot14:
# 1.99993896484375
MAX_F2DOT14 = 0x7FFF / (1 << 14)
def otRound(value):
"""Round float value to nearest integer towards +Infinity.
For fractional values of 0.5 and higher, take the next higher integer;

View File

@ -1,5 +1,6 @@
from fontTools.misc.py23 import *
from array import array
from fontTools.misc.fixedTools import MAX_F2DOT14
from fontTools.pens.basePen import LoggingPen
from fontTools.pens.transformPen import TransformPen
from fontTools.ttLib.tables import ttProgram
@ -11,11 +12,6 @@ from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
__all__ = ["TTGlyphPen"]
# the max value that can still fit in an F2Dot14:
# 1.99993896484375
MAX_F2DOT14 = 0x7FFF / (1 << 14)
class TTGlyphPen(LoggingPen):
"""Pen used for drawing to a TrueType glyph.

View File

@ -4,16 +4,17 @@ The module exports an `instantiateVariableFont` function and CLI that allow to
create full instances (i.e. static fonts) from variable fonts, as well as "partial"
variable fonts that only contain a subset of the original variation space.
For example, if you wish to pin the width axis to a given location while keeping
the rest of the axes, you can do:
For example, if you wish to pin the width axis to a given location while also
restricting the weight axis to 400..700 range, you can do:
$ fonttools varLib.instancer ./NotoSans-VF.ttf wdth=85
$ fonttools varLib.instancer ./NotoSans-VF.ttf wdth=85 wght=400:700
See `fonttools varLib.instancer --help` for more info on the CLI options.
The module's entry point is the `instantiateVariableFont` function, which takes
a TTFont object and a dict specifying a location along either some or all the axes,
and returns a new TTFont representing respectively a partial or a full instance.
a TTFont object and a dict specifying either axis coodinates or (min, max) ranges,
and returns a new TTFont representing either a partial VF, or full instance if all
the VF axes were given an explicit coordinate.
E.g. here's how to pin the wght axis at a given location in a wght+wdth variable
font, keeping only the deltas associated with the wdth axis:
@ -50,7 +51,7 @@ 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 two "levels" of partial instancing,
The module currently supports only the first three "levels" of partial instancing,
with the rest planned to be implemented in the future, namely:
L1) dropping one or more axes while leaving the default tables unmodified;
L2) dropping one or more axes while pinning them at non-default locations;
@ -65,7 +66,12 @@ are supported, but support for CFF2 variable fonts will be added soon.
The discussion and implementation of these features are tracked at
https://github.com/fonttools/fonttools/issues/1537
"""
from fontTools.misc.fixedTools import floatToFixedToFloat, otRound
from fontTools.misc.fixedTools import (
floatToFixedToFloat,
strToFixedToFloat,
otRound,
MAX_F2DOT14,
)
from fontTools.varLib.models import supportScalar, normalizeValue, piecewiseLinearMap
from fontTools.ttLib import TTFont
from fontTools.ttLib.tables.TupleVariation import TupleVariation
@ -90,12 +96,44 @@ import re
log = logging.getLogger("fontTools.varLib.instancer")
def instantiateTupleVariationStore(variations, location, origCoords=None, endPts=None):
"""Instantiate TupleVariation list at the given location.
class AxisRange(collections.namedtuple("AxisRange", "minimum maximum")):
def __new__(cls, *args, **kwargs):
self = super().__new__(cls, *args, **kwargs)
if self.minimum > self.maximum:
raise ValueError(
f"Range minimum ({self.minimum:g}) must be <= maximum ({self.maximum:g})"
)
return self
def __repr__(self):
return f"{type(self).__name__}({self.minimum:g}, {self.maximum:g})"
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")
if self.minimum > 0:
raise ValueError(f"Expected axis range minimum <= 0; got {self.minimum}")
if self.maximum < 0:
raise ValueError(f"Expected axis range maximum >= 0; got {self.maximum}")
return self
def instantiateTupleVariationStore(
variations, axisLimits, origCoords=None, endPts=None
):
"""Instantiate TupleVariation list at the given location, or limit axes' min/max.
The 'variations' list of TupleVariation objects is modified in-place.
The input location can describe either a full instance (all the axes are assigned an
explicit coordinate) or partial (some of the axes are omitted).
The 'axisLimits' (dict) maps axis tags (str) to either a single coordinate along the
axis (float), or to minimum/maximum coordinates (NormalizedAxisRange).
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
when some of the axes are omitted, or restricted with a new range.
Tuples that do not participate are kept as they are. Those that have 0 influence
at the given location are removed from the variation store.
Those that are fully instantiated (i.e. all their axes are being pinned) are also
@ -107,7 +145,8 @@ def instantiateTupleVariationStore(variations, location, origCoords=None, endPts
Args:
variations: List[TupleVariation] from either 'gvar' or 'cvar'.
location: Dict[str, float]: axes coordinates for the full or partial instance.
axisLimits: Dict[str, Union[float, NormalizedAxisRange]]: axes' coordinates for
the full or partial instance, or ranges for restricting an axis' min/max.
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.
@ -115,7 +154,44 @@ def instantiateTupleVariationStore(variations, location, origCoords=None, endPts
Returns:
List[float]: the overall delta adjustment after applicable deltas were summed.
"""
newVariations = collections.OrderedDict()
pinnedLocation, axisRanges = splitAxisLocationAndRanges(
axisLimits, rangeType=NormalizedAxisRange
)
newVariations = variations
if pinnedLocation:
newVariations = pinTupleVariationAxes(variations, pinnedLocation)
if axisRanges:
newVariations = limitTupleVariationAxisRanges(newVariations, axisRanges)
mergedVariations = collections.OrderedDict()
for var in newVariations:
# compute inferred deltas only for gvar ('origCoords' is None for cvar)
if origCoords is not None:
var.calcInferredDeltas(origCoords, endPts)
# merge TupleVariations with overlapping "tents"
axes = frozenset(var.axes.items())
if axes in mergedVariations:
mergedVariations[axes] += var
else:
mergedVariations[axes] = var
# drop TupleVariation if all axes have been pinned (var.axes.items() is empty);
# its deltas will be added to the default instance's coordinates
defaultVar = mergedVariations.pop(frozenset(), None)
for var in mergedVariations.values():
var.roundDeltas()
variations[:] = list(mergedVariations.values())
return defaultVar.coordinates if defaultVar is not None else []
def pinTupleVariationAxes(variations, location):
newVariations = []
for var in variations:
# Compute the scalar support of the axes to be pinned at the desired location,
# excluding any axes that we are not pinning.
@ -127,31 +203,119 @@ def instantiateTupleVariationStore(variations, location, origCoords=None, endPts
# no influence, drop the TupleVariation
continue
# compute inferred deltas only for gvar ('origCoords' is None for cvar)
if origCoords is not None:
var.calcInferredDeltas(origCoords, endPts)
var.scaleDeltas(scalar)
newVariations.append(var)
return newVariations
# merge TupleVariations with overlapping "tents"
axes = tuple(var.axes.items())
if axes in newVariations:
newVariations[axes] += var
def limitTupleVariationAxisRanges(variations, axisRanges):
for axisTag, axisRange in sorted(axisRanges.items()):
newVariations = []
for var in variations:
newVariations.extend(limitTupleVariationAxisRange(var, axisTag, axisRange))
variations = newVariations
return variations
def _negate(*values):
yield from (-1 * v for v in values)
def limitTupleVariationAxisRange(var, axisTag, axisRange):
if not isinstance(axisRange, NormalizedAxisRange):
axisRange = NormalizedAxisRange(*axisRange)
# skip when current axis is missing (i.e. doesn't participate), or when the
# 'tent' isn't fully on either the negative or positive side
lower, peak, upper = var.axes.get(axisTag, (-1, 0, 1))
if peak == 0 or lower > peak or peak > upper or (lower < 0 and upper > 0):
return [var]
negative = lower < 0
if negative:
if axisRange.minimum == -1.0:
return [var]
elif axisRange.minimum == 0.0:
return []
else:
newVariations[axes] = var
if axisRange.maximum == 1.0:
return [var]
elif axisRange.maximum == 0.0:
return []
# drop TupleVariation if all axes have been pinned (var.axes.items() is empty);
# its deltas will be added to the default instance's coordinates
defaultVar = newVariations.pop(tuple(), None)
limit = axisRange.minimum if negative else axisRange.maximum
for var in newVariations.values():
var.roundDeltas()
variations[:] = list(newVariations.values())
# Rebase axis bounds onto the new limit, which then becomes the new -1.0 or +1.0.
# The results are always positive, because both dividend and divisor are either
# all positive or all negative.
newLower = lower / limit
newPeak = peak / limit
newUpper = upper / limit
# for negative TupleVariation, swap lower and upper to simplify procedure
if negative:
newLower, newUpper = newUpper, newLower
return defaultVar.coordinates if defaultVar is not None else []
# special case when innermost bound == peak == limit
if newLower == newPeak == 1.0:
var.axes[axisTag] = (-1.0, -1.0, -1.0) if negative else (1.0, 1.0, 1.0)
return [var]
# case 1: the whole deltaset falls outside the new limit; we can drop it
elif newLower >= 1.0:
return []
# case 2: only the peak and outermost bound fall outside the new limit;
# we keep the deltaset, update peak and outermost bound and and scale deltas
# by the scalar value for the restricted axis at the new limit.
elif newPeak >= 1.0:
scalar = supportScalar({axisTag: limit}, {axisTag: (lower, peak, upper)})
var.scaleDeltas(scalar)
newPeak = 1.0
newUpper = 1.0
if negative:
newLower, newPeak, newUpper = _negate(newUpper, newPeak, newLower)
var.axes[axisTag] = (newLower, newPeak, newUpper)
return [var]
# case 3: peak falls inside but outermost limit still fits within F2Dot14 bounds;
# we keep deltas as is and only scale the axes bounds. Deltas beyond -1.0
# or +1.0 will never be applied as implementations must clamp to that range.
elif newUpper <= 2.0:
if negative:
newLower, newPeak, newUpper = _negate(newUpper, newPeak, newLower)
elif MAX_F2DOT14 < newUpper <= 2.0:
# we clamp +2.0 to the max F2Dot14 (~1.99994) for convenience
newUpper = MAX_F2DOT14
var.axes[axisTag] = (newLower, newPeak, newUpper)
return [var]
# case 4: new limit doesn't fit; we need to chop the deltaset into two 'tents',
# because the shape of a triangle with part of one side cut off cannot be
# represented as a triangle itself. It can be represented as sum of two triangles.
# NOTE: This increases the file size!
else:
# duplicate the tent, then adjust lower/peak/upper so that the outermost limit
# of the original tent is +/-2.0, whereas the new tent's starts as the old
# one peaks and maxes out at +/-1.0.
newVar = TupleVariation(var.axes, var.coordinates)
if negative:
var.axes[axisTag] = (-2.0, -1 * newPeak, -1 * newLower)
newVar.axes[axisTag] = (-1.0, -1.0, -1 * newPeak)
else:
var.axes[axisTag] = (newLower, newPeak, MAX_F2DOT14)
newVar.axes[axisTag] = (newPeak, 1.0, 1.0)
# the new tent's deltas are scaled by the difference between the scalar value
# for the old tent at the desired limit...
scalar1 = supportScalar({axisTag: limit}, {axisTag: (lower, peak, upper)})
# ... and the scalar value for the clamped tent (with outer limit +/-2.0),
# which can be simplified like this:
scalar2 = 1 / (2 - newPeak)
newVar.scaleDeltas(scalar1 - scalar2)
return [var, newVar]
def instantiateGvarGlyph(varfont, glyphname, location, optimize=True):
def instantiateGvarGlyph(varfont, glyphname, axisLimits, optimize=True):
glyf = varfont["glyf"]
coordinates, ctrl = glyf.getCoordinatesAndControls(glyphname, varfont)
endPts = ctrl.endPts
@ -163,7 +327,7 @@ def instantiateGvarGlyph(varfont, glyphname, location, optimize=True):
if tupleVarStore:
defaultDeltas = instantiateTupleVariationStore(
tupleVarStore, location, coordinates, endPts
tupleVarStore, axisLimits, coordinates, endPts
)
if defaultDeltas:
@ -191,7 +355,7 @@ def instantiateGvarGlyph(varfont, glyphname, location, optimize=True):
var.optimize(coordinates, endPts, isComposite)
def instantiateGvar(varfont, location, optimize=True):
def instantiateGvar(varfont, axisLimits, optimize=True):
log.info("Instantiating glyf/gvar tables")
gvar = varfont["gvar"]
@ -210,7 +374,7 @@ def instantiateGvar(varfont, location, optimize=True):
),
)
for glyphname in glyphnames:
instantiateGvarGlyph(varfont, glyphname, location, optimize=optimize)
instantiateGvarGlyph(varfont, glyphname, axisLimits, optimize=optimize)
if not gvar.variations:
del varfont["gvar"]
@ -222,12 +386,12 @@ def setCvarDeltas(cvt, deltas):
cvt[i] += otRound(delta)
def instantiateCvar(varfont, location):
def instantiateCvar(varfont, axisLimits):
log.info("Instantiating cvt/cvar tables")
cvar = varfont["cvar"]
defaultDeltas = instantiateTupleVariationStore(cvar.variations, location)
defaultDeltas = instantiateTupleVariationStore(cvar.variations, axisLimits)
if defaultDeltas:
setCvarDeltas(varfont["cvt "], defaultDeltas)
@ -253,13 +417,13 @@ def setMvarDeltas(varfont, deltas):
)
def instantiateMVAR(varfont, location):
def instantiateMVAR(varfont, axisLimits):
log.info("Instantiating MVAR table")
mvar = varfont["MVAR"].table
fvarAxes = varfont["fvar"].axes
varStore = mvar.VarStore
defaultDeltas = instantiateItemVariationStore(varStore, fvarAxes, location)
defaultDeltas = instantiateItemVariationStore(varStore, fvarAxes, axisLimits)
setMvarDeltas(varfont, defaultDeltas)
if varStore.VarRegionList.Region:
@ -277,12 +441,14 @@ def _remapVarIdxMap(table, attrName, varIndexMapping, glyphOrder):
# TODO(anthrotype) Add support for HVAR/VVAR in CFF2
def _instantiateVHVAR(varfont, location, tableFields):
def _instantiateVHVAR(varfont, axisLimits, tableFields):
tableTag = tableFields.tableTag
fvarAxes = varfont["fvar"].axes
# Deltas from gvar table have already been applied to the hmtx/vmtx. For full
# instances (i.e. all axes pinned), we can simply drop HVAR/VVAR and return
if set(location).issuperset(axis.axisTag for axis in fvarAxes):
if set(
axisTag for axisTag, value in axisLimits.items() if not isinstance(value, tuple)
).issuperset(axis.axisTag for axis in fvarAxes):
log.info("Dropping %s table", tableTag)
del varfont[tableTag]
return
@ -291,7 +457,7 @@ def _instantiateVHVAR(varfont, location, tableFields):
vhvar = varfont[tableTag].table
varStore = vhvar.VarStore
# since deltas were already applied, the return value here is ignored
instantiateItemVariationStore(varStore, fvarAxes, location)
instantiateItemVariationStore(varStore, fvarAxes, axisLimits)
if varStore.VarRegionList.Region:
# Only re-optimize VarStore if the HVAR/VVAR already uses indirect AdvWidthMap
@ -309,16 +475,14 @@ def _instantiateVHVAR(varfont, location, tableFields):
_remapVarIdxMap(
vhvar, tableFields.vOrigMapping, varIndexMapping, glyphOrder
)
else:
del varfont[tableTag]
def instantiateHVAR(varfont, location):
return _instantiateVHVAR(varfont, location, varLib.HVAR_FIELDS)
def instantiateHVAR(varfont, axisLimits):
return _instantiateVHVAR(varfont, axisLimits, varLib.HVAR_FIELDS)
def instantiateVVAR(varfont, location):
return _instantiateVHVAR(varfont, location, varLib.VVAR_FIELDS)
def instantiateVVAR(varfont, axisLimits):
return _instantiateVHVAR(varfont, axisLimits, varLib.VVAR_FIELDS)
class _TupleVarStoreAdapter(object):
@ -345,30 +509,47 @@ class _TupleVarStoreAdapter(object):
itemCounts.append(varData.ItemCount)
return cls(regions, axisOrder, tupleVarData, itemCounts)
def dropAxes(self, axes):
prunedRegions = (
frozenset(
(axisTag, support)
for axisTag, support in region.items()
if axisTag not in axes
def rebuildRegions(self):
# Collect the set of all unique region axes from the current TupleVariations.
# We use an OrderedDict to de-duplicate regions while keeping the order.
uniqueRegions = collections.OrderedDict.fromkeys(
(
frozenset(var.axes.items())
for variations in self.tupleVarData
for var in variations
)
for region in self.regions
)
# dedup regions while keeping original order
uniqueRegions = collections.OrderedDict.fromkeys(prunedRegions)
self.regions = [dict(items) for items in uniqueRegions if items]
self.axisOrder = [axisTag for axisTag in self.axisOrder if axisTag not in axes]
# Maintain the original order for the regions that pre-existed, appending
# the new regions at the end of the region list.
newRegions = []
for region in self.regions:
regionAxes = frozenset(region.items())
if regionAxes in uniqueRegions:
newRegions.append(region)
del uniqueRegions[regionAxes]
if uniqueRegions:
newRegions.extend(dict(region) for region in uniqueRegions)
self.regions = newRegions
def instantiate(self, location):
def instantiate(self, axisLimits):
defaultDeltaArray = []
for variations, itemCount in zip(self.tupleVarData, self.itemCounts):
defaultDeltas = instantiateTupleVariationStore(variations, location)
defaultDeltas = instantiateTupleVariationStore(variations, axisLimits)
if not defaultDeltas:
defaultDeltas = [0] * itemCount
defaultDeltaArray.append(defaultDeltas)
# remove pinned axes from all the regions
self.dropAxes(location.keys())
# rebuild regions whose axes were dropped or limited
self.rebuildRegions()
pinnedAxes = {
axisTag
for axisTag, value in axisLimits.items()
if not isinstance(value, tuple)
}
self.axisOrder = [
axisTag for axisTag in self.axisOrder if axisTag not in pinnedAxes
]
return defaultDeltaArray
@ -396,11 +577,12 @@ class _TupleVarStoreAdapter(object):
return itemVarStore
def instantiateItemVariationStore(itemVarStore, fvarAxes, location):
def instantiateItemVariationStore(itemVarStore, fvarAxes, axisLimits):
""" Compute deltas at partial location, and update varStore in-place.
Remove regions in which all axes were instanced, and scale the deltas of
the remaining regions where only some of the axes were instanced.
Remove regions in which all axes were instanced, or fall outside the new axis
limits. Scale the deltas of the remaining regions where only some of the axes
were instanced.
The number of VarData subtables, and the number of items within each, are
not modified, in order to keep the existing VariationIndex valid.
@ -409,15 +591,16 @@ def instantiateItemVariationStore(itemVarStore, fvarAxes, location):
Args:
varStore: An otTables.VarStore object (Item Variation Store)
fvarAxes: list of fvar's Axis objects
location: Dict[str, float] mapping axis tags to normalized axis coordinates.
May not specify coordinates for all the fvar axes.
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.
Returns:
defaultDeltas: to be added to the default instance, of type dict of floats
keyed by VariationIndex compound values: i.e. (outer << 16) + inner.
"""
tupleVarStore = _TupleVarStoreAdapter.fromItemVarStore(itemVarStore, fvarAxes)
defaultDeltaArray = tupleVarStore.instantiate(location)
defaultDeltaArray = tupleVarStore.instantiate(axisLimits)
newItemVarStore = tupleVarStore.asItemVarStore()
itemVarStore.VarRegionList = newItemVarStore.VarRegionList
@ -432,7 +615,7 @@ def instantiateItemVariationStore(itemVarStore, fvarAxes, location):
return defaultDeltas
def instantiateOTL(varfont, location):
def instantiateOTL(varfont, axisLimits):
# TODO(anthrotype) Support partial instancing of JSTF and BASE tables
if (
@ -452,7 +635,7 @@ def instantiateOTL(varfont, location):
varStore = gdef.VarStore
fvarAxes = varfont["fvar"].axes
defaultDeltas = instantiateItemVariationStore(varStore, fvarAxes, location)
defaultDeltas = instantiateItemVariationStore(varStore, fvarAxes, axisLimits)
# When VF are built, big lookups may overflow and be broken into multiple
# subtables. MutatorMerger (which inherits from AligningMerger) reattaches
@ -491,7 +674,7 @@ def instantiateOTL(varfont, location):
del varfont["GDEF"]
def instantiateFeatureVariations(varfont, location):
def instantiateFeatureVariations(varfont, axisLimits):
for tableTag in ("GPOS", "GSUB"):
if tableTag not in varfont or not hasattr(
varfont[tableTag].table, "FeatureVariations"
@ -499,7 +682,7 @@ def instantiateFeatureVariations(varfont, location):
continue
log.info("Instantiating FeatureVariations of %s table", tableTag)
_instantiateFeatureVariations(
varfont[tableTag].table, varfont["fvar"].axes, location
varfont[tableTag].table, varfont["fvar"].axes, axisLimits
)
# remove unreferenced lookups
varfont[tableTag].prune_lookups()
@ -527,10 +710,44 @@ def _featureVariationRecordIsUnique(rec, seen):
return True
def _limitFeatureVariationConditionRange(condition, axisRange):
minValue = condition.FilterRangeMinValue
maxValue = condition.FilterRangeMaxValue
if (
minValue > maxValue
or minValue > axisRange.maximum
or maxValue < axisRange.minimum
):
# condition invalid or out of range
return
values = [minValue, maxValue]
for i, value in enumerate(values):
if value < 0:
if axisRange.minimum == 0:
newValue = 0
else:
newValue = value / abs(axisRange.minimum)
if newValue <= -1.0:
newValue = -1.0
elif value > 0:
if axisRange.maximum == 0:
newValue = 0
else:
newValue = value / axisRange.maximum
if newValue >= 1.0:
newValue = 1.0
else:
newValue = 0
values[i] = newValue
return AxisRange(*values)
def _instantiateFeatureVariationRecord(
record, recIdx, location, fvarAxes, axisIndexMap
):
shouldKeep = False
applies = True
newConditions = []
for i, condition in enumerate(record.ConditionSet.ConditionTable):
@ -562,11 +779,48 @@ def _instantiateFeatureVariationRecord(
if newConditions:
record.ConditionSet.ConditionTable = newConditions
shouldKeep = True
else:
shouldKeep = False
return applies, shouldKeep
def _instantiateFeatureVariations(table, fvarAxes, location):
def _limitFeatureVariationRecord(record, axisRanges, fvarAxes):
newConditions = []
for i, condition in enumerate(record.ConditionSet.ConditionTable):
if condition.Format == 1:
axisIdx = condition.AxisIndex
axisTag = fvarAxes[axisIdx].axisTag
if axisTag in axisRanges:
axisRange = axisRanges[axisTag]
newRange = _limitFeatureVariationConditionRange(condition, axisRange)
if newRange:
# keep condition with updated limits and remapped axis index
condition.FilterRangeMinValue = newRange.minimum
condition.FilterRangeMaxValue = newRange.maximum
newConditions.append(condition)
else:
# condition out of range, remove entire record
newConditions = None
break
else:
newConditions.append(condition)
else:
newConditions.append(condition)
if newConditions:
record.ConditionSet.ConditionTable = newConditions
shouldKeep = True
else:
shouldKeep = False
return shouldKeep
def _instantiateFeatureVariations(table, fvarAxes, axisLimits):
location, axisRanges = splitAxisLocationAndRanges(
axisLimits, rangeType=NormalizedAxisRange
)
pinnedAxes = set(location.keys())
axisOrder = [axis.axisTag for axis in fvarAxes if axis.axisTag not in pinnedAxes]
axisIndexMap = {axisTag: axisOrder.index(axisTag) for axisTag in axisOrder}
@ -580,7 +834,9 @@ def _instantiateFeatureVariations(table, fvarAxes, location):
record, i, location, fvarAxes, axisIndexMap
)
if shouldKeep:
if _featureVariationRecordIsUnique(record, uniqueRecords):
shouldKeep = _limitFeatureVariationRecord(record, axisRanges, fvarAxes)
if shouldKeep and _featureVariationRecordIsUnique(record, uniqueRecords):
newRecords.append(record)
if applies and not featureVariationApplied:
@ -597,23 +853,111 @@ def _instantiateFeatureVariations(table, fvarAxes, location):
del table.FeatureVariations
def instantiateAvar(varfont, location):
def _isValidAvarSegmentMap(axisTag, segmentMap):
if not segmentMap:
return True
if not {(-1.0, -1.0), (0, 0), (1.0, 1.0)}.issubset(segmentMap.items()):
log.warning(
f"Invalid avar SegmentMap record for axis '{axisTag}': does not "
"include all required value maps {-1.0: -1.0, 0: 0, 1.0: 1.0}"
)
return False
previousValue = None
for fromCoord, toCoord in sorted(segmentMap.items()):
if previousValue is not None and previousValue > toCoord:
log.warning(
f"Invalid avar AxisValueMap({fromCoord}, {toCoord}) record "
f"for axis '{axisTag}': the toCoordinate value must be >= to "
f"the toCoordinate value of the preceding record ({previousValue})."
)
return False
previousValue = toCoord
return True
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
if set(location).issuperset(segments):
pinnedAxes = set(location.keys())
if pinnedAxes.issuperset(segments):
log.info("Dropping avar table")
del varfont["avar"]
return
log.info("Instantiating avar table")
for axis in location:
for axis in pinnedAxes:
if axis in segments:
del segments[axis]
# First compute the default normalization for axisRanges 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
# mappings by dividing the key/value pairs by the desired new min/max values,
# 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, axisRanges, usingAvar=False)
newSegments = {}
for axisTag, mapping in segments.items():
if not _isValidAvarSegmentMap(axisTag, mapping):
continue
if mapping and axisTag in normalizedRanges:
axisRange = normalizedRanges[axisTag]
mappedMin = floatToFixedToFloat(
piecewiseLinearMap(axisRange.minimum, mapping), 14
)
mappedMax = floatToFixedToFloat(
piecewiseLinearMap(axisRange.maximum, mapping), 14
)
newMapping = {}
for fromCoord, toCoord in mapping.items():
if fromCoord < 0:
if axisRange.minimum == 0 or fromCoord < axisRange.minimum:
continue
else:
fromCoord /= abs(axisRange.minimum)
elif fromCoord > 0:
if axisRange.maximum == 0 or fromCoord > axisRange.maximum:
continue
else:
fromCoord /= axisRange.maximum
if toCoord < 0:
assert mappedMin != 0
assert toCoord >= mappedMin
toCoord /= abs(mappedMin)
elif toCoord > 0:
assert mappedMax != 0
assert toCoord <= mappedMax
toCoord /= mappedMax
fromCoord = floatToFixedToFloat(fromCoord, 14)
toCoord = floatToFixedToFloat(toCoord, 14)
newMapping[fromCoord] = toCoord
newMapping.update({-1.0: -1.0, 1.0: 1.0})
newSegments[axisTag] = newMapping
else:
newSegments[axisTag] = mapping
varfont["avar"].segments = newSegments
def instantiateFvar(varfont, location):
# 'location' dict must contain user-space (non-normalized) coordinates
def isInstanceWithinAxisRanges(location, axisRanges):
for axisTag, coord in location.items():
if axisTag in axisRanges:
axisRange = axisRanges[axisTag]
if coord < axisRange.minimum or coord > axisRange.maximum:
return False
return True
def instantiateFvar(varfont, axisLimits):
# 'axisLimits' dict must contain user-space (non-normalized) coordinates
location, axisRanges = splitAxisLocationAndRanges(axisLimits, rangeType=AxisRange)
fvar = varfont["fvar"]
@ -625,73 +969,84 @@ def instantiateFvar(varfont, location):
log.info("Instantiating fvar table")
fvar.axes = [axis for axis in fvar.axes if axis.axisTag not in location]
axes = []
for axis in fvar.axes:
axisTag = axis.axisTag
if axisTag in location:
continue
if axisTag in axisRanges:
axis.minValue, axis.maxValue = axisRanges[axisTag]
axes.append(axis)
fvar.axes = axes
# only keep NamedInstances whose coordinates == pinned axis location
instances = []
for instance in fvar.instances:
if any(instance.coordinates[axis] != value for axis, value in location.items()):
continue
for axis in location:
del instance.coordinates[axis]
for axisTag in location:
del instance.coordinates[axisTag]
if not isInstanceWithinAxisRanges(instance.coordinates, axisRanges):
continue
instances.append(instance)
fvar.instances = instances
def instantiateSTAT(varfont, location):
pinnedAxes = set(location.keys())
def instantiateSTAT(varfont, axisLimits):
# 'axisLimits' dict must contain user-space (non-normalized) coordinates
stat = varfont["STAT"].table
if not stat.DesignAxisRecord:
return # skip empty STAT table
if not stat.DesignAxisRecord or not (
stat.AxisValueArray and stat.AxisValueArray.AxisValue
):
return # STAT table empty, nothing to do
designAxes = stat.DesignAxisRecord.Axis
pinnedAxisIndices = {
i for i, axis in enumerate(designAxes) if axis.AxisTag in pinnedAxes
}
location, axisRanges = splitAxisLocationAndRanges(axisLimits, rangeType=AxisRange)
if len(pinnedAxisIndices) == len(designAxes):
log.info("Dropping STAT table")
del varfont["STAT"]
return
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:
return True
return False
log.info("Instantiating STAT table")
# only keep DesignAxis that were not instanced, and build a mapping from old
# to new axis indices
newDesignAxes = []
axisIndexMap = {}
for i, axis in enumerate(designAxes):
if i not in pinnedAxisIndices:
axisIndexMap[i] = len(newDesignAxes)
newDesignAxes.append(axis)
if stat.AxisValueArray and stat.AxisValueArray.AxisValue:
# drop all AxisValue tables that reference any of the pinned axes
# only keep AxisValues whose axis is not pinned nor restricted, or is pinned at the
# exact (nominal) value, or is restricted but the value is within the new range
designAxes = stat.DesignAxisRecord.Axis
newAxisValueTables = []
for axisValueTable in stat.AxisValueArray.AxisValue:
if axisValueTable.Format in (1, 2, 3):
if axisValueTable.AxisIndex in pinnedAxisIndices:
continue
axisValueTable.AxisIndex = axisIndexMap[axisValueTable.AxisIndex]
newAxisValueTables.append(axisValueTable)
elif axisValueTable.Format == 4:
if any(
rec.AxisIndex in pinnedAxisIndices
for rec in axisValueTable.AxisValueRecord
):
continue
for rec in axisValueTable.AxisValueRecord:
rec.AxisIndex = axisIndexMap[rec.AxisIndex]
newAxisValueTables.append(axisValueTable)
axisValueFormat = axisValueTable.Format
if axisValueFormat in (1, 2, 3):
axisTag = designAxes[axisValueTable.AxisIndex].AxisTag
if axisValueFormat == 2:
axisValue = axisValueTable.NominalValue
else:
raise NotImplementedError(axisValueTable.Format)
axisValue = axisValueTable.Value
if isAxisValueOutsideLimits(axisTag, axisValue):
continue
elif axisValueFormat == 4:
# drop 'non-analytic' AxisValue if _any_ AxisValueRecord doesn't match
# the pinned location or is outside range
dropAxisValueTable = False
for rec in axisValueTable.AxisValueRecord:
axisTag = designAxes[rec.AxisIndex].AxisTag
axisValue = rec.Value
if isAxisValueOutsideLimits(axisTag, axisValue):
dropAxisValueTable = True
break
if dropAxisValueTable:
continue
else:
log.warn("Unknown AxisValue table format (%s); ignored", axisValueFormat)
newAxisValueTables.append(axisValueTable)
stat.AxisValueArray.AxisValue = newAxisValueTables
stat.AxisValueCount = len(stat.AxisValueArray.AxisValue)
stat.DesignAxisRecord.Axis[:] = newDesignAxes
stat.DesignAxisCount = len(stat.DesignAxisRecord.Axis)
def getVariationNameIDs(varfont):
used = []
@ -758,7 +1113,7 @@ def normalize(value, triple, avarMapping):
return floatToFixedToFloat(value, 14)
def normalizeAxisLimits(varfont, axisLimits):
def normalizeAxisLimits(varfont, axisLimits, usingAvar=True):
fvar = varfont["fvar"]
badLimits = set(axisLimits.keys()).difference(a.axisTag for a in fvar.axes)
if badLimits:
@ -771,15 +1126,26 @@ def normalizeAxisLimits(varfont, axisLimits):
}
avarSegments = {}
if "avar" in varfont:
if usingAvar and "avar" in varfont:
avarSegments = varfont["avar"].segments
for axis_tag, (_, default, _) in axes.items():
value = axisLimits[axis_tag]
if isinstance(value, tuple):
minV, maxV = value
if minV > default or maxV < default:
raise NotImplementedError(
f"Unsupported range {axis_tag}={minV:g}:{maxV:g}; "
f"can't change default position ({axis_tag}={default:g})"
)
normalizedLimits = {}
for axis_tag, triple in axes.items():
avarMapping = avarSegments.get(axis_tag, None)
value = axisLimits[axis_tag]
if isinstance(value, tuple):
normalizedLimits[axis_tag] = tuple(
normalize(v, triple, avarMapping) for v in axisLimits[axis_tag]
normalizedLimits[axis_tag] = NormalizedAxisRange(
*(normalize(v, triple, avarMapping) for v in value)
)
else:
normalizedLimits[axis_tag] = normalize(value, triple, avarMapping)
@ -841,18 +1207,14 @@ def instantiateVariableFont(
"""
sanityCheckVariableTables(varfont)
if not inplace:
varfont = deepcopy(varfont)
axisLimits = populateAxisDefaults(varfont, axisLimits)
normalizedLimits = normalizeAxisLimits(varfont, axisLimits)
log.info("Normalized limits: %s", normalizedLimits)
# TODO Remove this check once ranges are supported
if any(isinstance(v, tuple) for v in axisLimits.values()):
raise NotImplementedError("Axes range limits are not supported yet")
if not inplace:
varfont = deepcopy(varfont)
if "gvar" in varfont:
instantiateGvar(varfont, normalizedLimits, optimize=optimize)
@ -874,7 +1236,7 @@ def instantiateVariableFont(
instantiateFeatureVariations(varfont, normalizedLimits)
if "avar" in varfont:
instantiateAvar(varfont, normalizedLimits)
instantiateAvar(varfont, axisLimits)
with pruningUnusedNames(varfont):
if "STAT" in varfont:
@ -898,6 +1260,23 @@ def instantiateVariableFont(
return varfont
def splitAxisLocationAndRanges(axisLimits, rangeType=AxisRange):
location, axisRanges = {}, {}
for axisTag, value in axisLimits.items():
if isinstance(value, rangeType):
axisRanges[axisTag] = value
elif isinstance(value, (int, float)):
location[axisTag] = value
elif isinstance(value, tuple):
axisRanges[axisTag] = rangeType(*value)
else:
raise TypeError(
f"Expected number or {rangeType.__name__}, "
f"got {type(value).__name__}: {value!r}"
)
return location, axisRanges
def parseLimits(limits):
result = {}
for limitString in limits:
@ -908,12 +1287,12 @@ def parseLimits(limits):
if match.group(2): # 'drop'
lbound = None
else:
lbound = float(match.group(3))
lbound = strToFixedToFloat(match.group(3), precisionBits=16)
ubound = lbound
if match.group(4):
ubound = float(match.group(4))
ubound = strToFixedToFloat(match.group(4), precisionBits=16)
if lbound != ubound:
result[tag] = (lbound, ubound)
result[tag] = AxisRange(lbound, ubound)
else:
result[tag] = lbound
return result

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.41">
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.0">
<GlyphOrder>
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
@ -14,16 +14,16 @@
<!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="1.0"/>
<fontRevision value="2.001"/>
<checkSumAdjustment value="0x6b1f158e"/>
<checkSumAdjustment value="0x605c3e60"/>
<magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00000011"/>
<unitsPerEm value="1000"/>
<created value="Tue Mar 15 19:50:39 2016"/>
<modified value="Tue May 21 16:23:19 2019"/>
<xMin value="-621"/>
<yMin value="-389"/>
<xMax value="2800"/>
<yMax value="1067"/>
<modified value="Thu Oct 17 14:43:10 2019"/>
<xMin value="0"/>
<yMin value="0"/>
<xMax value="638"/>
<yMax value="944"/>
<macStyle value="00000000 00000000"/>
<lowestRecPPEM value="6"/>
<fontDirectionHint value="2"/>
@ -36,10 +36,10 @@
<ascent value="1069"/>
<descent value="-293"/>
<lineGap value="0"/>
<advanceWidthMax value="2840"/>
<minLeftSideBearing value="-621"/>
<minRightSideBearing value="-620"/>
<xMaxExtent value="2800"/>
<advanceWidthMax value="639"/>
<minLeftSideBearing value="0"/>
<minRightSideBearing value="1"/>
<xMaxExtent value="638"/>
<caretSlopeRise value="1"/>
<caretSlopeRun value="0"/>
<caretOffset value="0"/>
@ -55,10 +55,10 @@
<!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="0x10000"/>
<numGlyphs value="5"/>
<maxPoints value="202"/>
<maxContours value="24"/>
<maxCompositePoints value="315"/>
<maxCompositeContours value="21"/>
<maxPoints value="19"/>
<maxContours value="2"/>
<maxCompositePoints value="32"/>
<maxCompositeContours value="3"/>
<maxZones value="1"/>
<maxTwilightPoints value="0"/>
<maxStorage value="0"/>
@ -66,8 +66,8 @@
<maxInstructionDefs value="0"/>
<maxStackElements value="0"/>
<maxSizeOfInstructions value="0"/>
<maxComponentElements value="8"/>
<maxComponentDepth value="8"/>
<maxComponentElements value="2"/>
<maxComponentDepth value="1"/>
</maxp>
<OS_2>
@ -107,8 +107,8 @@
<ulUnicodeRange4 value="00000000 00010000 00000000 00000000"/>
<achVendID value="GOOG"/>
<fsSelection value="00000001 01000000"/>
<usFirstCharIndex value="0"/>
<usLastCharIndex value="65533"/>
<usFirstCharIndex value="65"/>
<usLastCharIndex value="192"/>
<sTypoAscender value="1069"/>
<sTypoDescender value="-293"/>
<sTypoLineGap value="0"/>
@ -1037,15 +1037,104 @@
<Axis index="0">
<AxisTag value="wght"/>
<AxisNameID value="256"/> <!-- Weight -->
<AxisOrdering value="0"/>
<AxisOrdering value="1"/>
</Axis>
<Axis index="1">
<AxisTag value="wdth"/>
<AxisNameID value="257"/> <!-- Width -->
<AxisOrdering value="1"/>
<AxisOrdering value="0"/>
</Axis>
</DesignAxisRecord>
<!-- AxisValueCount=0 -->
<!-- AxisValueCount=13 -->
<AxisValueArray>
<AxisValue index="0" Format="1">
<AxisIndex value="0"/>
<Flags value="0"/>
<ValueNameID value="258"/> <!-- Thin -->
<Value value="100.0"/>
</AxisValue>
<AxisValue index="1" Format="1">
<AxisIndex value="0"/>
<Flags value="0"/>
<ValueNameID value="259"/> <!-- ExtraLight -->
<Value value="200.0"/>
</AxisValue>
<AxisValue index="2" Format="1">
<AxisIndex value="0"/>
<Flags value="0"/>
<ValueNameID value="260"/> <!-- Light -->
<Value value="300.0"/>
</AxisValue>
<AxisValue index="3" Format="3">
<AxisIndex value="0"/>
<Flags value="2"/>
<ValueNameID value="261"/> <!-- Regular -->
<Value value="400.0"/>
<LinkedValue value="700.0"/>
</AxisValue>
<AxisValue index="4" Format="1">
<AxisIndex value="0"/>
<Flags value="0"/>
<ValueNameID value="262"/> <!-- Medium -->
<Value value="500.0"/>
</AxisValue>
<AxisValue index="5" Format="1">
<AxisIndex value="0"/>
<Flags value="0"/>
<ValueNameID value="263"/> <!-- SemiBold -->
<Value value="600.0"/>
</AxisValue>
<AxisValue index="6" Format="1">
<AxisIndex value="0"/>
<Flags value="0"/>
<ValueNameID value="264"/> <!-- Bold -->
<Value value="700.0"/>
</AxisValue>
<AxisValue index="7" Format="1">
<AxisIndex value="0"/>
<Flags value="0"/>
<ValueNameID value="265"/> <!-- ExtraBold -->
<Value value="800.0"/>
</AxisValue>
<AxisValue index="8" Format="1">
<AxisIndex value="0"/>
<Flags value="0"/>
<ValueNameID value="266"/> <!-- Black -->
<Value value="900.0"/>
</AxisValue>
<AxisValue index="9" Format="2">
<AxisIndex value="1"/>
<Flags value="2"/>
<ValueNameID value="261"/> <!-- Regular -->
<NominalValue value="100.0"/>
<RangeMinValue value="93.75"/>
<RangeMaxValue value="100.0"/>
</AxisValue>
<AxisValue index="10" Format="2">
<AxisIndex value="1"/>
<Flags value="0"/>
<ValueNameID value="270"/> <!-- SemiCondensed -->
<NominalValue value="87.5"/>
<RangeMinValue value="81.25"/>
<RangeMaxValue value="93.75"/>
</AxisValue>
<AxisValue index="11" Format="2">
<AxisIndex value="1"/>
<Flags value="0"/>
<ValueNameID value="279"/> <!-- Condensed -->
<NominalValue value="75.0"/>
<RangeMinValue value="68.75"/>
<RangeMaxValue value="81.25"/>
</AxisValue>
<AxisValue index="12" Format="2">
<AxisIndex value="1"/>
<Flags value="0"/>
<ValueNameID value="288"/> <!-- ExtraCondensed -->
<NominalValue value="62.5"/>
<RangeMinValue value="62.5"/>
<RangeMaxValue value="68.75"/>
</AxisValue>
</AxisValueArray>
<ElidedFallbackNameID value="2"/> <!-- Regular -->
</STAT>

View File

@ -14,12 +14,12 @@
<!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="1.0"/>
<fontRevision value="2.001"/>
<checkSumAdjustment value="0x982d27a8"/>
<checkSumAdjustment value="0x90f1c28"/>
<magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00000011"/>
<unitsPerEm value="1000"/>
<created value="Tue Mar 15 19:50:39 2016"/>
<modified value="Tue May 21 16:23:19 2019"/>
<modified value="Thu Oct 17 14:43:10 2019"/>
<xMin value="0"/>
<yMin value="0"/>
<xMax value="577"/>
@ -238,6 +238,18 @@
</glyf>
<name>
<namerecord nameID="256" platformID="1" platEncID="0" langID="0x0" unicode="True">
Weight
</namerecord>
<namerecord nameID="257" platformID="1" platEncID="0" langID="0x0" unicode="True">
Width
</namerecord>
<namerecord nameID="258" platformID="1" platEncID="0" langID="0x0" unicode="True">
Thin
</namerecord>
<namerecord nameID="261" platformID="1" platEncID="0" langID="0x0" unicode="True">
Regular
</namerecord>
<namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
Copyright 2015 Google Inc. All Rights Reserved.
</namerecord>
@ -283,6 +295,18 @@
<namerecord nameID="14" platformID="3" platEncID="1" langID="0x409">
http://scripts.sil.org/OFL
</namerecord>
<namerecord nameID="256" platformID="3" platEncID="1" langID="0x409">
Weight
</namerecord>
<namerecord nameID="257" platformID="3" platEncID="1" langID="0x409">
Width
</namerecord>
<namerecord nameID="258" platformID="3" platEncID="1" langID="0x409">
Thin
</namerecord>
<namerecord nameID="261" platformID="3" platEncID="1" langID="0x409">
Regular
</namerecord>
</name>
<post>
@ -481,4 +505,40 @@
</LookupList>
</GSUB>
<STAT>
<Version value="0x00010001"/>
<DesignAxisRecordSize value="8"/>
<!-- DesignAxisCount=2 -->
<DesignAxisRecord>
<Axis index="0">
<AxisTag value="wght"/>
<AxisNameID value="256"/> <!-- Weight -->
<AxisOrdering value="1"/>
</Axis>
<Axis index="1">
<AxisTag value="wdth"/>
<AxisNameID value="257"/> <!-- Width -->
<AxisOrdering value="0"/>
</Axis>
</DesignAxisRecord>
<!-- AxisValueCount=2 -->
<AxisValueArray>
<AxisValue index="0" Format="1">
<AxisIndex value="0"/>
<Flags value="0"/>
<ValueNameID value="258"/> <!-- Thin -->
<Value value="100.0"/>
</AxisValue>
<AxisValue index="1" Format="2">
<AxisIndex value="1"/>
<Flags value="2"/>
<ValueNameID value="261"/> <!-- Regular -->
<NominalValue value="100.0"/>
<RangeMinValue value="93.75"/>
<RangeMaxValue value="100.0"/>
</AxisValue>
</AxisValueArray>
<ElidedFallbackNameID value="2"/> <!-- Regular -->
</STAT>
</ttFont>

View File

@ -14,12 +14,12 @@
<!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="1.0"/>
<fontRevision value="2.001"/>
<checkSumAdjustment value="0x1d4f3a2e"/>
<checkSumAdjustment value="0x31525751"/>
<magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00000011"/>
<unitsPerEm value="1000"/>
<created value="Tue Mar 15 19:50:39 2016"/>
<modified value="Tue May 21 16:23:19 2019"/>
<modified value="Thu Oct 17 14:43:10 2019"/>
<xMin value="0"/>
<yMin value="0"/>
<xMax value="496"/>
@ -238,6 +238,18 @@
</glyf>
<name>
<namerecord nameID="256" platformID="1" platEncID="0" langID="0x0" unicode="True">
Weight
</namerecord>
<namerecord nameID="257" platformID="1" platEncID="0" langID="0x0" unicode="True">
Width
</namerecord>
<namerecord nameID="258" platformID="1" platEncID="0" langID="0x0" unicode="True">
Thin
</namerecord>
<namerecord nameID="288" platformID="1" platEncID="0" langID="0x0" unicode="True">
ExtraCondensed
</namerecord>
<namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
Copyright 2015 Google Inc. All Rights Reserved.
</namerecord>
@ -283,6 +295,18 @@
<namerecord nameID="14" platformID="3" platEncID="1" langID="0x409">
http://scripts.sil.org/OFL
</namerecord>
<namerecord nameID="256" platformID="3" platEncID="1" langID="0x409">
Weight
</namerecord>
<namerecord nameID="257" platformID="3" platEncID="1" langID="0x409">
Width
</namerecord>
<namerecord nameID="258" platformID="3" platEncID="1" langID="0x409">
Thin
</namerecord>
<namerecord nameID="288" platformID="3" platEncID="1" langID="0x409">
ExtraCondensed
</namerecord>
</name>
<post>
@ -481,4 +505,40 @@
</LookupList>
</GSUB>
<STAT>
<Version value="0x00010001"/>
<DesignAxisRecordSize value="8"/>
<!-- DesignAxisCount=2 -->
<DesignAxisRecord>
<Axis index="0">
<AxisTag value="wght"/>
<AxisNameID value="256"/> <!-- Weight -->
<AxisOrdering value="1"/>
</Axis>
<Axis index="1">
<AxisTag value="wdth"/>
<AxisNameID value="257"/> <!-- Width -->
<AxisOrdering value="0"/>
</Axis>
</DesignAxisRecord>
<!-- AxisValueCount=2 -->
<AxisValueArray>
<AxisValue index="0" Format="1">
<AxisIndex value="0"/>
<Flags value="0"/>
<ValueNameID value="258"/> <!-- Thin -->
<Value value="100.0"/>
</AxisValue>
<AxisValue index="1" Format="2">
<AxisIndex value="1"/>
<Flags value="0"/>
<ValueNameID value="288"/> <!-- ExtraCondensed -->
<NominalValue value="62.5"/>
<RangeMinValue value="62.5"/>
<RangeMaxValue value="68.75"/>
</AxisValue>
</AxisValueArray>
<ElidedFallbackNameID value="2"/> <!-- Regular -->
</STAT>
</ttFont>

View File

@ -14,12 +14,12 @@
<!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="1.0"/>
<fontRevision value="2.001"/>
<checkSumAdjustment value="0xf43664b4"/>
<checkSumAdjustment value="0x4b2d3480"/>
<magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00000011"/>
<unitsPerEm value="1000"/>
<created value="Tue Mar 15 19:50:39 2016"/>
<modified value="Tue May 21 16:23:19 2019"/>
<modified value="Thu Oct 17 14:43:10 2019"/>
<xMin value="0"/>
<yMin value="0"/>
<xMax value="638"/>
@ -238,6 +238,15 @@
</glyf>
<name>
<namerecord nameID="256" platformID="1" platEncID="0" langID="0x0" unicode="True">
Weight
</namerecord>
<namerecord nameID="257" platformID="1" platEncID="0" langID="0x0" unicode="True">
Width
</namerecord>
<namerecord nameID="261" platformID="1" platEncID="0" langID="0x0" unicode="True">
Regular
</namerecord>
<namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
Copyright 2015 Google Inc. All Rights Reserved.
</namerecord>
@ -283,6 +292,15 @@
<namerecord nameID="14" platformID="3" platEncID="1" langID="0x409">
http://scripts.sil.org/OFL
</namerecord>
<namerecord nameID="256" platformID="3" platEncID="1" langID="0x409">
Weight
</namerecord>
<namerecord nameID="257" platformID="3" platEncID="1" langID="0x409">
Width
</namerecord>
<namerecord nameID="261" platformID="3" platEncID="1" langID="0x409">
Regular
</namerecord>
</name>
<post>
@ -481,4 +499,41 @@
</LookupList>
</GSUB>
<STAT>
<Version value="0x00010001"/>
<DesignAxisRecordSize value="8"/>
<!-- DesignAxisCount=2 -->
<DesignAxisRecord>
<Axis index="0">
<AxisTag value="wght"/>
<AxisNameID value="256"/> <!-- Weight -->
<AxisOrdering value="1"/>
</Axis>
<Axis index="1">
<AxisTag value="wdth"/>
<AxisNameID value="257"/> <!-- Width -->
<AxisOrdering value="0"/>
</Axis>
</DesignAxisRecord>
<!-- AxisValueCount=2 -->
<AxisValueArray>
<AxisValue index="0" Format="3">
<AxisIndex value="0"/>
<Flags value="2"/>
<ValueNameID value="261"/> <!-- Regular -->
<Value value="400.0"/>
<LinkedValue value="700.0"/>
</AxisValue>
<AxisValue index="1" Format="2">
<AxisIndex value="1"/>
<Flags value="2"/>
<ValueNameID value="261"/> <!-- Regular -->
<NominalValue value="100.0"/>
<RangeMinValue value="93.75"/>
<RangeMaxValue value="100.0"/>
</AxisValue>
</AxisValueArray>
<ElidedFallbackNameID value="2"/> <!-- Regular -->
</STAT>
</ttFont>

View File

@ -14,12 +14,12 @@
<!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="1.0"/>
<fontRevision value="2.001"/>
<checkSumAdjustment value="0xd9290bac"/>
<checkSumAdjustment value="0x39ab2622"/>
<magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00000011"/>
<unitsPerEm value="1000"/>
<created value="Tue Mar 15 19:50:39 2016"/>
<modified value="Tue May 21 16:23:19 2019"/>
<modified value="Thu Oct 17 14:43:10 2019"/>
<xMin value="0"/>
<yMin value="0"/>
<xMax value="496"/>
@ -238,6 +238,18 @@
</glyf>
<name>
<namerecord nameID="256" platformID="1" platEncID="0" langID="0x0" unicode="True">
Weight
</namerecord>
<namerecord nameID="257" platformID="1" platEncID="0" langID="0x0" unicode="True">
Width
</namerecord>
<namerecord nameID="261" platformID="1" platEncID="0" langID="0x0" unicode="True">
Regular
</namerecord>
<namerecord nameID="288" platformID="1" platEncID="0" langID="0x0" unicode="True">
ExtraCondensed
</namerecord>
<namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
Copyright 2015 Google Inc. All Rights Reserved.
</namerecord>
@ -283,6 +295,18 @@
<namerecord nameID="14" platformID="3" platEncID="1" langID="0x409">
http://scripts.sil.org/OFL
</namerecord>
<namerecord nameID="256" platformID="3" platEncID="1" langID="0x409">
Weight
</namerecord>
<namerecord nameID="257" platformID="3" platEncID="1" langID="0x409">
Width
</namerecord>
<namerecord nameID="261" platformID="3" platEncID="1" langID="0x409">
Regular
</namerecord>
<namerecord nameID="288" platformID="3" platEncID="1" langID="0x409">
ExtraCondensed
</namerecord>
</name>
<post>
@ -481,4 +505,41 @@
</LookupList>
</GSUB>
<STAT>
<Version value="0x00010001"/>
<DesignAxisRecordSize value="8"/>
<!-- DesignAxisCount=2 -->
<DesignAxisRecord>
<Axis index="0">
<AxisTag value="wght"/>
<AxisNameID value="256"/> <!-- Weight -->
<AxisOrdering value="1"/>
</Axis>
<Axis index="1">
<AxisTag value="wdth"/>
<AxisNameID value="257"/> <!-- Width -->
<AxisOrdering value="0"/>
</Axis>
</DesignAxisRecord>
<!-- AxisValueCount=2 -->
<AxisValueArray>
<AxisValue index="0" Format="3">
<AxisIndex value="0"/>
<Flags value="2"/>
<ValueNameID value="261"/> <!-- Regular -->
<Value value="400.0"/>
<LinkedValue value="700.0"/>
</AxisValue>
<AxisValue index="1" Format="2">
<AxisIndex value="1"/>
<Flags value="0"/>
<ValueNameID value="288"/> <!-- ExtraCondensed -->
<NominalValue value="62.5"/>
<RangeMinValue value="62.5"/>
<RangeMaxValue value="68.75"/>
</AxisValue>
</AxisValueArray>
<ElidedFallbackNameID value="2"/> <!-- Regular -->
</STAT>
</ttFont>

View File

@ -14,12 +14,12 @@
<!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="1.0"/>
<fontRevision value="2.001"/>
<checkSumAdjustment value="0xa514fda"/>
<checkSumAdjustment value="0x7b5e7903"/>
<magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00000011"/>
<unitsPerEm value="1000"/>
<created value="Tue Mar 15 19:50:39 2016"/>
<modified value="Tue May 21 16:23:19 2019"/>
<modified value="Thu Oct 17 14:43:10 2019"/>
<xMin value="0"/>
<yMin value="0"/>
<xMax value="726"/>
@ -238,6 +238,18 @@
</glyf>
<name>
<namerecord nameID="256" platformID="1" platEncID="0" langID="0x0" unicode="True">
Weight
</namerecord>
<namerecord nameID="257" platformID="1" platEncID="0" langID="0x0" unicode="True">
Width
</namerecord>
<namerecord nameID="261" platformID="1" platEncID="0" langID="0x0" unicode="True">
Regular
</namerecord>
<namerecord nameID="266" platformID="1" platEncID="0" langID="0x0" unicode="True">
Black
</namerecord>
<namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
Copyright 2015 Google Inc. All Rights Reserved.
</namerecord>
@ -283,6 +295,18 @@
<namerecord nameID="14" platformID="3" platEncID="1" langID="0x409">
http://scripts.sil.org/OFL
</namerecord>
<namerecord nameID="256" platformID="3" platEncID="1" langID="0x409">
Weight
</namerecord>
<namerecord nameID="257" platformID="3" platEncID="1" langID="0x409">
Width
</namerecord>
<namerecord nameID="261" platformID="3" platEncID="1" langID="0x409">
Regular
</namerecord>
<namerecord nameID="266" platformID="3" platEncID="1" langID="0x409">
Black
</namerecord>
</name>
<post>
@ -481,4 +505,40 @@
</LookupList>
</GSUB>
<STAT>
<Version value="0x00010001"/>
<DesignAxisRecordSize value="8"/>
<!-- DesignAxisCount=2 -->
<DesignAxisRecord>
<Axis index="0">
<AxisTag value="wght"/>
<AxisNameID value="256"/> <!-- Weight -->
<AxisOrdering value="1"/>
</Axis>
<Axis index="1">
<AxisTag value="wdth"/>
<AxisNameID value="257"/> <!-- Width -->
<AxisOrdering value="0"/>
</Axis>
</DesignAxisRecord>
<!-- AxisValueCount=2 -->
<AxisValueArray>
<AxisValue index="0" Format="1">
<AxisIndex value="0"/>
<Flags value="0"/>
<ValueNameID value="266"/> <!-- Black -->
<Value value="900.0"/>
</AxisValue>
<AxisValue index="1" Format="2">
<AxisIndex value="1"/>
<Flags value="2"/>
<ValueNameID value="261"/> <!-- Regular -->
<NominalValue value="100.0"/>
<RangeMinValue value="93.75"/>
<RangeMaxValue value="100.0"/>
</AxisValue>
</AxisValueArray>
<ElidedFallbackNameID value="2"/> <!-- Regular -->
</STAT>
</ttFont>

View File

@ -14,12 +14,12 @@
<!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="1.0"/>
<fontRevision value="2.001"/>
<checkSumAdjustment value="0xc8e8b846"/>
<checkSumAdjustment value="0x7f9149e4"/>
<magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00000011"/>
<unitsPerEm value="1000"/>
<created value="Tue Mar 15 19:50:39 2016"/>
<modified value="Tue May 21 16:23:19 2019"/>
<modified value="Thu Oct 17 14:43:10 2019"/>
<xMin value="0"/>
<yMin value="0"/>
<xMax value="574"/>
@ -238,6 +238,18 @@
</glyf>
<name>
<namerecord nameID="256" platformID="1" platEncID="0" langID="0x0" unicode="True">
Weight
</namerecord>
<namerecord nameID="257" platformID="1" platEncID="0" langID="0x0" unicode="True">
Width
</namerecord>
<namerecord nameID="266" platformID="1" platEncID="0" langID="0x0" unicode="True">
Black
</namerecord>
<namerecord nameID="288" platformID="1" platEncID="0" langID="0x0" unicode="True">
ExtraCondensed
</namerecord>
<namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
Copyright 2015 Google Inc. All Rights Reserved.
</namerecord>
@ -283,6 +295,18 @@
<namerecord nameID="14" platformID="3" platEncID="1" langID="0x409">
http://scripts.sil.org/OFL
</namerecord>
<namerecord nameID="256" platformID="3" platEncID="1" langID="0x409">
Weight
</namerecord>
<namerecord nameID="257" platformID="3" platEncID="1" langID="0x409">
Width
</namerecord>
<namerecord nameID="266" platformID="3" platEncID="1" langID="0x409">
Black
</namerecord>
<namerecord nameID="288" platformID="3" platEncID="1" langID="0x409">
ExtraCondensed
</namerecord>
</name>
<post>
@ -481,4 +505,40 @@
</LookupList>
</GSUB>
<STAT>
<Version value="0x00010001"/>
<DesignAxisRecordSize value="8"/>
<!-- DesignAxisCount=2 -->
<DesignAxisRecord>
<Axis index="0">
<AxisTag value="wght"/>
<AxisNameID value="256"/> <!-- Weight -->
<AxisOrdering value="1"/>
</Axis>
<Axis index="1">
<AxisTag value="wdth"/>
<AxisNameID value="257"/> <!-- Width -->
<AxisOrdering value="0"/>
</Axis>
</DesignAxisRecord>
<!-- AxisValueCount=2 -->
<AxisValueArray>
<AxisValue index="0" Format="1">
<AxisIndex value="0"/>
<Flags value="0"/>
<ValueNameID value="266"/> <!-- Black -->
<Value value="900.0"/>
</AxisValue>
<AxisValue index="1" Format="2">
<AxisIndex value="1"/>
<Flags value="0"/>
<ValueNameID value="288"/> <!-- ExtraCondensed -->
<NominalValue value="62.5"/>
<RangeMinValue value="62.5"/>
<RangeMaxValue value="68.75"/>
</AxisValue>
</AxisValueArray>
<ElidedFallbackNameID value="2"/> <!-- Regular -->
</STAT>
</ttFont>

View File

@ -1,4 +1,5 @@
from fontTools.misc.py23 import *
from fontTools.misc.fixedTools import floatToFixedToFloat
from fontTools import ttLib
from fontTools import designspaceLib
from fontTools.feaLib.builder import addOpenTypeFeaturesFromString
@ -381,6 +382,26 @@ class InstantiateHVARTest(object):
assert "HVAR" not in varfont
def test_partial_instance_keep_empty_table(self, varfont):
# Append an additional dummy axis to fvar, for which the current HVAR table
# in our test 'varfont' contains no variation data.
# Instancing the other two wght and wdth axes should leave HVAR table empty,
# to signal there are variations to the glyph's advance widths.
fvar = varfont["fvar"]
axis = _f_v_a_r.Axis()
axis.axisTag = "TEST"
fvar.axes.append(axis)
instancer.instantiateHVAR(varfont, {"wght": 0, "wdth": 0})
assert "HVAR" in varfont
varStore = varfont["HVAR"].table.VarStore
assert varStore.VarRegionList.RegionCount == 0
assert not varStore.VarRegionList.Region
assert varStore.VarRegionList.RegionAxisCount == 1
class InstantiateItemVariationStoreTest(object):
def test_VarRegion_get_support(self):
@ -493,33 +514,40 @@ class TupleVarStoreAdapterTest(object):
[TupleVariation({"wdth": (-1.0, -1.0, 0)}, [-12, 8])],
]
def test_dropAxes(self):
def test_rebuildRegions(self):
regions = [
{"wght": (-1.0, -1.0, 0)},
{"wght": (0.0, 1.0, 1.0)},
{"wdth": (-1.0, -1.0, 0)},
{"opsz": (0.0, 1.0, 1.0)},
{"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)},
{"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)},
{"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)},
{"wght": (0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)},
]
axisOrder = ["wght", "wdth", "opsz"]
adapter = instancer._TupleVarStoreAdapter(regions, axisOrder, [], itemCounts=[])
axisOrder = ["wght", "wdth"]
variations = []
for region in regions:
variations.append(TupleVariation(region, [100]))
tupleVarData = [variations[:3], variations[3:]]
adapter = instancer._TupleVarStoreAdapter(
regions, axisOrder, tupleVarData, itemCounts=[1, 1]
)
adapter.dropAxes({"wdth"})
adapter.rebuildRegions()
assert adapter.regions == regions
del tupleVarData[0][2]
tupleVarData[1][0].axes = {"wght": (-1.0, -0.5, 0)}
tupleVarData[1][1].axes = {"wght": (0, 0.5, 1.0)}
adapter.rebuildRegions()
assert adapter.regions == [
{"wght": (-1.0, -1.0, 0)},
{"wght": (0.0, 1.0, 1.0)},
{"opsz": (0.0, 1.0, 1.0)},
{"wght": (0.0, 0.5, 1.0)},
{"wght": (0.5, 1.0, 1.0)},
{"wght": (-1.0, -0.5, 0)},
{"wght": (0, 0.5, 1.0)},
]
adapter.dropAxes({"wght", "opsz"})
assert adapter.regions == []
def test_roundtrip(self, fvarAxes):
regions = [
{"wght": (-1.0, -1.0, 0)},
@ -924,6 +952,208 @@ class InstantiateAvarTest(object):
assert "avar" not in varfont
@staticmethod
def quantizeF2Dot14Floats(mapping):
return {
floatToFixedToFloat(k, 14): floatToFixedToFloat(v, 14)
for k, v in mapping.items()
}
# the following values come from NotoSans-VF.ttf
DFLT_WGHT_MAPPING = {
-1.0: -1.0,
-0.6667: -0.7969,
-0.3333: -0.5,
0: 0,
0.2: 0.18,
0.4: 0.38,
0.6: 0.61,
0.8: 0.79,
1.0: 1.0,
}
DFLT_WDTH_MAPPING = {-1.0: -1.0, -0.6667: -0.7, -0.3333: -0.36664, 0: 0, 1.0: 1.0}
@pytest.fixture
def varfont(self):
fvarAxes = ("wght", (100, 400, 900)), ("wdth", (62.5, 100, 100))
avarSegments = {
"wght": self.quantizeF2Dot14Floats(self.DFLT_WGHT_MAPPING),
"wdth": self.quantizeF2Dot14Floats(self.DFLT_WDTH_MAPPING),
}
varfont = ttLib.TTFont()
varfont["name"] = ttLib.newTable("name")
varLib._add_fvar(varfont, _makeDSAxesDict(fvarAxes), instances=())
avar = varfont["avar"] = ttLib.newTable("avar")
avar.segments = avarSegments
return varfont
@pytest.mark.parametrize(
"axisLimits, expectedSegments",
[
pytest.param(
{"wght": (100, 900)},
{"wght": DFLT_WGHT_MAPPING, "wdth": DFLT_WDTH_MAPPING},
id="wght=100:900",
),
pytest.param(
{"wght": (400, 900)},
{
"wght": {
-1.0: -1.0,
0: 0,
0.2: 0.18,
0.4: 0.38,
0.6: 0.61,
0.8: 0.79,
1.0: 1.0,
},
"wdth": DFLT_WDTH_MAPPING,
},
id="wght=400:900",
),
pytest.param(
{"wght": (100, 400)},
{
"wght": {
-1.0: -1.0,
-0.6667: -0.7969,
-0.3333: -0.5,
0: 0,
1.0: 1.0,
},
"wdth": DFLT_WDTH_MAPPING,
},
id="wght=100:400",
),
pytest.param(
{"wght": (400, 800)},
{
"wght": {
-1.0: -1.0,
0: 0,
0.25: 0.22784,
0.50006: 0.48103,
0.75: 0.77214,
1.0: 1.0,
},
"wdth": DFLT_WDTH_MAPPING,
},
id="wght=400:800",
),
pytest.param(
{"wght": (400, 700)},
{
"wght": {
-1.0: -1.0,
0: 0,
0.3334: 0.2951,
0.66675: 0.623,
1.0: 1.0,
},
"wdth": DFLT_WDTH_MAPPING,
},
id="wght=400:700",
),
pytest.param(
{"wght": (400, 600)},
{
"wght": {-1.0: -1.0, 0: 0, 0.5: 0.47363, 1.0: 1.0},
"wdth": DFLT_WDTH_MAPPING,
},
id="wght=400:600",
),
pytest.param(
{"wdth": (62.5, 100)},
{
"wght": DFLT_WGHT_MAPPING,
"wdth": {
-1.0: -1.0,
-0.6667: -0.7,
-0.3333: -0.36664,
0: 0,
1.0: 1.0,
},
},
id="wdth=62.5:100",
),
pytest.param(
{"wdth": (70, 100)},
{
"wght": DFLT_WGHT_MAPPING,
"wdth": {
-1.0: -1.0,
-0.8334: -0.85364,
-0.4166: -0.44714,
0: 0,
1.0: 1.0,
},
},
id="wdth=70:100",
),
pytest.param(
{"wdth": (75, 100)},
{
"wght": DFLT_WGHT_MAPPING,
"wdth": {-1.0: -1.0, -0.49994: -0.52374, 0: 0, 1.0: 1.0},
},
id="wdth=75:100",
),
pytest.param(
{"wdth": (77, 100)},
{
"wght": DFLT_WGHT_MAPPING,
"wdth": {-1.0: -1.0, -0.54346: -0.56696, 0: 0, 1.0: 1.0},
},
id="wdth=77:100",
),
pytest.param(
{"wdth": (87.5, 100)},
{"wght": DFLT_WGHT_MAPPING, "wdth": {-1.0: -1.0, 0: 0, 1.0: 1.0}},
id="wdth=87.5:100",
),
],
)
def test_limit_axes(self, varfont, axisLimits, expectedSegments):
instancer.instantiateAvar(varfont, axisLimits)
newSegments = varfont["avar"].segments
expectedSegments = {
axisTag: self.quantizeF2Dot14Floats(mapping)
for axisTag, mapping in expectedSegments.items()
}
assert newSegments == expectedSegments
@pytest.mark.parametrize(
"invalidSegmentMap",
[
pytest.param({0.5: 0.5}, id="missing-required-maps-1"),
pytest.param({-1.0: -1.0, 1.0: 1.0}, id="missing-required-maps-2"),
pytest.param(
{-1.0: -1.0, 0: 0, 0.5: 0.5, 0.6: 0.4, 1.0: 1.0},
id="retrograde-value-maps",
),
],
)
def test_drop_invalid_segment_map(self, varfont, invalidSegmentMap, caplog):
varfont["avar"].segments["wght"] = invalidSegmentMap
with caplog.at_level(logging.WARNING, logger="fontTools.varLib.instancer"):
instancer.instantiateAvar(varfont, {"wght": (100, 400)})
assert "Invalid avar" in caplog.text
assert "wght" not in varfont["avar"].segments
def test_isValidAvarSegmentMap(self):
assert instancer._isValidAvarSegmentMap("FOOO", {})
assert instancer._isValidAvarSegmentMap("FOOO", {-1.0: -1.0, 0: 0, 1.0: 1.0})
assert instancer._isValidAvarSegmentMap(
"FOOO", {-1.0: -1.0, 0: 0, 0.5: 0.5, 1.0: 1.0}
)
assert instancer._isValidAvarSegmentMap(
"FOOO", {-1.0: -1.0, 0: 0, 0.5: 0.5, 0.7: 0.5, 1.0: 1.0}
)
class InstantiateFvarTest(object):
@pytest.mark.parametrize(
@ -979,7 +1209,7 @@ class InstantiateSTATTest(object):
@pytest.mark.parametrize(
"location, expected",
[
({"wght": 400}, ["Condensed", "Upright"]),
({"wght": 400}, ["Regular", "Condensed", "Upright"]),
({"wdth": 100}, ["Thin", "Regular", "Black", "Upright"]),
],
)
@ -989,7 +1219,7 @@ class InstantiateSTATTest(object):
stat = varfont["STAT"].table
designAxes = {a.AxisTag for a in stat.DesignAxisRecord.Axis}
assert designAxes == {"wght", "wdth", "ital"}.difference(location)
assert designAxes == {"wght", "wdth", "ital"}
name = varfont["name"]
valueNames = []
@ -999,7 +1229,23 @@ class InstantiateSTATTest(object):
assert valueNames == expected
def test_skip_empty_table(self, varfont):
def test_skip_table_no_axis_value_array(self, varfont):
varfont["STAT"].table.AxisValueArray = None
instancer.instantiateSTAT(varfont, {"wght": 100})
assert len(varfont["STAT"].table.DesignAxisRecord.Axis) == 3
assert varfont["STAT"].table.AxisValueArray is None
def test_skip_table_axis_value_array_empty(self, varfont):
varfont["STAT"].table.AxisValueArray.AxisValue = []
instancer.instantiateSTAT(varfont, {"wght": 100})
assert len(varfont["STAT"].table.DesignAxisRecord.Axis) == 3
assert not varfont["STAT"].table.AxisValueArray.AxisValue
def test_skip_table_no_design_axes(self, varfont):
stat = otTables.STAT()
stat.Version = 0x00010001
stat.populateDefaults()
@ -1011,21 +1257,88 @@ class InstantiateSTATTest(object):
assert not varfont["STAT"].table.DesignAxisRecord
def test_drop_table(self, varfont):
stat = otTables.STAT()
stat.Version = 0x00010001
stat.populateDefaults()
stat.DesignAxisRecord = otTables.AxisRecordArray()
axis = otTables.AxisRecord()
axis.AxisTag = "wght"
axis.AxisNameID = 0
axis.AxisOrdering = 0
stat.DesignAxisRecord.Axis = [axis]
varfont["STAT"].table = stat
@staticmethod
def get_STAT_axis_values(stat):
axes = stat.DesignAxisRecord.Axis
result = []
for axisValue in stat.AxisValueArray.AxisValue:
if axisValue.Format == 1:
result.append((axes[axisValue.AxisIndex].AxisTag, axisValue.Value))
elif axisValue.Format == 3:
result.append(
(
axes[axisValue.AxisIndex].AxisTag,
(axisValue.Value, axisValue.LinkedValue),
)
)
elif axisValue.Format == 2:
result.append(
(
axes[axisValue.AxisIndex].AxisTag,
(
axisValue.RangeMinValue,
axisValue.NominalValue,
axisValue.RangeMaxValue,
),
)
)
elif axisValue.Format == 4:
result.append(
tuple(
(axes[rec.AxisIndex].AxisTag, rec.Value)
for rec in axisValue.AxisValueRecord
)
)
else:
raise AssertionError(axisValue.Format)
return result
instancer.instantiateSTAT(varfont, {"wght": 100})
def test_limit_axes(self, varfont2):
instancer.instantiateSTAT(varfont2, {"wght": (400, 500), "wdth": (75, 100)})
assert "STAT" not in varfont
assert len(varfont2["STAT"].table.AxisValueArray.AxisValue) == 5
assert self.get_STAT_axis_values(varfont2["STAT"].table) == [
("wght", (400.0, 700.0)),
("wght", 500.0),
("wdth", (93.75, 100.0, 100.0)),
("wdth", (81.25, 87.5, 93.75)),
("wdth", (68.75, 75.0, 81.25)),
]
def test_limit_axis_value_format_4(self, varfont2):
stat = varfont2["STAT"].table
axisValue = otTables.AxisValue()
axisValue.Format = 4
axisValue.AxisValueRecord = []
for tag, value in (("wght", 575), ("wdth", 90)):
rec = otTables.AxisValueRecord()
rec.AxisIndex = next(
i for i, a in enumerate(stat.DesignAxisRecord.Axis) if a.AxisTag == tag
)
rec.Value = value
axisValue.AxisValueRecord.append(rec)
stat.AxisValueArray.AxisValue.append(axisValue)
instancer.instantiateSTAT(varfont2, {"wght": (100, 600)})
assert axisValue in varfont2["STAT"].table.AxisValueArray.AxisValue
instancer.instantiateSTAT(varfont2, {"wdth": (62.5, 87.5)})
assert axisValue not in varfont2["STAT"].table.AxisValueArray.AxisValue
def test_unknown_axis_value_format(self, varfont2, caplog):
stat = varfont2["STAT"].table
axisValue = otTables.AxisValue()
axisValue.Format = 5
stat.AxisValueArray.AxisValue.append(axisValue)
with caplog.at_level(logging.WARNING, logger="fontTools.varLib.instancer"):
instancer.instantiateSTAT(varfont2, {"wght": 400})
assert "Unknown AxisValue table format (5)" in caplog.text
assert axisValue in varfont2["STAT"].table.AxisValueArray.AxisValue
def test_pruningUnusedNames(varfont):
@ -1321,12 +1634,204 @@ class InstantiateFeatureVariationsTest(object):
assert rec1.ConditionSet.ConditionTable[0].Format == 2
class LimitTupleVariationAxisRangesTest:
def check_limit_single_var_axis_range(self, var, axisTag, axisRange, expected):
result = instancer.limitTupleVariationAxisRange(var, axisTag, axisRange)
print(result)
assert len(result) == len(expected)
for v1, v2 in zip(result, expected):
assert v1.coordinates == pytest.approx(v2.coordinates)
assert v1.axes.keys() == v2.axes.keys()
for k in v1.axes:
p, q = v1.axes[k], v2.axes[k]
assert p == pytest.approx(q)
@pytest.mark.parametrize(
"var, axisTag, newMax, expected",
[
(
TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]),
"wdth",
0.5,
[TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100])],
),
(
TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]),
"wght",
0.5,
[TupleVariation({"wght": (0.0, 1.0, 1.0)}, [50, 50])],
),
(
TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]),
"wght",
0.8,
[TupleVariation({"wght": (0.0, 1.0, 1.0)}, [80, 80])],
),
(
TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]),
"wght",
1.0,
[TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100])],
),
(TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]), "wght", 0.0, []),
(TupleVariation({"wght": (0.5, 1.0, 1.0)}, [100, 100]), "wght", 0.4, []),
(
TupleVariation({"wght": (0.0, 0.5, 1.0)}, [100, 100]),
"wght",
0.5,
[TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100])],
),
(
TupleVariation({"wght": (0.0, 0.5, 1.0)}, [100, 100]),
"wght",
0.4,
[TupleVariation({"wght": (0.0, 1.0, 1.0)}, [80, 80])],
),
(
TupleVariation({"wght": (0.0, 0.5, 1.0)}, [100, 100]),
"wght",
0.6,
[TupleVariation({"wght": (0.0, 0.833334, 1.666667)}, [100, 100])],
),
(
TupleVariation({"wght": (0.0, 0.2, 1.0)}, [100, 100]),
"wght",
0.4,
[
TupleVariation({"wght": (0.0, 0.5, 1.99994)}, [100, 100]),
TupleVariation({"wght": (0.5, 1.0, 1.0)}, [8.33333, 8.33333]),
],
),
(
TupleVariation({"wght": (0.0, 0.2, 1.0)}, [100, 100]),
"wght",
0.5,
[TupleVariation({"wght": (0.0, 0.4, 1.99994)}, [100, 100])],
),
(
TupleVariation({"wght": (0.5, 0.5, 1.0)}, [100, 100]),
"wght",
0.5,
[TupleVariation({"wght": (1.0, 1.0, 1.0)}, [100, 100])],
),
],
)
def test_positive_var(self, var, axisTag, newMax, expected):
axisRange = instancer.NormalizedAxisRange(0, newMax)
self.check_limit_single_var_axis_range(var, axisTag, axisRange, expected)
@pytest.mark.parametrize(
"var, axisTag, newMin, expected",
[
(
TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]),
"wdth",
-0.5,
[TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100])],
),
(
TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]),
"wght",
-0.5,
[TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [50, 50])],
),
(
TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]),
"wght",
-0.8,
[TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [80, 80])],
),
(
TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]),
"wght",
-1.0,
[TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100])],
),
(TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]), "wght", 0.0, []),
(
TupleVariation({"wght": (-1.0, -1.0, -0.5)}, [100, 100]),
"wght",
-0.4,
[],
),
(
TupleVariation({"wght": (-1.0, -0.5, 0.0)}, [100, 100]),
"wght",
-0.5,
[TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100])],
),
(
TupleVariation({"wght": (-1.0, -0.5, 0.0)}, [100, 100]),
"wght",
-0.4,
[TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [80, 80])],
),
(
TupleVariation({"wght": (-1.0, -0.5, 0.0)}, [100, 100]),
"wght",
-0.6,
[TupleVariation({"wght": (-1.666667, -0.833334, 0.0)}, [100, 100])],
),
(
TupleVariation({"wght": (-1.0, -0.2, 0.0)}, [100, 100]),
"wght",
-0.4,
[
TupleVariation({"wght": (-2.0, -0.5, -0.0)}, [100, 100]),
TupleVariation({"wght": (-1.0, -1.0, -0.5)}, [8.33333, 8.33333]),
],
),
(
TupleVariation({"wght": (-1.0, -0.2, 0.0)}, [100, 100]),
"wght",
-0.5,
[TupleVariation({"wght": (-2.0, -0.4, 0.0)}, [100, 100])],
),
(
TupleVariation({"wght": (-1.0, -0.5, -0.5)}, [100, 100]),
"wght",
-0.5,
[TupleVariation({"wght": (-1.0, -1.0, -1.0)}, [100, 100])],
),
],
)
def test_negative_var(self, var, axisTag, newMin, expected):
axisRange = instancer.NormalizedAxisRange(newMin, 0)
self.check_limit_single_var_axis_range(var, axisTag, axisRange, expected)
@pytest.mark.parametrize(
"oldRange, newRange, expected",
[
((1.0, -1.0), (-1.0, 1.0), None), # invalid oldRange min > max
((0.6, 1.0), (0, 0.5), None),
((-1.0, -0.6), (-0.5, 0), None),
((0.4, 1.0), (0, 0.5), (0.8, 1.0)),
((-1.0, -0.4), (-0.5, 0), (-1.0, -0.8)),
((0.4, 1.0), (0, 0.4), (1.0, 1.0)),
((-1.0, -0.4), (-0.4, 0), (-1.0, -1.0)),
((-0.5, 0.5), (-0.4, 0.4), (-1.0, 1.0)),
((0, 1.0), (-1.0, 0), (0, 0)), # or None?
((-1.0, 0), (0, 1.0), (0, 0)), # or None?
],
)
def test_limitFeatureVariationConditionRange(oldRange, newRange, expected):
condition = featureVars.buildConditionTable(0, *oldRange)
result = instancer._limitFeatureVariationConditionRange(
condition, instancer.NormalizedAxisRange(*newRange)
)
assert result == expected
@pytest.mark.parametrize(
"limits, expected",
[
(["wght=400", "wdth=100"], {"wght": 400, "wdth": 100}),
(["wght=400:900"], {"wght": (400, 900)}),
(["slnt=11.4"], {"slnt": 11.4}),
(["slnt=11.4"], {"slnt": pytest.approx(11.399994)}),
(["ABCD=drop"], {"ABCD": None}),
],
)
@ -1347,12 +1852,17 @@ def test_normalizeAxisLimits_tuple(varfont):
assert normalized == {"wght": (-1.0, 0)}
def test_normalizeAxisLimits_unsupported_range(varfont):
with pytest.raises(NotImplementedError, match="Unsupported range"):
instancer.normalizeAxisLimits(varfont, {"wght": (401, 700)})
def test_normalizeAxisLimits_no_avar(varfont):
del varfont["avar"]
normalized = instancer.normalizeAxisLimits(varfont, {"wght": (500, 600)})
normalized = instancer.normalizeAxisLimits(varfont, {"wght": (400, 500)})
assert normalized["wght"] == pytest.approx((0.2, 0.4), 1e-4)
assert normalized["wght"] == pytest.approx((0, 0.2), 1e-4)
def test_normalizeAxisLimits_missing_from_fvar(varfont):