Speeds up font compilation by around 200%
Cython is used to compile some hot paths into native Python extensions. These hot paths were identified through running ufocompile with the hotshot profiler and then converting file by file to Cython, starting with the "hottest" paths and continuing until returns were deminishing. This means that only a few Python files were converted to Cython. Closes #23 Closes #20 (really this time)
This commit is contained in:
parent
31ae014e0c
commit
8234b62ab7
108 changed files with 26933 additions and 110 deletions
360
misc/pylib/fontbuild/mix.pyx
Normal file
360
misc/pylib/fontbuild/mix.pyx
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
# 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, RGlyph
|
||||
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)
|
||||
self.kerning = dict(f.kerning)
|
||||
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, dtype=float)
|
||||
self.dataY = array(valuesY, dtype=float)
|
||||
|
||||
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 = (self._derefX(self.components[i][0] + 0, asInt=False),
|
||||
self._derefY(self.components[i][1] + 0, asInt=False))
|
||||
g.components[i].offset = (self._derefX(self.components[i][0] + 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, asInt=True):
|
||||
val = self.dataX[id]
|
||||
return int(round(val)) if asInt else val
|
||||
|
||||
def _derefY(self,id, asInt=True):
|
||||
val = self.dataY[id]
|
||||
return int(round(val)) if asInt else val
|
||||
|
||||
def addDiff(self,gB,gC):
|
||||
newGlyph = self + (gB - gC)
|
||||
return newGlyph
|
||||
|
||||
|
||||
|
||||
class Master:
|
||||
|
||||
def __init__(self, font=None, v=0, kernlist=None, overlay=None):
|
||||
if isinstance(font, FFont):
|
||||
self.font = None
|
||||
self.ffont = font
|
||||
elif isinstance(font,str):
|
||||
self.openFont(font,overlay)
|
||||
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):
|
||||
self.font = OpenFont(path)
|
||||
for g in self.font:
|
||||
size = len(g)
|
||||
csize = len(g.components)
|
||||
if (size > 0 and csize > 0):
|
||||
decomposeGlyph(self.font, self.font[g.name])
|
||||
|
||||
if overlayPath != None:
|
||||
overlayFont = OpenFont(overlayPath)
|
||||
font = self.font
|
||||
for overlayGlyph in overlayFont:
|
||||
font.insertGlyph(overlayGlyph)
|
||||
|
||||
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 master.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
|
||||
elif isinstance(gF, RGlyph):
|
||||
newFont[g.name] = gF.copy()
|
||||
else:
|
||||
gF.copyToGlyph(g)
|
||||
|
||||
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):
|
||||
# to yield correct kerning for Roboto output, we must emulate the behavior
|
||||
# of old versions of this code; namely, take the kerning values of the first
|
||||
# master instead of actually interpolating.
|
||||
# old code:
|
||||
# https://github.com/google/roboto/blob/7f083ac31241cc86d019ea6227fa508b9fcf39a6/scripts/lib/fontbuild/mix.py
|
||||
# bug:
|
||||
# https://github.com/google/roboto/issues/213
|
||||
# return dict(kA)
|
||||
|
||||
kerns = {}
|
||||
for pair, val in kA.items():
|
||||
kerns[pair] = interpolate(val, kB.get(pair, 0), v.x)
|
||||
for pair, val in kB.items():
|
||||
lerped_val = interpolate(val, kA.get(pair, 0), 1 - v.x)
|
||||
if pair in kerns:
|
||||
assert abs(kerns[pair] - lerped_val) < 1e-6
|
||||
else:
|
||||
kerns[pair] = lerped_val
|
||||
return kerns
|
||||
Reference in a new issue