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