# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from numpy import array, append import copy import json from robofab.objects.objectsRF import RPoint from robofab.world import OpenFont from decomposeGlyph import decomposeGlyph class FFont: "Font wrapper for floating point operations" def __init__(self,f=None): self.glyphs = {} self.hstems = [] self.vstems = [] self.kerning = {} if isinstance(f,FFont): #self.glyphs = [g.copy() for g in f.glyphs] for key,g in f.glyphs.iteritems(): self.glyphs[key] = g.copy() self.hstems = list(f.hstems) self.vstems = list(f.vstems) elif f != None: self.copyFromFont(f) def copyFromFont(self, f): for g in f: self.glyphs[g.name] = FGlyph(g) self.hstems = [s for s in f.info.postscriptStemSnapH] self.vstems = [s for s in f.info.postscriptStemSnapV] self.kerning = f.kerning.asDict() def copyToFont(self, f): for g in f: try: gF = self.glyphs[g.name] gF.copyToGlyph(g) except: print "Copy to glyph failed for" + g.name f.info.postscriptStemSnapH = self.hstems f.info.postscriptStemSnapV = self.vstems for pair in self.kerning: f.kerning[pair] = self.kerning[pair] def getGlyph(self, gname): try: return self.glyphs[gname] except: return None def setGlyph(self, gname, glyph): self.glyphs[gname] = glyph def addDiff(self,b,c): newFont = FFont(self) for key,g in newFont.glyphs.iteritems(): gB = b.getGlyph(key) gC = c.getGlyph(key) try: newFont.glyphs[key] = g.addDiff(gB,gC) except: print "Add diff failed for '%s'" %key return newFont class FGlyph: "provides a temporary floating point compatible glyph data structure" def __init__(self, g=None): self.contours = [] self.width = 0. self.components = [] self.anchors = [] if g != None: self.copyFromGlyph(g) def copyFromGlyph(self,g): self.name = g.name valuesX = [] valuesY = [] self.width = len(valuesX) valuesX.append(g.width) for c in g.components: self.components.append((len(valuesX), len(valuesY))) valuesX.append(c.scale[0]) valuesY.append(c.scale[1]) valuesX.append(c.offset[0]) valuesY.append(c.offset[1]) for a in g.anchors: self.anchors.append((len(valuesX), len(valuesY))) valuesX.append(a.x) valuesY.append(a.y) for i in range(len(g)): self.contours.append([]) for j in range (len(g[i].points)): self.contours[i].append((len(valuesX), len(valuesY))) valuesX.append(g[i].points[j].x) valuesY.append(g[i].points[j].y) self.dataX = array(valuesX) self.dataY = array(valuesY) def copyToGlyph(self,g): g.width = self._derefX(self.width) if len(g.components) == len(self.components): for i in range(len(self.components)): g.components[i].scale[0] = self._derefX(self.components[i][0] + 0) g.components[i].scale[1] = self._derefY(self.components[i][1] + 0) g.components[i].offset[0] = self._derefX(self.components[i][0] + 1) g.components[i].offset[1] = self._derefY(self.components[i][1] + 1) if len(g.anchors) == len(self.anchors): for i in range(len(self.anchors)): g.anchors[i].x = self._derefX( self.anchors[i][0]) g.anchors[i].y = self._derefY( self.anchors[i][1]) for i in range(len(g)) : for j in range (len(g[i].points)): g[i].points[j].x = self._derefX(self.contours[i][j][0]) g[i].points[j].y = self._derefY(self.contours[i][j][1]) def isCompatible(self, g): return (len(self.dataX) == len(g.dataX) and len(self.dataY) == len(g.dataY) and len(g.contours) == len(self.contours)) def __add__(self,g): if self.isCompatible(g): newGlyph = self.copy() newGlyph.dataX = self.dataX + g.dataX newGlyph.dataY = self.dataY + g.dataY return newGlyph else: print "Add failed for '%s'" %(self.name) raise Exception def __sub__(self,g): if self.isCompatible(g): newGlyph = self.copy() newGlyph.dataX = self.dataX - g.dataX newGlyph.dataY = self.dataY - g.dataY return newGlyph else: print "Subtract failed for '%s'" %(self.name) raise Exception def __mul__(self,scalar): newGlyph = self.copy() newGlyph.dataX = self.dataX * scalar newGlyph.dataY = self.dataY * scalar return newGlyph def scaleX(self,scalar): newGlyph = self.copy() if len(self.dataX) > 0: newGlyph.dataX = self.dataX * scalar for i in range(len(newGlyph.components)): newGlyph.dataX[newGlyph.components[i][0]] = self.dataX[newGlyph.components[i][0]] return newGlyph def shift(self,ammount): newGlyph = self.copy() newGlyph.dataX = self.dataX + ammount for i in range(len(newGlyph.components)): newGlyph.dataX[newGlyph.components[i][0]] = self.dataX[newGlyph.components[i][0]] return newGlyph def interp(self, g, v): gF = self.copy() if not self.isCompatible(g): print "Interpolate failed for '%s'; outlines incompatible" %(self.name) raise Exception gF.dataX += (g.dataX - gF.dataX) * v.x gF.dataY += (g.dataY - gF.dataY) * v.y return gF def copy(self): ng = FGlyph() ng.contours = list(self.contours) ng.width = self.width ng.components = list(self.components) ng.anchors = list(self.anchors) ng.dataX = self.dataX.copy() ng.dataY = self.dataY.copy() ng.name = self.name return ng def _derefX(self,id): return int(round(self.dataX[id])) def _derefY(self,id): return int(round(self.dataY[id])) def addDiff(self,gB,gC): newGlyph = self + (gB - gC) return newGlyph class Master: def __init__(self, font=None, v=0, kernlist=None, overlay=None, anchorPath=None): if isinstance(font, FFont): self.font = None self.ffont = font elif isinstance(font,str): self.openFont(font,overlay, anchorPath) elif isinstance(font,Mix): self.font = font else: self.font = font self.ffont = FFont(font) if isinstance(v,float) or isinstance(v,int): self.v = RPoint(v, v) else: self.v = v if kernlist != None: kerns = [i.strip().split() for i in open(kernlist).readlines()] self.kernlist = [{'left':k[0], 'right':k[1], 'value': k[2]} for k in kerns if not k[0].startswith("#") and not k[0] == ""] #TODO implement class based kerning / external kerning file def openFont(self, path, overlayPath=None, anchorPath=None): self.font = OpenFont(path) for g in self.font: size = len(g) csize = len(g.components) if (size > 0 and csize > 0): decomposeGlyph(g) if overlayPath != None: overlayFont = OpenFont(overlayPath) font = self.font for overlayGlyph in overlayFont: font.insertGlyph(overlayGlyph) # work around a bug with vfb2ufo in which anchors are dropped from # glyphs containing components and no contours. "anchorPath" should # point to the output of src/v2/get_dropped_anchors.py if anchorPath: anchorData = json.load(open(anchorPath)) for glyphName, anchors in anchorData.items(): # another bug: some entire glyphs are dropped during conversion. # example: gbar_uni1ABE try: glyph = self.font[glyphName] except KeyError: continue # another bug: some glyphs are decomposed during conversion, in # which case they unexpectedly don't drop anchors. # examples: uni04BA, Gbar (partially decomposed) if glyph.anchors: continue for name, (x, y) in anchors.items(): glyph.appendAnchor(str(name), (x, y)) self.ffont = FFont(self.font) class Mix: def __init__(self,masters,v): self.masters = masters if isinstance(v,float) or isinstance(v,int): self.v = RPoint(v,v) else: self.v = v def getFGlyph(self, master, gname): if isinstance(master.font, Mix): return font.mixGlyphs(gname) return master.ffont.getGlyph(gname) def getGlyphMasters(self,gname): masters = self.masters if len(masters) <= 2: return self.getFGlyph(masters[0], gname), self.getFGlyph(masters[-1], gname) def generateFFont(self): ffont = FFont(self.masters[0].ffont) for key,g in ffont.glyphs.iteritems(): ffont.glyphs[key] = self.mixGlyphs(key) ffont.kerning = self.mixKerns() return ffont def generateFont(self, baseFont): newFont = baseFont.copy() #self.mixStems(newFont) todo _ fix stems code for g in newFont: gF = self.mixGlyphs(g.name) if gF == None: g.mark = True else: # FIX THIS #print gF.name, g.name, len(gF.nodes),len(g.nodes),len(gF.components),len(g.components) try: gF.copyToGlyph(g) except: "Nodes incompatible" newFont.kerning.clear() newFont.kerning.update(self.mixKerns() or {}) return newFont def mixGlyphs(self,gname): gA,gB = self.getGlyphMasters(gname) try: return gA.interp(gB,self.v) except: print "mixglyph failed for %s" %(gname) if gA != None: return gA.copy() def getKerning(self, master): if isinstance(master.font, Mix): return master.font.mixKerns() return master.ffont.kerning def mixKerns(self): masters = self.masters kA, kB = self.getKerning(masters[0]), self.getKerning(masters[-1]) return interpolateKerns(kA, kB, self.v) def narrowFLGlyph(g, gThin, factor=.75): gF = FGlyph(g) if not isinstance(gThin,FGlyph): gThin = FGlyph(gThin) gCondensed = gThin.scaleX(factor) try: gNarrow = gF + (gCondensed - gThin) gNarrow.copyToGlyph(g) except: print "No dice for: " + g.name def interpolate(a,b,v,e=0): if e == 0: return a+(b-a)*v qe = (b-a)*v*v*v + a #cubic easing le = a+(b-a)*v # linear easing return le + (qe-le) * e def interpolateKerns(kA, kB, v): kerns = {} for pair in kA.keys(): matchedKern = kB.get(pair) # if matchedkern == None: # matchedkern = Kern(kA) # matchedkern.value = 0 if matchedKern != None: kerns[pair] = interpolate(kA[pair], matchedKern, v.x) return kerns