Merge pull request #2861 from fonttools/l4-fixes

[instancer-l4] expand singles/tuples to triples upfront and use triples throughout
This commit is contained in:
Cosimo Lupo 2022-10-21 18:47:42 +01:00 committed by GitHub
commit 65a65b5f66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 434 additions and 237 deletions

View File

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

View File

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

View File

@ -728,7 +728,7 @@
<AxisOrdering value="2"/>
</Axis>
</DesignAxisRecord>
<!-- AxisValueCount=5 -->
<!-- AxisValueCount=7 -->
<AxisValueArray>
<AxisValue index="0" Format="1">
<AxisIndex value="0"/>
@ -743,7 +743,13 @@
<Value value="400.0"/>
<LinkedValue value="700.0"/>
</AxisValue>
<AxisValue index="2" Format="2">
<AxisValue index="2" Format="1">
<AxisIndex value="0"/>
<Flags value="0"/>
<ValueNameID value="262"/> <!-- Medium -->
<Value value="500.0"/>
</AxisValue>
<AxisValue index="3" Format="2">
<AxisIndex value="0"/>
<Flags value="0"/>
<ValueNameID value="266"/> <!-- Black -->
@ -751,7 +757,7 @@
<RangeMinValue value="801.0"/>
<RangeMaxValue value="900.0"/>
</AxisValue>
<AxisValue index="3" Format="4">
<AxisValue index="4" Format="4">
<!-- AxisCount=1 -->
<Flags value="0"/>
<ValueNameID value="279"/> <!-- Condensed -->
@ -760,14 +766,14 @@
<Value value="79.0"/>
</AxisValueRecord>
</AxisValue>
<AxisValue index="4" Format="3">
<AxisValue index="5" Format="3">
<AxisIndex value="2"/>
<Flags value="2"/>
<ValueNameID value="295"/> <!-- Upright -->
<Value value="0.0"/>
<LinkedValue value="1.0"/>
</AxisValue>
<AxisValue index="3" Format="4">
<AxisValue index="6" Format="4">
<!-- AxisCount=1 -->
<Flags value="2"/>
<ValueNameID value="297"/> <!-- Normal -->

View File

@ -112,6 +112,8 @@ class InstantiateGvarTest(object):
],
)
def test_pin_and_drop_axis(self, varfont, glyph_name, location, expected, optimize):
location = instancer.NormalizedAxisLimits(location)
instancer.instantiateGvar(varfont, location, optimize=optimize)
assert _get_coordinates(varfont, glyph_name) == expected[glyph_name]
@ -124,9 +126,9 @@ class InstantiateGvarTest(object):
)
def test_full_instance(self, varfont, optimize):
instancer.instantiateGvar(
varfont, {"wght": 0.0, "wdth": -0.5}, optimize=optimize
)
location = instancer.NormalizedAxisLimits(wght=0.0, wdth=-0.5)
instancer.instantiateGvar(varfont, location, optimize=optimize)
assert _get_coordinates(varfont, "hyphen") == [
(33.5, 229),
@ -169,7 +171,7 @@ class InstantiateGvarTest(object):
assert hmtx["minus"] == (422, 40)
assert vmtx["minus"] == (536, 229)
location = {"wght": -1.0, "wdth": -1.0}
location = instancer.NormalizedAxisLimits(wght=-1.0, wdth=-1.0)
instancer.instantiateGvar(varfont, location)
@ -206,6 +208,8 @@ class InstantiateCvarTest(object):
],
)
def test_pin_and_drop_axis(self, varfont, location, expected):
location = instancer.NormalizedAxisLimits(location)
instancer.instantiateCvar(varfont, location)
assert list(varfont["cvt "].values) == expected
@ -217,7 +221,9 @@ class InstantiateCvarTest(object):
)
def test_full_instance(self, varfont):
instancer.instantiateCvar(varfont, {"wght": -0.5, "wdth": -0.5})
location = instancer.NormalizedAxisLimits(wght=-0.5, wdth=-0.5)
instancer.instantiateCvar(varfont, location)
assert list(varfont["cvt "].values) == [500, -400, 165, 225]
@ -272,6 +278,8 @@ class InstantiateMVARTest(object):
assert mvar.VarStore.VarData[1].VarRegionCount == 1
assert all(len(item) == 1 for item in mvar.VarStore.VarData[1].Item)
location = instancer.NormalizedAxisLimits(location)
instancer.instantiateMVAR(varfont, location)
for mvar_tag, expected_value in expected.items():
@ -312,6 +320,8 @@ class InstantiateMVARTest(object):
],
)
def test_full_instance(self, varfont, location, expected):
location = instancer.NormalizedAxisLimits(location)
instancer.instantiateMVAR(varfont, location)
for mvar_tag, expected_value in expected.items():
@ -344,6 +354,8 @@ class InstantiateHVARTest(object):
],
)
def test_partial_instance(self, varfont, location, expectedRegions, expectedDeltas):
location = instancer.NormalizedAxisLimits(location)
instancer.instantiateHVAR(varfont, location)
assert "HVAR" in varfont
@ -376,7 +388,9 @@ class InstantiateHVARTest(object):
assert varStore.VarData[varIdx >> 16].Item[varIdx & 0xFFFF] == expectedDeltas
def test_full_instance(self, varfont):
instancer.instantiateHVAR(varfont, {"wght": 0, "wdth": 0})
location = instancer.NormalizedAxisLimits(wght=0, wdth=0)
instancer.instantiateHVAR(varfont, location)
assert "HVAR" not in varfont
@ -390,7 +404,9 @@ class InstantiateHVARTest(object):
axis.axisTag = "TEST"
fvar.axes.append(axis)
instancer.instantiateHVAR(varfont, {"wght": 0, "wdth": 0})
location = instancer.NormalizedAxisLimits(wght=0, wdth=0)
instancer.instantiateHVAR(varfont, location)
assert "HVAR" in varfont
@ -452,6 +468,8 @@ class InstantiateItemVariationStoreTest(object):
def test_instantiate_default_deltas(
self, varStore, fvarAxes, location, expected_deltas, num_regions
):
location = instancer.NormalizedAxisLimits(location)
defaultDeltas = instancer.instantiateItemVariationStore(
varStore, fvarAxes, location
)
@ -504,8 +522,9 @@ class TupleVarStoreAdapterTest(object):
adapter = instancer._TupleVarStoreAdapter(
regions, axisOrder, tupleVarData, itemCounts=[2, 2]
)
location = instancer.NormalizedAxisLimits(wght=0.5)
defaultDeltaArray = adapter.instantiate({"wght": 0.5})
defaultDeltaArray = adapter.instantiate(location)
assert defaultDeltaArray == [[15, 45], [0, 0]]
assert adapter.regions == [{"wdth": (-1.0, -1.0, 0)}]
@ -747,6 +766,8 @@ class InstantiateOTLTest(object):
vf = varfontGDEF
assert "GDEF" in vf
location = instancer.NormalizedAxisLimits(location)
instancer.instantiateOTL(vf, location)
assert "GDEF" in vf
@ -778,6 +799,8 @@ class InstantiateOTLTest(object):
vf = varfontGDEF
assert "GDEF" in vf
location = instancer.NormalizedAxisLimits(location)
instancer.instantiateOTL(vf, location)
assert "GDEF" in vf
@ -806,6 +829,8 @@ class InstantiateOTLTest(object):
assert "GDEF" in vf
assert "GPOS" in vf
location = instancer.NormalizedAxisLimits(location)
instancer.instantiateOTL(vf, location)
gdef = vf["GDEF"].table
@ -839,6 +864,8 @@ class InstantiateOTLTest(object):
assert "GDEF" in vf
assert "GPOS" in vf
location = instancer.NormalizedAxisLimits(location)
instancer.instantiateOTL(vf, location)
assert "GDEF" not in vf
@ -870,6 +897,8 @@ class InstantiateOTLTest(object):
assert "GDEF" in vf
assert "GPOS" in vf
location = instancer.NormalizedAxisLimits(location)
instancer.instantiateOTL(vf, location)
v1, v2 = expected
@ -915,6 +944,8 @@ class InstantiateOTLTest(object):
assert "GDEF" in vf
assert "GPOS" in vf
location = instancer.NormalizedAxisLimits(location)
instancer.instantiateOTL(vf, location)
v1, v2 = expected
@ -955,7 +986,7 @@ class InstantiateOTLTest(object):
# check that MutatorMerger for ValueRecord doesn't raise AttributeError
# when XAdvDevice is present but there's no corresponding XAdvance.
instancer.instantiateOTL(vf, {"wght": 0.5})
instancer.instantiateOTL(vf, instancer.NormalizedAxisLimits(wght=0.5))
pairPos = vf["GPOS"].table.LookupList.Lookup[0].SubTable[0]
assert pairPos.ValueFormat1 == 0x4
@ -967,12 +998,16 @@ class InstantiateOTLTest(object):
class InstantiateAvarTest(object):
@pytest.mark.parametrize("location", [{"wght": 0.0}, {"wdth": 0.0}])
def test_pin_and_drop_axis(self, varfont, location):
location = instancer.AxisLimits(location)
instancer.instantiateAvar(varfont, location)
assert set(varfont["avar"].segments).isdisjoint(location)
def test_full_instance(self, varfont):
instancer.instantiateAvar(varfont, {"wght": 0.0, "wdth": 0.0})
location = instancer.AxisLimits(wght=0.0, wdth=0.0)
instancer.instantiateAvar(varfont, location)
assert "avar" not in varfont
@ -1139,6 +1174,8 @@ class InstantiateAvarTest(object):
],
)
def test_limit_axes(self, varfont, axisLimits, expectedSegments):
axisLimits = instancer.AxisLimits(axisLimits)
instancer.instantiateAvar(varfont, axisLimits)
newSegments = varfont["avar"].segments
@ -1162,8 +1199,10 @@ class InstantiateAvarTest(object):
def test_drop_invalid_segment_map(self, varfont, invalidSegmentMap, caplog):
varfont["avar"].segments["wght"] = invalidSegmentMap
axisLimits = instancer.AxisLimits(wght=(100, 400))
with caplog.at_level(logging.WARNING, logger="fontTools.varLib.instancer"):
instancer.instantiateAvar(varfont, {"wght": (100, 400)})
instancer.instantiateAvar(varfont, axisLimits)
assert "Invalid avar" in caplog.text
assert "wght" not in varfont["avar"].segments
@ -1210,6 +1249,8 @@ class InstantiateFvarTest(object):
],
)
def test_pin_and_drop_axis(self, varfont, location, instancesLeft):
location = instancer.AxisLimits(location)
instancer.instantiateFvar(varfont, location)
fvar = varfont["fvar"]
@ -1224,7 +1265,9 @@ class InstantiateFvarTest(object):
] == instancesLeft
def test_full_instance(self, varfont):
instancer.instantiateFvar(varfont, {"wght": 0.0, "wdth": 0.0})
location = instancer.AxisLimits({"wght": 0.0, "wdth": 0.0})
instancer.instantiateFvar(varfont, location)
assert "fvar" not in varfont
@ -1234,10 +1277,15 @@ class InstantiateSTATTest(object):
"location, expected",
[
({"wght": 400}, ["Regular", "Condensed", "Upright", "Normal"]),
({"wdth": 100}, ["Thin", "Regular", "Black", "Upright", "Normal"]),
(
{"wdth": 100},
["Thin", "Regular", "Medium", "Black", "Upright", "Normal"],
),
],
)
def test_pin_and_drop_axis(self, varfont, location, expected):
location = instancer.AxisLimits(location)
instancer.instantiateSTAT(varfont, location)
stat = varfont["STAT"].table
@ -1256,7 +1304,7 @@ class InstantiateSTATTest(object):
def test_skip_table_no_axis_value_array(self, varfont):
varfont["STAT"].table.AxisValueArray = None
instancer.instantiateSTAT(varfont, {"wght": 100})
instancer.instantiateSTAT(varfont, instancer.AxisLimits(wght=100))
assert len(varfont["STAT"].table.DesignAxisRecord.Axis) == 3
assert varfont["STAT"].table.AxisValueArray is None
@ -1318,7 +1366,9 @@ class InstantiateSTATTest(object):
return result
def test_limit_axes(self, varfont2):
instancer.instantiateSTAT(varfont2, {"wght": (400, 500), "wdth": (75, 100)})
axisLimits = instancer.AxisLimits({"wght": (400, 500), "wdth": (75, 100)})
instancer.instantiateSTAT(varfont2, axisLimits)
assert len(varfont2["STAT"].table.AxisValueArray.AxisValue) == 5
assert self.get_STAT_axis_values(varfont2["STAT"].table) == [
@ -1344,11 +1394,11 @@ class InstantiateSTATTest(object):
axisValue.AxisValueRecord.append(rec)
stat.AxisValueArray.AxisValue.append(axisValue)
instancer.instantiateSTAT(varfont2, {"wght": (100, 600)})
instancer.instantiateSTAT(varfont2, instancer.AxisLimits(wght=(100, 600)))
assert axisValue in varfont2["STAT"].table.AxisValueArray.AxisValue
instancer.instantiateSTAT(varfont2, {"wdth": (62.5, 87.5)})
instancer.instantiateSTAT(varfont2, instancer.AxisLimits(wdth=(62.5, 87.5)))
assert axisValue not in varfont2["STAT"].table.AxisValueArray.AxisValue
@ -1359,7 +1409,7 @@ class InstantiateSTATTest(object):
stat.AxisValueArray.AxisValue.append(axisValue)
with caplog.at_level(logging.WARNING, logger="fontTools.varLib.instancer"):
instancer.instantiateSTAT(varfont2, {"wght": 400})
instancer.instantiateSTAT(varfont2, instancer.AxisLimits(wght=400))
assert "Unknown AxisValue table format (5)" in caplog.text
assert axisValue in varfont2["STAT"].table.AxisValueArray.AxisValue
@ -1482,17 +1532,15 @@ class InstantiateVariableFontTest(object):
location = {"wght": 280, "opsz": 18}
instance = instancer.instantiateVariableFont(
varfont, location,
varfont,
location,
)
expected = _get_expected_instance_ttx(
"SinglePos", *location.values()
)
expected = _get_expected_instance_ttx("SinglePos", *location.values())
assert _dump_ttx(instance) == expected
def _conditionSetAsDict(conditionSet, axisOrder):
result = {}
for cond in conditionSet.ConditionTable:
@ -1577,15 +1625,15 @@ class InstantiateFeatureVariationsTest(object):
],
),
(
{"cntr": (-.5, 0, 1.0)},
{"cntr": (-0.5, 0, 1.0)},
{},
[
(
{"wght": (0.20886, 1.0), "cntr": (.75, 1)},
{"wght": (0.20886, 1.0), "cntr": (0.75, 1)},
{"uni0024": "uni0024.nostroke", "uni0041": "uni0061"},
),
(
{"wght": (-1.0, -0.45654), "cntr": (0, .25)},
{"wght": (-1.0, -0.45654), "cntr": (0, 0.25)},
{"uni0061": "uni0041"},
),
(
@ -1599,7 +1647,7 @@ class InstantiateFeatureVariationsTest(object):
],
),
(
{"cntr": (.8, .9, 1.0)},
{"cntr": (0.8, 0.9, 1.0)},
{"uni0041": "uni0061"},
[
(
@ -1626,8 +1674,7 @@ class InstantiateFeatureVariationsTest(object):
]
)
limits = {tag:instancer.NormalizedAxisTent(l, l, l) if not isinstance(l, tuple) else instancer.NormalizedAxisTent(*l)
for tag,l in location.items()}
limits = instancer.NormalizedAxisLimits(location)
instancer.instantiateFeatureVariations(font, limits)
gsub = font["GSUB"].table
@ -1635,7 +1682,11 @@ class InstantiateFeatureVariationsTest(object):
assert featureVariations.FeatureVariationCount == len(expectedRecords)
axisOrder = [a.axisTag for a in font["fvar"].axes if a.axisTag not in location or isinstance(location[a.axisTag], tuple)]
axisOrder = [
a.axisTag
for a in font["fvar"].axes
if a.axisTag not in location or isinstance(location[a.axisTag], tuple)
]
for i, (expectedConditionSet, expectedSubs) in enumerate(expectedRecords):
rec = featureVariations.FeatureVariationRecord[i]
conditionSet = _conditionSetAsDict(rec.ConditionSet, axisOrder)
@ -1681,6 +1732,8 @@ class InstantiateFeatureVariationsTest(object):
assert gsub.FeatureVariations
assert gsub.Version == 0x00010001
location = instancer.NormalizedAxisLimits(location)
instancer.instantiateFeatureVariations(font, location)
assert not hasattr(gsub, "FeatureVariations")
@ -1707,7 +1760,9 @@ class InstantiateFeatureVariationsTest(object):
rec1.ConditionSet.ConditionTable[0].Format = 2
with caplog.at_level(logging.WARNING, logger="fontTools.varLib.instancer"):
instancer.instantiateFeatureVariations(font, {"wdth": instancer.NormalizedAxisTent(0,0,0)})
instancer.instantiateFeatureVariations(
font, instancer.NormalizedAxisLimits(wdth=0)
)
assert (
"Condition table 0 of FeatureVariationRecord 0 "
@ -1819,7 +1874,7 @@ class LimitTupleVariationAxisRangesTest:
],
)
def test_positive_var(self, var, axisTag, newMax, expected):
axisRange = instancer.NormalizedAxisRange(0, newMax)
axisRange = instancer.NormalizedAxisTriple(0, 0, newMax)
self.check_limit_single_var_axis_range(var, axisTag, axisRange, expected)
@pytest.mark.parametrize(
@ -1898,7 +1953,7 @@ class LimitTupleVariationAxisRangesTest:
],
)
def test_negative_var(self, var, axisTag, newMin, expected):
axisRange = instancer.NormalizedAxisRange(newMin, 0)
axisRange = instancer.NormalizedAxisTriple(newMin, 0, 0)
self.check_limit_single_var_axis_range(var, axisTag, axisRange, expected)
@ -1921,7 +1976,7 @@ def test_limitFeatureVariationConditionRange(oldRange, newLimit, expected):
condition = featureVars.buildConditionTable(0, *oldRange)
result = instancer._limitFeatureVariationConditionRange(
condition, instancer.NormalizedAxisTent(*newLimit)
condition, instancer.NormalizedAxisTriple(*newLimit)
)
assert result == expected
@ -1933,12 +1988,23 @@ def test_limitFeatureVariationConditionRange(oldRange, newLimit, expected):
(["wght=400", "wdth=100"], {"wght": 400, "wdth": 100}),
(["wght=400:900"], {"wght": (400, 900)}),
(["wght=400:700:900"], {"wght": (400, 700, 900)}),
(["slnt=11.4"], {"slnt": pytest.approx(11.399994)}),
(["slnt=11.4"], {"slnt": 11.399994}),
(["ABCD=drop"], {"ABCD": None}),
],
)
def test_parseLimits(limits, expected):
assert instancer.parseLimits(limits) == expected
limits = instancer.parseLimits(limits)
expected = instancer.AxisLimits(expected)
assert limits.keys() == expected.keys()
for axis, triple in limits.items():
expected_triple = expected[axis]
if expected_triple is None:
assert triple is None
else:
assert isinstance(triple, instancer.AxisTriple)
assert isinstance(expected_triple, instancer.AxisTriple)
assert triple == pytest.approx(expected_triple)
@pytest.mark.parametrize(
@ -1949,22 +2015,34 @@ def test_parseLimits_invalid(limits):
instancer.parseLimits(limits)
def test_normalizeAxisLimits_tuple(varfont):
normalized = instancer.normalizeAxisLimits(varfont, {"wght": (100, 400)})
assert normalized == {"wght": (-1.0, 0, 0)}
@pytest.mark.parametrize(
"limits, expected",
[
({"wght": (100, 400)}, {"wght": (-1.0, 0, 0)}),
({"wght": (100, 400, 400)}, {"wght": (-1.0, 0, 0)}),
({"wght": (100, 300, 400)}, {"wght": (-1.0, -0.5, 0)}),
],
)
def test_normalizeAxisLimits(varfont, limits, expected):
limits = instancer.AxisLimits(limits)
normalized = limits.normalize(varfont)
assert normalized == instancer.NormalizedAxisLimits(expected)
def test_normalizeAxisLimits_no_avar(varfont):
del varfont["avar"]
normalized = instancer.normalizeAxisLimits(varfont, {"wght": (400, 500)})
limits = instancer.AxisLimits(wght=(400, 400, 500))
normalized = limits.normalize(varfont)
assert normalized["wght"] == pytest.approx((0, 0, 0.2), 1e-4)
def test_normalizeAxisLimits_missing_from_fvar(varfont):
with pytest.raises(ValueError, match="not present in fvar"):
instancer.normalizeAxisLimits(varfont, {"ZZZZ": 1000})
instancer.AxisLimits({"ZZZZ": 1000}).normalize(varfont)
def test_sanityCheckVariableTables(varfont):

View File

@ -115,7 +115,7 @@ def _test_name_records(varfont, expected, isNonRIBBI, platforms=[0x409]):
),
# Condensed with unpinned weights
(
{"wdth": 79, "wght": instancer.AxisRange(400, 900)},
{"wdth": 79, "wght": (400, 900)},
{
(1, 3, 1, 0x409): "Test Variable Font Condensed",
(2, 3, 1, 0x409): "Regular",
@ -126,6 +126,19 @@ def _test_name_records(varfont, expected, isNonRIBBI, platforms=[0x409]):
},
True,
),
# Restrict weight and move default, new minimum (500) > old default (400)
(
{"wght": (500, 900)},
{
(1, 3, 1, 0x409): "Test Variable Font Medium",
(2, 3, 1, 0x409): "Regular",
(3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Medium",
(6, 3, 1, 0x409): "TestVariableFont-Medium",
(16, 3, 1, 0x409): "Test Variable Font",
(17, 3, 1, 0x409): "Medium",
},
True,
),
],
)
def test_updateNameTable_with_registered_axes_ribbi(
@ -215,7 +228,7 @@ def test_updateNameTable_with_multilingual_names(varfont, limits, expected, isNo
def test_updateNameTable_missing_axisValues(varfont):
with pytest.raises(ValueError, match="Cannot find Axis Values \['wght=200'\]"):
with pytest.raises(ValueError, match="Cannot find Axis Values {'wght': 200}"):
instancer.names.updateNameTable(varfont, {"wght": 200})
@ -257,7 +270,7 @@ def test_updateNameTable_missing_stat(varfont):
def test_updateNameTable_vf_with_italic_attribute(
varfont, limits, expected, isNonRIBBI
):
font_link_axisValue = varfont["STAT"].table.AxisValueArray.AxisValue[4]
font_link_axisValue = varfont["STAT"].table.AxisValueArray.AxisValue[5]
# Unset ELIDABLE_AXIS_VALUE_NAME flag
font_link_axisValue.Flags &= ~instancer.names.ELIDABLE_AXIS_VALUE_NAME
font_link_axisValue.ValueNameID = 294 # Roman --> Italic