diff options
Diffstat (limited to 'Lib/fontTools/varLib/models.py')
-rw-r--r-- | Lib/fontTools/varLib/models.py | 938 |
1 files changed, 446 insertions, 492 deletions
diff --git a/Lib/fontTools/varLib/models.py b/Lib/fontTools/varLib/models.py index c548fbca..9296deda 100644 --- a/Lib/fontTools/varLib/models.py +++ b/Lib/fontTools/varLib/models.py @@ -1,530 +1,484 @@ """Variation fonts interpolation models.""" -__all__ = [ - "nonNone", - "allNone", - "allEqual", - "allEqualTo", - "subList", - "normalizeValue", - "normalizeLocation", - "supportScalar", - "VariationModel", -] +__all__ = ['nonNone', 'allNone', 'allEqual', 'allEqualTo', 'subList', + 'normalizeValue', 'normalizeLocation', + 'supportScalar', + 'VariationModel'] from fontTools.misc.roundTools import noRound from .errors import VariationModelError def nonNone(lst): - return [l for l in lst if l is not None] - + return [l for l in lst if l is not None] def allNone(lst): - return all(l is None for l in lst) - + return all(l is None for l in lst) def allEqualTo(ref, lst, mapper=None): - if mapper is None: - return all(ref == item for item in lst) - - mapped = mapper(ref) - return all(mapped == mapper(item) for item in lst) - + if mapper is None: + return all(ref == item for item in lst) + else: + mapped = mapper(ref) + return all(mapped == mapper(item) for item in lst) def allEqual(lst, mapper=None): - if not lst: - return True - it = iter(lst) - try: - first = next(it) - except StopIteration: - return True - return allEqualTo(first, it, mapper=mapper) - + if not lst: + return True + it = iter(lst) + try: + first = next(it) + except StopIteration: + return True + return allEqualTo(first, it, mapper=mapper) def subList(truth, lst): - assert len(truth) == len(lst) - return [l for l, t in zip(lst, truth) if t] - + assert len(truth) == len(lst) + return [l for l,t in zip(lst,truth) if t] def normalizeValue(v, triple): - """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 - """ - lower, default, upper = triple - if not (lower <= default <= upper): - raise ValueError( - 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) - else: - v = (v - default) / (upper - default) - return v - + """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 + """ + lower, default, upper = triple + if not (lower <= default <= upper): + raise ValueError( + 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. + elif v < default: + v = (v - default) / (default - lower) + else: + v = (v - default) / (upper - default) + return v def normalizeLocation(location, axes): - """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} - """ - out = {} - for tag, triple in axes.items(): - v = location.get(tag, triple[1]) - out[tag] = normalizeValue(v, triple) - return out - + """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} + """ + out = {} + for tag,triple in axes.items(): + v = location.get(tag, triple[1]) + out[tag] = normalizeValue(v, triple) + return out def supportScalar(location, support, ot=True): - """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. - >>> supportScalar({}, {}) - 1.0 - >>> supportScalar({'wght':.2}, {}) - 1.0 - >>> supportScalar({'wght':.2}, {'wght':(0,2,3)}) - 0.1 - >>> supportScalar({'wght':2.5}, {'wght':(0,2,4)}) - 0.75 - >>> supportScalar({'wght':2.5, 'wdth':0}, {'wght':(0,2,4), 'wdth':(-1,0,+1)}) - 0.75 - >>> supportScalar({'wght':2.5, 'wdth':.5}, {'wght':(0,2,4), 'wdth':(-1,0,+1)}, ot=False) - 0.375 - >>> supportScalar({'wght':2.5, 'wdth':0}, {'wght':(0,2,4), 'wdth':(-1,0,+1)}) - 0.75 - >>> supportScalar({'wght':2.5, 'wdth':.5}, {'wght':(0,2,4), 'wdth':(-1,0,+1)}) - 0.75 - """ - scalar = 1.0 - for axis, (lower, peak, upper) in support.items(): - if ot: - # OpenType-specific case handling - if peak == 0.0: - continue - if lower > peak or peak > upper: - continue - if lower < 0.0 and upper > 0.0: - continue - v = location.get(axis, 0.0) - else: - assert axis in location - v = location[axis] - if v == peak: - continue - if v <= lower or upper <= v: - scalar = 0.0 - break - if v < peak: - scalar *= (v - lower) / (peak - lower) - else: # v > peak - scalar *= (v - upper) / (peak - upper) - return scalar + """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. + >>> supportScalar({}, {}) + 1.0 + >>> supportScalar({'wght':.2}, {}) + 1.0 + >>> supportScalar({'wght':.2}, {'wght':(0,2,3)}) + 0.1 + >>> supportScalar({'wght':2.5}, {'wght':(0,2,4)}) + 0.75 + >>> supportScalar({'wght':2.5, 'wdth':0}, {'wght':(0,2,4), 'wdth':(-1,0,+1)}) + 0.75 + >>> supportScalar({'wght':2.5, 'wdth':.5}, {'wght':(0,2,4), 'wdth':(-1,0,+1)}, ot=False) + 0.375 + >>> supportScalar({'wght':2.5, 'wdth':0}, {'wght':(0,2,4), 'wdth':(-1,0,+1)}) + 0.75 + >>> supportScalar({'wght':2.5, 'wdth':.5}, {'wght':(0,2,4), 'wdth':(-1,0,+1)}) + 0.75 + """ + scalar = 1. + for axis,(lower,peak,upper) in support.items(): + if ot: + # OpenType-specific case handling + if peak == 0.: + continue + if lower > peak or peak > upper: + continue + if lower < 0. and upper > 0.: + continue + v = location.get(axis, 0.) + else: + assert axis in location + v = location[axis] + if v == peak: + continue + if v <= lower or upper <= v: + scalar = 0. + break + if v < peak: + scalar *= (v - lower) / (peak - lower) + else: # v > peak + scalar *= (v - upper) / (peak - upper) + return scalar class VariationModel(object): - """ - Locations must be in normalized space. Ie. base master - is at origin (0):: - - >>> from pprint import pprint - >>> locations = [ \ - {'wght':100}, \ - {'wght':-100}, \ - {'wght':-180}, \ - {'wdth':+.3}, \ - {'wght':+120,'wdth':.3}, \ - {'wght':+120,'wdth':.2}, \ - {}, \ - {'wght':+180,'wdth':.3}, \ - {'wght':+180}, \ - ] - >>> model = VariationModel(locations, axisOrder=['wght']) - >>> pprint(model.locations) - [{}, - {'wght': -100}, - {'wght': -180}, - {'wght': 100}, - {'wght': 180}, - {'wdth': 0.3}, - {'wdth': 0.3, 'wght': 180}, - {'wdth': 0.3, 'wght': 120}, - {'wdth': 0.2, 'wght': 120}] - >>> pprint(model.deltaWeights) - [{}, - {0: 1.0}, - {0: 1.0}, - {0: 1.0}, - {0: 1.0}, - {0: 1.0}, - {0: 1.0, 4: 1.0, 5: 1.0}, - {0: 1.0, 3: 0.75, 4: 0.25, 5: 1.0, 6: 0.6666666666666666}, - {0: 1.0, - 3: 0.75, - 4: 0.25, - 5: 0.6666666666666667, - 6: 0.4444444444444445, - 7: 0.6666666666666667}] + """ + Locations must be in normalized space. Ie. base master + is at origin (0). + >>> from pprint import pprint + >>> locations = [ \ + {'wght':100}, \ + {'wght':-100}, \ + {'wght':-180}, \ + {'wdth':+.3}, \ + {'wght':+120,'wdth':.3}, \ + {'wght':+120,'wdth':.2}, \ + {}, \ + {'wght':+180,'wdth':.3}, \ + {'wght':+180}, \ + ] + >>> model = VariationModel(locations, axisOrder=['wght']) + >>> pprint(model.locations) + [{}, + {'wght': -100}, + {'wght': -180}, + {'wght': 100}, + {'wght': 180}, + {'wdth': 0.3}, + {'wdth': 0.3, 'wght': 180}, + {'wdth': 0.3, 'wght': 120}, + {'wdth': 0.2, 'wght': 120}] + >>> pprint(model.deltaWeights) + [{}, + {0: 1.0}, + {0: 1.0}, + {0: 1.0}, + {0: 1.0}, + {0: 1.0}, + {0: 1.0, 4: 1.0, 5: 1.0}, + {0: 1.0, 3: 0.75, 4: 0.25, 5: 1.0, 6: 0.6666666666666666}, + {0: 1.0, + 3: 0.75, + 4: 0.25, + 5: 0.6666666666666667, + 6: 0.4444444444444445, + 7: 0.6666666666666667}] """ - def __init__(self, locations, axisOrder=None): - 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 [] - - locations = [{k: v for k, v in loc.items() if v != 0.0} for loc in locations] - keyFunc = self.getMasterLocationsSortKeyFunc( - locations, axisOrder=self.axisOrder - ) - self.locations = sorted(locations, key=keyFunc) - - # Mapping from user's master order to our master order - self.mapping = [self.locations.index(l) for l in locations] - self.reverseMapping = [locations.index(l) for l in self.locations] - - self._computeMasterSupports() - self._subModels = {} - - def getSubModel(self, items): - if None not in items: - return self, items - key = tuple(v is not None for v in items) - subModel = self._subModels.get(key) - if subModel is None: - subModel = VariationModel(subList(key, self.origLocations), self.axisOrder) - self._subModels[key] = subModel - return subModel, subList(key, items) - - @staticmethod - def getMasterLocationsSortKeyFunc(locations, axisOrder=[]): - if {} not in locations: - raise VariationModelError("Base master not found.") - axisPoints = {} - for loc in locations: - if len(loc) != 1: - continue - axis = next(iter(loc)) - value = loc[axis] - if axis not in axisPoints: - axisPoints[axis] = {0.0} - assert ( - value not in axisPoints[axis] - ), 'Value "%s" in axisPoints["%s"] --> %s' % (value, axis, axisPoints) - axisPoints[axis].add(value) - - def getKey(axisPoints, axisOrder): - def sign(v): - return -1 if v < 0 else +1 if v > 0 else 0 - - def key(loc): - rank = len(loc) - onPointAxes = [ - axis - for axis, value in loc.items() - if axis in axisPoints and value in axisPoints[axis] - ] - orderedAxes = [axis for axis in axisOrder if axis in loc] - orderedAxes.extend( - [axis for axis in sorted(loc.keys()) if axis not in axisOrder] - ) - return ( - rank, # First, order by increasing rank - -len(onPointAxes), # Next, by decreasing number of onPoint axes - tuple( - axisOrder.index(axis) if axis in axisOrder else 0x10000 - for axis in orderedAxes - ), # Next, by known axes - tuple(orderedAxes), # Next, by all axes - tuple( - sign(loc[axis]) for axis in orderedAxes - ), # Next, by signs of axis values - tuple( - abs(loc[axis]) for axis in orderedAxes - ), # Next, by absolute value of axis values - ) - - return key - - ret = getKey(axisPoints, axisOrder) - return ret - - def reorderMasters(self, master_list, mapping): - # For changing the master data order without - # recomputing supports and deltaWeights. - new_list = [master_list[idx] for idx in mapping] - self.origLocations = [self.origLocations[idx] for idx in mapping] - locations = [ - {k: v for k, v in loc.items() if v != 0.0} for loc in self.origLocations - ] - self.mapping = [self.locations.index(l) for l in locations] - self.reverseMapping = [locations.index(l) for l in self.locations] - self._subModels = {} - return new_list - - def _computeMasterSupports(self): - self.supports = [] - regions = self._locationsToRegions() - for i, region in enumerate(regions): - locAxes = set(region.keys()) - # 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): - 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 ( - prev_region[axis][1] == peak - or lower < prev_region[axis][1] < upper - ): - relevant = False - break - if not relevant: - continue - - # Split the box for new master; split in whatever direction - # that has largest range ratio. - # - # For symmetry, we actually cut across multiple axes - # if they have the largest, equal, ratio. - # https://github.com/fonttools/fonttools/commit/7ee81c8821671157968b097f3e55309a1faa511e#commitcomment-31054804 - - bestAxes = {} - bestRatio = -1 - for axis in prev_region.keys(): - val = prev_region[axis][1] - assert axis in region - lower, locV, upper = region[axis] - newLower, newUpper = lower, upper - if val < locV: - newLower = val - ratio = (val - locV) / (lower - locV) - elif locV < val: - newUpper = val - ratio = (val - locV) / (upper - locV) - else: # val == locV - # Can't split box in this direction. - continue - if ratio > bestRatio: - bestAxes = {} - bestRatio = ratio - if ratio == bestRatio: - bestAxes[axis] = (newLower, locV, newUpper) - - for axis, triple in bestAxes.items(): - region[axis] = triple - self.supports.append(region) - self._computeDeltaWeights() - - def _locationsToRegions(self): - locations = self.locations - # Compute min/max across each axis, use it as total range. - # TODO Take this as input from outside? - minV = {} - maxV = {} - for l in locations: - for k, v in l.items(): - minV[k] = min(v, minV.get(k, v)) - maxV[k] = max(v, maxV.get(k, v)) - - regions = [] - for loc in locations: - region = {} - for axis, locV in loc.items(): - if locV > 0: - region[axis] = (0, locV, maxV[axis]) - else: - region[axis] = (minV[axis], locV, 0) - regions.append(region) - return regions - - def _computeDeltaWeights(self): - self.deltaWeights = [] - for i, loc in enumerate(self.locations): - deltaWeight = {} - # Walk over previous masters now, populate deltaWeight - for j, support in enumerate(self.supports[:i]): - scalar = supportScalar(loc, support) - if scalar: - deltaWeight[j] = scalar - self.deltaWeights.append(deltaWeight) - - def getDeltas(self, masterValues, *, round=noRound): - assert len(masterValues) == len(self.deltaWeights) - mapping = self.reverseMapping - out = [] - for i, weights in enumerate(self.deltaWeights): - delta = masterValues[mapping[i]] - for j, weight in weights.items(): - if weight == 1: - delta -= out[j] - else: - delta -= out[j] * weight - out.append(round(delta)) - return out - - def getDeltasAndSupports(self, items, *, round=noRound): - model, items = self.getSubModel(items) - return model.getDeltas(items, round=round), model.supports - - def getScalars(self, loc): - return [supportScalar(loc, support) for support in self.supports] - - @staticmethod - def interpolateFromDeltasAndScalars(deltas, scalars): - v = None - assert len(deltas) == len(scalars) - for delta, scalar in zip(deltas, scalars): - if not scalar: - continue - contribution = delta * scalar - if v is None: - v = contribution - else: - v += contribution - return v - - def interpolateFromDeltas(self, loc, deltas): - scalars = self.getScalars(loc) - return self.interpolateFromDeltasAndScalars(deltas, scalars) - - def interpolateFromMasters(self, loc, masterValues, *, round=noRound): - deltas = self.getDeltas(masterValues, round=round) - return self.interpolateFromDeltas(loc, deltas) - - def interpolateFromMastersAndScalars(self, masterValues, scalars, *, round=noRound): - deltas = self.getDeltas(masterValues, round=round) - return self.interpolateFromDeltasAndScalars(deltas, scalars) + def __init__(self, locations, axisOrder=None): + 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 [] + + locations = [{k:v for k,v in loc.items() if v != 0.} for loc in locations] + keyFunc = self.getMasterLocationsSortKeyFunc(locations, axisOrder=self.axisOrder) + self.locations = sorted(locations, key=keyFunc) + + # Mapping from user's master order to our master order + self.mapping = [self.locations.index(l) for l in locations] + self.reverseMapping = [locations.index(l) for l in self.locations] + + self._computeMasterSupports(keyFunc.axisPoints) + self._subModels = {} + + def getSubModel(self, items): + if None not in items: + return self, items + key = tuple(v is not None for v in items) + subModel = self._subModels.get(key) + if subModel is None: + subModel = VariationModel(subList(key, self.origLocations), self.axisOrder) + self._subModels[key] = subModel + return subModel, subList(key, items) + + @staticmethod + def getMasterLocationsSortKeyFunc(locations, axisOrder=[]): + if {} not in locations: + raise VariationModelError("Base master not found.") + axisPoints = {} + for loc in locations: + if len(loc) != 1: + continue + axis = next(iter(loc)) + value = loc[axis] + if axis not in axisPoints: + axisPoints[axis] = {0.} + assert value not in axisPoints[axis], ( + 'Value "%s" in axisPoints["%s"] --> %s' % (value, axis, axisPoints) + ) + axisPoints[axis].add(value) + + def getKey(axisPoints, axisOrder): + def sign(v): + return -1 if v < 0 else +1 if v > 0 else 0 + def key(loc): + rank = len(loc) + onPointAxes = [ + axis for axis, value in loc.items() + if axis in axisPoints + and value in axisPoints[axis] + ] + orderedAxes = [axis for axis in axisOrder if axis in loc] + orderedAxes.extend([axis for axis in sorted(loc.keys()) if axis not in axisOrder]) + return ( + rank, # First, order by increasing rank + -len(onPointAxes), # Next, by decreasing number of onPoint axes + tuple(axisOrder.index(axis) if axis in axisOrder else 0x10000 for axis in orderedAxes), # Next, by known axes + tuple(orderedAxes), # Next, by all axes + tuple(sign(loc[axis]) for axis in orderedAxes), # Next, by signs of axis values + tuple(abs(loc[axis]) for axis in orderedAxes), # Next, by absolute value of axis values + ) + return key + + ret = getKey(axisPoints, axisOrder) + ret.axisPoints = axisPoints + return ret + + def reorderMasters(self, master_list, mapping): + # For changing the master data order without + # recomputing supports and deltaWeights. + new_list = [master_list[idx] for idx in mapping] + self.origLocations = [self.origLocations[idx] for idx in mapping] + locations = [{k:v for k,v in loc.items() if v != 0.} + for loc in self.origLocations] + self.mapping = [self.locations.index(l) for l in locations] + self.reverseMapping = [locations.index(l) for l in self.locations] + self._subModels = {} + return new_list + + def _computeMasterSupports(self, axisPoints): + supports = [] + regions = self._locationsToRegions() + for i,region in enumerate(regions): + locAxes = set(region.keys()) + # Walk over previous masters now + for j,prev_region in enumerate(regions[:i]): + # Master with extra axes do not participte + if not set(prev_region.keys()).issubset(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 (prev_region[axis][1] == peak or lower < prev_region[axis][1] < upper): + relevant = False + break + if not relevant: + continue + + # Split the box for new master; split in whatever direction + # that has largest range ratio. + # + # For symmetry, we actually cut across multiple axes + # if they have the largest, equal, ratio. + # https://github.com/fonttools/fonttools/commit/7ee81c8821671157968b097f3e55309a1faa511e#commitcomment-31054804 + + bestAxes = {} + bestRatio = -1 + for axis in prev_region.keys(): + val = prev_region[axis][1] + assert axis in region + lower,locV,upper = region[axis] + newLower, newUpper = lower, upper + if val < locV: + newLower = val + ratio = (val - locV) / (lower - locV) + elif locV < val: + newUpper = val + ratio = (val - locV) / (upper - locV) + else: # val == locV + # Can't split box in this direction. + continue + if ratio > bestRatio: + bestAxes = {} + bestRatio = ratio + if ratio == bestRatio: + bestAxes[axis] = (newLower, locV, newUpper) + + for axis,triple in bestAxes.items (): + region[axis] = triple + supports.append(region) + self.supports = supports + self._computeDeltaWeights() + + def _locationsToRegions(self): + locations = self.locations + # Compute min/max across each axis, use it as total range. + # TODO Take this as input from outside? + minV = {} + maxV = {} + for l in locations: + for k,v in l.items(): + minV[k] = min(v, minV.get(k, v)) + maxV[k] = max(v, maxV.get(k, v)) + + regions = [] + for i,loc in enumerate(locations): + region = {} + for axis,locV in loc.items(): + if locV > 0: + region[axis] = (0, locV, maxV[axis]) + else: + region[axis] = (minV[axis], locV, 0) + regions.append(region) + return regions + + def _computeDeltaWeights(self): + deltaWeights = [] + for i,loc in enumerate(self.locations): + deltaWeight = {} + # Walk over previous masters now, populate deltaWeight + for j,m in enumerate(self.locations[:i]): + scalar = supportScalar(loc, self.supports[j]) + if scalar: + deltaWeight[j] = scalar + deltaWeights.append(deltaWeight) + self.deltaWeights = deltaWeights + + def getDeltas(self, masterValues, *, round=noRound): + assert len(masterValues) == len(self.deltaWeights) + mapping = self.reverseMapping + out = [] + for i,weights in enumerate(self.deltaWeights): + delta = masterValues[mapping[i]] + for j,weight in weights.items(): + delta -= out[j] * weight + out.append(round(delta)) + return out + + def getDeltasAndSupports(self, items, *, round=noRound): + model, items = self.getSubModel(items) + return model.getDeltas(items, round=round), model.supports + + def getScalars(self, loc): + return [supportScalar(loc, support) for support in self.supports] + + @staticmethod + def interpolateFromDeltasAndScalars(deltas, scalars): + v = None + assert len(deltas) == len(scalars) + for delta, scalar in zip(deltas, scalars): + if not scalar: continue + contribution = delta * scalar + if v is None: + v = contribution + else: + v += contribution + return v + + def interpolateFromDeltas(self, loc, deltas): + scalars = self.getScalars(loc) + return self.interpolateFromDeltasAndScalars(deltas, scalars) + + def interpolateFromMasters(self, loc, masterValues, *, round=noRound): + deltas = self.getDeltas(masterValues, round=round) + return self.interpolateFromDeltas(loc, deltas) + + def interpolateFromMastersAndScalars(self, masterValues, scalars, *, round=noRound): + deltas = self.getDeltas(masterValues, round=round) + return self.interpolateFromDeltasAndScalars(deltas, scalars) def piecewiseLinearMap(v, mapping): - keys = mapping.keys() - if not keys: - return v - if v in keys: - return mapping[v] - k = min(keys) - if v < k: - return v + mapping[k] - k - k = max(keys) - if v > k: - return v + mapping[k] - k - # Interpolate - a = max(k for k in keys if k < v) - b = min(k for k in keys if k > v) - va = mapping[a] - vb = mapping[b] - return va + (vb - va) * (v - a) / (b - a) + keys = mapping.keys() + if not keys: + return v + if v in keys: + return mapping[v] + k = min(keys) + if v < k: + return v + mapping[k] - k + k = max(keys) + if v > k: + return v + mapping[k] - k + # Interpolate + a = max(k for k in keys if k < v) + b = min(k for k in keys if k > v) + va = mapping[a] + vb = mapping[b] + return va + (vb - va) * (v - a) / (b - a) def main(args=None): - """Normalize locations on a given designspace""" - from fontTools import configLogger - import argparse - - parser = argparse.ArgumentParser( - "fonttools varLib.models", - description=main.__doc__, - ) - parser.add_argument( - "--loglevel", - metavar="LEVEL", - default="INFO", - help="Logging level (defaults to INFO)", - ) - - group = parser.add_mutually_exclusive_group(required=True) - group.add_argument("-d", "--designspace", metavar="DESIGNSPACE", type=str) - group.add_argument( - "-l", - "--locations", - metavar="LOCATION", - nargs="+", - help="Master locations as comma-separate coordinates. One must be all zeros.", - ) - - args = parser.parse_args(args) - - configLogger(level=args.loglevel) - from pprint import pprint - - if args.designspace: - from fontTools.designspaceLib import DesignSpaceDocument - - doc = DesignSpaceDocument() - doc.read(args.designspace) - locs = [s.location for s in doc.sources] - print("Original locations:") - pprint(locs) - doc.normalize() - print("Normalized locations:") - locs = [s.location for s in doc.sources] - pprint(locs) - else: - axes = [chr(c) for c in range(ord("A"), ord("Z") + 1)] - locs = [ - dict(zip(axes, (float(v) for v in s.split(",")))) for s in args.locations - ] - - model = VariationModel(locs) - print("Sorted locations:") - pprint(model.locations) - print("Supports:") - pprint(model.supports) - + """Normalize locations on a given designspace""" + from fontTools import configLogger + import argparse + + parser = argparse.ArgumentParser( + "fonttools varLib.models", + description=main.__doc__, + ) + parser.add_argument('--loglevel', metavar='LEVEL', default="INFO", + help="Logging level (defaults to INFO)") + + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('-d', '--designspace',metavar="DESIGNSPACE",type=str) + group.add_argument('-l', '--locations', metavar='LOCATION', nargs='+', + help="Master locations as comma-separate coordinates. One must be all zeros.") + + args = parser.parse_args(args) + + configLogger(level=args.loglevel) + from pprint import pprint + + if args.designspace: + from fontTools.designspaceLib import DesignSpaceDocument + doc = DesignSpaceDocument() + doc.read(args.designspace) + locs = [s.location for s in doc.sources] + print("Original locations:") + pprint(locs) + doc.normalize() + print("Normalized locations:") + locs = [s.location for s in doc.sources] + pprint(locs) + else: + axes = [chr(c) for c in range(ord('A'), ord('Z')+1)] + locs = [dict(zip(axes, (float(v) for v in s.split(',')))) for s in args.locations] + + model = VariationModel(locs) + print("Sorted locations:") + pprint(model.locations) + print("Supports:") + pprint(model.supports) if __name__ == "__main__": - import doctest, sys + import doctest, sys - if len(sys.argv) > 1: - sys.exit(main()) + if len(sys.argv) > 1: + sys.exit(main()) - sys.exit(doctest.testmod().failed) + sys.exit(doctest.testmod().failed) |