[instancer] Allow relaxed limits syntax (#3323)
* [instancer] Allow relaxed limits syntax Fixes https://github.com/fonttools/fonttools/issues/3322 Co-authored-by: Cosimo Lupo <clupo@google.com>
This commit is contained in:
parent
2d9b80acd1
commit
ae69e9e8be
@ -139,26 +139,36 @@ def NormalizedAxisRange(minimum, maximum):
|
||||
class AxisTriple(Sequence):
|
||||
"""A triple of (min, default, max) axis values.
|
||||
|
||||
The default value can be None, in which case the limitRangeAndPopulateDefault()
|
||||
method can be used to fill in the missing default value based on the fvar axis
|
||||
default.
|
||||
Any of the values can be None, in which case the limitRangeAndPopulateDefaults()
|
||||
method can be used to fill in the missing values based on the fvar axis values.
|
||||
"""
|
||||
|
||||
minimum: float
|
||||
default: Optional[float] # if None, filled with by limitRangeAndPopulateDefault
|
||||
maximum: float
|
||||
minimum: Optional[float]
|
||||
default: Optional[float]
|
||||
maximum: Optional[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)
|
||||
if (
|
||||
(
|
||||
self.minimum is not None
|
||||
and self.default is not None
|
||||
and self.minimum > self.default
|
||||
)
|
||||
or (
|
||||
self.default is not None
|
||||
and self.maximum is not None
|
||||
and self.default > self.maximum
|
||||
)
|
||||
or (
|
||||
self.minimum is not None
|
||||
and self.maximum is not None
|
||||
and 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})"
|
||||
f"{type(self).__name__} minimum ({self.minimum}), default ({self.default}), maximum ({self.maximum}) must be in sorted order"
|
||||
)
|
||||
|
||||
def __getitem__(self, i):
|
||||
@ -210,7 +220,7 @@ class AxisTriple(Sequence):
|
||||
raise ValueError(f"expected sequence of 2 or 3; got {n}: {v!r}")
|
||||
return cls(minimum, default, maximum)
|
||||
|
||||
def limitRangeAndPopulateDefault(self, fvarTriple) -> "AxisTriple":
|
||||
def limitRangeAndPopulateDefaults(self, fvarTriple) -> "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,
|
||||
@ -219,13 +229,17 @@ class AxisTriple(Sequence):
|
||||
If the default value is already set, return self.
|
||||
"""
|
||||
minimum = self.minimum
|
||||
maximum = self.maximum
|
||||
if minimum is None:
|
||||
minimum = fvarTriple[0]
|
||||
default = self.default
|
||||
if default is None:
|
||||
default = fvarTriple[1]
|
||||
maximum = self.maximum
|
||||
if maximum is None:
|
||||
maximum = fvarTriple[2]
|
||||
|
||||
minimum = max(self.minimum, fvarTriple[0])
|
||||
maximum = max(self.maximum, fvarTriple[0])
|
||||
minimum = max(minimum, fvarTriple[0])
|
||||
maximum = max(maximum, fvarTriple[0])
|
||||
minimum = min(minimum, fvarTriple[2])
|
||||
maximum = min(maximum, fvarTriple[2])
|
||||
default = max(minimum, min(maximum, default))
|
||||
@ -372,7 +386,7 @@ class AxisLimits(_BaseAxisLimits):
|
||||
if triple is None:
|
||||
newLimits[axisTag] = AxisTriple(default, default, default)
|
||||
else:
|
||||
newLimits[axisTag] = triple.limitRangeAndPopulateDefault(fvarTriple)
|
||||
newLimits[axisTag] = triple.limitRangeAndPopulateDefaults(fvarTriple)
|
||||
return type(self)(newLimits)
|
||||
|
||||
def normalize(self, varfont, usingAvar=True) -> "NormalizedAxisLimits":
|
||||
@ -1326,29 +1340,27 @@ def parseLimits(limits: Iterable[str]) -> Dict[str, Optional[AxisTriple]]:
|
||||
result = {}
|
||||
for limitString in limits:
|
||||
match = re.match(
|
||||
r"^(\w{1,4})=(?:(drop)|(?:([^:]+)(?:[:]([^:]+))?(?:[:]([^:]+))?))$",
|
||||
r"^(\w{1,4})=(?:(drop)|(?:([^:]*)(?:[:]([^:]*))?(?:[:]([^:]*))?))$",
|
||||
limitString,
|
||||
)
|
||||
if not match:
|
||||
raise ValueError("invalid location format: %r" % limitString)
|
||||
tag = match.group(1).ljust(4)
|
||||
if match.group(2): # 'drop'
|
||||
lbound = None
|
||||
else:
|
||||
lbound = strToFixedToFloat(match.group(3), precisionBits=16)
|
||||
ubound = default = lbound
|
||||
if match.group(4):
|
||||
ubound = default = strToFixedToFloat(match.group(4), precisionBits=16)
|
||||
default = None
|
||||
if match.group(5):
|
||||
default = ubound
|
||||
ubound = strToFixedToFloat(match.group(5), precisionBits=16)
|
||||
|
||||
if all(v is None for v in (lbound, default, ubound)):
|
||||
if match.group(2): # 'drop'
|
||||
result[tag] = None
|
||||
continue
|
||||
|
||||
result[tag] = AxisTriple(lbound, default, ubound)
|
||||
triple = match.group(3, 4, 5)
|
||||
|
||||
if triple[1] is None: # "value" syntax
|
||||
triple = (triple[0], triple[0], triple[0])
|
||||
elif triple[2] is None: # "min:max" syntax
|
||||
triple = (triple[0], None, triple[1])
|
||||
|
||||
triple = tuple(float(v) if v else None for v in triple)
|
||||
|
||||
result[tag] = AxisTriple(*triple)
|
||||
|
||||
return result
|
||||
|
||||
@ -1377,9 +1389,11 @@ def parseArgs(args):
|
||||
metavar="AXIS=LOC",
|
||||
nargs="*",
|
||||
help="List of space separated locations. A location consists of "
|
||||
"the tag of a variation axis, followed by '=' and one of number, "
|
||||
"number:number or the literal string 'drop'. "
|
||||
"E.g.: wdth=100 or wght=75.0:125.0 or wght=drop",
|
||||
"the tag of a variation axis, followed by '=' and the literal, "
|
||||
"string 'drop', or comma-separate list of one to three values, "
|
||||
"each of which is the empty string, or a number. "
|
||||
"E.g.: wdth=100 or wght=75.0:125.0 or wght=100:400:700 or wght=:500: "
|
||||
"or wght=drop",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
|
@ -1280,16 +1280,29 @@ class InstantiateFvarTest(object):
|
||||
|
||||
assert "fvar" not in varfont
|
||||
|
||||
def test_out_of_range_instance(self, varfont):
|
||||
location = instancer.AxisLimits({"wght": (30, 40, 700)})
|
||||
@pytest.mark.parametrize(
|
||||
"location, expected",
|
||||
[
|
||||
({"wght": (30, 40, 700)}, (100, 100, 700)),
|
||||
({"wght": (30, 40, None)}, (100, 100, 900)),
|
||||
({"wght": (30, None, 700)}, (100, 400, 700)),
|
||||
({"wght": (None, 200, 700)}, (100, 200, 700)),
|
||||
({"wght": (40, None, None)}, (100, 400, 900)),
|
||||
({"wght": (None, 40, None)}, (100, 100, 900)),
|
||||
({"wght": (None, None, 700)}, (100, 400, 700)),
|
||||
({"wght": (None, None, None)}, (100, 400, 900)),
|
||||
],
|
||||
)
|
||||
def test_axis_limits(self, varfont, location, expected):
|
||||
location = instancer.AxisLimits(location)
|
||||
|
||||
varfont = instancer.instantiateVariableFont(varfont, location)
|
||||
|
||||
fvar = varfont["fvar"]
|
||||
axes = {a.axisTag: a for a in fvar.axes}
|
||||
assert axes["wght"].minValue == 100
|
||||
assert axes["wght"].defaultValue == 100
|
||||
assert axes["wght"].maxValue == 700
|
||||
assert axes["wght"].minValue == expected[0]
|
||||
assert axes["wght"].defaultValue == expected[1]
|
||||
assert axes["wght"].maxValue == expected[2]
|
||||
|
||||
|
||||
class InstantiateSTATTest(object):
|
||||
@ -2085,6 +2098,15 @@ def test_limitFeatureVariationConditionRange(oldRange, newLimit, expected):
|
||||
(["wght=400:700:900"], {"wght": (400, 700, 900)}),
|
||||
(["slnt=11.4"], {"slnt": 11.399994}),
|
||||
(["ABCD=drop"], {"ABCD": None}),
|
||||
(["wght=:500:"], {"wght": (None, 500, None)}),
|
||||
(["wght=::700"], {"wght": (None, None, 700)}),
|
||||
(["wght=200::"], {"wght": (200, None, None)}),
|
||||
(["wght=200:300:"], {"wght": (200, 300, None)}),
|
||||
(["wght=:300:500"], {"wght": (None, 300, 500)}),
|
||||
(["wght=300::700"], {"wght": (300, None, 700)}),
|
||||
(["wght=300:700"], {"wght": (300, None, 700)}),
|
||||
(["wght=:700"], {"wght": (None, None, 700)}),
|
||||
(["wght=200:"], {"wght": (200, None, None)}),
|
||||
],
|
||||
)
|
||||
def test_parseLimits(limits, expected):
|
||||
|
Loading…
x
Reference in New Issue
Block a user