Skip to content

Commit

Permalink
Merge pull request #3179 from fonttools/L4-fixes
Browse files Browse the repository at this point in the history
L4 fixes
  • Loading branch information
behdad committed Jul 11, 2023
2 parents 5ac1e5b + 0893ba9 commit fb56e7b
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 98 deletions.
99 changes: 88 additions & 11 deletions Lib/fontTools/varLib/instancer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,8 @@ def expand(
default = None
if n == 2:
minimum, maximum = v
elif n == 3:
minimum, default, maximum = v
elif n >= 3:
return cls(*v)
else:
raise ValueError(f"expected sequence of 2 or 3; got {n}: {v!r}")
return cls(minimum, default, maximum)
Expand Down Expand Up @@ -251,6 +251,70 @@ def __post_init__(self):
)


@dataclasses.dataclass(frozen=True, order=True, repr=False)
class NormalizedAxisTripleAndDistances(AxisTriple):
"""A triple of (min, default, max) normalized axis values,
with distances between min and default, and default and max,
in the *pre-normalized* space."""

minimum: float
default: float
maximum: float
distanceNegative: Optional[float] = 1
distancePositive: Optional[float] = 1

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(
"Normalized axis values not in -1..+1 range; got "
f"minimum={self.minimum:g}, default={self.default:g}, maximum={self.maximum:g})"
)

def reverse_negate(self):
v = self
return self.__class__(-v[2], -v[1], -v[0], v[4], v[3])

def renormalizeValue(self, v, extrapolate=True):
"""Renormalizes a normalized value v to the range of this axis,
considering the pre-normalized distances as well as the new
axis limits."""

lower, default, upper, distanceNegative, distancePositive = self
assert lower <= default <= upper

if not extrapolate:
v = max(lower, min(upper, v))

if v == default:
return 0

if default < 0:
return -self.reverse_negate().renormalizeValue(-v, extrapolate=extrapolate)

# default >= 0 and v != default

if v > default:
return (v - default) / (upper - default)

# v < default

if lower >= 0:
return (v - default) / (default - lower)

# lower < 0 and v < default

totalDistance = distanceNegative * -lower + distancePositive * default

if v >= 0:
vDistance = (default - v) * distancePositive
else:
vDistance = -v * distanceNegative + distancePositive * default

return -vDistance / totalDistance


class _BaseAxisLimits(Mapping[str, AxisTriple]):
def __getitem__(self, key: str) -> AxisTriple:
return self._data[key]
Expand Down Expand Up @@ -334,8 +398,13 @@ def normalize(self, varfont, usingAvar=True) -> "NormalizedAxisLimits":
normalizedLimits = {}

for axis_tag, triple in axes.items():
distanceNegative = triple[1] - triple[0]
distancePositive = triple[2] - triple[1]

if self[axis_tag] is None:
normalizedLimits[axis_tag] = NormalizedAxisTriple(0, 0, 0)
normalizedLimits[axis_tag] = NormalizedAxisTripleAndDistances(
0, 0, 0, distanceNegative, distancePositive
)
continue

minV, defaultV, maxV = self[axis_tag]
Expand All @@ -344,8 +413,10 @@ def normalize(self, varfont, usingAvar=True) -> "NormalizedAxisLimits":
defaultV = triple[1]

avarMapping = avarSegments.get(axis_tag, None)
normalizedLimits[axis_tag] = NormalizedAxisTriple(
*(normalize(v, triple, avarMapping) for v in (minV, defaultV, maxV))
normalizedLimits[axis_tag] = NormalizedAxisTripleAndDistances(
*(normalize(v, triple, avarMapping) for v in (minV, defaultV, maxV)),
distanceNegative,
distancePositive,
)

return NormalizedAxisLimits(normalizedLimits)
Expand All @@ -358,7 +429,7 @@ def __init__(self, *args, **kwargs):
self._data = data = {}
for k, v in dict(*args, **kwargs).items():
try:
triple = NormalizedAxisTriple.expand(v)
triple = NormalizedAxisTripleAndDistances.expand(v)
except ValueError as e:
raise ValueError(f"Invalid axis limits for {k!r}: {v!r}") from e
data[k] = triple
Expand Down Expand Up @@ -442,7 +513,7 @@ def changeTupleVariationsAxisLimits(variations, axisLimits):


def changeTupleVariationAxisLimit(var, axisTag, axisLimit):
assert isinstance(axisLimit, NormalizedAxisTriple)
assert isinstance(axisLimit, NormalizedAxisTripleAndDistances)

# Skip when current axis is missing (i.e. doesn't participate),
lower, peak, upper = var.axes.get(axisTag, (-1, 0, 1))
Expand Down Expand Up @@ -505,7 +576,7 @@ def _instantiateGvarGlyph(
"Instancing accross VarComposite axes with variation is not supported."
)
limits = axisLimits[tag]
loc = normalizeValue(loc, limits)
loc = limits.renormalizeValue(loc, extrapolate=False)
newLocation[tag] = loc
component.location = newLocation

Expand Down Expand Up @@ -925,15 +996,21 @@ def instantiateAvar(varfont, axisLimits):
mappedMax = floatToFixedToFloat(
piecewiseLinearMap(axisRange.maximum, mapping), 14
)
mappedAxisLimit = NormalizedAxisTripleAndDistances(
mappedMin,
mappedDef,
mappedMax,
axisRange.distanceNegative,
axisRange.distancePositive,
)
newMapping = {}
for fromCoord, toCoord in mapping.items():

if fromCoord < axisRange.minimum or fromCoord > axisRange.maximum:
continue
fromCoord = normalizeValue(fromCoord, axisRange)
fromCoord = axisRange.renormalizeValue(fromCoord)

assert mappedMin <= toCoord <= mappedMax
toCoord = normalizeValue(toCoord, (mappedMin, mappedDef, mappedMax))
toCoord = mappedAxisLimit.renormalizeValue(toCoord)

fromCoord = floatToFixedToFloat(fromCoord, 14)
toCoord = floatToFixedToFloat(toCoord, 14)
Expand Down
9 changes: 5 additions & 4 deletions Lib/fontTools/varLib/instancer/featureVars.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from fontTools.ttLib.tables import otTables as ot
from fontTools.varLib.models import normalizeValue
from copy import deepcopy
import logging

Expand Down Expand Up @@ -41,7 +40,9 @@ def _limitFeatureVariationConditionRange(condition, axisLimit):
# condition invalid or out of range
return

return tuple(normalizeValue(v, axisLimit) for v in (minValue, maxValue))
return tuple(
axisLimit.renormalizeValue(v, extrapolate=False) for v in (minValue, maxValue)
)


def _instantiateFeatureVariationRecord(
Expand All @@ -50,9 +51,9 @@ def _instantiateFeatureVariationRecord(
applies = True
shouldKeep = False
newConditions = []
from fontTools.varLib.instancer import NormalizedAxisTriple
from fontTools.varLib.instancer import NormalizedAxisTripleAndDistances

default_triple = NormalizedAxisTriple(-1, 0, +1)
default_triple = NormalizedAxisTripleAndDistances(-1, 0, +1)
for i, condition in enumerate(record.ConditionSet.ConditionTable):
if condition.Format == 1:
axisIdx = condition.AxisIndex
Expand Down
138 changes: 64 additions & 74 deletions Lib/fontTools/varLib/instancer/solver.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from fontTools.varLib.models import supportScalar, normalizeValue
from fontTools.varLib.models import supportScalar
from fontTools.misc.fixedTools import MAX_F2DOT14
from functools import lru_cache

Expand All @@ -12,15 +12,17 @@ def _reverse_negate(v):


def _solve(tent, axisLimit, negative=False):
axisMin, axisDef, axisMax = axisLimit
axisMin, axisDef, axisMax, _distanceNegative, _distancePositive = axisLimit
lower, peak, upper = tent

# Mirror the problem such that axisDef <= peak
if axisDef > peak:
return [
(scalar, _reverse_negate(t) if t is not None else None)
for scalar, t in _solve(
_reverse_negate(tent), _reverse_negate(axisLimit), not negative
_reverse_negate(tent),
axisLimit.reverse_negate(),
not negative,
)
]
# axisDef <= peak
Expand Down Expand Up @@ -98,9 +100,8 @@ def _solve(tent, axisLimit, negative=False):
# |
# crossing
if gain > outGain:

# Crossing point on the axis.
crossing = peak + ((1 - gain) * (upper - peak) / (1 - outGain))
crossing = peak + (1 - gain) * (upper - peak)

loc = (axisDef, peak, crossing)
scalar = 1
Expand All @@ -116,7 +117,7 @@ def _solve(tent, axisLimit, negative=False):
# the drawing above.
if upper >= axisMax:
loc = (crossing, axisMax, axisMax)
scalar = supportScalar({"tag": axisMax}, {"tag": tent})
scalar = outGain

out.append((scalar - gain, loc))

Expand Down Expand Up @@ -147,84 +148,73 @@ def _solve(tent, axisLimit, negative=False):

# Eternity justify.
loc2 = (upper, axisMax, axisMax)
scalar2 = supportScalar({"tag": axisMax}, {"tag": tent})
scalar2 = 0

out.append((scalar1 - gain, loc1))
out.append((scalar2 - gain, loc2))

# Case 3: 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.
#
# A second tent is needed for cases when gain is positive, though we add it
# unconditionally and it will be dropped because scalar ends up 0.
#
# TODO: See if we can just move upper closer to adjust the slope, instead of
# second tent.
#
# | peak |
# 1.........|............o...|..................
# | /x\ |
# | /xxx\ |
# | /xxxxx\|
# | /xxxxxxx+
# | /xxxxxxxx|\
# 0---|-----|------oxxxxxxxxx|xo---------------1
# axisMin | lower | upper
# | |
# axisDef axisMax
#
elif axisDef + (axisMax - axisDef) * 2 >= upper:

if not negative and axisDef + (axisMax - axisDef) * MAX_F2DOT14 < upper:
# we clamp +2.0 to the max F2Dot14 (~1.99994) for convenience
upper = axisDef + (axisMax - axisDef) * MAX_F2DOT14
assert peak < upper

else:
# Special-case if peak is at axisMax.
if axisMax == peak:
upper = peak

loc1 = (max(axisDef, lower), peak, upper)
scalar1 = 1

loc2 = (peak, upper, upper)
scalar2 = 0

# Don't add a dirac delta!
if axisDef < upper:
out.append((scalar1 - gain, loc1))
if peak < upper:
out.append((scalar2 - gain, loc2))
# Case 3:
# We keep delta as is and only scale the axis upper to achieve
# the desired new tent if feasible.
#
# peak
# 1.....................o....................
# / \_|
# ..................../....+_.........outGain
# / | \
# gain..............+......|..+_.............
# /| | | \
# 0---|-----------o | | | o----------1
# axisMin lower| | | upper
# | | newUpper
# axisDef axisMax
#
newUpper = peak + (1 - gain) * (upper - peak)
assert axisMax <= newUpper # Because outGain >= gain
if newUpper <= axisDef + (axisMax - axisDef) * 2:
upper = newUpper
if not negative and axisDef + (axisMax - axisDef) * MAX_F2DOT14 < upper:
# we clamp +2.0 to the max F2Dot14 (~1.99994) for convenience
upper = axisDef + (axisMax - axisDef) * MAX_F2DOT14
assert peak < upper

loc = (max(axisDef, lower), peak, upper)
scalar = 1

# Case 4: New limit doesn't fit; we need to chop into two tents,
# because the shape of a triangle with part of one side cut off
# cannot be represented as a triangle itself.
#
# | peak |
# 1.........|......o.|...................
# | /x\|
# | |xxy|\_
# | /xxxy| \_
# | |xxxxy| \_
# | /xxxxy| \_
# 0---|-----|-oxxxxxx| o----------1
# axisMin | lower | upper
# | |
# axisDef axisMax
#
else:
out.append((scalar - gain, loc))

loc1 = (max(axisDef, lower), peak, axisMax)
scalar1 = 1
# Case 4: New limit doesn't fit; we need to chop into two tents,
# because the shape of a triangle with part of one side cut off
# cannot be represented as a triangle itself.
#
# | peak |
# 1.........|......o.|....................
# ..........|...../x\|.............outGain
# | |xxy|\_
# | /xxxy| \_
# | |xxxxy| \_
# | /xxxxy| \_
# 0---|-----|-oxxxxxx| o----------1
# axisMin | lower | upper
# | |
# axisDef axisMax
#
else:
loc1 = (max(axisDef, lower), peak, axisMax)
scalar1 = 1

loc2 = (peak, axisMax, axisMax)
scalar2 = supportScalar({"tag": axisMax}, {"tag": tent})
loc2 = (peak, axisMax, axisMax)
scalar2 = outGain

out.append((scalar1 - gain, loc1))
# Don't add a dirac delta!
if peak < axisMax:
out.append((scalar2 - gain, loc2))
out.append((scalar1 - gain, loc1))
# Don't add a dirac delta!
if peak < axisMax:
out.append((scalar2 - gain, loc2))

# Now, the negative side

Expand Down Expand Up @@ -295,7 +285,7 @@ def rebaseTent(tent, axisLimit):
If tent value is None, that is a special deltaset that should
be always-enabled (called "gain")."""

axisMin, axisDef, axisMax = axisLimit
axisMin, axisDef, axisMax, _distanceNegative, _distancePositive = axisLimit
assert -1 <= axisMin <= axisDef <= axisMax <= +1

lower, peak, upper = tent
Expand All @@ -305,7 +295,7 @@ def rebaseTent(tent, axisLimit):

sols = _solve(tent, axisLimit)

n = lambda v: normalizeValue(v, axisLimit, extrapolate=True)
n = lambda v: axisLimit.renormalizeValue(v)
sols = [
(scalar, (n(v[0]), n(v[1]), n(v[2])) if v is not None else None)
for scalar, v in sols
Expand Down

0 comments on commit fb56e7b

Please sign in to comment.