xref: /aosp_15_r20/external/fonttools/Lib/fontTools/varLib/models.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughes"""Variation fonts interpolation models."""
2*e1fe3e4aSElliott Hughes
3*e1fe3e4aSElliott Hughes__all__ = [
4*e1fe3e4aSElliott Hughes    "normalizeValue",
5*e1fe3e4aSElliott Hughes    "normalizeLocation",
6*e1fe3e4aSElliott Hughes    "supportScalar",
7*e1fe3e4aSElliott Hughes    "piecewiseLinearMap",
8*e1fe3e4aSElliott Hughes    "VariationModel",
9*e1fe3e4aSElliott Hughes]
10*e1fe3e4aSElliott Hughes
11*e1fe3e4aSElliott Hughesfrom fontTools.misc.roundTools import noRound
12*e1fe3e4aSElliott Hughesfrom .errors import VariationModelError
13*e1fe3e4aSElliott Hughes
14*e1fe3e4aSElliott Hughes
15*e1fe3e4aSElliott Hughesdef nonNone(lst):
16*e1fe3e4aSElliott Hughes    return [l for l in lst if l is not None]
17*e1fe3e4aSElliott Hughes
18*e1fe3e4aSElliott Hughes
19*e1fe3e4aSElliott Hughesdef allNone(lst):
20*e1fe3e4aSElliott Hughes    return all(l is None for l in lst)
21*e1fe3e4aSElliott Hughes
22*e1fe3e4aSElliott Hughes
23*e1fe3e4aSElliott Hughesdef allEqualTo(ref, lst, mapper=None):
24*e1fe3e4aSElliott Hughes    if mapper is None:
25*e1fe3e4aSElliott Hughes        return all(ref == item for item in lst)
26*e1fe3e4aSElliott Hughes
27*e1fe3e4aSElliott Hughes    mapped = mapper(ref)
28*e1fe3e4aSElliott Hughes    return all(mapped == mapper(item) for item in lst)
29*e1fe3e4aSElliott Hughes
30*e1fe3e4aSElliott Hughes
31*e1fe3e4aSElliott Hughesdef allEqual(lst, mapper=None):
32*e1fe3e4aSElliott Hughes    if not lst:
33*e1fe3e4aSElliott Hughes        return True
34*e1fe3e4aSElliott Hughes    it = iter(lst)
35*e1fe3e4aSElliott Hughes    try:
36*e1fe3e4aSElliott Hughes        first = next(it)
37*e1fe3e4aSElliott Hughes    except StopIteration:
38*e1fe3e4aSElliott Hughes        return True
39*e1fe3e4aSElliott Hughes    return allEqualTo(first, it, mapper=mapper)
40*e1fe3e4aSElliott Hughes
41*e1fe3e4aSElliott Hughes
42*e1fe3e4aSElliott Hughesdef subList(truth, lst):
43*e1fe3e4aSElliott Hughes    assert len(truth) == len(lst)
44*e1fe3e4aSElliott Hughes    return [l for l, t in zip(lst, truth) if t]
45*e1fe3e4aSElliott Hughes
46*e1fe3e4aSElliott Hughes
47*e1fe3e4aSElliott Hughesdef normalizeValue(v, triple, extrapolate=False):
48*e1fe3e4aSElliott Hughes    """Normalizes value based on a min/default/max triple.
49*e1fe3e4aSElliott Hughes
50*e1fe3e4aSElliott Hughes    >>> normalizeValue(400, (100, 400, 900))
51*e1fe3e4aSElliott Hughes    0.0
52*e1fe3e4aSElliott Hughes    >>> normalizeValue(100, (100, 400, 900))
53*e1fe3e4aSElliott Hughes    -1.0
54*e1fe3e4aSElliott Hughes    >>> normalizeValue(650, (100, 400, 900))
55*e1fe3e4aSElliott Hughes    0.5
56*e1fe3e4aSElliott Hughes    """
57*e1fe3e4aSElliott Hughes    lower, default, upper = triple
58*e1fe3e4aSElliott Hughes    if not (lower <= default <= upper):
59*e1fe3e4aSElliott Hughes        raise ValueError(
60*e1fe3e4aSElliott Hughes            f"Invalid axis values, must be minimum, default, maximum: "
61*e1fe3e4aSElliott Hughes            f"{lower:3.3f}, {default:3.3f}, {upper:3.3f}"
62*e1fe3e4aSElliott Hughes        )
63*e1fe3e4aSElliott Hughes    if not extrapolate:
64*e1fe3e4aSElliott Hughes        v = max(min(v, upper), lower)
65*e1fe3e4aSElliott Hughes
66*e1fe3e4aSElliott Hughes    if v == default or lower == upper:
67*e1fe3e4aSElliott Hughes        return 0.0
68*e1fe3e4aSElliott Hughes
69*e1fe3e4aSElliott Hughes    if (v < default and lower != default) or (v > default and upper == default):
70*e1fe3e4aSElliott Hughes        return (v - default) / (default - lower)
71*e1fe3e4aSElliott Hughes    else:
72*e1fe3e4aSElliott Hughes        assert (v > default and upper != default) or (
73*e1fe3e4aSElliott Hughes            v < default and lower == default
74*e1fe3e4aSElliott Hughes        ), f"Ooops... v={v}, triple=({lower}, {default}, {upper})"
75*e1fe3e4aSElliott Hughes        return (v - default) / (upper - default)
76*e1fe3e4aSElliott Hughes
77*e1fe3e4aSElliott Hughes
78*e1fe3e4aSElliott Hughesdef normalizeLocation(location, axes, extrapolate=False):
79*e1fe3e4aSElliott Hughes    """Normalizes location based on axis min/default/max values from axes.
80*e1fe3e4aSElliott Hughes
81*e1fe3e4aSElliott Hughes    >>> axes = {"wght": (100, 400, 900)}
82*e1fe3e4aSElliott Hughes    >>> normalizeLocation({"wght": 400}, axes)
83*e1fe3e4aSElliott Hughes    {'wght': 0.0}
84*e1fe3e4aSElliott Hughes    >>> normalizeLocation({"wght": 100}, axes)
85*e1fe3e4aSElliott Hughes    {'wght': -1.0}
86*e1fe3e4aSElliott Hughes    >>> normalizeLocation({"wght": 900}, axes)
87*e1fe3e4aSElliott Hughes    {'wght': 1.0}
88*e1fe3e4aSElliott Hughes    >>> normalizeLocation({"wght": 650}, axes)
89*e1fe3e4aSElliott Hughes    {'wght': 0.5}
90*e1fe3e4aSElliott Hughes    >>> normalizeLocation({"wght": 1000}, axes)
91*e1fe3e4aSElliott Hughes    {'wght': 1.0}
92*e1fe3e4aSElliott Hughes    >>> normalizeLocation({"wght": 0}, axes)
93*e1fe3e4aSElliott Hughes    {'wght': -1.0}
94*e1fe3e4aSElliott Hughes    >>> axes = {"wght": (0, 0, 1000)}
95*e1fe3e4aSElliott Hughes    >>> normalizeLocation({"wght": 0}, axes)
96*e1fe3e4aSElliott Hughes    {'wght': 0.0}
97*e1fe3e4aSElliott Hughes    >>> normalizeLocation({"wght": -1}, axes)
98*e1fe3e4aSElliott Hughes    {'wght': 0.0}
99*e1fe3e4aSElliott Hughes    >>> normalizeLocation({"wght": 1000}, axes)
100*e1fe3e4aSElliott Hughes    {'wght': 1.0}
101*e1fe3e4aSElliott Hughes    >>> normalizeLocation({"wght": 500}, axes)
102*e1fe3e4aSElliott Hughes    {'wght': 0.5}
103*e1fe3e4aSElliott Hughes    >>> normalizeLocation({"wght": 1001}, axes)
104*e1fe3e4aSElliott Hughes    {'wght': 1.0}
105*e1fe3e4aSElliott Hughes    >>> axes = {"wght": (0, 1000, 1000)}
106*e1fe3e4aSElliott Hughes    >>> normalizeLocation({"wght": 0}, axes)
107*e1fe3e4aSElliott Hughes    {'wght': -1.0}
108*e1fe3e4aSElliott Hughes    >>> normalizeLocation({"wght": -1}, axes)
109*e1fe3e4aSElliott Hughes    {'wght': -1.0}
110*e1fe3e4aSElliott Hughes    >>> normalizeLocation({"wght": 500}, axes)
111*e1fe3e4aSElliott Hughes    {'wght': -0.5}
112*e1fe3e4aSElliott Hughes    >>> normalizeLocation({"wght": 1000}, axes)
113*e1fe3e4aSElliott Hughes    {'wght': 0.0}
114*e1fe3e4aSElliott Hughes    >>> normalizeLocation({"wght": 1001}, axes)
115*e1fe3e4aSElliott Hughes    {'wght': 0.0}
116*e1fe3e4aSElliott Hughes    """
117*e1fe3e4aSElliott Hughes    out = {}
118*e1fe3e4aSElliott Hughes    for tag, triple in axes.items():
119*e1fe3e4aSElliott Hughes        v = location.get(tag, triple[1])
120*e1fe3e4aSElliott Hughes        out[tag] = normalizeValue(v, triple, extrapolate=extrapolate)
121*e1fe3e4aSElliott Hughes    return out
122*e1fe3e4aSElliott Hughes
123*e1fe3e4aSElliott Hughes
124*e1fe3e4aSElliott Hughesdef supportScalar(location, support, ot=True, extrapolate=False, axisRanges=None):
125*e1fe3e4aSElliott Hughes    """Returns the scalar multiplier at location, for a master
126*e1fe3e4aSElliott Hughes    with support.  If ot is True, then a peak value of zero
127*e1fe3e4aSElliott Hughes    for support of an axis means "axis does not participate".  That
128*e1fe3e4aSElliott Hughes    is how OpenType Variation Font technology works.
129*e1fe3e4aSElliott Hughes
130*e1fe3e4aSElliott Hughes    If extrapolate is True, axisRanges must be a dict that maps axis
131*e1fe3e4aSElliott Hughes    names to (axisMin, axisMax) tuples.
132*e1fe3e4aSElliott Hughes
133*e1fe3e4aSElliott Hughes      >>> supportScalar({}, {})
134*e1fe3e4aSElliott Hughes      1.0
135*e1fe3e4aSElliott Hughes      >>> supportScalar({'wght':.2}, {})
136*e1fe3e4aSElliott Hughes      1.0
137*e1fe3e4aSElliott Hughes      >>> supportScalar({'wght':.2}, {'wght':(0,2,3)})
138*e1fe3e4aSElliott Hughes      0.1
139*e1fe3e4aSElliott Hughes      >>> supportScalar({'wght':2.5}, {'wght':(0,2,4)})
140*e1fe3e4aSElliott Hughes      0.75
141*e1fe3e4aSElliott Hughes      >>> supportScalar({'wght':2.5, 'wdth':0}, {'wght':(0,2,4), 'wdth':(-1,0,+1)})
142*e1fe3e4aSElliott Hughes      0.75
143*e1fe3e4aSElliott Hughes      >>> supportScalar({'wght':2.5, 'wdth':.5}, {'wght':(0,2,4), 'wdth':(-1,0,+1)}, ot=False)
144*e1fe3e4aSElliott Hughes      0.375
145*e1fe3e4aSElliott Hughes      >>> supportScalar({'wght':2.5, 'wdth':0}, {'wght':(0,2,4), 'wdth':(-1,0,+1)})
146*e1fe3e4aSElliott Hughes      0.75
147*e1fe3e4aSElliott Hughes      >>> supportScalar({'wght':2.5, 'wdth':.5}, {'wght':(0,2,4), 'wdth':(-1,0,+1)})
148*e1fe3e4aSElliott Hughes      0.75
149*e1fe3e4aSElliott Hughes      >>> supportScalar({'wght':3}, {'wght':(0,1,2)}, extrapolate=True, axisRanges={'wght':(0, 2)})
150*e1fe3e4aSElliott Hughes      -1.0
151*e1fe3e4aSElliott Hughes      >>> supportScalar({'wght':-1}, {'wght':(0,1,2)}, extrapolate=True, axisRanges={'wght':(0, 2)})
152*e1fe3e4aSElliott Hughes      -1.0
153*e1fe3e4aSElliott Hughes      >>> supportScalar({'wght':3}, {'wght':(0,2,2)}, extrapolate=True, axisRanges={'wght':(0, 2)})
154*e1fe3e4aSElliott Hughes      1.5
155*e1fe3e4aSElliott Hughes      >>> supportScalar({'wght':-1}, {'wght':(0,2,2)}, extrapolate=True, axisRanges={'wght':(0, 2)})
156*e1fe3e4aSElliott Hughes      -0.5
157*e1fe3e4aSElliott Hughes    """
158*e1fe3e4aSElliott Hughes    if extrapolate and axisRanges is None:
159*e1fe3e4aSElliott Hughes        raise TypeError("axisRanges must be passed when extrapolate is True")
160*e1fe3e4aSElliott Hughes    scalar = 1.0
161*e1fe3e4aSElliott Hughes    for axis, (lower, peak, upper) in support.items():
162*e1fe3e4aSElliott Hughes        if ot:
163*e1fe3e4aSElliott Hughes            # OpenType-specific case handling
164*e1fe3e4aSElliott Hughes            if peak == 0.0:
165*e1fe3e4aSElliott Hughes                continue
166*e1fe3e4aSElliott Hughes            if lower > peak or peak > upper:
167*e1fe3e4aSElliott Hughes                continue
168*e1fe3e4aSElliott Hughes            if lower < 0.0 and upper > 0.0:
169*e1fe3e4aSElliott Hughes                continue
170*e1fe3e4aSElliott Hughes            v = location.get(axis, 0.0)
171*e1fe3e4aSElliott Hughes        else:
172*e1fe3e4aSElliott Hughes            assert axis in location
173*e1fe3e4aSElliott Hughes            v = location[axis]
174*e1fe3e4aSElliott Hughes        if v == peak:
175*e1fe3e4aSElliott Hughes            continue
176*e1fe3e4aSElliott Hughes
177*e1fe3e4aSElliott Hughes        if extrapolate:
178*e1fe3e4aSElliott Hughes            axisMin, axisMax = axisRanges[axis]
179*e1fe3e4aSElliott Hughes            if v < axisMin and lower <= axisMin:
180*e1fe3e4aSElliott Hughes                if peak <= axisMin and peak < upper:
181*e1fe3e4aSElliott Hughes                    scalar *= (v - upper) / (peak - upper)
182*e1fe3e4aSElliott Hughes                    continue
183*e1fe3e4aSElliott Hughes                elif axisMin < peak:
184*e1fe3e4aSElliott Hughes                    scalar *= (v - lower) / (peak - lower)
185*e1fe3e4aSElliott Hughes                    continue
186*e1fe3e4aSElliott Hughes            elif axisMax < v and axisMax <= upper:
187*e1fe3e4aSElliott Hughes                if axisMax <= peak and lower < peak:
188*e1fe3e4aSElliott Hughes                    scalar *= (v - lower) / (peak - lower)
189*e1fe3e4aSElliott Hughes                    continue
190*e1fe3e4aSElliott Hughes                elif peak < axisMax:
191*e1fe3e4aSElliott Hughes                    scalar *= (v - upper) / (peak - upper)
192*e1fe3e4aSElliott Hughes                    continue
193*e1fe3e4aSElliott Hughes
194*e1fe3e4aSElliott Hughes        if v <= lower or upper <= v:
195*e1fe3e4aSElliott Hughes            scalar = 0.0
196*e1fe3e4aSElliott Hughes            break
197*e1fe3e4aSElliott Hughes
198*e1fe3e4aSElliott Hughes        if v < peak:
199*e1fe3e4aSElliott Hughes            scalar *= (v - lower) / (peak - lower)
200*e1fe3e4aSElliott Hughes        else:  # v > peak
201*e1fe3e4aSElliott Hughes            scalar *= (v - upper) / (peak - upper)
202*e1fe3e4aSElliott Hughes    return scalar
203*e1fe3e4aSElliott Hughes
204*e1fe3e4aSElliott Hughes
205*e1fe3e4aSElliott Hughesclass VariationModel(object):
206*e1fe3e4aSElliott Hughes    """Locations must have the base master at the origin (ie. 0).
207*e1fe3e4aSElliott Hughes
208*e1fe3e4aSElliott Hughes    If the extrapolate argument is set to True, then values are extrapolated
209*e1fe3e4aSElliott Hughes    outside the axis range.
210*e1fe3e4aSElliott Hughes
211*e1fe3e4aSElliott Hughes      >>> from pprint import pprint
212*e1fe3e4aSElliott Hughes      >>> locations = [ \
213*e1fe3e4aSElliott Hughes      {'wght':100}, \
214*e1fe3e4aSElliott Hughes      {'wght':-100}, \
215*e1fe3e4aSElliott Hughes      {'wght':-180}, \
216*e1fe3e4aSElliott Hughes      {'wdth':+.3}, \
217*e1fe3e4aSElliott Hughes      {'wght':+120,'wdth':.3}, \
218*e1fe3e4aSElliott Hughes      {'wght':+120,'wdth':.2}, \
219*e1fe3e4aSElliott Hughes      {}, \
220*e1fe3e4aSElliott Hughes      {'wght':+180,'wdth':.3}, \
221*e1fe3e4aSElliott Hughes      {'wght':+180}, \
222*e1fe3e4aSElliott Hughes      ]
223*e1fe3e4aSElliott Hughes      >>> model = VariationModel(locations, axisOrder=['wght'])
224*e1fe3e4aSElliott Hughes      >>> pprint(model.locations)
225*e1fe3e4aSElliott Hughes      [{},
226*e1fe3e4aSElliott Hughes       {'wght': -100},
227*e1fe3e4aSElliott Hughes       {'wght': -180},
228*e1fe3e4aSElliott Hughes       {'wght': 100},
229*e1fe3e4aSElliott Hughes       {'wght': 180},
230*e1fe3e4aSElliott Hughes       {'wdth': 0.3},
231*e1fe3e4aSElliott Hughes       {'wdth': 0.3, 'wght': 180},
232*e1fe3e4aSElliott Hughes       {'wdth': 0.3, 'wght': 120},
233*e1fe3e4aSElliott Hughes       {'wdth': 0.2, 'wght': 120}]
234*e1fe3e4aSElliott Hughes      >>> pprint(model.deltaWeights)
235*e1fe3e4aSElliott Hughes      [{},
236*e1fe3e4aSElliott Hughes       {0: 1.0},
237*e1fe3e4aSElliott Hughes       {0: 1.0},
238*e1fe3e4aSElliott Hughes       {0: 1.0},
239*e1fe3e4aSElliott Hughes       {0: 1.0},
240*e1fe3e4aSElliott Hughes       {0: 1.0},
241*e1fe3e4aSElliott Hughes       {0: 1.0, 4: 1.0, 5: 1.0},
242*e1fe3e4aSElliott Hughes       {0: 1.0, 3: 0.75, 4: 0.25, 5: 1.0, 6: 0.6666666666666666},
243*e1fe3e4aSElliott Hughes       {0: 1.0,
244*e1fe3e4aSElliott Hughes        3: 0.75,
245*e1fe3e4aSElliott Hughes        4: 0.25,
246*e1fe3e4aSElliott Hughes        5: 0.6666666666666667,
247*e1fe3e4aSElliott Hughes        6: 0.4444444444444445,
248*e1fe3e4aSElliott Hughes        7: 0.6666666666666667}]
249*e1fe3e4aSElliott Hughes    """
250*e1fe3e4aSElliott Hughes
251*e1fe3e4aSElliott Hughes    def __init__(self, locations, axisOrder=None, extrapolate=False):
252*e1fe3e4aSElliott Hughes        if len(set(tuple(sorted(l.items())) for l in locations)) != len(locations):
253*e1fe3e4aSElliott Hughes            raise VariationModelError("Locations must be unique.")
254*e1fe3e4aSElliott Hughes
255*e1fe3e4aSElliott Hughes        self.origLocations = locations
256*e1fe3e4aSElliott Hughes        self.axisOrder = axisOrder if axisOrder is not None else []
257*e1fe3e4aSElliott Hughes        self.extrapolate = extrapolate
258*e1fe3e4aSElliott Hughes        self.axisRanges = self.computeAxisRanges(locations) if extrapolate else None
259*e1fe3e4aSElliott Hughes
260*e1fe3e4aSElliott Hughes        locations = [{k: v for k, v in loc.items() if v != 0.0} for loc in locations]
261*e1fe3e4aSElliott Hughes        keyFunc = self.getMasterLocationsSortKeyFunc(
262*e1fe3e4aSElliott Hughes            locations, axisOrder=self.axisOrder
263*e1fe3e4aSElliott Hughes        )
264*e1fe3e4aSElliott Hughes        self.locations = sorted(locations, key=keyFunc)
265*e1fe3e4aSElliott Hughes
266*e1fe3e4aSElliott Hughes        # Mapping from user's master order to our master order
267*e1fe3e4aSElliott Hughes        self.mapping = [self.locations.index(l) for l in locations]
268*e1fe3e4aSElliott Hughes        self.reverseMapping = [locations.index(l) for l in self.locations]
269*e1fe3e4aSElliott Hughes
270*e1fe3e4aSElliott Hughes        self._computeMasterSupports()
271*e1fe3e4aSElliott Hughes        self._subModels = {}
272*e1fe3e4aSElliott Hughes
273*e1fe3e4aSElliott Hughes    def getSubModel(self, items):
274*e1fe3e4aSElliott Hughes        """Return a sub-model and the items that are not None.
275*e1fe3e4aSElliott Hughes
276*e1fe3e4aSElliott Hughes        The sub-model is necessary for working with the subset
277*e1fe3e4aSElliott Hughes        of items when some are None.
278*e1fe3e4aSElliott Hughes
279*e1fe3e4aSElliott Hughes        The sub-model is cached."""
280*e1fe3e4aSElliott Hughes        if None not in items:
281*e1fe3e4aSElliott Hughes            return self, items
282*e1fe3e4aSElliott Hughes        key = tuple(v is not None for v in items)
283*e1fe3e4aSElliott Hughes        subModel = self._subModels.get(key)
284*e1fe3e4aSElliott Hughes        if subModel is None:
285*e1fe3e4aSElliott Hughes            subModel = VariationModel(subList(key, self.origLocations), self.axisOrder)
286*e1fe3e4aSElliott Hughes            self._subModels[key] = subModel
287*e1fe3e4aSElliott Hughes        return subModel, subList(key, items)
288*e1fe3e4aSElliott Hughes
289*e1fe3e4aSElliott Hughes    @staticmethod
290*e1fe3e4aSElliott Hughes    def computeAxisRanges(locations):
291*e1fe3e4aSElliott Hughes        axisRanges = {}
292*e1fe3e4aSElliott Hughes        allAxes = {axis for loc in locations for axis in loc.keys()}
293*e1fe3e4aSElliott Hughes        for loc in locations:
294*e1fe3e4aSElliott Hughes            for axis in allAxes:
295*e1fe3e4aSElliott Hughes                value = loc.get(axis, 0)
296*e1fe3e4aSElliott Hughes                axisMin, axisMax = axisRanges.get(axis, (value, value))
297*e1fe3e4aSElliott Hughes                axisRanges[axis] = min(value, axisMin), max(value, axisMax)
298*e1fe3e4aSElliott Hughes        return axisRanges
299*e1fe3e4aSElliott Hughes
300*e1fe3e4aSElliott Hughes    @staticmethod
301*e1fe3e4aSElliott Hughes    def getMasterLocationsSortKeyFunc(locations, axisOrder=[]):
302*e1fe3e4aSElliott Hughes        if {} not in locations:
303*e1fe3e4aSElliott Hughes            raise VariationModelError("Base master not found.")
304*e1fe3e4aSElliott Hughes        axisPoints = {}
305*e1fe3e4aSElliott Hughes        for loc in locations:
306*e1fe3e4aSElliott Hughes            if len(loc) != 1:
307*e1fe3e4aSElliott Hughes                continue
308*e1fe3e4aSElliott Hughes            axis = next(iter(loc))
309*e1fe3e4aSElliott Hughes            value = loc[axis]
310*e1fe3e4aSElliott Hughes            if axis not in axisPoints:
311*e1fe3e4aSElliott Hughes                axisPoints[axis] = {0.0}
312*e1fe3e4aSElliott Hughes            assert (
313*e1fe3e4aSElliott Hughes                value not in axisPoints[axis]
314*e1fe3e4aSElliott Hughes            ), 'Value "%s" in axisPoints["%s"] -->  %s' % (value, axis, axisPoints)
315*e1fe3e4aSElliott Hughes            axisPoints[axis].add(value)
316*e1fe3e4aSElliott Hughes
317*e1fe3e4aSElliott Hughes        def getKey(axisPoints, axisOrder):
318*e1fe3e4aSElliott Hughes            def sign(v):
319*e1fe3e4aSElliott Hughes                return -1 if v < 0 else +1 if v > 0 else 0
320*e1fe3e4aSElliott Hughes
321*e1fe3e4aSElliott Hughes            def key(loc):
322*e1fe3e4aSElliott Hughes                rank = len(loc)
323*e1fe3e4aSElliott Hughes                onPointAxes = [
324*e1fe3e4aSElliott Hughes                    axis
325*e1fe3e4aSElliott Hughes                    for axis, value in loc.items()
326*e1fe3e4aSElliott Hughes                    if axis in axisPoints and value in axisPoints[axis]
327*e1fe3e4aSElliott Hughes                ]
328*e1fe3e4aSElliott Hughes                orderedAxes = [axis for axis in axisOrder if axis in loc]
329*e1fe3e4aSElliott Hughes                orderedAxes.extend(
330*e1fe3e4aSElliott Hughes                    [axis for axis in sorted(loc.keys()) if axis not in axisOrder]
331*e1fe3e4aSElliott Hughes                )
332*e1fe3e4aSElliott Hughes                return (
333*e1fe3e4aSElliott Hughes                    rank,  # First, order by increasing rank
334*e1fe3e4aSElliott Hughes                    -len(onPointAxes),  # Next, by decreasing number of onPoint axes
335*e1fe3e4aSElliott Hughes                    tuple(
336*e1fe3e4aSElliott Hughes                        axisOrder.index(axis) if axis in axisOrder else 0x10000
337*e1fe3e4aSElliott Hughes                        for axis in orderedAxes
338*e1fe3e4aSElliott Hughes                    ),  # Next, by known axes
339*e1fe3e4aSElliott Hughes                    tuple(orderedAxes),  # Next, by all axes
340*e1fe3e4aSElliott Hughes                    tuple(
341*e1fe3e4aSElliott Hughes                        sign(loc[axis]) for axis in orderedAxes
342*e1fe3e4aSElliott Hughes                    ),  # Next, by signs of axis values
343*e1fe3e4aSElliott Hughes                    tuple(
344*e1fe3e4aSElliott Hughes                        abs(loc[axis]) for axis in orderedAxes
345*e1fe3e4aSElliott Hughes                    ),  # Next, by absolute value of axis values
346*e1fe3e4aSElliott Hughes                )
347*e1fe3e4aSElliott Hughes
348*e1fe3e4aSElliott Hughes            return key
349*e1fe3e4aSElliott Hughes
350*e1fe3e4aSElliott Hughes        ret = getKey(axisPoints, axisOrder)
351*e1fe3e4aSElliott Hughes        return ret
352*e1fe3e4aSElliott Hughes
353*e1fe3e4aSElliott Hughes    def reorderMasters(self, master_list, mapping):
354*e1fe3e4aSElliott Hughes        # For changing the master data order without
355*e1fe3e4aSElliott Hughes        # recomputing supports and deltaWeights.
356*e1fe3e4aSElliott Hughes        new_list = [master_list[idx] for idx in mapping]
357*e1fe3e4aSElliott Hughes        self.origLocations = [self.origLocations[idx] for idx in mapping]
358*e1fe3e4aSElliott Hughes        locations = [
359*e1fe3e4aSElliott Hughes            {k: v for k, v in loc.items() if v != 0.0} for loc in self.origLocations
360*e1fe3e4aSElliott Hughes        ]
361*e1fe3e4aSElliott Hughes        self.mapping = [self.locations.index(l) for l in locations]
362*e1fe3e4aSElliott Hughes        self.reverseMapping = [locations.index(l) for l in self.locations]
363*e1fe3e4aSElliott Hughes        self._subModels = {}
364*e1fe3e4aSElliott Hughes        return new_list
365*e1fe3e4aSElliott Hughes
366*e1fe3e4aSElliott Hughes    def _computeMasterSupports(self):
367*e1fe3e4aSElliott Hughes        self.supports = []
368*e1fe3e4aSElliott Hughes        regions = self._locationsToRegions()
369*e1fe3e4aSElliott Hughes        for i, region in enumerate(regions):
370*e1fe3e4aSElliott Hughes            locAxes = set(region.keys())
371*e1fe3e4aSElliott Hughes            # Walk over previous masters now
372*e1fe3e4aSElliott Hughes            for prev_region in regions[:i]:
373*e1fe3e4aSElliott Hughes                # Master with extra axes do not participte
374*e1fe3e4aSElliott Hughes                if set(prev_region.keys()) != locAxes:
375*e1fe3e4aSElliott Hughes                    continue
376*e1fe3e4aSElliott Hughes                # If it's NOT in the current box, it does not participate
377*e1fe3e4aSElliott Hughes                relevant = True
378*e1fe3e4aSElliott Hughes                for axis, (lower, peak, upper) in region.items():
379*e1fe3e4aSElliott Hughes                    if not (
380*e1fe3e4aSElliott Hughes                        prev_region[axis][1] == peak
381*e1fe3e4aSElliott Hughes                        or lower < prev_region[axis][1] < upper
382*e1fe3e4aSElliott Hughes                    ):
383*e1fe3e4aSElliott Hughes                        relevant = False
384*e1fe3e4aSElliott Hughes                        break
385*e1fe3e4aSElliott Hughes                if not relevant:
386*e1fe3e4aSElliott Hughes                    continue
387*e1fe3e4aSElliott Hughes
388*e1fe3e4aSElliott Hughes                # Split the box for new master; split in whatever direction
389*e1fe3e4aSElliott Hughes                # that has largest range ratio.
390*e1fe3e4aSElliott Hughes                #
391*e1fe3e4aSElliott Hughes                # For symmetry, we actually cut across multiple axes
392*e1fe3e4aSElliott Hughes                # if they have the largest, equal, ratio.
393*e1fe3e4aSElliott Hughes                # https://github.com/fonttools/fonttools/commit/7ee81c8821671157968b097f3e55309a1faa511e#commitcomment-31054804
394*e1fe3e4aSElliott Hughes
395*e1fe3e4aSElliott Hughes                bestAxes = {}
396*e1fe3e4aSElliott Hughes                bestRatio = -1
397*e1fe3e4aSElliott Hughes                for axis in prev_region.keys():
398*e1fe3e4aSElliott Hughes                    val = prev_region[axis][1]
399*e1fe3e4aSElliott Hughes                    assert axis in region
400*e1fe3e4aSElliott Hughes                    lower, locV, upper = region[axis]
401*e1fe3e4aSElliott Hughes                    newLower, newUpper = lower, upper
402*e1fe3e4aSElliott Hughes                    if val < locV:
403*e1fe3e4aSElliott Hughes                        newLower = val
404*e1fe3e4aSElliott Hughes                        ratio = (val - locV) / (lower - locV)
405*e1fe3e4aSElliott Hughes                    elif locV < val:
406*e1fe3e4aSElliott Hughes                        newUpper = val
407*e1fe3e4aSElliott Hughes                        ratio = (val - locV) / (upper - locV)
408*e1fe3e4aSElliott Hughes                    else:  # val == locV
409*e1fe3e4aSElliott Hughes                        # Can't split box in this direction.
410*e1fe3e4aSElliott Hughes                        continue
411*e1fe3e4aSElliott Hughes                    if ratio > bestRatio:
412*e1fe3e4aSElliott Hughes                        bestAxes = {}
413*e1fe3e4aSElliott Hughes                        bestRatio = ratio
414*e1fe3e4aSElliott Hughes                    if ratio == bestRatio:
415*e1fe3e4aSElliott Hughes                        bestAxes[axis] = (newLower, locV, newUpper)
416*e1fe3e4aSElliott Hughes
417*e1fe3e4aSElliott Hughes                for axis, triple in bestAxes.items():
418*e1fe3e4aSElliott Hughes                    region[axis] = triple
419*e1fe3e4aSElliott Hughes            self.supports.append(region)
420*e1fe3e4aSElliott Hughes        self._computeDeltaWeights()
421*e1fe3e4aSElliott Hughes
422*e1fe3e4aSElliott Hughes    def _locationsToRegions(self):
423*e1fe3e4aSElliott Hughes        locations = self.locations
424*e1fe3e4aSElliott Hughes        # Compute min/max across each axis, use it as total range.
425*e1fe3e4aSElliott Hughes        # TODO Take this as input from outside?
426*e1fe3e4aSElliott Hughes        minV = {}
427*e1fe3e4aSElliott Hughes        maxV = {}
428*e1fe3e4aSElliott Hughes        for l in locations:
429*e1fe3e4aSElliott Hughes            for k, v in l.items():
430*e1fe3e4aSElliott Hughes                minV[k] = min(v, minV.get(k, v))
431*e1fe3e4aSElliott Hughes                maxV[k] = max(v, maxV.get(k, v))
432*e1fe3e4aSElliott Hughes
433*e1fe3e4aSElliott Hughes        regions = []
434*e1fe3e4aSElliott Hughes        for loc in locations:
435*e1fe3e4aSElliott Hughes            region = {}
436*e1fe3e4aSElliott Hughes            for axis, locV in loc.items():
437*e1fe3e4aSElliott Hughes                if locV > 0:
438*e1fe3e4aSElliott Hughes                    region[axis] = (0, locV, maxV[axis])
439*e1fe3e4aSElliott Hughes                else:
440*e1fe3e4aSElliott Hughes                    region[axis] = (minV[axis], locV, 0)
441*e1fe3e4aSElliott Hughes            regions.append(region)
442*e1fe3e4aSElliott Hughes        return regions
443*e1fe3e4aSElliott Hughes
444*e1fe3e4aSElliott Hughes    def _computeDeltaWeights(self):
445*e1fe3e4aSElliott Hughes        self.deltaWeights = []
446*e1fe3e4aSElliott Hughes        for i, loc in enumerate(self.locations):
447*e1fe3e4aSElliott Hughes            deltaWeight = {}
448*e1fe3e4aSElliott Hughes            # Walk over previous masters now, populate deltaWeight
449*e1fe3e4aSElliott Hughes            for j, support in enumerate(self.supports[:i]):
450*e1fe3e4aSElliott Hughes                scalar = supportScalar(loc, support)
451*e1fe3e4aSElliott Hughes                if scalar:
452*e1fe3e4aSElliott Hughes                    deltaWeight[j] = scalar
453*e1fe3e4aSElliott Hughes            self.deltaWeights.append(deltaWeight)
454*e1fe3e4aSElliott Hughes
455*e1fe3e4aSElliott Hughes    def getDeltas(self, masterValues, *, round=noRound):
456*e1fe3e4aSElliott Hughes        assert len(masterValues) == len(self.deltaWeights)
457*e1fe3e4aSElliott Hughes        mapping = self.reverseMapping
458*e1fe3e4aSElliott Hughes        out = []
459*e1fe3e4aSElliott Hughes        for i, weights in enumerate(self.deltaWeights):
460*e1fe3e4aSElliott Hughes            delta = masterValues[mapping[i]]
461*e1fe3e4aSElliott Hughes            for j, weight in weights.items():
462*e1fe3e4aSElliott Hughes                if weight == 1:
463*e1fe3e4aSElliott Hughes                    delta -= out[j]
464*e1fe3e4aSElliott Hughes                else:
465*e1fe3e4aSElliott Hughes                    delta -= out[j] * weight
466*e1fe3e4aSElliott Hughes            out.append(round(delta))
467*e1fe3e4aSElliott Hughes        return out
468*e1fe3e4aSElliott Hughes
469*e1fe3e4aSElliott Hughes    def getDeltasAndSupports(self, items, *, round=noRound):
470*e1fe3e4aSElliott Hughes        model, items = self.getSubModel(items)
471*e1fe3e4aSElliott Hughes        return model.getDeltas(items, round=round), model.supports
472*e1fe3e4aSElliott Hughes
473*e1fe3e4aSElliott Hughes    def getScalars(self, loc):
474*e1fe3e4aSElliott Hughes        """Return scalars for each delta, for the given location.
475*e1fe3e4aSElliott Hughes        If interpolating many master-values at the same location,
476*e1fe3e4aSElliott Hughes        this function allows speed up by fetching the scalars once
477*e1fe3e4aSElliott Hughes        and using them with interpolateFromMastersAndScalars()."""
478*e1fe3e4aSElliott Hughes        return [
479*e1fe3e4aSElliott Hughes            supportScalar(
480*e1fe3e4aSElliott Hughes                loc, support, extrapolate=self.extrapolate, axisRanges=self.axisRanges
481*e1fe3e4aSElliott Hughes            )
482*e1fe3e4aSElliott Hughes            for support in self.supports
483*e1fe3e4aSElliott Hughes        ]
484*e1fe3e4aSElliott Hughes
485*e1fe3e4aSElliott Hughes    def getMasterScalars(self, targetLocation):
486*e1fe3e4aSElliott Hughes        """Return multipliers for each master, for the given location.
487*e1fe3e4aSElliott Hughes        If interpolating many master-values at the same location,
488*e1fe3e4aSElliott Hughes        this function allows speed up by fetching the scalars once
489*e1fe3e4aSElliott Hughes        and using them with interpolateFromValuesAndScalars().
490*e1fe3e4aSElliott Hughes
491*e1fe3e4aSElliott Hughes        Note that the scalars used in interpolateFromMastersAndScalars(),
492*e1fe3e4aSElliott Hughes        are *not* the same as the ones returned here. They are the result
493*e1fe3e4aSElliott Hughes        of getScalars()."""
494*e1fe3e4aSElliott Hughes        out = self.getScalars(targetLocation)
495*e1fe3e4aSElliott Hughes        for i, weights in reversed(list(enumerate(self.deltaWeights))):
496*e1fe3e4aSElliott Hughes            for j, weight in weights.items():
497*e1fe3e4aSElliott Hughes                out[j] -= out[i] * weight
498*e1fe3e4aSElliott Hughes
499*e1fe3e4aSElliott Hughes        out = [out[self.mapping[i]] for i in range(len(out))]
500*e1fe3e4aSElliott Hughes        return out
501*e1fe3e4aSElliott Hughes
502*e1fe3e4aSElliott Hughes    @staticmethod
503*e1fe3e4aSElliott Hughes    def interpolateFromValuesAndScalars(values, scalars):
504*e1fe3e4aSElliott Hughes        """Interpolate from values and scalars coefficients.
505*e1fe3e4aSElliott Hughes
506*e1fe3e4aSElliott Hughes        If the values are master-values, then the scalars should be
507*e1fe3e4aSElliott Hughes        fetched from getMasterScalars().
508*e1fe3e4aSElliott Hughes
509*e1fe3e4aSElliott Hughes        If the values are deltas, then the scalars should be fetched
510*e1fe3e4aSElliott Hughes        from getScalars(); in which case this is the same as
511*e1fe3e4aSElliott Hughes        interpolateFromDeltasAndScalars().
512*e1fe3e4aSElliott Hughes        """
513*e1fe3e4aSElliott Hughes        v = None
514*e1fe3e4aSElliott Hughes        assert len(values) == len(scalars)
515*e1fe3e4aSElliott Hughes        for value, scalar in zip(values, scalars):
516*e1fe3e4aSElliott Hughes            if not scalar:
517*e1fe3e4aSElliott Hughes                continue
518*e1fe3e4aSElliott Hughes            contribution = value * scalar
519*e1fe3e4aSElliott Hughes            if v is None:
520*e1fe3e4aSElliott Hughes                v = contribution
521*e1fe3e4aSElliott Hughes            else:
522*e1fe3e4aSElliott Hughes                v += contribution
523*e1fe3e4aSElliott Hughes        return v
524*e1fe3e4aSElliott Hughes
525*e1fe3e4aSElliott Hughes    @staticmethod
526*e1fe3e4aSElliott Hughes    def interpolateFromDeltasAndScalars(deltas, scalars):
527*e1fe3e4aSElliott Hughes        """Interpolate from deltas and scalars fetched from getScalars()."""
528*e1fe3e4aSElliott Hughes        return VariationModel.interpolateFromValuesAndScalars(deltas, scalars)
529*e1fe3e4aSElliott Hughes
530*e1fe3e4aSElliott Hughes    def interpolateFromDeltas(self, loc, deltas):
531*e1fe3e4aSElliott Hughes        """Interpolate from deltas, at location loc."""
532*e1fe3e4aSElliott Hughes        scalars = self.getScalars(loc)
533*e1fe3e4aSElliott Hughes        return self.interpolateFromDeltasAndScalars(deltas, scalars)
534*e1fe3e4aSElliott Hughes
535*e1fe3e4aSElliott Hughes    def interpolateFromMasters(self, loc, masterValues, *, round=noRound):
536*e1fe3e4aSElliott Hughes        """Interpolate from master-values, at location loc."""
537*e1fe3e4aSElliott Hughes        scalars = self.getMasterScalars(loc)
538*e1fe3e4aSElliott Hughes        return self.interpolateFromValuesAndScalars(masterValues, scalars)
539*e1fe3e4aSElliott Hughes
540*e1fe3e4aSElliott Hughes    def interpolateFromMastersAndScalars(self, masterValues, scalars, *, round=noRound):
541*e1fe3e4aSElliott Hughes        """Interpolate from master-values, and scalars fetched from
542*e1fe3e4aSElliott Hughes        getScalars(), which is useful when you want to interpolate
543*e1fe3e4aSElliott Hughes        multiple master-values with the same location."""
544*e1fe3e4aSElliott Hughes        deltas = self.getDeltas(masterValues, round=round)
545*e1fe3e4aSElliott Hughes        return self.interpolateFromDeltasAndScalars(deltas, scalars)
546*e1fe3e4aSElliott Hughes
547*e1fe3e4aSElliott Hughes
548*e1fe3e4aSElliott Hughesdef piecewiseLinearMap(v, mapping):
549*e1fe3e4aSElliott Hughes    keys = mapping.keys()
550*e1fe3e4aSElliott Hughes    if not keys:
551*e1fe3e4aSElliott Hughes        return v
552*e1fe3e4aSElliott Hughes    if v in keys:
553*e1fe3e4aSElliott Hughes        return mapping[v]
554*e1fe3e4aSElliott Hughes    k = min(keys)
555*e1fe3e4aSElliott Hughes    if v < k:
556*e1fe3e4aSElliott Hughes        return v + mapping[k] - k
557*e1fe3e4aSElliott Hughes    k = max(keys)
558*e1fe3e4aSElliott Hughes    if v > k:
559*e1fe3e4aSElliott Hughes        return v + mapping[k] - k
560*e1fe3e4aSElliott Hughes    # Interpolate
561*e1fe3e4aSElliott Hughes    a = max(k for k in keys if k < v)
562*e1fe3e4aSElliott Hughes    b = min(k for k in keys if k > v)
563*e1fe3e4aSElliott Hughes    va = mapping[a]
564*e1fe3e4aSElliott Hughes    vb = mapping[b]
565*e1fe3e4aSElliott Hughes    return va + (vb - va) * (v - a) / (b - a)
566*e1fe3e4aSElliott Hughes
567*e1fe3e4aSElliott Hughes
568*e1fe3e4aSElliott Hughesdef main(args=None):
569*e1fe3e4aSElliott Hughes    """Normalize locations on a given designspace"""
570*e1fe3e4aSElliott Hughes    from fontTools import configLogger
571*e1fe3e4aSElliott Hughes    import argparse
572*e1fe3e4aSElliott Hughes
573*e1fe3e4aSElliott Hughes    parser = argparse.ArgumentParser(
574*e1fe3e4aSElliott Hughes        "fonttools varLib.models",
575*e1fe3e4aSElliott Hughes        description=main.__doc__,
576*e1fe3e4aSElliott Hughes    )
577*e1fe3e4aSElliott Hughes    parser.add_argument(
578*e1fe3e4aSElliott Hughes        "--loglevel",
579*e1fe3e4aSElliott Hughes        metavar="LEVEL",
580*e1fe3e4aSElliott Hughes        default="INFO",
581*e1fe3e4aSElliott Hughes        help="Logging level (defaults to INFO)",
582*e1fe3e4aSElliott Hughes    )
583*e1fe3e4aSElliott Hughes
584*e1fe3e4aSElliott Hughes    group = parser.add_mutually_exclusive_group(required=True)
585*e1fe3e4aSElliott Hughes    group.add_argument("-d", "--designspace", metavar="DESIGNSPACE", type=str)
586*e1fe3e4aSElliott Hughes    group.add_argument(
587*e1fe3e4aSElliott Hughes        "-l",
588*e1fe3e4aSElliott Hughes        "--locations",
589*e1fe3e4aSElliott Hughes        metavar="LOCATION",
590*e1fe3e4aSElliott Hughes        nargs="+",
591*e1fe3e4aSElliott Hughes        help="Master locations as comma-separate coordinates. One must be all zeros.",
592*e1fe3e4aSElliott Hughes    )
593*e1fe3e4aSElliott Hughes
594*e1fe3e4aSElliott Hughes    args = parser.parse_args(args)
595*e1fe3e4aSElliott Hughes
596*e1fe3e4aSElliott Hughes    configLogger(level=args.loglevel)
597*e1fe3e4aSElliott Hughes    from pprint import pprint
598*e1fe3e4aSElliott Hughes
599*e1fe3e4aSElliott Hughes    if args.designspace:
600*e1fe3e4aSElliott Hughes        from fontTools.designspaceLib import DesignSpaceDocument
601*e1fe3e4aSElliott Hughes
602*e1fe3e4aSElliott Hughes        doc = DesignSpaceDocument()
603*e1fe3e4aSElliott Hughes        doc.read(args.designspace)
604*e1fe3e4aSElliott Hughes        locs = [s.location for s in doc.sources]
605*e1fe3e4aSElliott Hughes        print("Original locations:")
606*e1fe3e4aSElliott Hughes        pprint(locs)
607*e1fe3e4aSElliott Hughes        doc.normalize()
608*e1fe3e4aSElliott Hughes        print("Normalized locations:")
609*e1fe3e4aSElliott Hughes        locs = [s.location for s in doc.sources]
610*e1fe3e4aSElliott Hughes        pprint(locs)
611*e1fe3e4aSElliott Hughes    else:
612*e1fe3e4aSElliott Hughes        axes = [chr(c) for c in range(ord("A"), ord("Z") + 1)]
613*e1fe3e4aSElliott Hughes        locs = [
614*e1fe3e4aSElliott Hughes            dict(zip(axes, (float(v) for v in s.split(",")))) for s in args.locations
615*e1fe3e4aSElliott Hughes        ]
616*e1fe3e4aSElliott Hughes
617*e1fe3e4aSElliott Hughes    model = VariationModel(locs)
618*e1fe3e4aSElliott Hughes    print("Sorted locations:")
619*e1fe3e4aSElliott Hughes    pprint(model.locations)
620*e1fe3e4aSElliott Hughes    print("Supports:")
621*e1fe3e4aSElliott Hughes    pprint(model.supports)
622*e1fe3e4aSElliott Hughes
623*e1fe3e4aSElliott Hughes
624*e1fe3e4aSElliott Hughesif __name__ == "__main__":
625*e1fe3e4aSElliott Hughes    import doctest, sys
626*e1fe3e4aSElliott Hughes
627*e1fe3e4aSElliott Hughes    if len(sys.argv) > 1:
628*e1fe3e4aSElliott Hughes        sys.exit(main())
629*e1fe3e4aSElliott Hughes
630*e1fe3e4aSElliott Hughes    sys.exit(doctest.testmod().failed)
631