diff options
Diffstat (limited to 'Lib/fontTools/varLib/models.py')
-rw-r--r-- | Lib/fontTools/varLib/models.py | 169 |
1 files changed, 99 insertions, 70 deletions
diff --git a/Lib/fontTools/varLib/models.py b/Lib/fontTools/varLib/models.py index a7e020b0..5bd66dba 100644 --- a/Lib/fontTools/varLib/models.py +++ b/Lib/fontTools/varLib/models.py @@ -43,15 +43,15 @@ def subList(truth, lst): return [l for l, t in zip(lst, truth) if t] -def normalizeValue(v, triple): +def normalizeValue(v, triple, extrapolate=False): """Normalizes value based on a min/default/max triple. - >>> normalizeValue(400, (100, 400, 900)) - 0.0 - >>> normalizeValue(100, (100, 400, 900)) - -1.0 - >>> normalizeValue(650, (100, 400, 900)) - 0.5 + >>> normalizeValue(400, (100, 400, 900)) + 0.0 + >>> normalizeValue(100, (100, 400, 900)) + -1.0 + >>> normalizeValue(650, (100, 400, 900)) + 0.5 """ lower, default, upper = triple if not (lower <= default <= upper): @@ -59,68 +59,76 @@ def normalizeValue(v, triple): f"Invalid axis values, must be minimum, default, maximum: " f"{lower:3.3f}, {default:3.3f}, {upper:3.3f}" ) - v = max(min(v, upper), lower) - if v == default: - v = 0.0 - elif v < default: - v = (v - default) / (default - lower) + if not extrapolate: + v = max(min(v, upper), lower) + + if v == default or lower == upper: + return 0.0 + + if (v < default and lower != default) or (v > default and upper == default): + return (v - default) / (default - lower) else: - v = (v - default) / (upper - default) - return v + assert (v > default and upper != default) or ( + v < default and lower == default + ), f"Ooops... v={v}, triple=({lower}, {default}, {upper})" + return (v - default) / (upper - default) -def normalizeLocation(location, axes): +def normalizeLocation(location, axes, extrapolate=False): """Normalizes location based on axis min/default/max values from axes. - >>> axes = {"wght": (100, 400, 900)} - >>> normalizeLocation({"wght": 400}, axes) - {'wght': 0.0} - >>> normalizeLocation({"wght": 100}, axes) - {'wght': -1.0} - >>> normalizeLocation({"wght": 900}, axes) - {'wght': 1.0} - >>> normalizeLocation({"wght": 650}, axes) - {'wght': 0.5} - >>> normalizeLocation({"wght": 1000}, axes) - {'wght': 1.0} - >>> normalizeLocation({"wght": 0}, axes) - {'wght': -1.0} - >>> axes = {"wght": (0, 0, 1000)} - >>> normalizeLocation({"wght": 0}, axes) - {'wght': 0.0} - >>> normalizeLocation({"wght": -1}, axes) - {'wght': 0.0} - >>> normalizeLocation({"wght": 1000}, axes) - {'wght': 1.0} - >>> normalizeLocation({"wght": 500}, axes) - {'wght': 0.5} - >>> normalizeLocation({"wght": 1001}, axes) - {'wght': 1.0} - >>> axes = {"wght": (0, 1000, 1000)} - >>> normalizeLocation({"wght": 0}, axes) - {'wght': -1.0} - >>> normalizeLocation({"wght": -1}, axes) - {'wght': -1.0} - >>> normalizeLocation({"wght": 500}, axes) - {'wght': -0.5} - >>> normalizeLocation({"wght": 1000}, axes) - {'wght': 0.0} - >>> normalizeLocation({"wght": 1001}, axes) - {'wght': 0.0} + >>> axes = {"wght": (100, 400, 900)} + >>> normalizeLocation({"wght": 400}, axes) + {'wght': 0.0} + >>> normalizeLocation({"wght": 100}, axes) + {'wght': -1.0} + >>> normalizeLocation({"wght": 900}, axes) + {'wght': 1.0} + >>> normalizeLocation({"wght": 650}, axes) + {'wght': 0.5} + >>> normalizeLocation({"wght": 1000}, axes) + {'wght': 1.0} + >>> normalizeLocation({"wght": 0}, axes) + {'wght': -1.0} + >>> axes = {"wght": (0, 0, 1000)} + >>> normalizeLocation({"wght": 0}, axes) + {'wght': 0.0} + >>> normalizeLocation({"wght": -1}, axes) + {'wght': 0.0} + >>> normalizeLocation({"wght": 1000}, axes) + {'wght': 1.0} + >>> normalizeLocation({"wght": 500}, axes) + {'wght': 0.5} + >>> normalizeLocation({"wght": 1001}, axes) + {'wght': 1.0} + >>> axes = {"wght": (0, 1000, 1000)} + >>> normalizeLocation({"wght": 0}, axes) + {'wght': -1.0} + >>> normalizeLocation({"wght": -1}, axes) + {'wght': -1.0} + >>> normalizeLocation({"wght": 500}, axes) + {'wght': -0.5} + >>> normalizeLocation({"wght": 1000}, axes) + {'wght': 0.0} + >>> normalizeLocation({"wght": 1001}, axes) + {'wght': 0.0} """ out = {} for tag, triple in axes.items(): v = location.get(tag, triple[1]) - out[tag] = normalizeValue(v, triple) + out[tag] = normalizeValue(v, triple, extrapolate=extrapolate) return out -def supportScalar(location, support, ot=True, extrapolate=False): +def supportScalar(location, support, ot=True, extrapolate=False, axisRanges=None): """Returns the scalar multiplier at location, for a master with support. If ot is True, then a peak value of zero for support of an axis means "axis does not participate". That is how OpenType Variation Font technology works. + If extrapolate is True, axisRanges must be a dict that maps axis + names to (axisMin, axisMax) tuples. + >>> supportScalar({}, {}) 1.0 >>> supportScalar({'wght':.2}, {}) @@ -137,11 +145,17 @@ def supportScalar(location, support, ot=True, extrapolate=False): 0.75 >>> supportScalar({'wght':2.5, 'wdth':.5}, {'wght':(0,2,4), 'wdth':(-1,0,+1)}) 0.75 - >>> supportScalar({'wght':4}, {'wght':(0,2,3)}, extrapolate=True) - 2.0 - >>> supportScalar({'wght':4}, {'wght':(0,2,2)}, extrapolate=True) - 2.0 + >>> supportScalar({'wght':3}, {'wght':(0,1,2)}, extrapolate=True, axisRanges={'wght':(0, 2)}) + -1.0 + >>> supportScalar({'wght':-1}, {'wght':(0,1,2)}, extrapolate=True, axisRanges={'wght':(0, 2)}) + -1.0 + >>> supportScalar({'wght':3}, {'wght':(0,2,2)}, extrapolate=True, axisRanges={'wght':(0, 2)}) + 1.5 + >>> supportScalar({'wght':-1}, {'wght':(0,2,2)}, extrapolate=True, axisRanges={'wght':(0, 2)}) + -0.5 """ + if extrapolate and axisRanges is None: + raise TypeError("axisRanges must be passed when extrapolate is True") scalar = 1.0 for axis, (lower, peak, upper) in support.items(): if ot: @@ -160,18 +174,19 @@ def supportScalar(location, support, ot=True, extrapolate=False): continue if extrapolate: - if v < -1 and lower <= -1: - if peak <= -1 and peak < upper: + axisMin, axisMax = axisRanges[axis] + if v < axisMin and lower <= axisMin: + if peak <= axisMin and peak < upper: scalar *= (v - upper) / (peak - upper) continue - elif -1 < peak: + elif axisMin < peak: scalar *= (v - lower) / (peak - lower) continue - elif +1 < v and +1 <= upper: - if +1 <= peak and lower < peak: + elif axisMax < v and axisMax <= upper: + if axisMax <= peak and lower < peak: scalar *= (v - lower) / (peak - lower) continue - elif peak < +1: + elif peak < axisMax: scalar *= (v - upper) / (peak - upper) continue @@ -189,9 +204,8 @@ def supportScalar(location, support, ot=True, extrapolate=False): class VariationModel(object): """Locations must have the base master at the origin (ie. 0). - If the extrapolate argument is set to True, then location values are - interpretted in the normalized space, ie. in the [-1,+1] range, and - values are extrapolated outside this range. + If the extrapolate argument is set to True, then values are extrapolated + outside the axis range. >>> from pprint import pprint >>> locations = [ \ @@ -234,13 +248,13 @@ class VariationModel(object): """ def __init__(self, locations, axisOrder=None, extrapolate=False): - if len(set(tuple(sorted(l.items())) for l in locations)) != len(locations): raise VariationModelError("Locations must be unique.") self.origLocations = locations self.axisOrder = axisOrder if axisOrder is not None else [] self.extrapolate = extrapolate + self.axisRanges = self.computeAxisRanges(locations) if extrapolate else None locations = [{k: v for k, v in loc.items() if v != 0.0} for loc in locations] keyFunc = self.getMasterLocationsSortKeyFunc( @@ -266,6 +280,17 @@ class VariationModel(object): return subModel, subList(key, items) @staticmethod + def computeAxisRanges(locations): + axisRanges = {} + allAxes = {axis for loc in locations for axis in loc.keys()} + for loc in locations: + for axis in allAxes: + value = loc.get(axis, 0) + axisMin, axisMax = axisRanges.get(axis, (value, value)) + axisRanges[axis] = min(value, axisMin), max(value, axisMax) + return axisRanges + + @staticmethod def getMasterLocationsSortKeyFunc(locations, axisOrder=[]): if {} not in locations: raise VariationModelError("Base master not found.") @@ -339,12 +364,12 @@ class VariationModel(object): # Walk over previous masters now for prev_region in regions[:i]: # Master with extra axes do not participte - if not set(prev_region.keys()).issubset(locAxes): + if set(prev_region.keys()) != locAxes: continue # If it's NOT in the current box, it does not participate relevant = True for axis, (lower, peak, upper) in region.items(): - if axis not in prev_region or not ( + if not ( prev_region[axis][1] == peak or lower < prev_region[axis][1] < upper ): @@ -439,8 +464,12 @@ class VariationModel(object): return model.getDeltas(items, round=round), model.supports def getScalars(self, loc): - return [supportScalar(loc, support, extrapolate=self.extrapolate) - for support in self.supports] + return [ + supportScalar( + loc, support, extrapolate=self.extrapolate, axisRanges=self.axisRanges + ) + for support in self.supports + ] @staticmethod def interpolateFromDeltasAndScalars(deltas, scalars): |