1# Copyright 2015 Google Inc. All Rights Reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15 16from numpy import array, append 17import copy 18import json 19from robofab.objects.objectsRF import RPoint 20from robofab.world import OpenFont 21from decomposeGlyph import decomposeGlyph 22 23 24class FFont: 25 "Font wrapper for floating point operations" 26 27 def __init__(self,f=None): 28 self.glyphs = {} 29 self.hstems = [] 30 self.vstems = [] 31 self.kerning = {} 32 if isinstance(f,FFont): 33 #self.glyphs = [g.copy() for g in f.glyphs] 34 for key,g in f.glyphs.iteritems(): 35 self.glyphs[key] = g.copy() 36 self.hstems = list(f.hstems) 37 self.vstems = list(f.vstems) 38 self.kerning = dict(f.kerning) 39 elif f != None: 40 self.copyFromFont(f) 41 42 def copyFromFont(self, f): 43 for g in f: 44 self.glyphs[g.name] = FGlyph(g) 45 self.hstems = [s for s in f.info.postscriptStemSnapH] 46 self.vstems = [s for s in f.info.postscriptStemSnapV] 47 self.kerning = f.kerning.asDict() 48 49 50 def copyToFont(self, f): 51 for g in f: 52 try: 53 gF = self.glyphs[g.name] 54 gF.copyToGlyph(g) 55 except: 56 print "Copy to glyph failed for" + g.name 57 f.info.postscriptStemSnapH = self.hstems 58 f.info.postscriptStemSnapV = self.vstems 59 for pair in self.kerning: 60 f.kerning[pair] = self.kerning[pair] 61 62 def getGlyph(self, gname): 63 try: 64 return self.glyphs[gname] 65 except: 66 return None 67 68 def setGlyph(self, gname, glyph): 69 self.glyphs[gname] = glyph 70 71 def addDiff(self,b,c): 72 newFont = FFont(self) 73 for key,g in newFont.glyphs.iteritems(): 74 gB = b.getGlyph(key) 75 gC = c.getGlyph(key) 76 try: 77 newFont.glyphs[key] = g.addDiff(gB,gC) 78 except: 79 print "Add diff failed for '%s'" %key 80 return newFont 81 82class FGlyph: 83 "provides a temporary floating point compatible glyph data structure" 84 85 def __init__(self, g=None): 86 self.contours = [] 87 self.width = 0. 88 self.components = [] 89 self.anchors = [] 90 if g != None: 91 self.copyFromGlyph(g) 92 93 def copyFromGlyph(self,g): 94 self.name = g.name 95 valuesX = [] 96 valuesY = [] 97 self.width = len(valuesX) 98 valuesX.append(g.width) 99 for c in g.components: 100 self.components.append((len(valuesX), len(valuesY))) 101 valuesX.append(c.scale[0]) 102 valuesY.append(c.scale[1]) 103 valuesX.append(c.offset[0]) 104 valuesY.append(c.offset[1]) 105 106 for a in g.anchors: 107 self.anchors.append((len(valuesX), len(valuesY))) 108 valuesX.append(a.x) 109 valuesY.append(a.y) 110 111 for i in range(len(g)): 112 self.contours.append([]) 113 for j in range (len(g[i].points)): 114 self.contours[i].append((len(valuesX), len(valuesY))) 115 valuesX.append(g[i].points[j].x) 116 valuesY.append(g[i].points[j].y) 117 118 self.dataX = array(valuesX, dtype=float) 119 self.dataY = array(valuesY, dtype=float) 120 121 def copyToGlyph(self,g): 122 g.width = self._derefX(self.width) 123 if len(g.components) == len(self.components): 124 for i in range(len(self.components)): 125 g.components[i].scale = (self._derefX(self.components[i][0] + 0, asInt=False), 126 self._derefY(self.components[i][1] + 0, asInt=False)) 127 g.components[i].offset = (self._derefX(self.components[i][0] + 1), 128 self._derefY(self.components[i][1] + 1)) 129 if len(g.anchors) == len(self.anchors): 130 for i in range(len(self.anchors)): 131 g.anchors[i].x = self._derefX( self.anchors[i][0]) 132 g.anchors[i].y = self._derefY( self.anchors[i][1]) 133 for i in range(len(g)) : 134 for j in range (len(g[i].points)): 135 g[i].points[j].x = self._derefX(self.contours[i][j][0]) 136 g[i].points[j].y = self._derefY(self.contours[i][j][1]) 137 138 def isCompatible(self, g): 139 return (len(self.dataX) == len(g.dataX) and 140 len(self.dataY) == len(g.dataY) and 141 len(g.contours) == len(self.contours)) 142 143 def __add__(self,g): 144 if self.isCompatible(g): 145 newGlyph = self.copy() 146 newGlyph.dataX = self.dataX + g.dataX 147 newGlyph.dataY = self.dataY + g.dataY 148 return newGlyph 149 else: 150 print "Add failed for '%s'" %(self.name) 151 raise Exception 152 153 def __sub__(self,g): 154 if self.isCompatible(g): 155 newGlyph = self.copy() 156 newGlyph.dataX = self.dataX - g.dataX 157 newGlyph.dataY = self.dataY - g.dataY 158 return newGlyph 159 else: 160 print "Subtract failed for '%s'" %(self.name) 161 raise Exception 162 163 def __mul__(self,scalar): 164 newGlyph = self.copy() 165 newGlyph.dataX = self.dataX * scalar 166 newGlyph.dataY = self.dataY * scalar 167 return newGlyph 168 169 def scaleX(self,scalar): 170 newGlyph = self.copy() 171 if len(self.dataX) > 0: 172 newGlyph.dataX = self.dataX * scalar 173 for i in range(len(newGlyph.components)): 174 newGlyph.dataX[newGlyph.components[i][0]] = self.dataX[newGlyph.components[i][0]] 175 return newGlyph 176 177 def shift(self,ammount): 178 newGlyph = self.copy() 179 newGlyph.dataX = self.dataX + ammount 180 for i in range(len(newGlyph.components)): 181 newGlyph.dataX[newGlyph.components[i][0]] = self.dataX[newGlyph.components[i][0]] 182 return newGlyph 183 184 def interp(self, g, v): 185 gF = self.copy() 186 if not self.isCompatible(g): 187 print "Interpolate failed for '%s'; outlines incompatible" %(self.name) 188 raise Exception 189 190 gF.dataX += (g.dataX - gF.dataX) * v.x 191 gF.dataY += (g.dataY - gF.dataY) * v.y 192 return gF 193 194 def copy(self): 195 ng = FGlyph() 196 ng.contours = list(self.contours) 197 ng.width = self.width 198 ng.components = list(self.components) 199 ng.anchors = list(self.anchors) 200 ng.dataX = self.dataX.copy() 201 ng.dataY = self.dataY.copy() 202 ng.name = self.name 203 return ng 204 205 def _derefX(self,id, asInt=True): 206 val = self.dataX[id] 207 return int(round(val)) if asInt else val 208 209 def _derefY(self,id, asInt=True): 210 val = self.dataY[id] 211 return int(round(val)) if asInt else val 212 213 def addDiff(self,gB,gC): 214 newGlyph = self + (gB - gC) 215 return newGlyph 216 217 218 219class Master: 220 221 def __init__(self, font=None, v=0, kernlist=None, overlay=None): 222 if isinstance(font, FFont): 223 self.font = None 224 self.ffont = font 225 elif isinstance(font,str): 226 self.openFont(font,overlay) 227 elif isinstance(font,Mix): 228 self.font = font 229 else: 230 self.font = font 231 self.ffont = FFont(font) 232 if isinstance(v,float) or isinstance(v,int): 233 self.v = RPoint(v, v) 234 else: 235 self.v = v 236 if kernlist != None: 237 kerns = [i.strip().split() for i in open(kernlist).readlines()] 238 239 self.kernlist = [{'left':k[0], 'right':k[1], 'value': k[2]} 240 for k in kerns 241 if not k[0].startswith("#") 242 and not k[0] == ""] 243 #TODO implement class based kerning / external kerning file 244 245 def openFont(self, path, overlayPath=None): 246 self.font = OpenFont(path) 247 for g in self.font: 248 size = len(g) 249 csize = len(g.components) 250 if (size > 0 and csize > 0): 251 decomposeGlyph(self.font, g.name) 252 253 if overlayPath != None: 254 overlayFont = OpenFont(overlayPath) 255 font = self.font 256 for overlayGlyph in overlayFont: 257 font.insertGlyph(overlayGlyph) 258 259 self.ffont = FFont(self.font) 260 261 262class Mix: 263 def __init__(self,masters,v): 264 self.masters = masters 265 if isinstance(v,float) or isinstance(v,int): 266 self.v = RPoint(v,v) 267 else: 268 self.v = v 269 270 def getFGlyph(self, master, gname): 271 if isinstance(master.font, Mix): 272 return font.mixGlyphs(gname) 273 return master.ffont.getGlyph(gname) 274 275 def getGlyphMasters(self,gname): 276 masters = self.masters 277 if len(masters) <= 2: 278 return self.getFGlyph(masters[0], gname), self.getFGlyph(masters[-1], gname) 279 280 def generateFFont(self): 281 ffont = FFont(self.masters[0].ffont) 282 for key,g in ffont.glyphs.iteritems(): 283 ffont.glyphs[key] = self.mixGlyphs(key) 284 ffont.kerning = self.mixKerns() 285 return ffont 286 287 def generateFont(self, baseFont): 288 newFont = baseFont.copy() 289 #self.mixStems(newFont) todo _ fix stems code 290 for g in newFont: 291 gF = self.mixGlyphs(g.name) 292 if gF == None: 293 g.mark = True 294 else: 295 gF.copyToGlyph(g) 296 newFont.kerning.clear() 297 newFont.kerning.update(self.mixKerns() or {}) 298 return newFont 299 300 def mixGlyphs(self,gname): 301 gA,gB = self.getGlyphMasters(gname) 302 try: 303 return gA.interp(gB,self.v) 304 except: 305 print "mixglyph failed for %s" %(gname) 306 if gA != None: 307 return gA.copy() 308 309 def getKerning(self, master): 310 if isinstance(master.font, Mix): 311 return master.font.mixKerns() 312 return master.ffont.kerning 313 314 def mixKerns(self): 315 masters = self.masters 316 kA, kB = self.getKerning(masters[0]), self.getKerning(masters[-1]) 317 return interpolateKerns(kA, kB, self.v) 318 319 320def narrowFLGlyph(g, gThin, factor=.75): 321 gF = FGlyph(g) 322 if not isinstance(gThin,FGlyph): 323 gThin = FGlyph(gThin) 324 gCondensed = gThin.scaleX(factor) 325 try: 326 gNarrow = gF + (gCondensed - gThin) 327 gNarrow.copyToGlyph(g) 328 except: 329 print "No dice for: " + g.name 330 331def interpolate(a,b,v,e=0): 332 if e == 0: 333 return a+(b-a)*v 334 qe = (b-a)*v*v*v + a #cubic easing 335 le = a+(b-a)*v # linear easing 336 return le + (qe-le) * e 337 338def interpolateKerns(kA, kB, v): 339 kerns = {} 340 for pair, val in kA.items(): 341 kerns[pair] = interpolate(val, kB.get(pair, 0), v.x) 342 for pair, val in kB.items(): 343 lerped_val = interpolate(val, kA.get(pair, 0), 1 - v.x) 344 if pair in kerns: 345 assert abs(kerns[pair] - lerped_val) < 1e-6 346 else: 347 kerns[pair] = lerped_val 348 return kerns 349