Merge pull request #1753 from anthrotype/l3-instancer
[varLib.instancer] implement restricting axis ranges (aka L3)
This commit is contained in:
commit
6725b34566
@ -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;
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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:
|
||||
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:
|
||||
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);
|
||||
# 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 []
|
||||
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,8 +834,10 @@ def _instantiateFeatureVariations(table, fvarAxes, location):
|
||||
record, i, location, fvarAxes, axisIndexMap
|
||||
)
|
||||
if shouldKeep:
|
||||
if _featureVariationRecordIsUnique(record, uniqueRecords):
|
||||
newRecords.append(record)
|
||||
shouldKeep = _limitFeatureVariationRecord(record, axisRanges, fvarAxes)
|
||||
|
||||
if shouldKeep and _featureVariationRecordIsUnique(record, uniqueRecords):
|
||||
newRecords.append(record)
|
||||
|
||||
if applies and not featureVariationApplied:
|
||||
assert record.FeatureTableSubstitution.Version == 0x00010000
|
||||
@ -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,72 +969,83 @@ 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
|
||||
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)
|
||||
# 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:
|
||||
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)
|
||||
stat.AxisValueArray.AxisValue = newAxisValueTables
|
||||
stat.AxisValueCount = len(stat.AxisValueArray.AxisValue)
|
||||
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.DesignAxisRecord.Axis[:] = newDesignAxes
|
||||
stat.DesignAxisCount = len(stat.DesignAxisRecord.Axis)
|
||||
stat.AxisValueArray.AxisValue = newAxisValueTables
|
||||
stat.AxisValueCount = len(stat.AxisValueArray.AxisValue)
|
||||
|
||||
|
||||
def getVariationNameIDs(varfont):
|
||||
@ -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
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user