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): def otRound(value):
"""Round float value to nearest integer towards +Infinity. """Round float value to nearest integer towards +Infinity.
For fractional values of 0.5 and higher, take the next higher integer; 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 fontTools.misc.py23 import *
from array import array from array import array
from fontTools.misc.fixedTools import MAX_F2DOT14
from fontTools.pens.basePen import LoggingPen from fontTools.pens.basePen import LoggingPen
from fontTools.pens.transformPen import TransformPen from fontTools.pens.transformPen import TransformPen
from fontTools.ttLib.tables import ttProgram from fontTools.ttLib.tables import ttProgram
@ -11,11 +12,6 @@ from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
__all__ = ["TTGlyphPen"] __all__ = ["TTGlyphPen"]
# the max value that can still fit in an F2Dot14:
# 1.99993896484375
MAX_F2DOT14 = 0x7FFF / (1 << 14)
class TTGlyphPen(LoggingPen): class TTGlyphPen(LoggingPen):
"""Pen used for drawing to a TrueType glyph. """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" 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. 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 For example, if you wish to pin the width axis to a given location while also
the rest of the axes, you can do: 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. See `fonttools varLib.instancer --help` for more info on the CLI options.
The module's entry point is the `instantiateVariableFont` function, which takes 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, a TTFont object and a dict specifying either axis coodinates or (min, max) ranges,
and returns a new TTFont representing respectively a partial or a full instance. 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 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: 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, location, the varLib.instancer will keep the axis and the corresponding deltas,
whereas mutator implicitly drops the axis at its default coordinate. 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: with the rest planned to be implemented in the future, namely:
L1) dropping one or more axes while leaving the default tables unmodified; 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; 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 The discussion and implementation of these features are tracked at
https://github.com/fonttools/fonttools/issues/1537 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.varLib.models import supportScalar, normalizeValue, piecewiseLinearMap
from fontTools.ttLib import TTFont from fontTools.ttLib import TTFont
from fontTools.ttLib.tables.TupleVariation import TupleVariation from fontTools.ttLib.tables.TupleVariation import TupleVariation
@ -90,12 +96,44 @@ import re
log = logging.getLogger("fontTools.varLib.instancer") log = logging.getLogger("fontTools.varLib.instancer")
def instantiateTupleVariationStore(variations, location, origCoords=None, endPts=None): class AxisRange(collections.namedtuple("AxisRange", "minimum maximum")):
"""Instantiate TupleVariation list at the given location. 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 'variations' list of TupleVariation objects is modified in-place.
The input location can describe either a full instance (all the axes are assigned an The 'axisLimits' (dict) maps axis tags (str) to either a single coordinate along the
explicit coordinate) or partial (some of the axes are omitted). 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 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. 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 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: Args:
variations: List[TupleVariation] from either 'gvar' or 'cvar'. 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' origCoords: GlyphCoordinates: default instance's coordinates for computing 'gvar'
inferred points (cf. table__g_l_y_f.getCoordinatesAndControls). inferred points (cf. table__g_l_y_f.getCoordinatesAndControls).
endPts: List[int]: indices of contour end points, for inferring 'gvar' deltas. endPts: List[int]: indices of contour end points, for inferring 'gvar' deltas.
@ -115,7 +154,44 @@ def instantiateTupleVariationStore(variations, location, origCoords=None, endPts
Returns: Returns:
List[float]: the overall delta adjustment after applicable deltas were summed. 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: for var in variations:
# Compute the scalar support of the axes to be pinned at the desired location, # Compute the scalar support of the axes to be pinned at the desired location,
# excluding any axes that we are not pinning. # excluding any axes that we are not pinning.
@ -127,31 +203,119 @@ def instantiateTupleVariationStore(variations, location, origCoords=None, endPts
# no influence, drop the TupleVariation # no influence, drop the TupleVariation
continue continue
# compute inferred deltas only for gvar ('origCoords' is None for cvar)
if origCoords is not None:
var.calcInferredDeltas(origCoords, endPts)
var.scaleDeltas(scalar) var.scaleDeltas(scalar)
newVariations.append(var)
return newVariations
# merge TupleVariations with overlapping "tents"
axes = tuple(var.axes.items()) def limitTupleVariationAxisRanges(variations, axisRanges):
if axes in newVariations: for axisTag, axisRange in sorted(axisRanges.items()):
newVariations[axes] += var 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:
if axisRange.maximum == 1.0:
return [var]
elif axisRange.maximum == 0.0:
return []
limit = axisRange.minimum if negative else axisRange.maximum
# 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
# 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: else:
newVariations[axes] = var 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)
# drop TupleVariation if all axes have been pinned (var.axes.items() is empty); return [var, newVar]
# its deltas will be added to the default instance's coordinates
defaultVar = newVariations.pop(tuple(), None)
for var in newVariations.values():
var.roundDeltas()
variations[:] = list(newVariations.values())
return defaultVar.coordinates if defaultVar is not None else []
def instantiateGvarGlyph(varfont, glyphname, location, optimize=True): def instantiateGvarGlyph(varfont, glyphname, axisLimits, optimize=True):
glyf = varfont["glyf"] glyf = varfont["glyf"]
coordinates, ctrl = glyf.getCoordinatesAndControls(glyphname, varfont) coordinates, ctrl = glyf.getCoordinatesAndControls(glyphname, varfont)
endPts = ctrl.endPts endPts = ctrl.endPts
@ -163,7 +327,7 @@ def instantiateGvarGlyph(varfont, glyphname, location, optimize=True):
if tupleVarStore: if tupleVarStore:
defaultDeltas = instantiateTupleVariationStore( defaultDeltas = instantiateTupleVariationStore(
tupleVarStore, location, coordinates, endPts tupleVarStore, axisLimits, coordinates, endPts
) )
if defaultDeltas: if defaultDeltas:
@ -191,7 +355,7 @@ def instantiateGvarGlyph(varfont, glyphname, location, optimize=True):
var.optimize(coordinates, endPts, isComposite) var.optimize(coordinates, endPts, isComposite)
def instantiateGvar(varfont, location, optimize=True): def instantiateGvar(varfont, axisLimits, optimize=True):
log.info("Instantiating glyf/gvar tables") log.info("Instantiating glyf/gvar tables")
gvar = varfont["gvar"] gvar = varfont["gvar"]
@ -210,7 +374,7 @@ def instantiateGvar(varfont, location, optimize=True):
), ),
) )
for glyphname in glyphnames: for glyphname in glyphnames:
instantiateGvarGlyph(varfont, glyphname, location, optimize=optimize) instantiateGvarGlyph(varfont, glyphname, axisLimits, optimize=optimize)
if not gvar.variations: if not gvar.variations:
del varfont["gvar"] del varfont["gvar"]
@ -222,12 +386,12 @@ def setCvarDeltas(cvt, deltas):
cvt[i] += otRound(delta) cvt[i] += otRound(delta)
def instantiateCvar(varfont, location): def instantiateCvar(varfont, axisLimits):
log.info("Instantiating cvt/cvar tables") log.info("Instantiating cvt/cvar tables")
cvar = varfont["cvar"] cvar = varfont["cvar"]
defaultDeltas = instantiateTupleVariationStore(cvar.variations, location) defaultDeltas = instantiateTupleVariationStore(cvar.variations, axisLimits)
if defaultDeltas: if defaultDeltas:
setCvarDeltas(varfont["cvt "], 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") log.info("Instantiating MVAR table")
mvar = varfont["MVAR"].table mvar = varfont["MVAR"].table
fvarAxes = varfont["fvar"].axes fvarAxes = varfont["fvar"].axes
varStore = mvar.VarStore varStore = mvar.VarStore
defaultDeltas = instantiateItemVariationStore(varStore, fvarAxes, location) defaultDeltas = instantiateItemVariationStore(varStore, fvarAxes, axisLimits)
setMvarDeltas(varfont, defaultDeltas) setMvarDeltas(varfont, defaultDeltas)
if varStore.VarRegionList.Region: if varStore.VarRegionList.Region:
@ -277,12 +441,14 @@ def _remapVarIdxMap(table, attrName, varIndexMapping, glyphOrder):
# TODO(anthrotype) Add support for HVAR/VVAR in CFF2 # TODO(anthrotype) Add support for HVAR/VVAR in CFF2
def _instantiateVHVAR(varfont, location, tableFields): def _instantiateVHVAR(varfont, axisLimits, tableFields):
tableTag = tableFields.tableTag tableTag = tableFields.tableTag
fvarAxes = varfont["fvar"].axes fvarAxes = varfont["fvar"].axes
# Deltas from gvar table have already been applied to the hmtx/vmtx. For full # 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 # 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) log.info("Dropping %s table", tableTag)
del varfont[tableTag] del varfont[tableTag]
return return
@ -291,7 +457,7 @@ def _instantiateVHVAR(varfont, location, tableFields):
vhvar = varfont[tableTag].table vhvar = varfont[tableTag].table
varStore = vhvar.VarStore varStore = vhvar.VarStore
# since deltas were already applied, the return value here is ignored # since deltas were already applied, the return value here is ignored
instantiateItemVariationStore(varStore, fvarAxes, location) instantiateItemVariationStore(varStore, fvarAxes, axisLimits)
if varStore.VarRegionList.Region: if varStore.VarRegionList.Region:
# Only re-optimize VarStore if the HVAR/VVAR already uses indirect AdvWidthMap # Only re-optimize VarStore if the HVAR/VVAR already uses indirect AdvWidthMap
@ -309,16 +475,14 @@ def _instantiateVHVAR(varfont, location, tableFields):
_remapVarIdxMap( _remapVarIdxMap(
vhvar, tableFields.vOrigMapping, varIndexMapping, glyphOrder vhvar, tableFields.vOrigMapping, varIndexMapping, glyphOrder
) )
else:
del varfont[tableTag]
def instantiateHVAR(varfont, location): def instantiateHVAR(varfont, axisLimits):
return _instantiateVHVAR(varfont, location, varLib.HVAR_FIELDS) return _instantiateVHVAR(varfont, axisLimits, varLib.HVAR_FIELDS)
def instantiateVVAR(varfont, location): def instantiateVVAR(varfont, axisLimits):
return _instantiateVHVAR(varfont, location, varLib.VVAR_FIELDS) return _instantiateVHVAR(varfont, axisLimits, varLib.VVAR_FIELDS)
class _TupleVarStoreAdapter(object): class _TupleVarStoreAdapter(object):
@ -345,30 +509,47 @@ class _TupleVarStoreAdapter(object):
itemCounts.append(varData.ItemCount) itemCounts.append(varData.ItemCount)
return cls(regions, axisOrder, tupleVarData, itemCounts) return cls(regions, axisOrder, tupleVarData, itemCounts)
def dropAxes(self, axes): def rebuildRegions(self):
prunedRegions = ( # Collect the set of all unique region axes from the current TupleVariations.
frozenset( # We use an OrderedDict to de-duplicate regions while keeping the order.
(axisTag, support) uniqueRegions = collections.OrderedDict.fromkeys(
for axisTag, support in region.items() (
if axisTag not in axes frozenset(var.axes.items())
for variations in self.tupleVarData
for var in variations
) )
for region in self.regions
) )
# dedup regions while keeping original order # Maintain the original order for the regions that pre-existed, appending
uniqueRegions = collections.OrderedDict.fromkeys(prunedRegions) # the new regions at the end of the region list.
self.regions = [dict(items) for items in uniqueRegions if items] newRegions = []
self.axisOrder = [axisTag for axisTag in self.axisOrder if axisTag not in axes] 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 = [] defaultDeltaArray = []
for variations, itemCount in zip(self.tupleVarData, self.itemCounts): for variations, itemCount in zip(self.tupleVarData, self.itemCounts):
defaultDeltas = instantiateTupleVariationStore(variations, location) defaultDeltas = instantiateTupleVariationStore(variations, axisLimits)
if not defaultDeltas: if not defaultDeltas:
defaultDeltas = [0] * itemCount defaultDeltas = [0] * itemCount
defaultDeltaArray.append(defaultDeltas) defaultDeltaArray.append(defaultDeltas)
# remove pinned axes from all the regions # rebuild regions whose axes were dropped or limited
self.dropAxes(location.keys()) 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 return defaultDeltaArray
@ -396,11 +577,12 @@ class _TupleVarStoreAdapter(object):
return itemVarStore return itemVarStore
def instantiateItemVariationStore(itemVarStore, fvarAxes, location): def instantiateItemVariationStore(itemVarStore, fvarAxes, axisLimits):
""" Compute deltas at partial location, and update varStore in-place. """ Compute deltas at partial location, and update varStore in-place.
Remove regions in which all axes were instanced, and scale the deltas of Remove regions in which all axes were instanced, or fall outside the new axis
the remaining regions where only some of the axes were instanced. 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 The number of VarData subtables, and the number of items within each, are
not modified, in order to keep the existing VariationIndex valid. not modified, in order to keep the existing VariationIndex valid.
@ -409,15 +591,16 @@ def instantiateItemVariationStore(itemVarStore, fvarAxes, location):
Args: Args:
varStore: An otTables.VarStore object (Item Variation Store) varStore: An otTables.VarStore object (Item Variation Store)
fvarAxes: list of fvar's Axis objects fvarAxes: list of fvar's Axis objects
location: Dict[str, float] mapping axis tags to normalized axis coordinates. axisLimits: Dict[str, float] mapping axis tags to normalized axis coordinates
May not specify coordinates for all the fvar axes. (float) or ranges for restricting an axis' min/max (NormalizedAxisRange).
May not specify coordinates/ranges for all the fvar axes.
Returns: Returns:
defaultDeltas: to be added to the default instance, of type dict of floats defaultDeltas: to be added to the default instance, of type dict of floats
keyed by VariationIndex compound values: i.e. (outer << 16) + inner. keyed by VariationIndex compound values: i.e. (outer << 16) + inner.
""" """
tupleVarStore = _TupleVarStoreAdapter.fromItemVarStore(itemVarStore, fvarAxes) tupleVarStore = _TupleVarStoreAdapter.fromItemVarStore(itemVarStore, fvarAxes)
defaultDeltaArray = tupleVarStore.instantiate(location) defaultDeltaArray = tupleVarStore.instantiate(axisLimits)
newItemVarStore = tupleVarStore.asItemVarStore() newItemVarStore = tupleVarStore.asItemVarStore()
itemVarStore.VarRegionList = newItemVarStore.VarRegionList itemVarStore.VarRegionList = newItemVarStore.VarRegionList
@ -432,7 +615,7 @@ def instantiateItemVariationStore(itemVarStore, fvarAxes, location):
return defaultDeltas return defaultDeltas
def instantiateOTL(varfont, location): def instantiateOTL(varfont, axisLimits):
# TODO(anthrotype) Support partial instancing of JSTF and BASE tables # TODO(anthrotype) Support partial instancing of JSTF and BASE tables
if ( if (
@ -452,7 +635,7 @@ def instantiateOTL(varfont, location):
varStore = gdef.VarStore varStore = gdef.VarStore
fvarAxes = varfont["fvar"].axes 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 # When VF are built, big lookups may overflow and be broken into multiple
# subtables. MutatorMerger (which inherits from AligningMerger) reattaches # subtables. MutatorMerger (which inherits from AligningMerger) reattaches
@ -491,7 +674,7 @@ def instantiateOTL(varfont, location):
del varfont["GDEF"] del varfont["GDEF"]
def instantiateFeatureVariations(varfont, location): def instantiateFeatureVariations(varfont, axisLimits):
for tableTag in ("GPOS", "GSUB"): for tableTag in ("GPOS", "GSUB"):
if tableTag not in varfont or not hasattr( if tableTag not in varfont or not hasattr(
varfont[tableTag].table, "FeatureVariations" varfont[tableTag].table, "FeatureVariations"
@ -499,7 +682,7 @@ def instantiateFeatureVariations(varfont, location):
continue continue
log.info("Instantiating FeatureVariations of %s table", tableTag) log.info("Instantiating FeatureVariations of %s table", tableTag)
_instantiateFeatureVariations( _instantiateFeatureVariations(
varfont[tableTag].table, varfont["fvar"].axes, location varfont[tableTag].table, varfont["fvar"].axes, axisLimits
) )
# remove unreferenced lookups # remove unreferenced lookups
varfont[tableTag].prune_lookups() varfont[tableTag].prune_lookups()
@ -527,10 +710,44 @@ def _featureVariationRecordIsUnique(rec, seen):
return True 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( def _instantiateFeatureVariationRecord(
record, recIdx, location, fvarAxes, axisIndexMap record, recIdx, location, fvarAxes, axisIndexMap
): ):
shouldKeep = False
applies = True applies = True
newConditions = [] newConditions = []
for i, condition in enumerate(record.ConditionSet.ConditionTable): for i, condition in enumerate(record.ConditionSet.ConditionTable):
@ -562,11 +779,48 @@ def _instantiateFeatureVariationRecord(
if newConditions: if newConditions:
record.ConditionSet.ConditionTable = newConditions record.ConditionSet.ConditionTable = newConditions
shouldKeep = True shouldKeep = True
else:
shouldKeep = False
return applies, shouldKeep 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()) pinnedAxes = set(location.keys())
axisOrder = [axis.axisTag for axis in fvarAxes if axis.axisTag not in pinnedAxes] axisOrder = [axis.axisTag for axis in fvarAxes if axis.axisTag not in pinnedAxes]
axisIndexMap = {axisTag: axisOrder.index(axisTag) for axisTag in axisOrder} axisIndexMap = {axisTag: axisOrder.index(axisTag) for axisTag in axisOrder}
@ -580,8 +834,10 @@ def _instantiateFeatureVariations(table, fvarAxes, location):
record, i, location, fvarAxes, axisIndexMap record, i, location, fvarAxes, axisIndexMap
) )
if shouldKeep: if shouldKeep:
if _featureVariationRecordIsUnique(record, uniqueRecords): shouldKeep = _limitFeatureVariationRecord(record, axisRanges, fvarAxes)
newRecords.append(record)
if shouldKeep and _featureVariationRecordIsUnique(record, uniqueRecords):
newRecords.append(record)
if applies and not featureVariationApplied: if applies and not featureVariationApplied:
assert record.FeatureTableSubstitution.Version == 0x00010000 assert record.FeatureTableSubstitution.Version == 0x00010000
@ -597,23 +853,111 @@ def _instantiateFeatureVariations(table, fvarAxes, location):
del table.FeatureVariations 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 segments = varfont["avar"].segments
# drop table if we instantiate all the axes # 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") log.info("Dropping avar table")
del varfont["avar"] del varfont["avar"]
return return
log.info("Instantiating avar table") log.info("Instantiating avar table")
for axis in location: for axis in pinnedAxes:
if axis in segments: if axis in segments:
del segments[axis] 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"] fvar = varfont["fvar"]
@ -625,72 +969,83 @@ def instantiateFvar(varfont, location):
log.info("Instantiating fvar table") 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 # only keep NamedInstances whose coordinates == pinned axis location
instances = [] instances = []
for instance in fvar.instances: for instance in fvar.instances:
if any(instance.coordinates[axis] != value for axis, value in location.items()): if any(instance.coordinates[axis] != value for axis, value in location.items()):
continue continue
for axis in location: for axisTag in location:
del instance.coordinates[axis] del instance.coordinates[axisTag]
if not isInstanceWithinAxisRanges(instance.coordinates, axisRanges):
continue
instances.append(instance) instances.append(instance)
fvar.instances = instances fvar.instances = instances
def instantiateSTAT(varfont, location): def instantiateSTAT(varfont, axisLimits):
pinnedAxes = set(location.keys()) # 'axisLimits' dict must contain user-space (non-normalized) coordinates
stat = varfont["STAT"].table stat = varfont["STAT"].table
if not stat.DesignAxisRecord: if not stat.DesignAxisRecord or not (
return # skip empty STAT table stat.AxisValueArray and stat.AxisValueArray.AxisValue
):
return # STAT table empty, nothing to do
designAxes = stat.DesignAxisRecord.Axis location, axisRanges = splitAxisLocationAndRanges(axisLimits, rangeType=AxisRange)
pinnedAxisIndices = {
i for i, axis in enumerate(designAxes) if axis.AxisTag in pinnedAxes
}
if len(pinnedAxisIndices) == len(designAxes): def isAxisValueOutsideLimits(axisTag, axisValue):
log.info("Dropping STAT table") if axisTag in location and axisValue != location[axisTag]:
del varfont["STAT"] return True
return elif axisTag in axisRanges:
axisRange = axisRanges[axisTag]
if axisValue < axisRange.minimum or axisValue > axisRange.maximum:
return True
return False
log.info("Instantiating STAT table") log.info("Instantiating STAT table")
# only keep DesignAxis that were not instanced, and build a mapping from old # only keep AxisValues whose axis is not pinned nor restricted, or is pinned at the
# to new axis indices # exact (nominal) value, or is restricted but the value is within the new range
newDesignAxes = [] designAxes = stat.DesignAxisRecord.Axis
axisIndexMap = {} newAxisValueTables = []
for i, axis in enumerate(designAxes): for axisValueTable in stat.AxisValueArray.AxisValue:
if i not in pinnedAxisIndices: axisValueFormat = axisValueTable.Format
axisIndexMap[i] = len(newDesignAxes) if axisValueFormat in (1, 2, 3):
newDesignAxes.append(axis) axisTag = designAxes[axisValueTable.AxisIndex].AxisTag
if axisValueFormat == 2:
if stat.AxisValueArray and stat.AxisValueArray.AxisValue: axisValue = axisValueTable.NominalValue
# drop all AxisValue tables that reference any of the pinned axes
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)
else: else:
raise NotImplementedError(axisValueTable.Format) axisValue = axisValueTable.Value
stat.AxisValueArray.AxisValue = newAxisValueTables if isAxisValueOutsideLimits(axisTag, axisValue):
stat.AxisValueCount = len(stat.AxisValueArray.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.DesignAxisRecord.Axis[:] = newDesignAxes stat.AxisValueArray.AxisValue = newAxisValueTables
stat.DesignAxisCount = len(stat.DesignAxisRecord.Axis) stat.AxisValueCount = len(stat.AxisValueArray.AxisValue)
def getVariationNameIDs(varfont): def getVariationNameIDs(varfont):
@ -758,7 +1113,7 @@ def normalize(value, triple, avarMapping):
return floatToFixedToFloat(value, 14) return floatToFixedToFloat(value, 14)
def normalizeAxisLimits(varfont, axisLimits): def normalizeAxisLimits(varfont, axisLimits, usingAvar=True):
fvar = varfont["fvar"] fvar = varfont["fvar"]
badLimits = set(axisLimits.keys()).difference(a.axisTag for a in fvar.axes) badLimits = set(axisLimits.keys()).difference(a.axisTag for a in fvar.axes)
if badLimits: if badLimits:
@ -771,15 +1126,26 @@ def normalizeAxisLimits(varfont, axisLimits):
} }
avarSegments = {} avarSegments = {}
if "avar" in varfont: if usingAvar and "avar" in varfont:
avarSegments = varfont["avar"].segments 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 = {} normalizedLimits = {}
for axis_tag, triple in axes.items(): for axis_tag, triple in axes.items():
avarMapping = avarSegments.get(axis_tag, None) avarMapping = avarSegments.get(axis_tag, None)
value = axisLimits[axis_tag] value = axisLimits[axis_tag]
if isinstance(value, tuple): if isinstance(value, tuple):
normalizedLimits[axis_tag] = tuple( normalizedLimits[axis_tag] = NormalizedAxisRange(
normalize(v, triple, avarMapping) for v in axisLimits[axis_tag] *(normalize(v, triple, avarMapping) for v in value)
) )
else: else:
normalizedLimits[axis_tag] = normalize(value, triple, avarMapping) normalizedLimits[axis_tag] = normalize(value, triple, avarMapping)
@ -841,18 +1207,14 @@ def instantiateVariableFont(
""" """
sanityCheckVariableTables(varfont) sanityCheckVariableTables(varfont)
if not inplace:
varfont = deepcopy(varfont)
axisLimits = populateAxisDefaults(varfont, axisLimits) axisLimits = populateAxisDefaults(varfont, axisLimits)
normalizedLimits = normalizeAxisLimits(varfont, axisLimits) normalizedLimits = normalizeAxisLimits(varfont, axisLimits)
log.info("Normalized limits: %s", normalizedLimits) log.info("Normalized limits: %s", normalizedLimits)
# TODO Remove this check once ranges are supported if not inplace:
if any(isinstance(v, tuple) for v in axisLimits.values()): varfont = deepcopy(varfont)
raise NotImplementedError("Axes range limits are not supported yet")
if "gvar" in varfont: if "gvar" in varfont:
instantiateGvar(varfont, normalizedLimits, optimize=optimize) instantiateGvar(varfont, normalizedLimits, optimize=optimize)
@ -874,7 +1236,7 @@ def instantiateVariableFont(
instantiateFeatureVariations(varfont, normalizedLimits) instantiateFeatureVariations(varfont, normalizedLimits)
if "avar" in varfont: if "avar" in varfont:
instantiateAvar(varfont, normalizedLimits) instantiateAvar(varfont, axisLimits)
with pruningUnusedNames(varfont): with pruningUnusedNames(varfont):
if "STAT" in varfont: if "STAT" in varfont:
@ -898,6 +1260,23 @@ def instantiateVariableFont(
return varfont 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): def parseLimits(limits):
result = {} result = {}
for limitString in limits: for limitString in limits:
@ -908,12 +1287,12 @@ def parseLimits(limits):
if match.group(2): # 'drop' if match.group(2): # 'drop'
lbound = None lbound = None
else: else:
lbound = float(match.group(3)) lbound = strToFixedToFloat(match.group(3), precisionBits=16)
ubound = lbound ubound = lbound
if match.group(4): if match.group(4):
ubound = float(match.group(4)) ubound = strToFixedToFloat(match.group(4), precisionBits=16)
if lbound != ubound: if lbound != ubound:
result[tag] = (lbound, ubound) result[tag] = AxisRange(lbound, ubound)
else: else:
result[tag] = lbound result[tag] = lbound
return result return result

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?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> <GlyphOrder>
<!-- The 'id' attribute is only for humans; it is ignored when parsed. --> <!-- 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 --> <!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="1.0"/> <tableVersion value="1.0"/>
<fontRevision value="2.001"/> <fontRevision value="2.001"/>
<checkSumAdjustment value="0x6b1f158e"/> <checkSumAdjustment value="0x605c3e60"/>
<magicNumber value="0x5f0f3cf5"/> <magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00000011"/> <flags value="00000000 00000011"/>
<unitsPerEm value="1000"/> <unitsPerEm value="1000"/>
<created value="Tue Mar 15 19:50:39 2016"/> <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="-621"/> <xMin value="0"/>
<yMin value="-389"/> <yMin value="0"/>
<xMax value="2800"/> <xMax value="638"/>
<yMax value="1067"/> <yMax value="944"/>
<macStyle value="00000000 00000000"/> <macStyle value="00000000 00000000"/>
<lowestRecPPEM value="6"/> <lowestRecPPEM value="6"/>
<fontDirectionHint value="2"/> <fontDirectionHint value="2"/>
@ -36,10 +36,10 @@
<ascent value="1069"/> <ascent value="1069"/>
<descent value="-293"/> <descent value="-293"/>
<lineGap value="0"/> <lineGap value="0"/>
<advanceWidthMax value="2840"/> <advanceWidthMax value="639"/>
<minLeftSideBearing value="-621"/> <minLeftSideBearing value="0"/>
<minRightSideBearing value="-620"/> <minRightSideBearing value="1"/>
<xMaxExtent value="2800"/> <xMaxExtent value="638"/>
<caretSlopeRise value="1"/> <caretSlopeRise value="1"/>
<caretSlopeRun value="0"/> <caretSlopeRun value="0"/>
<caretOffset value="0"/> <caretOffset value="0"/>
@ -55,10 +55,10 @@
<!-- Most of this table will be recalculated by the compiler --> <!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="0x10000"/> <tableVersion value="0x10000"/>
<numGlyphs value="5"/> <numGlyphs value="5"/>
<maxPoints value="202"/> <maxPoints value="19"/>
<maxContours value="24"/> <maxContours value="2"/>
<maxCompositePoints value="315"/> <maxCompositePoints value="32"/>
<maxCompositeContours value="21"/> <maxCompositeContours value="3"/>
<maxZones value="1"/> <maxZones value="1"/>
<maxTwilightPoints value="0"/> <maxTwilightPoints value="0"/>
<maxStorage value="0"/> <maxStorage value="0"/>
@ -66,8 +66,8 @@
<maxInstructionDefs value="0"/> <maxInstructionDefs value="0"/>
<maxStackElements value="0"/> <maxStackElements value="0"/>
<maxSizeOfInstructions value="0"/> <maxSizeOfInstructions value="0"/>
<maxComponentElements value="8"/> <maxComponentElements value="2"/>
<maxComponentDepth value="8"/> <maxComponentDepth value="1"/>
</maxp> </maxp>
<OS_2> <OS_2>
@ -107,8 +107,8 @@
<ulUnicodeRange4 value="00000000 00010000 00000000 00000000"/> <ulUnicodeRange4 value="00000000 00010000 00000000 00000000"/>
<achVendID value="GOOG"/> <achVendID value="GOOG"/>
<fsSelection value="00000001 01000000"/> <fsSelection value="00000001 01000000"/>
<usFirstCharIndex value="0"/> <usFirstCharIndex value="65"/>
<usLastCharIndex value="65533"/> <usLastCharIndex value="192"/>
<sTypoAscender value="1069"/> <sTypoAscender value="1069"/>
<sTypoDescender value="-293"/> <sTypoDescender value="-293"/>
<sTypoLineGap value="0"/> <sTypoLineGap value="0"/>
@ -1037,15 +1037,104 @@
<Axis index="0"> <Axis index="0">
<AxisTag value="wght"/> <AxisTag value="wght"/>
<AxisNameID value="256"/> <!-- Weight --> <AxisNameID value="256"/> <!-- Weight -->
<AxisOrdering value="0"/> <AxisOrdering value="1"/>
</Axis> </Axis>
<Axis index="1"> <Axis index="1">
<AxisTag value="wdth"/> <AxisTag value="wdth"/>
<AxisNameID value="257"/> <!-- Width --> <AxisNameID value="257"/> <!-- Width -->
<AxisOrdering value="1"/> <AxisOrdering value="0"/>
</Axis> </Axis>
</DesignAxisRecord> </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 --> <ElidedFallbackNameID value="2"/> <!-- Regular -->
</STAT> </STAT>

View File

@ -14,12 +14,12 @@
<!-- Most of this table will be recalculated by the compiler --> <!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="1.0"/> <tableVersion value="1.0"/>
<fontRevision value="2.001"/> <fontRevision value="2.001"/>
<checkSumAdjustment value="0x982d27a8"/> <checkSumAdjustment value="0x90f1c28"/>
<magicNumber value="0x5f0f3cf5"/> <magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00000011"/> <flags value="00000000 00000011"/>
<unitsPerEm value="1000"/> <unitsPerEm value="1000"/>
<created value="Tue Mar 15 19:50:39 2016"/> <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"/> <xMin value="0"/>
<yMin value="0"/> <yMin value="0"/>
<xMax value="577"/> <xMax value="577"/>
@ -238,6 +238,18 @@
</glyf> </glyf>
<name> <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"> <namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
Copyright 2015 Google Inc. All Rights Reserved. Copyright 2015 Google Inc. All Rights Reserved.
</namerecord> </namerecord>
@ -283,6 +295,18 @@
<namerecord nameID="14" platformID="3" platEncID="1" langID="0x409"> <namerecord nameID="14" platformID="3" platEncID="1" langID="0x409">
http://scripts.sil.org/OFL http://scripts.sil.org/OFL
</namerecord> </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> </name>
<post> <post>
@ -481,4 +505,40 @@
</LookupList> </LookupList>
</GSUB> </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> </ttFont>

View File

@ -14,12 +14,12 @@
<!-- Most of this table will be recalculated by the compiler --> <!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="1.0"/> <tableVersion value="1.0"/>
<fontRevision value="2.001"/> <fontRevision value="2.001"/>
<checkSumAdjustment value="0x1d4f3a2e"/> <checkSumAdjustment value="0x31525751"/>
<magicNumber value="0x5f0f3cf5"/> <magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00000011"/> <flags value="00000000 00000011"/>
<unitsPerEm value="1000"/> <unitsPerEm value="1000"/>
<created value="Tue Mar 15 19:50:39 2016"/> <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"/> <xMin value="0"/>
<yMin value="0"/> <yMin value="0"/>
<xMax value="496"/> <xMax value="496"/>
@ -238,6 +238,18 @@
</glyf> </glyf>
<name> <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"> <namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
Copyright 2015 Google Inc. All Rights Reserved. Copyright 2015 Google Inc. All Rights Reserved.
</namerecord> </namerecord>
@ -283,6 +295,18 @@
<namerecord nameID="14" platformID="3" platEncID="1" langID="0x409"> <namerecord nameID="14" platformID="3" platEncID="1" langID="0x409">
http://scripts.sil.org/OFL http://scripts.sil.org/OFL
</namerecord> </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> </name>
<post> <post>
@ -481,4 +505,40 @@
</LookupList> </LookupList>
</GSUB> </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> </ttFont>

View File

@ -14,12 +14,12 @@
<!-- Most of this table will be recalculated by the compiler --> <!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="1.0"/> <tableVersion value="1.0"/>
<fontRevision value="2.001"/> <fontRevision value="2.001"/>
<checkSumAdjustment value="0xf43664b4"/> <checkSumAdjustment value="0x4b2d3480"/>
<magicNumber value="0x5f0f3cf5"/> <magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00000011"/> <flags value="00000000 00000011"/>
<unitsPerEm value="1000"/> <unitsPerEm value="1000"/>
<created value="Tue Mar 15 19:50:39 2016"/> <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"/> <xMin value="0"/>
<yMin value="0"/> <yMin value="0"/>
<xMax value="638"/> <xMax value="638"/>
@ -238,6 +238,15 @@
</glyf> </glyf>
<name> <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"> <namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
Copyright 2015 Google Inc. All Rights Reserved. Copyright 2015 Google Inc. All Rights Reserved.
</namerecord> </namerecord>
@ -283,6 +292,15 @@
<namerecord nameID="14" platformID="3" platEncID="1" langID="0x409"> <namerecord nameID="14" platformID="3" platEncID="1" langID="0x409">
http://scripts.sil.org/OFL http://scripts.sil.org/OFL
</namerecord> </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> </name>
<post> <post>
@ -481,4 +499,41 @@
</LookupList> </LookupList>
</GSUB> </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> </ttFont>

View File

@ -14,12 +14,12 @@
<!-- Most of this table will be recalculated by the compiler --> <!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="1.0"/> <tableVersion value="1.0"/>
<fontRevision value="2.001"/> <fontRevision value="2.001"/>
<checkSumAdjustment value="0xd9290bac"/> <checkSumAdjustment value="0x39ab2622"/>
<magicNumber value="0x5f0f3cf5"/> <magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00000011"/> <flags value="00000000 00000011"/>
<unitsPerEm value="1000"/> <unitsPerEm value="1000"/>
<created value="Tue Mar 15 19:50:39 2016"/> <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"/> <xMin value="0"/>
<yMin value="0"/> <yMin value="0"/>
<xMax value="496"/> <xMax value="496"/>
@ -238,6 +238,18 @@
</glyf> </glyf>
<name> <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"> <namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
Copyright 2015 Google Inc. All Rights Reserved. Copyright 2015 Google Inc. All Rights Reserved.
</namerecord> </namerecord>
@ -283,6 +295,18 @@
<namerecord nameID="14" platformID="3" platEncID="1" langID="0x409"> <namerecord nameID="14" platformID="3" platEncID="1" langID="0x409">
http://scripts.sil.org/OFL http://scripts.sil.org/OFL
</namerecord> </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> </name>
<post> <post>
@ -481,4 +505,41 @@
</LookupList> </LookupList>
</GSUB> </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> </ttFont>

View File

@ -14,12 +14,12 @@
<!-- Most of this table will be recalculated by the compiler --> <!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="1.0"/> <tableVersion value="1.0"/>
<fontRevision value="2.001"/> <fontRevision value="2.001"/>
<checkSumAdjustment value="0xa514fda"/> <checkSumAdjustment value="0x7b5e7903"/>
<magicNumber value="0x5f0f3cf5"/> <magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00000011"/> <flags value="00000000 00000011"/>
<unitsPerEm value="1000"/> <unitsPerEm value="1000"/>
<created value="Tue Mar 15 19:50:39 2016"/> <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"/> <xMin value="0"/>
<yMin value="0"/> <yMin value="0"/>
<xMax value="726"/> <xMax value="726"/>
@ -238,6 +238,18 @@
</glyf> </glyf>
<name> <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"> <namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
Copyright 2015 Google Inc. All Rights Reserved. Copyright 2015 Google Inc. All Rights Reserved.
</namerecord> </namerecord>
@ -283,6 +295,18 @@
<namerecord nameID="14" platformID="3" platEncID="1" langID="0x409"> <namerecord nameID="14" platformID="3" platEncID="1" langID="0x409">
http://scripts.sil.org/OFL http://scripts.sil.org/OFL
</namerecord> </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> </name>
<post> <post>
@ -481,4 +505,40 @@
</LookupList> </LookupList>
</GSUB> </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> </ttFont>

View File

@ -14,12 +14,12 @@
<!-- Most of this table will be recalculated by the compiler --> <!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="1.0"/> <tableVersion value="1.0"/>
<fontRevision value="2.001"/> <fontRevision value="2.001"/>
<checkSumAdjustment value="0xc8e8b846"/> <checkSumAdjustment value="0x7f9149e4"/>
<magicNumber value="0x5f0f3cf5"/> <magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00000011"/> <flags value="00000000 00000011"/>
<unitsPerEm value="1000"/> <unitsPerEm value="1000"/>
<created value="Tue Mar 15 19:50:39 2016"/> <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"/> <xMin value="0"/>
<yMin value="0"/> <yMin value="0"/>
<xMax value="574"/> <xMax value="574"/>
@ -238,6 +238,18 @@
</glyf> </glyf>
<name> <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"> <namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
Copyright 2015 Google Inc. All Rights Reserved. Copyright 2015 Google Inc. All Rights Reserved.
</namerecord> </namerecord>
@ -283,6 +295,18 @@
<namerecord nameID="14" platformID="3" platEncID="1" langID="0x409"> <namerecord nameID="14" platformID="3" platEncID="1" langID="0x409">
http://scripts.sil.org/OFL http://scripts.sil.org/OFL
</namerecord> </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> </name>
<post> <post>
@ -481,4 +505,40 @@
</LookupList> </LookupList>
</GSUB> </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> </ttFont>

View File

@ -1,4 +1,5 @@
from fontTools.misc.py23 import * from fontTools.misc.py23 import *
from fontTools.misc.fixedTools import floatToFixedToFloat
from fontTools import ttLib from fontTools import ttLib
from fontTools import designspaceLib from fontTools import designspaceLib
from fontTools.feaLib.builder import addOpenTypeFeaturesFromString from fontTools.feaLib.builder import addOpenTypeFeaturesFromString
@ -381,6 +382,26 @@ class InstantiateHVARTest(object):
assert "HVAR" not in varfont 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): class InstantiateItemVariationStoreTest(object):
def test_VarRegion_get_support(self): def test_VarRegion_get_support(self):
@ -493,33 +514,40 @@ class TupleVarStoreAdapterTest(object):
[TupleVariation({"wdth": (-1.0, -1.0, 0)}, [-12, 8])], [TupleVariation({"wdth": (-1.0, -1.0, 0)}, [-12, 8])],
] ]
def test_dropAxes(self): def test_rebuildRegions(self):
regions = [ regions = [
{"wght": (-1.0, -1.0, 0)}, {"wght": (-1.0, -1.0, 0)},
{"wght": (0.0, 1.0, 1.0)}, {"wght": (0.0, 1.0, 1.0)},
{"wdth": (-1.0, -1.0, 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": (-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, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)},
{"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)},
] ]
axisOrder = ["wght", "wdth", "opsz"] axisOrder = ["wght", "wdth"]
adapter = instancer._TupleVarStoreAdapter(regions, axisOrder, [], itemCounts=[]) 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 == [ assert adapter.regions == [
{"wght": (-1.0, -1.0, 0)}, {"wght": (-1.0, -1.0, 0)},
{"wght": (0.0, 1.0, 1.0)}, {"wght": (0.0, 1.0, 1.0)},
{"opsz": (0.0, 1.0, 1.0)}, {"wght": (-1.0, -0.5, 0)},
{"wght": (0.0, 0.5, 1.0)}, {"wght": (0, 0.5, 1.0)},
{"wght": (0.5, 1.0, 1.0)},
] ]
adapter.dropAxes({"wght", "opsz"})
assert adapter.regions == []
def test_roundtrip(self, fvarAxes): def test_roundtrip(self, fvarAxes):
regions = [ regions = [
{"wght": (-1.0, -1.0, 0)}, {"wght": (-1.0, -1.0, 0)},
@ -924,6 +952,208 @@ class InstantiateAvarTest(object):
assert "avar" not in varfont 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): class InstantiateFvarTest(object):
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -979,7 +1209,7 @@ class InstantiateSTATTest(object):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"location, expected", "location, expected",
[ [
({"wght": 400}, ["Condensed", "Upright"]), ({"wght": 400}, ["Regular", "Condensed", "Upright"]),
({"wdth": 100}, ["Thin", "Regular", "Black", "Upright"]), ({"wdth": 100}, ["Thin", "Regular", "Black", "Upright"]),
], ],
) )
@ -989,7 +1219,7 @@ class InstantiateSTATTest(object):
stat = varfont["STAT"].table stat = varfont["STAT"].table
designAxes = {a.AxisTag for a in stat.DesignAxisRecord.Axis} designAxes = {a.AxisTag for a in stat.DesignAxisRecord.Axis}
assert designAxes == {"wght", "wdth", "ital"}.difference(location) assert designAxes == {"wght", "wdth", "ital"}
name = varfont["name"] name = varfont["name"]
valueNames = [] valueNames = []
@ -999,7 +1229,23 @@ class InstantiateSTATTest(object):
assert valueNames == expected 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 = otTables.STAT()
stat.Version = 0x00010001 stat.Version = 0x00010001
stat.populateDefaults() stat.populateDefaults()
@ -1011,21 +1257,88 @@ class InstantiateSTATTest(object):
assert not varfont["STAT"].table.DesignAxisRecord assert not varfont["STAT"].table.DesignAxisRecord
def test_drop_table(self, varfont): @staticmethod
stat = otTables.STAT() def get_STAT_axis_values(stat):
stat.Version = 0x00010001 axes = stat.DesignAxisRecord.Axis
stat.populateDefaults() result = []
stat.DesignAxisRecord = otTables.AxisRecordArray() for axisValue in stat.AxisValueArray.AxisValue:
axis = otTables.AxisRecord() if axisValue.Format == 1:
axis.AxisTag = "wght" result.append((axes[axisValue.AxisIndex].AxisTag, axisValue.Value))
axis.AxisNameID = 0 elif axisValue.Format == 3:
axis.AxisOrdering = 0 result.append(
stat.DesignAxisRecord.Axis = [axis] (
varfont["STAT"].table = stat 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): def test_pruningUnusedNames(varfont):
@ -1321,12 +1634,204 @@ class InstantiateFeatureVariationsTest(object):
assert rec1.ConditionSet.ConditionTable[0].Format == 2 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( @pytest.mark.parametrize(
"limits, expected", "limits, expected",
[ [
(["wght=400", "wdth=100"], {"wght": 400, "wdth": 100}), (["wght=400", "wdth=100"], {"wght": 400, "wdth": 100}),
(["wght=400:900"], {"wght": (400, 900)}), (["wght=400:900"], {"wght": (400, 900)}),
(["slnt=11.4"], {"slnt": 11.4}), (["slnt=11.4"], {"slnt": pytest.approx(11.399994)}),
(["ABCD=drop"], {"ABCD": None}), (["ABCD=drop"], {"ABCD": None}),
], ],
) )
@ -1347,12 +1852,17 @@ def test_normalizeAxisLimits_tuple(varfont):
assert normalized == {"wght": (-1.0, 0)} 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): def test_normalizeAxisLimits_no_avar(varfont):
del varfont["avar"] 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): def test_normalizeAxisLimits_missing_from_fvar(varfont):