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):
|
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;
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user