class PointsModel(object): """ See: https://docs.microsoft.com/en-us/typography/opentype/spec/otvaroverview >>> #model = Model(ds) >>> #model.getScalars(dict(SHPE=0.25, wght=0.25)) >>> masterValues = [0, 100, 200, 100, 200, 0, 0, 0, 0, 0, 0, 0, 0, 0, 100] >>> location = dict(SHPE=0.5, wght=0.5) >>> #model.interpolatePoints(location, masters) 25.0 """ def __init__(self, designSpace, masters): self.ds = designSpace self.vm = VariationModel(self.ds.normalizedLocations, axisOrder=self.ds.axisOrder) self.masters = masters # List of master fonts, in the right order. self._masterValues = {} # Cache of master values. Keys id def __repr__(self): return '<PageBot %s %s>' % (self.__class__.__name__, self.ds.familyName) def getScalars(self, location): return self.vm.getScalars(location) def getDeltas(self, glyphName): deltas = [] mvsX, mvsY = self.getMasterValues(glyphName) #print(len(self.vm.deltaWeights), len(mvsX)) for index in range(len(mvsX)): deltas.append((self.vm.getDeltas(mvsX[index]), self.vm.getDeltas(mvsY[index]))) return deltas def getMasterValues(self, glyphName): if self._masterValues is None: mvx = [] mvy = [] self._masterValues = mvx, mvy for master in self.masters: points = getPoints(master[glyphName]) for pIndex, point in enumerate(points): if len(mvx) <= pIndex: mvx.append([]) mvy.append([]) mvx[pIndex].append(point.x) mvy[pIndex].append(point.y) return self._masterValues def interpolatePoints(self, glyphName, location): interpolatedPoints = [] mvsX, mvsY = self.getMasterValues(glyphName) for index in range(len(mvsX)): interpolatedPoints.append( (self.vm.interpolateFromMasters(location, mvsX[index]), self.vm.interpolateFromMasters(location, mvsY[index]))) return interpolatedPoints
def preview(self, position: dict = {}, font=None, forceRefresh=True): locationKey = ','.join( [k + ':' + str(v) for k, v in position.items()]) if position else ','.join([ k + ':' + str(v) for k, v in self.normalizedValueToMinMaxValue(position, self).items() ]) if locationKey in self.previewLocationsStore: for p in self.previewLocationsStore[locationKey]: yield p return # if not forceRefresh and self.previewGlyph: # print('AE has previewGlyph', self.previewGlyph) # return self.previewGlyph # if not position: # position = self.getLocation() # print(position) # print("AE %s position"%self.name, position, "\n") position = self.normalizedValueToMinMaxValue_clamped(position, self) # position = self._clampLocation(position) # for k in position: # if position[k] > 1: # position[k] = 1 locations = [{}] locations.extend([ self.normalizedValueToMinMaxValue(x["location"], self) for x in self._glyphVariations.getList() if x["on"] ]) # print("AE %s locations"%self.name, locations, "\n") # print(locations,'\n') # locations.extend([{k:self.normalizedValueToMinMaxValue(v, self) for k, v in x["location"].items()} for x in self._glyphVariations.getList() if x["on"]]) # self.frozenPreview = [] self.previewGlyph = [] if font is None: font = self.getParent() model = VariationModel(locations) layerGlyphs = [] for variation in self._glyphVariations.getList(): if not variation.get("on"): continue try: g = font._RFont.getLayer(variation["layerName"])[self.name] except Exception as e: print(e) continue layerGlyphs.append( CustomMathGlyph( font._RFont.getLayer(variation["layerName"])[self.name])) resultGlyph = model.interpolateFromMasters( position, [CustomMathGlyph(self._RGlyph), *layerGlyphs]) # resultGlyph.removeOverlap() # self.frozenPreview.append(resultGlyph) resultGlyph = self.ResultGlyph(resultGlyph) self.previewGlyph = [resultGlyph] self.previewLocationsStore[','.join( [k + ':' + str(v) for k, v in position.items()])] = [resultGlyph] yield resultGlyph
def __init__(self, items, axes, model=None): # items: list of locationdict, value tuples # axes: list of axis dictionaried, not axisdescriptor objects. # model: a model, if we want to share one self.axisOrder = [a.name for a in axes] self.axisMapper = AxisMapper(axes) self.axes = {} for a in axes: self.axes[a.name] = (a.minimum, a.default, a.maximum) if model is None: self.model = VariationModel([self._normalize(a) for a, b in items], axisOrder=self.axisOrder) else: self.model = model self.masters = [b for a, b in items]
def test_onlineVarStoreBuilder(locations, masterValues): axisTags = sorted({k for loc in locations for k in loc}) model = VariationModel(locations) builder = OnlineVarStoreBuilder(axisTags) builder.setModel(model) varIdxs = [] for masters in masterValues: _, varIdx = builder.storeMasters(masters) varIdxs.append(varIdx) varStore = builder.finish() mapping = varStore.optimize() varIdxs = [mapping[varIdx] for varIdx in varIdxs] dummyFont = TTFont() writer = OTTableWriter() varStore.compile(writer, dummyFont) data = writer.getAllData() reader = OTTableReader(data) varStore = VarStore() varStore.decompile(reader, dummyFont) fvarAxes = [buildAxis(axisTag) for axisTag in axisTags] instancer = VarStoreInstancer(varStore, fvarAxes) for masters, varIdx in zip(masterValues, varIdxs): base, *rest = masters for expectedValue, loc in zip(masters, locations): instancer.setLocation(loc) value = base + instancer[varIdx] assert expectedValue == value
def test_init(self, locations, axisOrder, sortedLocs, supports, deltaWeights): model = VariationModel(locations, axisOrder=axisOrder) assert model.locations == sortedLocs assert model.supports == supports assert model.deltaWeights == deltaWeights
def __init__(self, designSpace, masters, instances=None): """Create a VariableFont interpolation model. DesignSpace """ assert masters, ValueError('%s No master fonts defined. The dictionay shouls master the design space.' % self) self.ds = designSpace # Property self.masterList creates font list in order of the design space. self.masters = masters # Can be an empty list, if the design space is new. self.vm = VariationModel(self.ds.normalizedLocations, axisOrder=self.ds.axisOrder) # Property self.instanceList creates font list in order of the design space. if instances is None: instances = {} self.instances = instances # Initialize cached property values. self.clear() # Set the self.paths (path->fontInfo) dictionary, so all fonts are enabled by default. # Set the path->fontInfo dictionary of fonts from the design space that are disable. # This allows the calling function to enable/disable fonts from interpolation. self.disabledMasterPaths = [] # Paths of masters not to be used in delta calculation.
def test_init_duplicate_locations(self): with pytest.raises(VariationModelError, match="Locations must be unique."): VariationModel( [ {"foo": 0.0, "bar": 0.0}, {"foo": 1.0, "bar": 1.0}, {"bar": 1.0, "foo": 1.0}, ] )
def __init__(self, items, axes, model=None): # items: list of locationdict, value tuples # axes: list of axis dictionaried, not axisdescriptor objects. # model: a model, if we want to share one self.axisOrder = [a.name for a in axes] self.axisMapper = AxisMapper(axes) self.axes = {} for a in axes: mappedMinimum, mappedDefault, mappedMaximum = a.map_forward( a.minimum), a.map_forward(a.default), a.map_forward(a.maximum) #self.axes[a.name] = (a.minimum, a.default, a.maximum) self.axes[a.name] = (mappedMinimum, mappedDefault, mappedMaximum) if model is None: dd = [self._normalize(a) for a, b in items] ee = self.axisOrder self.model = VariationModel(dd, axisOrder=ee) else: self.model = model self.masters = [b for a, b in items] self.locations = [a for a, b in items]
class VariationModelMutator(object): """ a thing that looks like a mutator on the outside, but uses the fonttools varlib logic to calculate. """ def __init__(self, items, axes, model=None): # items: list of locationdict, value tuples # axes: list of axis dictionaried, not axisdescriptor objects. # model: a model, if we want to share one self.axisOrder = [a.name for a in axes] self.axisMapper = AxisMapper(axes) self.axes = {} for a in axes: self.axes[a.name] = (a.minimum, a.default, a.maximum) if model is None: self.model = VariationModel([self._normalize(a) for a, b in items], axisOrder=self.axisOrder) else: self.model = model self.masters = [b for a, b in items] def get(self, key): if key in self.model.locations: i = self.model.locations.index(key) return self.masters[i] return None def getFactors(self, location): nl = self._normalize(location) return self.model.getScalars(nl) def makeInstance(self, location, bend=False): # check for anisotropic locations here if bend: location = self.axisMapper(location) nl = self._normalize(location) return self.model.interpolateFromMasters(nl, self.masters) def _normalize(self, location): return normalizeLocation(location, self.axes)
def precompileAllComponents(vcData, allLocations, axisTags): precompiled = {} masterModel = VariationModel(allLocations, axisTags) storeBuilder = OnlineVarStoreBuilder(axisTags) for gn in vcData.keys(): components, locations = vcData[gn] sparseMapping = [None] * len(allLocations) for locIndex, loc in enumerate(locations): allIndex = allLocations.index(loc) sparseMapping[allIndex] = locIndex subModel, mapping = masterModel.getSubModel(sparseMapping) storeBuilder.setModel(subModel) # reorder master values according to allLocations components = [[c[i] for i in mapping] for c in components] precompiledGlyph = precompileVarComponents( gn, components, storeBuilder, axisTags ) if precompiledGlyph is not None: # glyph components do not contain data that has to go to the 'VarC' table precompiled[gn] = precompiledGlyph return precompiled, storeBuilder.finish()
def plotLocationsSurfaces(locations, fig, names=None, **kwargs): assert len(locations[0].keys()) == 2 if names is None: names = [''] n = len(locations) cols = math.ceil(n**.5) rows = math.ceil(n / cols) model = VariationModel(locations) names = [names[model.reverseMapping[i]] for i in range(len(names))] ax1, ax2 = sorted(locations[0].keys()) for i, (support, color, name) in enumerate( zip(model.supports, cycle(pyplot.cm.Set1.colors), cycle(names))): axis3D = fig.add_subplot(rows, cols, i + 1, projection='3d') axis3D.set_title(name) axis3D.set_xlabel(ax1) axis3D.set_ylabel(ax2) pyplot.xlim(-1., +1.) pyplot.ylim(-1., +1.) Xs = support.get(ax1, (-1., 0., +1.)) Ys = support.get(ax2, (-1., 0., +1.)) for x in stops(Xs): X, Y, Z = [], [], [] for y in Ys: z = supportScalar({ax1: x, ax2: y}, support) X.append(x) Y.append(y) Z.append(z) axis3D.plot(X, Y, Z, color=color, **kwargs) for y in stops(Ys): X, Y, Z = [], [], [] for x in Xs: z = supportScalar({ax1: x, ax2: y}, support) X.append(x) Y.append(y) Z.append(z) axis3D.plot(X, Y, Z, color=color, **kwargs) plotLocations(model.locations, [ax1, ax2], axis3D)
def test_VariationModel(): locations = [ {}, { 'bar': 0.5 }, { 'bar': 1.0 }, { 'foo': 1.0 }, { 'bar': 0.5, 'foo': 1.0 }, { 'bar': 1.0, 'foo': 1.0 }, ] model = VariationModel(locations) assert model.locations == locations assert model.supports == [ {}, { 'bar': (0, 0.5, 1.0) }, { 'bar': (0.5, 1.0, 1.0) }, { 'foo': (0, 1.0, 1.0) }, { 'bar': (0, 0.5, 1.0), 'foo': (0, 1.0, 1.0) }, { 'bar': (0.5, 1.0, 1.0), 'foo': (0, 1.0, 1.0) }, ]
def _makeWarpFromList(self, axisName, mapData): # check for the extremes, add if necessary minimum, default, maximum = self.axes[axisName] if not sum([a == minimum for a, b in mapData]): mapData = [(minimum, minimum)] + mapData if not sum([a == maximum for a, b in mapData]): mapData.append((maximum, maximum)) if not (default, default) in mapData: mapData.append((default, default)) mapLocations = [] mapValues = [] for x, y in mapData: l = normalizeLocation(dict(w=x), dict(w=[minimum, default, maximum])) mapLocations.append(l) mapValues.append(y) self.models[axisName] = VariationModel(mapLocations, axisOrder=['w']) self.values[axisName] = mapValues
def test_modeling_error(numLocations, numSamples): # https://github.com/fonttools/fonttools/issues/2213 locations = [{ "axis": float(i) / numLocations } for i in range(numLocations)] masterValues = [100.0 if i else 0.0 for i in range(numLocations)] model = VariationModel(locations) for i in range(numSamples): loc = {"axis": float(i) / numSamples} scalars = model.getScalars(loc) deltas_float = model.getDeltas(masterValues) deltas_round = model.getDeltas(masterValues, round=round) expected = model.interpolateFromDeltasAndScalars(deltas_float, scalars) actual = model.interpolateFromDeltasAndScalars(deltas_round, scalars) err = abs(actual - expected) assert err <= 0.5, (i, err)
def plotLocations(locations, fig, names=None, **kwargs): n = len(locations) cols = math.ceil(n**.5) rows = math.ceil(n / cols) if names is None: names = [None] * len(locations) model = VariationModel(locations) names = [names[model.reverseMapping[i]] for i in range(len(names))] axes = sorted(locations[0].keys()) if len(axes) == 1: _plotLocations2D(model, axes[0], fig, cols, rows, names=names, **kwargs) elif len(axes) == 2: _plotLocations3D(model, axes, fig, cols, rows, names=names, **kwargs) else: raise ValueError("Only 1 or 2 axes are supported")
def prepareVariableComponentData(vcFont, axisTags, globalAxisNames, neutralOnly=False): storeBuilder = OnlineVarStoreBuilder(axisTags) vcData = {} for glyphName in sorted(vcFont.keys()): glyph = vcFont[glyphName] axisTags = { axisTag for v in glyph.variations for axisTag in v.location } if neutralOnly and not axisTags - globalAxisNames: masters = [glyph] else: masters = [glyph] + glyph.variations if not glyph.outline.isEmpty() and glyph.components: # This glyph mixes outlines and classic components, it will have been # flattened upon TTF compilation continue locations = [m.location for m in masters] storeBuilder.setModel(VariationModel(locations)) components = [] for i in range(len(glyph.components)): assert allEqual([m.components[i].name for m in masters]) baseName = masters[0].components[i].name coords = [dict(m.components[i].coord) for m in masters] sanitizeCoords(coords, vcFont[baseName]) transforms = [m.components[i].transform for m in masters] for t in transforms[1:]: assert t.keys() == transforms[0].keys() coordMasterValues = { k: [coord[k] for coord in coords] for k in coords[0].keys() } transformMasterValues = { k: [transform[k] for transform in transforms] for k in transforms[0].keys() } coord = compileMasterValuesDict(storeBuilder, coordMasterValues, 14) # 2.14 transform = compileMasterValuesDict(storeBuilder, transformMasterValues, 16) # 16.16 components.append((baseName, coord, transform)) if components: vcData[glyphName] = components varStore = storeBuilder.finish() mapping = varStore.optimize() assert 0xFFFFFFFF not in mapping mapping[0xFFFFFFFF] = 0xFFFFFFFF for glyphName, components in vcData.items(): for baseName, coord, transform in components: remapValuesDict(coord, mapping) remapValuesDict(transform, mapping) return vcData, varStore
def test_VariationModel(): 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']) assert 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 }] assert 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 model(self): """Returns a :py:class:`fontTools.varLib.models.VariationModel` object used for interpolating values in this variation space.""" locations = list(self.values.keys()) return VariationModel(locations)
def model(self): locations = [dict(self._normalized_location(k)) for k in self.values.keys()] return VariationModel(locations)
def _postParse(self, glyphSet): """This gets called soon after parsing the .glif file. Any layer glyphs and variation info is unpacked here, and put into a subglyph, as part of the self.variations list. """ self.outline, classicComponents = self.outline.splitComponents() for baseGlyphName, affineTransform in classicComponents: xx, xy, yx, yy, dx, dy = affineTransform rotation, scalex, scaley, skewx, skewy = decomposeTwoByTwo( (xx, xy, yx, yy)) assert abs( skewx) < 0.00001, f"x skew is not supported ({self.name})" assert abs( skewy) < 0.00001, f"y skew is not supported ({self.name})" transform = MathDict( x=dx, y=dy, scalex=scalex, scaley=scaley, rotation=math.degrees(rotation), tcenterx=0, tcentery=0, ) self.components.append( Component(baseGlyphName, MathDict(), transform)) dcNames = [] for dc in self.lib.get("robocjk.deepComponents", []): dcNames.append(dc["name"]) self.components.append(_unpackDeepComponent(dc)) axes = {} for axisDict in self.lib.get("robocjk.axes", []): minValue = axisDict["minValue"] maxValue = axisDict["maxValue"] defaultValue = axisDict.get("defaultValue", minValue) minValue, maxValue = sorted([minValue, maxValue]) axes[axisDict["name"]] = minValue, defaultValue, maxValue self.axes = axes self.status = self.lib.get("robocjk.status", 0) variationGlyphs = self.lib.get("robocjk.variationGlyphs") if variationGlyphs is None: return self.glyphNotInLayer = [] for varDict in variationGlyphs: if not varDict.get("on", True): # This source is "off", and should not be used. # They are a bit like background layers. continue layerName = varDict.get("layerName") if (not self.outline.isEmpty() or classicComponents) and layerName: layer = glyphSet.getLayer(layerName) if self.name in layer: varGlyph = layer.getGlyphNoCache(self.name) else: # Layer glyph does not exist, make one up by copying # self.width and self.outline self.glyphNotInLayer.append(layerName) logger.warning( f"glyph {self.name} not found in layer {layerName}") varGlyph = self.__class__() varGlyph.width = self.width varGlyph.outline = self.outline else: varGlyph = self.__class__() varGlyph.width = self.width varGlyph.layerName = layerName varGlyph.sourceName = varDict.get("sourceName") if "width" in varDict: varGlyph.width = varDict["width"] varGlyph.status = varDict.get("status", 0) varGlyph.location = varDict["location"] if isLocationOutOfBounds(varGlyph.location, self.axes): logger.warning(f"location out of bounds for {self.name}; " f"location: {_formatDict(varGlyph.location)} " f"axes: {_formatDict(self.axes)}") deepComponents = varDict.get("deepComponents", []) if len(dcNames) != len(deepComponents): raise ComponentMismatchError( "different number of components in variations: " f"{len(dcNames)} vs {len(deepComponents)}") for dc, dcName in zip(deepComponents, dcNames): varGlyph.components.append(_unpackDeepComponent(dc, dcName)) assert len(varGlyph.components) == len(self.components), ( self.name, [c.name for c in varGlyph.components], [c.name for c in self.components], ) self.variations.append(varGlyph) locations = [{}] + [ normalizeLocation(variation.location, self.axes) for variation in self.variations ] self.model = VariationModel(locations)
class VariationModelMutator(object): """ a thing that looks like a mutator on the outside, but uses the fonttools varlib logic to calculate. """ def __init__(self, items, axes, model=None): # items: list of locationdict, value tuples # axes: list of axis dictionaried, not axisdescriptor objects. # model: a model, if we want to share one self.axisOrder = [a.name for a in axes] self.axisMapper = AxisMapper(axes) self.axes = {} for a in axes: mappedMinimum, mappedDefault, mappedMaximum = a.map_forward( a.minimum), a.map_forward(a.default), a.map_forward(a.maximum) #self.axes[a.name] = (a.minimum, a.default, a.maximum) self.axes[a.name] = (mappedMinimum, mappedDefault, mappedMaximum) if model is None: dd = [self._normalize(a) for a, b in items] ee = self.axisOrder self.model = VariationModel(dd, axisOrder=ee) else: self.model = model self.masters = [b for a, b in items] self.locations = [a for a, b in items] def get(self, key): if key in self.model.locations: i = self.model.locations.index(key) return self.masters[i] return None def getFactors(self, location): nl = self._normalize(location) return self.model.getScalars(nl) def getMasters(self): return self.masters def getSupports(self): return self.model.supports def getReach(self): items = [] for supportIndex, s in enumerate(self.getSupports()): sortedOrder = self.model.reverseMapping[supportIndex] #print("getReach", self.masters[sortedOrder], s) #print("getReach", self.locations[sortedOrder]) items.append((self.masters[sortedOrder], s)) return items def makeInstance(self, location, bend=False): # check for anisotropic locations here #print("\t1", location) if bend: location = self.axisMapper(location) #print("\t2", location) nl = self._normalize(location) return self.model.interpolateFromMasters(nl, self.masters) def _normalize(self, location): return normalizeLocation(location, self.axes)
def __init__(self, designSpace, masters): self.ds = designSpace self.vm = VariationModel(self.ds.normalizedLocations, axisOrder=self.ds.axisOrder) self.masters = masters # List of master fonts, in the right order. self._masterValues = {} # Cache of master values. Keys id
def _postParse(self, ufos, locations): # Filter out and collect component info from the outline self.outline, components = self.outline.splitComponents() # Build Component objects vcComponentData = self.lib.get("varco.components", []) if vcComponentData: assert len(components) == len(vcComponentData), ( self.name, len(components), len(vcComponentData), components, ) else: vcComponentData = [None] * len(components) assert len(self.components) == 0 for (baseGlyph, affine), vcCompo in zip(components, vcComponentData): if vcCompo is None: xx, xy, yx, yy, dx, dy = affine assert xy == 0, "rotation and skew are not implemented" assert yx == 0, "rotation and skew are not implemented" coord = {} transform = MathDict( x=dx, y=dy, rotation=0, scalex=xx, scaley=yy, skewx=0, skewy=0, tcenterx=0, tcentery=0, ) else: assert affine[:4] == (1, 0, 0, 1) x, y = affine[4:] coord = vcCompo["coord"] transformDict = vcCompo["transform"] transform = MathDict( x=affine[4], y=affine[5], rotation=transformDict.get("rotation", 0), scalex=transformDict.get("scalex", 1), scaley=transformDict.get("scaley", 1), skewx=transformDict.get("skewx", 0), skewy=transformDict.get("skewy", 0), tcenterx=transformDict.get("tcenterx", 0), tcentery=transformDict.get("tcentery", 0), ) self.components.append(Component(baseGlyph, MathDict(coord), transform)) assert len(self.variations) == 0 if ufos: assert len(ufos) == len(locations) for ufo, location in zip(ufos[1:], locations[1:]): if self.name not in ufo: continue for axisName, axisValue in location.items(): assert -1 <= axisValue <= 1, (axisName, axisValue) varGlyph = self.__class__.loadFromGlyphObject(ufo[self.name]) varGlyph._postParse([], []) varGlyph.location = location self.variations.append(varGlyph) if self.variations: locations = [{}] + [variation.location for variation in self.variations] self.model = VariationModel(locations)
class Model: """ See: https://docs.microsoft.com/en-us/typography/opentype/spec/otvaroverview """ """ FIXME: raises NotImplementedError, this only works if NewFont() is registered? Maybe create first and the run on a file? >>> import os >>> from fontParts.world import NewFont >>> x = y = 800 >>> x2 = x//2 >>> x4 = x//4 >>> cy = y//2 >>> FAMILY = 'PageBotTest' >>> PATH = '/tmp/%s-%s.ufo' >>> GNAME = 'Q' >>> fReg = NewFont() >>> fReg.axes = {} # Pretend to be a VF >>> fReg.info.familyName = FAMILY >>> fReg.info.styleName = 'Regular' >>> gReg = fReg.newGlyph(GNAME) >>> pen = gReg.getPen() >>> pen.moveTo((0, 0)) >>> pen.lineTo((0, y)) >>> pen.lineTo((x2, y)) >>> pen.lineTo((x2, 0)) >>> pen.lineTo((0, 0)) >>> pen.closePath() >>> gReg.leftMargin = gReg.rightMargin = 50 >>> gReg.width 500 >>> fReg.save(PATH % (FAMILY, fReg.info.styleName)) >>> fBld = NewFont() >>> fBld.axes = {} # Pretend to be a VF >>> fBld.info.familyName = FAMILY >>> fBld.info.styleName = 'Bold' >>> gBld = fBld.newGlyph(GNAME) >>> pen = gBld.getPen() >>> pen.moveTo((0, 0)) >>> pen.lineTo((0, y)) >>> pen.lineTo((x, y)) >>> pen.lineTo((x, 0)) >>> pen.lineTo((0, 0)) >>> pen.closePath() >>> gBld.leftMargin = gBld.rightMargin = 30 >>> gBld.width 860 >>> fBld.save(PATH % (FAMILY, fBld.info.styleName)) >>> fLght = NewFont() >>> fLght.axes = {} # Pretend to be a VF >>> fLght.info.familyName = FAMILY >>> fLght.info.styleName = 'Light' >>> gLght = fLght.newGlyph(GNAME) >>> pen = gLght.getPen() >>> pen.moveTo((0, 0)) >>> pen.lineTo((0, y)) >>> pen.lineTo((x4, y)) >>> pen.lineTo((x4, 0)) >>> pen.lineTo((0, 0)) >>> pen.closePath() >>> gLght.leftMargin = gLght.rightMargin = 70 >>> gLght.width 340 >>> fLght.save(PATH % (FAMILY, fLght.info.styleName)) >>> fCnd = NewFont() >>> fCnd.info.familyName = FAMILY >>> fCnd.info.styleName = 'Condensed' >>> gCnd = fCnd.newGlyph(GNAME) >>> pen = gCnd.getPen() >>> pen.moveTo((0, 0)) >>> pen.lineTo((0, y/2)) >>> pen.lineTo((x, y/2)) >>> pen.lineTo((x, 0)) >>> pen.lineTo((0, 0)) >>> pen.closePath() >>> gCnd.leftMargin = gCnd.rightMargin = 20 >>> gCnd.width 840 >>> fCnd.save(PATH % (FAMILY, fCnd.info.styleName)) >>> # We created the masters now build the design space from it. >>> ds = DesignSpace() # Start empty design space, not reading from a file. >>> # Construct axes as list, to maintain the defined order. >>> axis1 = Axis(tag='wght', name='Weight', minimum=100, default=400, maximum=900) >>> axis2 = Axis(tag='YTUC', name='Y-Transparancy-UC', minimum=0, default=100, maximum=100) >>> ds.appendAxes((axis1, axis2)) >>> # Construct master info as list, to maintain the defined order. >>> # Default masters contains "info" attribute. >>> loc = Location(wght=100, YTUC=100) >>> iLght = FontInfo(name=fLght.info.styleName, familyName=fLght.info.familyName, path=fLght.path, location=loc, styleName=fLght.info.styleName) >>> loc = Location(wght=400, YTUC=100) >>> iReg = FontInfo(info=dict(copy=True), name=fReg.info.styleName, familyName=fReg.info.familyName, path=fReg.path, location=loc, styleName=fReg.info.styleName) >>> loc = Location(wght=900, YTUC=100) >>> iBld = FontInfo(name=fBld.info.styleName, familyName=fBld.info.familyName, path=fBld.path, location=loc, styleName=fBld.info.styleName) >>> loc = Location(wght=400, YTUC=0) >>> iCnd = FontInfo(name=fCnd.info.styleName, familyName=fCnd.info.familyName, path=fCnd.path, location=loc, styleName=fCnd.info.styleName) >>> ds.appendMasters((iLght, iReg, iBld, iCnd)) # Set list of master info >>> ds.masterList # DesignSpace.masterLust gives list of FontInfo items, in defined order. [<FontInfo PageBotTest-Light>, <FontInfo PageBotTest-Regular>, <FontInfo PageBotTest-Bold>, <FontInfo PageBotTest-Condensed>] >>> len(ds.masters) 4 >>> ds.save('/tmp/%s.designspace' % FAMILY) >>> masters = {fReg.path: fReg, fBld.path: fBld, fLght.path: fLght, fCnd.path: fCnd } >>> # Now we have a design space and master dictionary, we can create the model >>> m = Model(ds, masters) >>> m <PageBot Model PageBotTest axes:2 masters:4> >>> [f.info.styleName for f in m.masterList] # Sorted as defined, with Regular as #1. ['Regular', 'Light', 'Bold', 'Condensed'] >>> # Get combined coordinates from all masters. This is why they need to be compatible. >>> mpx, mpy, mcx, mcy, mt = m.getMasterValues(GNAME) # Coordinates in order of masterList for (p0, p1, p2, p3) >>> mpx # [[px0, px0, px0, px0], [px1, px1, px1, px1], [px2, px2, px2, px2], [px3, px3, px3, px3]] [[50, 70, 30, 20], [450, 270, 830, 820], [450, 270, 830, 820], [50, 70, 30, 20]] >>> mpy # [[py1, py1, py1, py1], [py2, py2, py2, py2], ...] [[800, 800, 800, 400.0], [800, 800, 800, 400.0], [0, 0, 0, 0], [0, 0, 0, 0]] >>> mcx, mcy # No components here ([], []) >>> mt [[500, 340, 860, 840]] >>> dpx, dpy, dcx, dcy, dt = m.getDeltas(GNAME) # Point deltas, component deltas, metrics deltas >>> dpx # [[dx1, dx1, dx1, dx1], [dx2, dx2, dx2, dx2], ...] [[70, -20.0, -40.0, -50.0], [270, 180.0, 560.0, 550.0], [270, 180.0, 560.0, 550.0], [70, -20.0, -40.0, -50.0]] >>> dt [[340, 160.0, 520.0, 500.0]] >>> sorted(m.ds.axes.keys()) ['YTUC', 'wght'] >>> sorted(m.supports()) # List with normalized master locations [{}, {'YTUC': (-1.0, -1.0, 0)}, {'wght': (-1.0, -1.0, 0)}, {'wght': (0, 1.0, 1.0)}] >>> m.getScalars(dict(wght=1, YTUC=0)) # Order: Regular, Light, Bold, Condensed [1.0, 0.0, 1.0, 0.0] >>> m.getScalars(dict(wght=0, YTUC=0)) [1.0, 0.0, 0.0, 0.0] >>> m.getScalars(dict(wght=-1, YTUC=0)) [1.0, 1.0, 0.0, 0.0] >>> m.getScalars(dict(wght=1, YTUC=-1)) [1.0, 0.0, 1.0, 1.0] >>> m.getScalars(dict(wght=0, YTUC=-1)) [1.0, 0.0, 0.0, 1.0] >>> m.getScalars(dict(wght=-1, YTUC=-1)) [1.0, 1.0, 0.0, 1.0] >>> m.getScalars(dict(wght=0.8, YTUC=-0.3)) [1.0, 0.0, 0.8, 0.3] >>> from fontParts.world import NewFont >>> fInt = NewFont() >>> gInt = fInt.newGlyph(GNAME) >>> loc = dict(wght=0, YTUC=500) >>> #points, components, metrics = m.interpolateValues(GNAME, loc) >>> #points #[(50.0, 800.0), (450.0, 800.0), (450.0, 0.0), (50.0, 0.0)] >>> #components #[] >>> #metrics #[500.0] >>> loc = dict(wght=-1, YTUC=0) # Location outside the boundaries of an axis answers min/max of the axis >>> #points, components, metrics = m.interpolateValues(GNAME, loc) >>> #points [(0.0, 400.0), (1000.0, 400.0), (1000.0, 0.0), (0.0, 0.0)] >>> #components #[] >>> #metrics #[1000.0] """ def __init__(self, designSpace, masters, instances=None): """Create a VariableFont interpolation model. DesignSpace """ assert masters, ValueError('%s No master fonts defined. The dictionay shouls master the design space.' % self) self.ds = designSpace # Property self.masterList creates font list in order of the design space. self.masters = masters # Can be an empty list, if the design space is new. self.vm = VariationModel(self.ds.normalizedLocations, axisOrder=self.ds.axisOrder) # Property self.instanceList creates font list in order of the design space. if instances is None: instances = {} self.instances = instances # Initialize cached property values. self.clear() # Set the self.paths (path->fontInfo) dictionary, so all fonts are enabled by default. # Set the path->fontInfo dictionary of fonts from the design space that are disable. # This allows the calling function to enable/disable fonts from interpolation. self.disabledMasterPaths = [] # Paths of masters not to be used in delta calculation. def __repr__(self): return '<PageBot %s %s axes:%d masters:%d>' % (self.__class__.__name__, self.ds.familyName, len(self.ds.axes), len(self.masters)) def _get_masters(self): return self._masters def _set_masters(self, masters): self._masters = masters self._masterList = None masters = property(_get_masters, _set_masters) def _get_defaultMaster(self): """Answers the master font at default location. Answer None if it does not exist or cannot be found.""" defaultMaster = None defaultMasterInfo = self.ds.defaultMaster if defaultMasterInfo is not None: defaultMaster = self.masters.get(defaultMasterInfo.path) return defaultMaster defaultMaster = property(_get_defaultMaster) def _get_masterList(self): """Dictionary of real master Font instances, path is key. Always start with the default, then omit the default if it else where in the list. """ defaultMaster = self.defaultMaster # Get the ufo master for the default location. if self._masterList is None: self._masterList = [] if defaultMaster is not None: self._masterList.append(defaultMaster) # Force to be first in the list. for masterPath in self.ds.masterPaths: if defaultMaster is not None and masterPath == defaultMaster.path: continue if not masterPath in self.disabledMasterPaths: self._masterList.append(self.masters[masterPath]) return self._masterList masterList = property(_get_masterList) def getAxisMasters(self, axis): """Answers a tuple of two lists of all masters on the axis, on either size of the default master, and including the default master. If mininum == default or default == maximum, then that side if the axis only contains the default master.""" # FIXME: minMasters, maxMasters unused. minMasters = [] maxMasters = [] for axisSide in self.ds.getAxisMasters(axis.tag): for masterInfo in axisSide: if masterInfo.path: # FIXME: masters doesn't exist. #masters.append(self.masters.get(masterInfo.path)) pass return minMasters, maxMasters def _get_instanceList(self): """Dictionary of real master Font instance, path is key.""" if self._instanceList is None: self._instanceList = [] for instancePath in self.ds.instancePaths: self._instanceList.append(self.instance[instancePath]) return self._instanceList instanceList = property(_get_instanceList) def clear(self): """Clear the cached master values for the last interpolated glyph. This forces the values to be collected for the next glyph interpolation.""" self._masterList = None self._instanceList = None self._masterValues = None self._defaultMaster = None # Rendering def getScalars(self, location): """Answers the list of scalars (multipliers 0..1) for each master in the given nLocation (normalized values -1..0..1): [1.0, 0.0, 0.8, 0.3] with respectively Regular, Light, Bold, Condensed as master ordering. """ return self.vm.getScalars(location) def supports(self): return self.vm.supports def getDeltas(self, glyphName): pointXDeltas = [] pointYDeltas = [] componentXDeltas = [] componentYDeltas = [] metricsDeltas = [] mvx, mvy, mvCx, mvCy, mt = self.getMasterValues(glyphName) for pIndex, _ in enumerate(mvx): pointXDeltas.append(self.vm.getDeltas(mvx[pIndex])) pointYDeltas.append(self.vm.getDeltas(mvy[pIndex])) for cIndex, _ in enumerate(mvCx): componentXDeltas.append(self.vm.getDeltas(mvCx[cIndex])) componentYDeltas.append(self.vm.getDeltas(mvCy[cIndex])) for mIndex, _ in enumerate(mt): metricsDeltas.append(self.vm.getDeltas(mt[mIndex])) return pointXDeltas, pointYDeltas, componentXDeltas, componentYDeltas, metricsDeltas def getMasterValues(self, glyphName): """Answers the (mvx, mvy, mvCx, mvCy), for point and component transformation, lists of corresponding master glyphs, in the same order as the self.masterList.""" if self._masterValues is None: mvx = [] # X values of point transformations. Length is the amount of masters mvy = [] # Y values mvCx = [] # X values of components transformation mvCy = [] # Y values mMt = [[]] # Metrics values self._masterValues = mvx, mvy, mvCx, mvCy, mMt # Initialize result tuple for master in self.masterList: if not glyphName in master: continue g = master[glyphName] points = getPoints(g) # Collect the master points for pIndex, point in enumerate(points): if len(mvx) <= pIndex: mvx.append([]) mvy.append([]) mvx[pIndex].append(point.x) mvy[pIndex].append(point.y) components = getComponents(g) # Collect the master components """ for cIndex, component in enumerate(components): t = component.transformation if len(mvCx) < cIndex: mvCx.append([]) mvCy.append([]) mvCx[cIndex].append(t[-2]) mvCy[cIndex].append(t[-1]) """ mMt[0].append(g.width) # Other interpolating metrics can be added later. return self._masterValues def interpolateValues(self, glyphName, location): """Interpolate the glyph from masters and answer the list of (x,y) points tuples, component transformation and metrics. The axis location in world values is normalized to (-1..0..1).""" nLocation = normalizeLocation(location, self.ds.tripleAxes) interpolatedPoints = [] interpolatedComponents = [] interpolatedMetrics = [] mvx, mvy, mvCx, mvCy, mMt = self.getMasterValues(glyphName) for pIndex, _ in enumerate(mvx): interpolatedPoints.append(( self.vm.interpolateFromMasters(nLocation, mvx[pIndex]), self.vm.interpolateFromMasters(nLocation, mvy[pIndex]) )) for cIndex, _ in enumerate(mvCx): interpolatedComponents.append(( self.vm.interpolateFromMasters(nLocation, mvCx[cIndex]), self.vm.interpolateFromMasters(nLocation, mvCy[cIndex]) )) for mIndex, _ in enumerate(mMt): interpolatedMetrics.append( self.vm.interpolateFromMasters(nLocation, mMt[mIndex]) ) return interpolatedPoints, interpolatedComponents, interpolatedMetrics def interpolateGlyph(self, glyph, location): """Interpolate the glyph from the masters. If glyph is not compatible with the masters, then first copy one of the masters into glyphs. Location is normalized to (-1..0..1).""" # If there are components, make sure to interpolate them first, then # interpolate (dx, dy). Check if the referenced glyph exists. # Otherwise copy it from one of the masters into the parent of glyph. mvx, mvy, mvCx, mvCy, mMt = self.getMasterValues(glyph.name) points = getPoints(glyph) components = getComponents(glyph) if components: font = glyph.getParent() for component in components: if component.baseGlyph in font: self.interpolateGlyph(font[component.baseGlyph], location) if len(points) != len(mvx): # Glyph may not exist, or not compatible. # Clear the glyph and copy from the first master in the list, so it always interpolates. glyph.clear() masterGlyph = self.masterList[0][glyph.name] masterGlyph.draw(glyph.getPen()) points = getPoints(glyph) # Get new set of points # Normalize to location (-1..0..1) nLocation = normalizeLocation(location, self.ds.tripleAxes) # Get the point of the glyph, and set their (x,y) to the calculated variable points. for pIndex, _ in enumerate(mvx): p = points[pIndex] p.x = self.vm.interpolateFromMasters(nLocation, mvx[pIndex]) p.y = self.vm.interpolateFromMasters(nLocation, mvy[pIndex]) for cIndex, _ in enumerate(mvCx): c = components[cIndex] t = list(c.transformation) t[-2] = self.vm.interpolateFromMasters(nLocation, mvCx[cIndex]) t[-1] = self.vm.interpolateFromMasters(nLocation, mvCy[cIndex]) c.transformation = t glyph.width = self.vm.interpolateFromMasters(nLocation, mMt[0])
def preview(self, position: dict = {}, deltasStore: dict = {}, font=None, forceRefresh=True, axisPreview=False): locationKey = ','.join( [k + ':' + str(v) for k, v in position.items()]) if position else ','.join([ k + ':' + str(v) for k, v in self.normalizedValueToMinMaxValue( self.getLocation(), self).items() ]) #### 3 CONDITIONS DE DESSIN POSSIBLE EN CAS D'ÉLEMENT SELECTIONNÉ #### redrawAndTransformAll = False redrawAndTransformSelected = False onlyTransformSelected = False if self.selectedElement and not self.reinterpolate: onlyTransformSelected = True elif self.selectedElement and self.reinterpolate and not axisPreview: redrawAndTransformAll = True elif self.selectedElement: redrawAndTransformSelected = True else: redrawAndTransformAll = True ############################################################ #### S'IL N'Y A PAS DE SELECTION, RECHERCHE D'UN CACHE #### previewLocationsStore = self.previewLocationsStore.get( locationKey, False) if axisPreview: redrawSeletedElement = self.redrawSelectedElementSource else: redrawSeletedElement = self.redrawSelectedElementPreview if not self.redrawSelectedElementSource: if previewLocationsStore: # print('I have cache', locationKey) for p in previewLocationsStore: yield p return if axisPreview and self.axisPreview: # print('DC has axisPreview', self.axisPreview) for e in self.axisPreview: yield e return elif not forceRefresh and self.previewGlyph and not axisPreview: # print('DC has previewGlyph', self.previewGlyph) for e in self.previewGlyph: yield e return ############################################################ #### S'IL N'Y A UNE SELECTION MAIS PAS D'INSTRUCTION DE REDESSIN, RECHERCHE D'UN CACHE #### if not redrawAndTransformAll and not redrawAndTransformSelected and not onlyTransformSelected: if previewLocationsStore: # print('I have cache', locationKey) for p in previewLocationsStore: yield p return ############################################################ else: #### IL Y A DES INSTRUCTION DE REDESSIN #### if axisPreview: preview = self.axisPreview else: preview = self.previewGlyph """ Dans cette condition on ne ce soucis pas du cache, on redessine tout""" if redrawAndTransformAll: if axisPreview: preview = self.axisPreview = [] else: preview = self.previewGlyph = [] else: """Dans cette condition on récupère ce qu'il y a dans le cache et on travaillera dessus après, soit pour modifier les instructions de transformation de l'element selectionné soit pour recalculer l'element et ses instructions de transformation. Les autres elements du cache ne seront pas recalculé""" if previewLocationsStore: if axisPreview: preview = self.axisPreview = previewLocationsStore else: preview = self.previewGlyph = previewLocationsStore if not position: position = self.getLocation() position = self.normalizedValueToMinMaxValue(position, self) # print("position", position, "\n") locations = [{}] locations.extend([ self.normalizedValueToMinMaxValue(x["location"], self) for x in self._glyphVariations if x["on"] ]) # print("location", locations, "\n\n") model = VariationModel(locations) if redrawAndTransformAll: masterDeepComponents = self._deepComponents axesDeepComponents = [ variation.get("deepComponents") for variation in self._glyphVariations.getList() if variation.get("on") == 1 ] else: masterDeepComponents = [ x for i, x in enumerate(self._deepComponents) if i in self.selectedElement ] axesDeepComponents = [[ x for i, x in enumerate(variation.get("deepComponents")) if i in self.selectedElement ] for variation in self._glyphVariations.getList() if variation.get("on") == 1] result = [] deltasList = [] for i, deepComponent in enumerate(masterDeepComponents): variations = [] for gv in axesDeepComponents: variations.append(gv[i]) deltas = model.getDeltas([deepComponent, *variations]) # result.append(model.interpolateFromMasters(position, [deepComponent, *variations])) result.append(model.interpolateFromDeltas(position, deltas)) deltasList.append(deltas) if font is None: font = self.getParent() for i, dc in enumerate(result): name = dc.get("name") if not set([name]) & (font.staticAtomicElementSet() | font.staticDeepComponentSet() | font.staticCharacterGlyphSet()): continue g = font[name] if onlyTransformSelected: preview[self.selectedElement[i]].transformation = dc.get( "transform") else: resultGlyph = RGlyph() pos = dc.get('coord') g = g.preview(pos, font, forceRefresh=True) for c in g: c = c.glyph c.draw(resultGlyph.getPen()) if redrawAndTransformSelected: preview[self.selectedElement[i]].resultGlyph = resultGlyph preview[self.selectedElement[i]].transformation = dc.get( "transform") else: preview.append( self.ResultGlyph(resultGlyph, dc.get("transform"))) self.previewLocationsStore[','.join( [k + ':' + str(v) for k, v in position.items()])] = preview if axisPreview: self.redrawSelectedElementSource = False else: self.redrawSelectedElementPreview = False for resultGlyph in preview: yield resultGlyph