def _plotLocations3D(model, axes, fig, rows, cols, names, **kwargs): ax1, ax2 = axes 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') if name is not None: 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) _plotLocationsDots(model.locations, [ax1, ax2], axis3D)
def _getScalar(self, regionIdx): scalar = self._scalars.get(regionIdx) if scalar is None: support = self._regions[regionIdx].get_support(self.fvar_axes) scalar = supportScalar(self.location, support) self._scalars[regionIdx] = scalar return scalar
def draw(self, pen): from fontTools.varLib.iup import iup_delta from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates from fontTools.varLib.models import supportScalar glyf = self._ttFont['glyf'] hMetrics = self._ttFont['hmtx'].metrics vMetrics = getattr(self._ttFont.get('vmtx'), 'metrics', None) variations = self._ttFont['gvar'].variations[self._glyphName] coordinates, _ = glyf._getCoordinatesAndControls(self._glyphName, hMetrics, vMetrics) origCoords, endPts = None, None for var in variations: scalar = supportScalar(self._location, var.axes) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords,control = glyf._getCoordinatesAndControls(self._glyphName, hMetrics, vMetrics) endPts = control[1] if control[0] >= 1 else list(range(len(control[1]))) delta = iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar glyph = copy(glyf[self._glyphName]) # Shallow copy horizontalAdvanceWidth, leftSideBearing, verticalAdvanceWidth = _setCoordinates(glyph, coordinates, glyf) self.width = horizontalAdvanceWidth self.height = verticalAdvanceWidth offset = leftSideBearing - glyph.xMin if hasattr(glyph, "xMin") else 0 glyph.draw(pen, glyf, offset)
def draw(self, pen): glyph = self._ttFont['glyf'][self._glyphName] glyph = self._copyGlyph(glyph, self._ttFont['glyf']) if self.drawBefore is not None: # Call if defined self.drawBefore(self, p, view) variables = self._ttFont['gvar'].variables[self._glyphName] coordinates, _ = _GetCoordinates(self._ttFont, self._glyphName) for var in variables: scalar = supportScalar(self._location, var.axes) if not scalar: continue # print(var.coordinates) if None in var.coordinates: print("warning: replacing missing deltas with (0, 0)") deltas = GlyphCoordinates([pt or (0, 0) for pt in var.coordinates]) coordinates += deltas * scalar horizontalAdvanceWidth, leftSideBearing = setCoordinates( glyph, coordinates, self._ttFont['glyf']) self.width = horizontalAdvanceWidth glyph.draw(pen, self._ttFont['glyf']) # XXX offset based on lsb if self.drawAfter is not None: # Call if defined self.drawAfter(self, p, view)
def instantiateTupleVariationStore(variations, location, origCoords=None, endPts=None): """Instantiate TupleVariation list at the given location. The 'variations' list of TupleVariation objects is modified in-place. The input location can describe either a full instance (all the axes are assigned an explicit coordinate) or partial (some of the axes are omitted). Tuples that do not participate are kept as they are. Those that have 0 influence at the given location are removed from the variation store. Those that are fully instantiated (i.e. all their axes are being pinned) are also removed from the variation store, their scaled deltas accummulated and returned, so that they can be added by the caller to the default instance's coordinates. Tuples that are only partially instantiated (i.e. not all the axes that they participate in are being pinned) are kept in the store, and their deltas multiplied by the scalar support of the axes to be pinned at the desired location. Args: variations: List[TupleVariation] from either 'gvar' or 'cvar'. location: Dict[str, float]: axes coordinates for the full or partial instance. origCoords: GlyphCoordinates: default instance's coordinates for computing 'gvar' inferred points (cf. table__g_l_y_f.getCoordinatesAndControls). endPts: List[int]: indices of contour end points, for inferring 'gvar' deltas. Returns: List[float]: the overall delta adjustment after applicable deltas were summed. """ newVariations = collections.OrderedDict() for var in variations: # Compute the scalar support of the axes to be pinned at the desired location, # excluding any axes that we are not pinning. # If a TupleVariation doesn't mention an axis, it implies that the axis peak # is 0 (i.e. the axis does not participate). support = {axis: var.axes.pop(axis, (-1, 0, +1)) for axis in location} scalar = supportScalar(location, support) if scalar == 0.0: # no influence, drop the TupleVariation continue # compute inferred deltas only for gvar ('origCoords' is None for cvar) if origCoords is not None: var.calcInferredDeltas(origCoords, endPts) var.scaleDeltas(scalar) # merge TupleVariations with overlapping "tents" axes = tuple(var.axes.items()) if axes in newVariations: newVariations[axes] += var else: newVariations[axes] = var # drop TupleVariation if all axes have been pinned (var.axes.items() is empty); # its deltas will be added to the default instance's coordinates defaultVar = newVariations.pop(tuple(), None) for var in newVariations.values(): var.roundDeltas() variations[:] = list(newVariations.values()) return defaultVar.coordinates if defaultVar is not None else []
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 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 main(args=None): if args is None: import sys args = sys.argv[1:] varfilename = args[0] locargs = args[1:] outfile = os.path.splitext(varfilename)[0] + '-instance.ttf' loc = {} for arg in locargs: tag, val = arg.split('=') assert len(tag) <= 4 loc[tag.ljust(4)] = float(val) print("Location:", loc) print("Loading variable font") varfont = TTFont(varfilename) fvar = varfont['fvar'] axes = { a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in fvar.axes } # TODO Round to F2Dot14? loc = normalizeLocation(loc, axes) # Location is normalized now print("Normalized location:", loc) gvar = varfont['gvar'] glyf = varfont['glyf'] # get list of glyph names in gvar sorted by component depth glyphnames = sorted( gvar.variations.keys(), key=lambda name: (glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphname in glyphnames: variations = gvar.variations[glyphname] coordinates, _ = _GetCoordinates(varfont, glyphname) for var in variations: scalar = supportScalar(loc, var.axes) if not scalar: continue # TODO Do IUP / handle None items coordinates += GlyphCoordinates(var.coordinates) * scalar _SetCoordinates(varfont, glyphname, coordinates) print("Removing variable tables") for tag in ('avar', 'cvar', 'fvar', 'gvar', 'HVAR', 'MVAR', 'VVAR', 'STAT'): if tag in varfont: del varfont[tag] print("Saving instance font", outfile) varfont.save(outfile)
def _plotLocations2D(model, axis, fig, cols, rows, names, **kwargs): for i, (support, color, name) in enumerate( zip(model.supports, cycle(pyplot.cm.Set1.colors), cycle(names))): subplot = fig.add_subplot(rows, cols, i + 1) if name is not None: subplot.set_title(name) subplot.set_xlabel(axis) pyplot.xlim(-1., +1.) Xs = support.get(axis, (-1., 0., +1.)) X, Y = [], [] for x in stops(Xs): y = supportScalar({axis: x}, support) X.append(x) Y.append(y) subplot.plot(X, Y, color=color, **kwargs) _plotLocationsDots(model.locations, [axis], subplot)
def instantiateGvarGlyph(varfont, location, glyphname): glyf = varfont["glyf"] gvar = varfont["gvar"] variations = gvar.variations[glyphname] coordinates = glyf.getCoordinates(glyphname, varfont) origCoords = None newVariations = [] pinnedAxes = set(location.keys()) defaultModified = False for var in variations: tupleAxes = set(var.axes.keys()) pinnedTupleAxes = tupleAxes & pinnedAxes if not pinnedTupleAxes: # A tuple for only axes being kept is untouched newVariations.append(var) continue else: # compute influence at pinned location only for the pinned axes pinnedAxesSupport = {a: var.axes[a] for a in pinnedTupleAxes} scalar = supportScalar(location, pinnedAxesSupport) if not scalar: # no influence (default value or out of range); drop tuple continue deltas = var.coordinates hasUntouchedPoints = None in deltas if hasUntouchedPoints: if origCoords is None: origCoords, g = glyf.getCoordinatesAndControls(glyphname, varfont) deltas = iup_delta(deltas, origCoords, g.endPts) scaledDeltas = GlyphCoordinates(deltas) * scalar if tupleAxes.issubset(pinnedAxes): # A tuple for only axes being pinned is discarded, and # it's contribution is reflected into the base outlines coordinates += scaledDeltas defaultModified = True else: # A tuple for some axes being pinned has to be adjusted var.coordinates = scaledDeltas for axis in pinnedTupleAxes: del var.axes[axis] newVariations.append(var) if defaultModified: glyf.setCoordinates(glyphname, coordinates, varfont) gvar.variations[glyphname] = newVariations
def main(args=None): if args is None: import sys args = sys.argv[1:] varfilename = args[0] locargs = args[1:] outfile = os.path.splitext(varfilename)[0] + '-instance.ttf' loc = {} for arg in locargs: tag,val = arg.split('=') assert len(tag) <= 4 loc[tag.ljust(4)] = float(val) print("Location:", loc) print("Loading GX font") varfont = TTFont(varfilename) fvar = varfont['fvar'] axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes} # TODO Round to F2Dot14? loc = normalizeLocation(loc, axes) # Location is normalized now print("Normalized location:", loc) gvar = varfont['gvar'] for glyphname,variations in gvar.variations.items(): coordinates,_ = _GetCoordinates(varfont, glyphname) for var in variations: scalar = supportScalar(loc, var.axes) if not scalar: continue # TODO Do IUP / handle None items coordinates += GlyphCoordinates(var.coordinates) * scalar _SetCoordinates(varfont, glyphname, coordinates) print("Removing GX tables") for tag in ('fvar','avar','gvar'): if tag in varfont: del varfont[tag] print("Saving instance font", outfile) varfont.save(outfile)
def instantiateCvar(varfont, location): log.info("Instantiating cvt/cvar tables") cvar = varfont["cvar"] cvt = varfont["cvt "] pinnedAxes = set(location.keys()) newVariations = [] deltas = {} for var in cvar.variations: tupleAxes = set(var.axes.keys()) pinnedTupleAxes = tupleAxes & pinnedAxes if not pinnedTupleAxes: # A tuple for only axes being kept is untouched newVariations.append(var) continue else: # compute influence at pinned location only for the pinned axes pinnedAxesSupport = {a: var.axes[a] for a in pinnedTupleAxes} scalar = supportScalar(location, pinnedAxesSupport) if not scalar: # no influence (default value or out of range); drop tuple continue if tupleAxes.issubset(pinnedAxes): for i, c in enumerate(var.coordinates): if c is not None: # Compute deltas which need to be applied to values in cvt deltas[i] = deltas.get(i, 0) + scalar * c else: # Apply influence to delta values for i, d in enumerate(var.coordinates): if d is not None: var.coordinates[i] = otRound(d * scalar) for axis in pinnedTupleAxes: del var.axes[axis] newVariations.append(var) if deltas: for i, delta in deltas.items(): cvt[i] += otRound(delta) if newVariations: cvar.variations = newVariations else: del varfont["cvar"]
def instantiateComponentOffsets(ttFont, glyphName, location): glyfTable = ttFont["glyf"] gvarTable = ttFont["gvar"] assert glyfTable[glyphName].isComposite() variations = gvarTable.variations[glyphName] coordinates, _ = glyfTable.getCoordinatesAndControls(glyphName, ttFont) origCoords, endPts = None, None for var in variations: scalar = supportScalar(location, var.axes) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords, g = glyfTable.getCoordinatesAndControls(glyphName, ttFont) endPts = g.endPts delta = iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar assert len(coordinates) == len(glyfTable[glyphName].components) + 4 return coordinates[:-4]
def generateInstance(variableFontPath, location, targetDirectory, normalize=False, force=False): instanceName = "" for k, v in sorted(location.items()): # TODO better way to normalize the location name to (0, 1000) v = min(v, 1000) v = max(v, 0) instanceName += "-%s%s" % (k, v) targetFileName = '.'.join(variableFontPath.split('/')[-1].split('.') [:-1]) + instanceName + '.ttf' if not targetDirectory.endswith('/'): targetDirectory += '/' if not os.path.exists(targetDirectory): os.makedirs(targetDirectory) outFile = targetDirectory + targetFileName if force or not os.path.exists(outFile): #print location #print("Loading variable font") varFont = TTFont(variableFontPath) fvar = varFont['fvar'] axes = { a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in fvar.axes } # TODO Apply avar # TODO Round to F2Dot14? loc = normalizeLocation(location, axes) # Location is normalized now #print("Normalized location:", loc, 'from', location) # Set the instance name IDs in the name table platforms = ((1, 0, 0), (3, 1, 0x409)) # Macintosh and Windows for platformID, platEncID, langID in platforms: familyName = varFont['name'].getName(1, platformID, platEncID, langID) # 1 Font Family name if not familyName: continue familyName = familyName.toUnicode() # NameRecord to unicode string styleName = unicode( instanceName) # TODO make sure this works in any case fullFontName = " ".join([familyName, styleName]) postscriptName = fullFontName.replace(" ", "-") varFont['name'].setName(styleName, 2, platformID, platEncID, langID) # 2 Font Subfamily name varFont['name'].setName(fullFontName, 4, platformID, platEncID, langID) # 4 Full font name varFont['name'].setName(postscriptName, 6, platformID, platEncID, langID) # 6 Postscript name for the font # Other important name IDs # 3 Unique font identifier (e.g. Version 0.000;NONE;Promise Bold Regular) # 25 Variables PostScript Name Prefix gvar = varFont['gvar'] glyf = varFont['glyf'] # get list of glyph names in gvar sorted by component depth glyphnames = sorted( gvar.variations.keys(), key=lambda name: (glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphname in glyphnames: variations = gvar.variations[glyphname] coordinates, _ = _GetCoordinates(varFont, glyphname) origCoords, endPts = None, None for var in variations: scalar = supportScalar(loc, var.axes) #, ot=True) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords, control = _GetCoordinates( varFont, glyphname) endPts = control[1] if control[0] >= 1 else list( range(len(control[1]))) delta = _iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar _SetCoordinates(varFont, glyphname, coordinates) #print("Removing variable tables") for tag in ('avar', 'cvar', 'fvar', 'gvar', 'HVAR', 'MVAR', 'VVAR', 'STAT'): if tag in varFont: del varFont[tag] #print("Saving instance font", outFile) varFont.save(outFile) # Installing the font in DrawBot. Answer font name and path. return c.installFont(outFile), outFile
def instantiateItemVariationStore(varfont, tableName, location): log.info("Instantiating ItemVariation store of %s table", tableName) table = varfont[tableName].table fvar = varfont["fvar"] newRegions = [] regionInfluenceMap = {} pinnedAxes = set(location.keys()) fvarAxisIndices = { axis.axisTag: index for index, axis in enumerate(fvar.axes) if axis.axisTag in pinnedAxes } for regionIndex, region in enumerate(table.VarStore.VarRegionList.Region): # collect set of axisTags which have influence: peak != 0 regionAxes = set( axis for axis, (start, peak, end) in region.get_support(fvar.axes).items() if peak != 0 ) pinnedRegionAxes = regionAxes & pinnedAxes if not pinnedRegionAxes: # A region where none of the axes having effect are pinned newRegions.append(region) continue if len(pinnedRegionAxes) == len(regionAxes): # All the axes having effect in this region are being pinned so # remove it regionInfluenceMap.update({regionIndex: None}) else: # This region will be retained but the deltas have to be adjusted. pinnedSupport = { axis: support for axis, support in region.get_support(fvar.axes).items() if axis in pinnedRegionAxes } pinnedScalar = supportScalar(location, pinnedSupport) regionInfluenceMap.update({regionIndex: pinnedScalar}) for axis in pinnedRegionAxes: # For all pinnedRegionAxes make their influence null by setting # PeakCoord to 0. index = fvarAxisIndices[axis] region.VarRegionAxis[index].PeakCoord = 0 newRegions.append(region) table.VarStore.VarRegionList.Region = newRegions if not table.VarStore.VarRegionList.Region: # Delete table if no more regions left. del varfont[tableName] return # Start modifying deltas. if regionInfluenceMap: regionsToBeRemoved = sorted( [ regionIndex for regionIndex, scalar in regionInfluenceMap.items() if scalar is None ] ) for vardata in table.VarStore.VarData: varRegionIndexMapping = {v: k for k, v in enumerate(vardata.VarRegionIndex)} # Apply scalars for regions to be retained. for regionIndex, scalar in regionInfluenceMap.items(): if scalar is not None: varRegionIndex = varRegionIndexMapping[regionIndex] for item in vardata.Item: item[varRegionIndex] = otRound(item[varRegionIndex] * scalar) if regionsToBeRemoved: # Delete deltas (in reverse order) for regions to be removed. for regionIndex in sorted( regionsToBeRemoved, key=lambda x: varRegionIndexMapping[x], reverse=True, ): varRegionIndex = varRegionIndexMapping[regionIndex] for item in vardata.Item: del item[varRegionIndex] # Adjust VarRegionIndex since we are deleting regions. newVarRegionIndex = [] for varRegionIndex in vardata.VarRegionIndex: if varRegionIndex not in regionsToBeRemoved: newVarRegionIndex.append( varRegionIndex - bisect.bisect_left(regionsToBeRemoved, varRegionIndex) ) vardata.VarRegionIndex = newVarRegionIndex
def instantiateVariableFont(varfont, location, inplace=False): """ Generate a static instance from a variable TTFont and a dictionary defining the desired location along the variable font's axes. The location values must be specified as user-space coordinates, e.g.: {'wght': 400, 'wdth': 100} By default, a new TTFont object is returned. If ``inplace`` is True, the input varfont is modified and reduced to a static font. """ if not inplace: # make a copy to leave input varfont unmodified stream = BytesIO() varfont.save(stream) stream.seek(0) varfont = TTFont(stream) fvar = varfont['fvar'] axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes} loc = normalizeLocation(location, axes) if 'avar' in varfont: maps = varfont['avar'].segments loc = {k: piecewiseLinearMap(v, maps[k]) for k,v in loc.items()} # Quantize to F2Dot14, to avoid surprise interpolations. loc = {k:floatToFixedToFloat(v, 14) for k,v in loc.items()} # Location is normalized now log.info("Normalized location: %s", loc) if 'gvar' in varfont: log.info("Mutating glyf/gvar tables") gvar = varfont['gvar'] glyf = varfont['glyf'] # get list of glyph names in gvar sorted by component depth glyphnames = sorted( gvar.variations.keys(), key=lambda name: ( glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphname in glyphnames: variations = gvar.variations[glyphname] coordinates,_ = _GetCoordinates(varfont, glyphname) origCoords, endPts = None, None for var in variations: scalar = supportScalar(loc, var.axes) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords,control = _GetCoordinates(varfont, glyphname) endPts = control[1] if control[0] >= 1 else list(range(len(control[1]))) delta = iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar _SetCoordinates(varfont, glyphname, coordinates) else: glyf = None if 'cvar' in varfont: log.info("Mutating cvt/cvar tables") cvar = varfont['cvar'] cvt = varfont['cvt '] deltas = {} for var in cvar.variations: scalar = supportScalar(loc, var.axes) if not scalar: continue for i, c in enumerate(var.coordinates): if c is not None: deltas[i] = deltas.get(i, 0) + scalar * c for i, delta in deltas.items(): cvt[i] += otRound(delta) if 'CFF2' in varfont: log.info("Mutating CFF2 table") glyphOrder = varfont.getGlyphOrder() CFF2 = varfont['CFF2'] topDict = CFF2.cff.topDictIndex[0] vsInstancer = VarStoreInstancer(topDict.VarStore.otVarStore, fvar.axes, loc) interpolateFromDeltas = vsInstancer.interpolateFromDeltas interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas) CFF2.desubroutinize() interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder) interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc) del topDict.rawDict['VarStore'] del topDict.VarStore if 'MVAR' in varfont: log.info("Mutating MVAR table") mvar = varfont['MVAR'].table varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, loc) records = mvar.ValueRecord for rec in records: mvarTag = rec.ValueTag if mvarTag not in MVAR_ENTRIES: continue tableTag, itemName = MVAR_ENTRIES[mvarTag] delta = otRound(varStoreInstancer[rec.VarIdx]) if not delta: continue setattr(varfont[tableTag], itemName, getattr(varfont[tableTag], itemName) + delta) log.info("Mutating FeatureVariations") for tableTag in 'GSUB','GPOS': if not tableTag in varfont: continue table = varfont[tableTag].table if not hasattr(table, 'FeatureVariations'): continue variations = table.FeatureVariations for record in variations.FeatureVariationRecord: applies = True for condition in record.ConditionSet.ConditionTable: if condition.Format == 1: axisIdx = condition.AxisIndex axisTag = fvar.axes[axisIdx].axisTag Min = condition.FilterRangeMinValue Max = condition.FilterRangeMaxValue v = loc[axisTag] if not (Min <= v <= Max): applies = False else: applies = False if not applies: break if applies: assert record.FeatureTableSubstitution.Version == 0x00010000 for rec in record.FeatureTableSubstitution.SubstitutionRecord: table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = rec.Feature break del table.FeatureVariations if 'GDEF' in varfont and varfont['GDEF'].table.Version >= 0x00010003: log.info("Mutating GDEF/GPOS/GSUB tables") gdef = varfont['GDEF'].table instancer = VarStoreInstancer(gdef.VarStore, fvar.axes, loc) merger = MutatorMerger(varfont, loc) merger.mergeTables(varfont, [varfont], ['GDEF', 'GPOS']) # Downgrade GDEF. del gdef.VarStore gdef.Version = 0x00010002 if gdef.MarkGlyphSetsDef is None: del gdef.MarkGlyphSetsDef gdef.Version = 0x00010000 if not (gdef.LigCaretList or gdef.MarkAttachClassDef or gdef.GlyphClassDef or gdef.AttachList or (gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)): del varfont['GDEF'] addidef = False if glyf: for glyph in glyf.glyphs.values(): if hasattr(glyph, "program"): instructions = glyph.program.getAssembly() # If GETVARIATION opcode is used in bytecode of any glyph add IDEF addidef = any(op.startswith("GETVARIATION") for op in instructions) if addidef: break if addidef: log.info("Adding IDEF to fpgm table for GETVARIATION opcode") asm = [] if 'fpgm' in varfont: fpgm = varfont['fpgm'] asm = fpgm.program.getAssembly() else: fpgm = newTable('fpgm') fpgm.program = ttProgram.Program() varfont['fpgm'] = fpgm asm.append("PUSHB[000] 145") asm.append("IDEF[ ]") args = [str(len(loc))] for a in fvar.axes: args.append(str(floatToFixed(loc[a.axisTag], 14))) asm.append("NPUSHW[ ] " + ' '.join(args)) asm.append("ENDF[ ]") fpgm.program.fromAssembly(asm) # Change maxp attributes as IDEF is added if 'maxp' in varfont: maxp = varfont['maxp'] if hasattr(maxp, "maxInstructionDefs"): maxp.maxInstructionDefs += 1 else: setattr(maxp, "maxInstructionDefs", 1) if hasattr(maxp, "maxStackElements"): maxp.maxStackElements = max(len(loc), maxp.maxStackElements) else: setattr(maxp, "maxInstructionDefs", len(loc)) if 'name' in varfont: log.info("Pruning name table") exclude = {a.axisNameID for a in fvar.axes} for i in fvar.instances: exclude.add(i.subfamilyNameID) exclude.add(i.postscriptNameID) if 'ltag' in varfont: # Drop the whole 'ltag' table if all its language tags are referenced by # name records to be pruned. # TODO: prune unused ltag tags and re-enumerate langIDs accordingly excludedUnicodeLangIDs = [ n.langID for n in varfont['name'].names if n.nameID in exclude and n.platformID == 0 and n.langID != 0xFFFF ] if set(excludedUnicodeLangIDs) == set(range(len((varfont['ltag'].tags)))): del varfont['ltag'] varfont['name'].names[:] = [ n for n in varfont['name'].names if n.nameID not in exclude ] if "wght" in location and "OS/2" in varfont: varfont["OS/2"].usWeightClass = otRound( max(1, min(location["wght"], 1000)) ) if "wdth" in location: wdth = location["wdth"] for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()): if wdth < percent: varfont["OS/2"].usWidthClass = widthClass break else: varfont["OS/2"].usWidthClass = 9 if "slnt" in location and "post" in varfont: varfont["post"].italicAngle = max(-90, min(location["slnt"], 90)) log.info("Removing variable tables") for tag in ('avar','cvar','fvar','gvar','HVAR','MVAR','VVAR','STAT'): if tag in varfont: del varfont[tag] return varfont
def instantiateVariableFont(varfont, location, inplace=False): """ Generate a static instance from a variable TTFont and a dictionary defining the desired location along the variable font's axes. The location values must be specified as user-space coordinates, e.g.: {'wght': 400, 'wdth': 100} By default, a new TTFont object is returned. If ``inplace`` is True, the input varfont is modified and reduced to a static font. """ if not inplace: # make a copy to leave input varfont unmodified stream = BytesIO() varfont.save(stream) stream.seek(0) varfont = TTFont(stream) fvar = varfont['fvar'] axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes} loc = normalizeLocation(location, axes) if 'avar' in varfont: maps = varfont['avar'].segments loc = {k: piecewiseLinearMap(v, maps[k]) for k,v in loc.items()} # Quantize to F2Dot14, to avoid surprise interpolations. loc = {k:floatToFixedToFloat(v, 14) for k,v in loc.items()} # Location is normalized now log.info("Normalized location: %s", loc) if 'gvar' in varfont: log.info("Mutating glyf/gvar tables") gvar = varfont['gvar'] glyf = varfont['glyf'] # get list of glyph names in gvar sorted by component depth glyphnames = sorted( gvar.variations.keys(), key=lambda name: ( glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphname in glyphnames: variations = gvar.variations[glyphname] coordinates,_ = _GetCoordinates(varfont, glyphname) origCoords, endPts = None, None for var in variations: scalar = supportScalar(loc, var.axes) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords,control = _GetCoordinates(varfont, glyphname) endPts = control[1] if control[0] >= 1 else list(range(len(control[1]))) delta = iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar _SetCoordinates(varfont, glyphname, coordinates) else: glyf = None if 'cvar' in varfont: log.info("Mutating cvt/cvar tables") cvar = varfont['cvar'] cvt = varfont['cvt '] deltas = {} for var in cvar.variations: scalar = supportScalar(loc, var.axes) if not scalar: continue for i, c in enumerate(var.coordinates): if c is not None: deltas[i] = deltas.get(i, 0) + scalar * c for i, delta in deltas.items(): cvt[i] += otRound(delta) if 'CFF2' in varfont: log.info("Mutating CFF2 table") glyphOrder = varfont.getGlyphOrder() CFF2 = varfont['CFF2'] topDict = CFF2.cff.topDictIndex[0] vsInstancer = VarStoreInstancer(topDict.VarStore.otVarStore, fvar.axes, loc) interpolateFromDeltas = vsInstancer.interpolateFromDeltas interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas) CFF2.desubroutinize(varfont) interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder) interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc) del topDict.rawDict['VarStore'] del topDict.VarStore if 'MVAR' in varfont: log.info("Mutating MVAR table") mvar = varfont['MVAR'].table varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, loc) records = mvar.ValueRecord for rec in records: mvarTag = rec.ValueTag if mvarTag not in MVAR_ENTRIES: continue tableTag, itemName = MVAR_ENTRIES[mvarTag] delta = otRound(varStoreInstancer[rec.VarIdx]) if not delta: continue setattr(varfont[tableTag], itemName, getattr(varfont[tableTag], itemName) + delta) log.info("Mutating FeatureVariations") for tableTag in 'GSUB','GPOS': if not tableTag in varfont: continue table = varfont[tableTag].table if not hasattr(table, 'FeatureVariations'): continue variations = table.FeatureVariations for record in variations.FeatureVariationRecord: applies = True for condition in record.ConditionSet.ConditionTable: if condition.Format == 1: axisIdx = condition.AxisIndex axisTag = fvar.axes[axisIdx].axisTag Min = condition.FilterRangeMinValue Max = condition.FilterRangeMaxValue v = loc[axisTag] if not (Min <= v <= Max): applies = False else: applies = False if not applies: break if applies: assert record.FeatureTableSubstitution.Version == 0x00010000 for rec in record.FeatureTableSubstitution.SubstitutionRecord: table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = rec.Feature break del table.FeatureVariations if 'GDEF' in varfont and varfont['GDEF'].table.Version >= 0x00010003: log.info("Mutating GDEF/GPOS/GSUB tables") gdef = varfont['GDEF'].table instancer = VarStoreInstancer(gdef.VarStore, fvar.axes, loc) merger = MutatorMerger(varfont, loc) merger.mergeTables(varfont, [varfont], ['GDEF', 'GPOS']) # Downgrade GDEF. del gdef.VarStore gdef.Version = 0x00010002 if gdef.MarkGlyphSetsDef is None: del gdef.MarkGlyphSetsDef gdef.Version = 0x00010000 if not (gdef.LigCaretList or gdef.MarkAttachClassDef or gdef.GlyphClassDef or gdef.AttachList or (gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)): del varfont['GDEF'] addidef = False if glyf: for glyph in glyf.glyphs.values(): if hasattr(glyph, "program"): instructions = glyph.program.getAssembly() # If GETVARIATION opcode is used in bytecode of any glyph add IDEF addidef = any(op.startswith("GETVARIATION") for op in instructions) if addidef: break if addidef: log.info("Adding IDEF to fpgm table for GETVARIATION opcode") asm = [] if 'fpgm' in varfont: fpgm = varfont['fpgm'] asm = fpgm.program.getAssembly() else: fpgm = newTable('fpgm') fpgm.program = ttProgram.Program() varfont['fpgm'] = fpgm asm.append("PUSHB[000] 145") asm.append("IDEF[ ]") args = [str(len(loc))] for a in fvar.axes: args.append(str(floatToFixed(loc[a.axisTag], 14))) asm.append("NPUSHW[ ] " + ' '.join(args)) asm.append("ENDF[ ]") fpgm.program.fromAssembly(asm) # Change maxp attributes as IDEF is added if 'maxp' in varfont: maxp = varfont['maxp'] if hasattr(maxp, "maxInstructionDefs"): maxp.maxInstructionDefs += 1 else: setattr(maxp, "maxInstructionDefs", 1) if hasattr(maxp, "maxStackElements"): maxp.maxStackElements = max(len(loc), maxp.maxStackElements) else: setattr(maxp, "maxInstructionDefs", len(loc)) if 'name' in varfont: log.info("Pruning name table") exclude = {a.axisNameID for a in fvar.axes} for i in fvar.instances: exclude.add(i.subfamilyNameID) exclude.add(i.postscriptNameID) varfont['name'].names[:] = [ n for n in varfont['name'].names if n.nameID not in exclude ] if "wght" in location and "OS/2" in varfont: varfont["OS/2"].usWeightClass = otRound( max(1, min(location["wght"], 1000)) ) if "wdth" in location: wdth = location["wdth"] for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()): if wdth < percent: varfont["OS/2"].usWidthClass = widthClass break else: varfont["OS/2"].usWidthClass = 9 if "slnt" in location and "post" in varfont: varfont["post"].italicAngle = max(-90, min(location["slnt"], 90)) log.info("Removing variable tables") for tag in ('avar','cvar','fvar','gvar','HVAR','MVAR','VVAR','STAT'): if tag in varfont: del varfont[tag] return varfont
def test_supportScalar(): assert supportScalar({}, {}) == 1.0 assert supportScalar({'wght': .2}, {}) == 1.0 assert supportScalar({'wght': .2}, {'wght': (0, 2, 3)}) == 0.1 assert supportScalar({'wght': 2.5}, {'wght': (0, 2, 4)}) == 0.75
def main(args=None): if args is None: import sys args = sys.argv[1:] varfilename = args[0] locargs = args[1:] outfile = os.path.splitext(varfilename)[0] + '-instance.ttf' loc = {} for arg in locargs: tag, val = arg.split('=') assert len(tag) <= 4 loc[tag.ljust(4)] = float(val) print("Location:", loc) print("Loading variable font") varfont = TTFont(varfilename) fvar = varfont['fvar'] axes = { a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in fvar.axes } # TODO Apply avar # TODO Round to F2Dot14? loc = normalizeLocation(loc, axes) # Location is normalized now print("Normalized location:", loc) gvar = varfont['gvar'] glyf = varfont['glyf'] # get list of glyph names in gvar sorted by component depth glyphnames = sorted( gvar.variations.keys(), key=lambda name: (glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphname in glyphnames: variations = gvar.variations[glyphname] coordinates, _ = _GetCoordinates(varfont, glyphname) origCoords, endPts = None, None for var in variations: scalar = supportScalar(loc, var.axes, ot=True) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords, control = _GetCoordinates(varfont, glyphname) endPts = control[1] if control[0] >= 1 else list( range(len(control[1]))) delta = _iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar _SetCoordinates(varfont, glyphname, coordinates) # Interpolate cvt if 'cvar' in varfont: cvar = varfont['cvar'] cvt = varfont['cvt '] deltas = {} for var in cvar.variations: scalar = supportScalar(loc, var.axes) if not scalar: continue for i, c in enumerate(var.coordinates): if c is not None: deltas[i] = deltas.get(i, 0) + scalar * c for i, delta in deltas.items(): cvt[i] += int(round(delta)) print("Removing variable tables") for tag in ('avar', 'cvar', 'fvar', 'gvar', 'HVAR', 'MVAR', 'VVAR', 'STAT'): if tag in varfont: del varfont[tag] print("Saving instance font", outfile) varfont.save(outfile)
def makeInstance(pathOrVarFont, location, dstPath=None, normalize=True, cached=True, lazy=True, kerning=None): """Instantiate an instance of a variable font at the specified location. Keyword arguments: - varfilename -- a variable font file path - location -- a dictionary of axis tag and value {"wght": 0.75, "wdth": -0.5} >>> vf = findFont('RobotoDelta-VF') >>> print(vf) <Font RobotoDelta-VF> >>> print(len(vf)) 188 >>> instance = makeInstance(vf.path, dict(opsz=8), cached=False) >>> instance <Font RobotoDelta-VF-opsz8> >>> len(instance) 241 >>> len(instance['H'].points) 12 >>> instance['Egrave'] <PageBot Glyph Egrave Pts:0/Cnt:0/Cmp:2> >>> len(instance['Egrave'].components) 2 """ # make a custom file name from the location e.g. # VariableFont-wghtXXX-wdthXXX.ttf instanceName = "" if isinstance(pathOrVarFont, Font): pathOrVarFont = pathOrVarFont.path varFont = Font(pathOrVarFont, lazy=lazy) ttFont = varFont.ttFont for k, v in sorted(location.items()): # TODO better way to normalize the location name to (0, 1000) v = min(v, 1000) v = max(v, 0) instanceName += "-%s%s" % (k, v) if dstPath is None: targetFileName = '.'.join(varFont.path.split('/')[-1].split('.') [:-1]) + instanceName + '.ttf' targetDirectory = getInstancePath() if not targetDirectory.endswith('/'): targetDirectory += '/' if not os.path.exists(targetDirectory): os.makedirs(targetDirectory) dstPath = targetDirectory + targetFileName # Instance does not exist as file. Create it. if not cached or not os.path.exists(dstPath): # Set the instance name IDs in the name table platforms = ((1, 0, 0), (3, 1, 0x409)) # Macintosh and Windows for platformID, platEncID, langID in platforms: familyName = ttFont['name'].getName(1, platformID, platEncID, langID) # 1 Font Family name if not familyName: continue familyName = familyName.toUnicode() # NameRecord to unicode string styleName = unicode( instanceName) # TODO make sure this works in any case fullFontName = " ".join([familyName, styleName]) postscriptName = fullFontName.replace(" ", "-") ttFont['name'].setName(styleName, 2, platformID, platEncID, langID) # 2 Font Subfamily name ttFont['name'].setName(fullFontName, 4, platformID, platEncID, langID) # 4 Full font name ttFont['name'].setName(postscriptName, 6, platformID, platEncID, langID) # 6 Postscript name for the font # Other important name IDs # 3 Unique font identifier (e.g. Version 0.000;NONE;Promise Bold Regular) # 25 Variables PostScript Name Prefix fvar = ttFont['fvar'] axes = { a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in fvar.axes } # TODO Apply avar # TODO Round to F2Dot14? loc = normalizeLocation(location, axes) # Location is normalized now #print("Normalized location:", loc) gvar = ttFont['gvar'] glyf = ttFont['glyf'] # get list of glyph names in gvar sorted by component depth glyphNames = sorted( gvar.variations.keys(), key=lambda name: (glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphName in glyphNames: variations = gvar.variations[glyphName] coordinates, _ = _GetCoordinates(ttFont, glyphName) origCoords, endPts = None, None for var in variations: scalar = supportScalar(loc, var.axes) #, ot=True) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords, control = _GetCoordinates( ttFont, glyphName) endPts = control[1] if control[0] >= 1 else list( range(len(control[1]))) delta = iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar _SetCoordinates(ttFont, glyphName, coordinates) # Interpolate cvt if 'cvar' in ttFont: cvar = ttFont['cvar'] cvt = ttFont['cvt '] deltas = {} for var in cvar.variations: scalar = supportScalar(loc, var.axes) if not scalar: continue for i, c in enumerate(var.coordinates): if c is not None: deltas[i] = deltas.get(i, 0) + scalar * c for i, delta in deltas.items(): cvt[i] += int(round(delta)) #print("Removing variable tables") for tag in ('avar', 'cvar', 'fvar', 'gvar', 'HVAR', 'MVAR', 'VVAR', 'STAT'): if tag in ttFont: del ttFont[tag] if kerning is not None: for pair, value in kerning.items(): varFont.kerning[pair] = value #print("Saving instance font", outFile) varFont.save(dstPath) # Answer instance. return Font(dstPath, lazy=lazy)
def main(args=None): if args is None: import sys args = sys.argv[1:] varfilename = args[0] locargs = args[1:] outfile = os.path.splitext(varfilename)[0] + '-instance.ttf' loc = {} for arg in locargs: tag,val = arg.split('=') assert len(tag) <= 4 loc[tag.ljust(4)] = float(val) print("Location:", loc) print("Loading variable font") varfont = TTFont(varfilename) fvar = varfont['fvar'] axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes} # TODO Apply avar # TODO Round to F2Dot14? loc = normalizeLocation(loc, axes) # Location is normalized now print("Normalized location:", loc) gvar = varfont['gvar'] glyf = varfont['glyf'] # get list of glyph names in gvar sorted by component depth glyphnames = sorted( gvar.variations.keys(), key=lambda name: ( glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphname in glyphnames: variations = gvar.variations[glyphname] coordinates,_ = _GetCoordinates(varfont, glyphname) origCoords, endPts = None, None for var in variations: scalar = supportScalar(loc, var.axes) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords,control = _GetCoordinates(varfont, glyphname) endPts = control[1] if control[0] >= 1 else list(range(len(control[1]))) delta = _iup_delta(delta, origCoords, endPts) # TODO Do IUP / handle None items coordinates += GlyphCoordinates(delta) * scalar _SetCoordinates(varfont, glyphname, coordinates) print("Removing variable tables") for tag in ('avar','cvar','fvar','gvar','HVAR','MVAR','VVAR','STAT'): if tag in varfont: del varfont[tag] print("Saving instance font", outfile) varfont.save(outfile)
def instantiateVariableFont(varfont, location, inplace=False): """ Generate a static instance from a variable TTFont and a dictionary defining the desired location along the variable font's axes. The location values must be specified as user-space coordinates, e.g.: {'wght': 400, 'wdth': 100} By default, a new TTFont object is returned. If ``inplace`` is True, the input varfont is modified and reduced to a static font. """ if not inplace: # make a copy to leave input varfont unmodified stream = BytesIO() varfont.save(stream) stream.seek(0) varfont = TTFont(stream) fvar = varfont['fvar'] axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes} loc = normalizeLocation(location, axes) if 'avar' in varfont: maps = varfont['avar'].segments loc = {k:_DesignspaceAxis._map(v, maps[k]) for k,v in loc.items()} # Quantize to F2Dot14, to avoid surprise interpolations. loc = {k:floatToFixedToFloat(v, 14) for k,v in loc.items()} # Location is normalized now log.info("Normalized location: %s", loc) log.info("Mutating glyf/gvar tables") gvar = varfont['gvar'] glyf = varfont['glyf'] # get list of glyph names in gvar sorted by component depth glyphnames = sorted( gvar.variations.keys(), key=lambda name: ( glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphname in glyphnames: variations = gvar.variations[glyphname] coordinates,_ = _GetCoordinates(varfont, glyphname) origCoords, endPts = None, None for var in variations: scalar = supportScalar(loc, var.axes) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords,control = _GetCoordinates(varfont, glyphname) endPts = control[1] if control[0] >= 1 else list(range(len(control[1]))) delta = iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar _SetCoordinates(varfont, glyphname, coordinates) if 'cvar' in varfont: log.info("Mutating cvt/cvar tables") cvar = varfont['cvar'] cvt = varfont['cvt '] deltas = {} for var in cvar.variations: scalar = supportScalar(loc, var.axes) if not scalar: continue for i, c in enumerate(var.coordinates): if c is not None: deltas[i] = deltas.get(i, 0) + scalar * c for i, delta in deltas.items(): cvt[i] += otRound(delta) if 'MVAR' in varfont: log.info("Mutating MVAR table") mvar = varfont['MVAR'].table varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, loc) records = mvar.ValueRecord for rec in records: mvarTag = rec.ValueTag if mvarTag not in MVAR_ENTRIES: continue tableTag, itemName = MVAR_ENTRIES[mvarTag] delta = otRound(varStoreInstancer[rec.VarIdx]) if not delta: continue setattr(varfont[tableTag], itemName, getattr(varfont[tableTag], itemName) + delta) if 'GDEF' in varfont: log.info("Mutating GDEF/GPOS/GSUB tables") merger = MutatorMerger(varfont, loc) log.info("Building interpolated tables") merger.instantiate() if 'name' in varfont: log.info("Pruning name table") exclude = {a.axisNameID for a in fvar.axes} for i in fvar.instances: exclude.add(i.subfamilyNameID) exclude.add(i.postscriptNameID) varfont['name'].names[:] = [ n for n in varfont['name'].names if n.nameID not in exclude ] if "wght" in location and "OS/2" in varfont: varfont["OS/2"].usWeightClass = otRound( max(1, min(location["wght"], 1000)) ) if "wdth" in location: wdth = location["wdth"] for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()): if wdth < percent: varfont["OS/2"].usWidthClass = widthClass break else: varfont["OS/2"].usWidthClass = 9 if "slnt" in location and "post" in varfont: varfont["post"].italicAngle = max(-90, min(location["slnt"], 90)) log.info("Removing variable tables") for tag in ('avar','cvar','fvar','gvar','HVAR','MVAR','VVAR','STAT'): if tag in varfont: del varfont[tag] return varfont
def generateInstance(variableFontPath, location, targetDirectory, normalize=True): u""" Instantiate an instance of a variable font at the specified location. Keyword arguments: varfilename -- a variable font file path location -- a dictionary of axis tag and value {"wght": 0.75, "wdth": -0.5} """ # make a custom file name from the location e.g. VariableFont-wghtXXX-wdthXXX.ttf instanceName = "" for k, v in sorted(location.items()): # TODO better way to normalize the location name to (0, 1000) v = min(v, 1000) v = max(v, 0) instanceName += "-%s%s" % (k, v) targetFileName = '.'.join(variableFontPath.split('/')[-1].split('.')[:-1]) + instanceName + '.ttf' if not targetDirectory.endswith('/'): targetDirectory += '/' if not os.path.exists(targetDirectory): os.makedirs(targetDirectory) outFile = targetDirectory + targetFileName if not os.path.exists(outFile): # Instance does not exist as file. Create it. # print("Loading GX font") varFont = TTFont(variableFontPath) # Set the instance name IDs in the name table platforms=((1, 0, 0), (3, 1, 0x409)) # Macintosh and Windows for platformID, platEncID, langID in platforms: familyName = varFont['name'].getName(1, platformID, platEncID, langID) # 1 Font Family name if not familyName: continue familyName = familyName.toUnicode() # NameRecord to unicode string styleName = unicode(instanceName) # TODO make sure this works in any case fullFontName = " ".join([familyName, styleName]) postscriptName = fullFontName.replace(" ", "-") varFont['name'].setName(styleName, 2, platformID, platEncID, langID) # 2 Font Subfamily name varFont['name'].setName(fullFontName, 4, platformID, platEncID, langID) # 4 Full font name varFont['name'].setName(postscriptName, 6, platformID, platEncID, langID) # 6 Postscript name for the font # Other important name IDs # 3 Unique font identifier (e.g. Version 0.000;NONE;Promise Bold Regular) # 25 Variables PostScript Name Prefix fvar = varFont['fvar'] axes = {a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in fvar.axes} # TODO Round to F2Dot14? if normalize: normalizedLoc = normalizeLocation(location, axes) else: normalizedLoc = location # Location is normalized now if DEBUG: print("Normalized location:", varFileName, normalizedLoc) gvar = varFont['gvar'] for glyphName, variations in gvar.variations.items(): coordinates, _ = _GetCoordinates(varFont, glyphName) for var in variations: scalar = supportScalar(normalizedLoc, var.axes) if not scalar: continue # TODO Do IUP / handle None items varCoords = [] for coord in var.coordinates: # TODO temp hack to avoid NoneType if coord is None: varCoords.append((0, 0)) else: varCoords.append(coord) coordinates += GlyphCoordinates(varCoords) * scalar # coordinates += GlyphCoordinates(var.coordinates) * scalar _SetCoordinates(varFont, glyphName, coordinates) # print("Removing GX tables") for tag in ('fvar', 'avar', 'gvar'): if tag in varFont: del varFont[tag] # Fix leading bug in drawbot by setting lineGap to 0 varFont['hhea'].lineGap = 0 if DEBUG: print("Saving instance font", outFile) varFont.save(outFile) # Installing the font in DrawBot. Answer font name and path. return installFont(outFile), outFile
def test_supportScalar(): assert supportScalar({}, {}) == 1.0 assert supportScalar({"wght": 0.2}, {}) == 1.0 assert supportScalar({"wght": 0.2}, {"wght": (0, 2, 3)}) == 0.1 assert supportScalar({"wght": 2.5}, {"wght": (0, 2, 4)}) == 0.75
def test_supportScalar(): assert supportScalar({}, {}) == 1.0 assert supportScalar({'wght':.2}, {}) == 1.0 assert supportScalar({'wght':.2}, {'wght':(0,2,3)}) == 0.1 assert supportScalar({'wght':2.5}, {'wght':(0,2,4)}) == 0.75
def generateInstance(variableFontPath, location, targetDirectory, normalize=True, cached=True, lazy=True): """ D E P R E C A T E D Use pagebot.fonttoolbox.objects.font.instantiateVariableFont instead (calling fontTools) Instantiate an instance of a variable font at the specified location. Keyword arguments: varfilename -- a variable font file path location -- a dictionary of axis tag and value {"wght": 0.75, "wdth": -0.5} """ # make a custom file name from the location e.g. VariableFont-wghtXXX-wdthXXX.ttf instanceName = "" for k, v in sorted(location.items()): # TODO better way to normalize the location name to (0, 1000) v = min(v, 1000) v = max(v, 0) instanceName += "-%s%s" % (k, v) targetFileName = '.'.join(variableFontPath.split('/')[-1].split('.') [:-1]) + instanceName + '.ttf' if not targetDirectory.endswith('/'): targetDirectory += '/' if not os.path.exists(targetDirectory): os.makedirs(targetDirectory) outFile = targetDirectory + targetFileName if not cached or not os.path.exists(outFile): # Instance does not exist as file. Create it. # print("Loading GX font") varfont = TTFont(variableFontPath, lazy=lazy) # Set the instance name IDs in the name table platforms = ((1, 0, 0), (3, 1, 0x409)) # Macintosh and Windows for platformID, platEncID, langID in platforms: familyName = varfont['name'].getName(1, platformID, platEncID, langID) # 1 Font Family name if not familyName: continue familyName = familyName.toUnicode() # NameRecord to unicode string styleName = unicode( instanceName) # TODO make sure this works in any case fullFontName = " ".join([familyName, styleName]) postscriptName = fullFontName.replace(" ", "-") varfont['name'].setName(styleName, 2, platformID, platEncID, langID) # 2 Font Subfamily name varfont['name'].setName(fullFontName, 4, platformID, platEncID, langID) # 4 Full font name varfont['name'].setName(postscriptName, 6, platformID, platEncID, langID) # 6 Postscript name for the font # Other important name IDs # 3 Unique font identifier (e.g. Version 0.000;NONE;Promise Bold Regular) # 25 Variables PostScript Name Prefix fvar = varfont['fvar'] axes = { a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in fvar.axes } # TODO Apply avar # TODO Round to F2Dot14? loc = normalizeLocation(location, axes) # Location is normalized now #print("Normalized location:", loc) gvar = varfont['gvar'] glyf = varfont['glyf'] # get list of glyph names in gvar sorted by component depth glyphnames = sorted( gvar.variations.keys(), key=lambda name: (glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphname in glyphnames: variations = gvar.variations[glyphname] coordinates, _ = _GetCoordinates(varfont, glyphname) origCoords, endPts = None, None for var in variations: scalar = supportScalar(loc, var.axes) #, ot=True) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords, control = _GetCoordinates( varfont, glyphname) endPts = control[1] if control[0] >= 1 else list( range(len(control[1]))) delta = iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar _SetCoordinates(varfont, glyphname, coordinates) # Interpolate cvt if 'cvar' in varfont: cvar = varfont['cvar'] cvt = varfont['cvt '] deltas = {} for var in cvar.variations: scalar = supportScalar(loc, var.axes) if not scalar: continue for i, c in enumerate(var.coordinates): if c is not None: deltas[i] = deltas.get(i, 0) + scalar * c for i, delta in deltas.items(): cvt[i] += int(round(delta)) #print("Removing variable tables") for tag in ('avar', 'cvar', 'fvar', 'gvar', 'HVAR', 'MVAR', 'VVAR', 'STAT'): if tag in varfont: del varfont[tag] #print("Saving instance font", outFile) varfont.save(outFile) # Answer the font name path. return outFile