summaryrefslogtreecommitdiff
path: root/scripts/lib
diff options
context:
space:
mode:
authorChristian Robertson <robertsonc@google.com>2014-05-19 16:09:20 -0700
committerChristian Robertson <robertsonc@google.com>2014-05-19 16:09:20 -0700
commit7051939fa74b5f8f453be605f5dc3b4c23e1d1d9 (patch)
tree65cb31b34a64b2b07f9f6fd1bcb58d9a4945a92d /scripts/lib
Importing Roboto 2.0
Diffstat (limited to 'scripts/lib')
-rwxr-xr-xscripts/lib/fontbuild/Build.py227
-rwxr-xr-xscripts/lib/fontbuild/__init__.py6
-rw-r--r--scripts/lib/fontbuild/alignpoints.py148
-rwxr-xr-xscripts/lib/fontbuild/anchors.py44
-rwxr-xr-xscripts/lib/fontbuild/convertCurves.py90
-rw-r--r--scripts/lib/fontbuild/curveFitPen.py402
-rwxr-xr-xscripts/lib/fontbuild/generateGlyph.py48
-rwxr-xr-xscripts/lib/fontbuild/instanceNames.py184
-rw-r--r--scripts/lib/fontbuild/italics.py267
-rw-r--r--scripts/lib/fontbuild/italics2.py148
-rwxr-xr-xscripts/lib/fontbuild/kerning.py26
-rwxr-xr-xscripts/lib/fontbuild/mitreGlyph.py121
-rwxr-xr-xscripts/lib/fontbuild/mix.py334
13 files changed, 2045 insertions, 0 deletions
diff --git a/scripts/lib/fontbuild/Build.py b/scripts/lib/fontbuild/Build.py
new file mode 100755
index 0000000..d10cc9d
--- /dev/null
+++ b/scripts/lib/fontbuild/Build.py
@@ -0,0 +1,227 @@
+from FL import *
+from fontbuild.mix import Mix,Master,narrowFLGlyph
+from fontbuild.instanceNames import setNames
+from fontbuild.italics import italicizeGlyph
+from fontbuild.convertCurves import glyphCurvesToQuadratic
+from fontbuild.mitreGlyph import mitreGlyph
+from fontbuild.generateGlyph import generateGlyph
+from fontTools.misc.transform import Transform
+from fontbuild.kerning import generateFLKernClassesFromOTString
+import ConfigParser
+import os
+
+
+class FontProject:
+
+ def __init__(self, basefont, basedir, configfile, thinfont = None):
+ self.basefont = basefont
+ self.thinfont = thinfont
+ self.basedir = basedir
+ self.config = ConfigParser.RawConfigParser()
+ self.configfile = self.basedir+"/"+configfile
+ self.config.read(self.configfile)
+
+ diacriticList = open(self.basedir + "/" + self.config.get("res","diacriticfile")).readlines()
+ self.diacriticList = [line.strip() for line in diacriticList if not line.startswith("#")]
+ self.ot_classes = open(self.basedir + "/" + self.config.get("res","ot_classesfile")).read()
+ self.ot_kerningclasses = open(self.basedir + "/" + self.config.get("res","ot_kerningclassesfile")).read()
+ self.ot_features = open(self.basedir + "/" + self.config.get("res","ot_featuresfile")).read()
+
+ self.builddir = "out"
+ self.decompose = self.config.get("glyphs","decompose").split()
+ self.predecompose = self.config.get("glyphs","predecompose").split()
+ self.lessItalic = self.config.get("glyphs","lessitalic").split()
+ self.deleteList = self.config.get("glyphs","delete").split()
+ self.buildnumber = self.loadBuildNumber()
+
+
+ def loadBuildNumber(self):
+ versionFile = open(self.basedir + "/" + self.config.get("main","buildnumberfile"), "r+")
+ buildnumber = int(versionFile.read().strip())
+ buildnumber = "%05d" %(int(buildnumber) + 1)
+ print "BuildNumber: %s" %(buildnumber)
+ versionFile.close()
+ return buildnumber
+
+ def incrementBuildNumber(self):
+ if len(self.buildnumber) > 0:
+ versionFile = open(self.basedir + "/" + self.config.get("main","buildnumberfile"), "r+")
+ versionFile.seek(0)
+ versionFile.write(self.buildnumber)
+ versionFile.truncate()
+ versionFile.close()
+ else:
+ raise Exception("Empty build number")
+
+
+ def generateFont(self, mix, names, italic=False, swapSuffixes=None, stemWidth=185, kern=True):
+
+ n = names.split("/")
+ log("---------------------\n%s %s\n----------------------" %(n[0],n[1]))
+ log(">> Mixing masters")
+ if isinstance( mix, Mix):
+ f = mix.generateFont(self.basefont)
+ else:
+ f = Font(mix)
+ fl.Add(f)
+ index = fl.ifont
+ fl.CallCommand(33239) # Sort glyphs by unicode
+ if italic == True:
+ log(">> Italicizing")
+ fl.UpdateFont(fl.ifont)
+ tweakAmmount = .085
+ narrowAmmount = .93
+ if names.find("Thin") != -1:
+ tweakAmmount = .05
+ if names.find("Condensed") != -1:
+ narrowAmmount = .96
+ i = 0
+ for g in f.glyphs:
+ i += 1
+ if i % 10 == 0: print g.name
+
+ # if i < 24:
+ # continue
+ # if i > 86:
+ # for i,g in enumerate(fl.font.glyphs):
+ # fl.UpdateGlyph(i)
+ # # break
+ # assert False
+
+ # print g.name
+ # if self.thinfont != None:
+ # narrowFLGlyph(g,self.thinfont.getGlyph(g.name),factor=narrowAmmount)
+
+ if g.name != "eight" or g.name != "Q":
+ g.RemoveOverlap()
+
+ # not sure why FontLab sometimes refuses, seems to work if called twice
+
+ if (g.name in self.lessItalic):
+ italicizeGlyph(g, 9, stemWidth=stemWidth)
+ else:
+ italicizeGlyph(g, 10, stemWidth=stemWidth)
+ g.RemoveOverlap()
+ g.width += 10
+ fl.UpdateGlyph(i-1)
+
+ if swapSuffixes != None:
+ for swap in swapSuffixes:
+ swapList = [g.name for g in f.glyphs if g.name.endswith(swap)]
+ for gname in swapList:
+ print gname
+ swapGlyphs(f, gname.replace(swap,""), gname)
+ for gname in self.predecompose:
+ g = f[f.FindGlyph(gname)]
+ if g != None:
+ g.Decompose()
+
+ log(">> Generating glyphs")
+ generateGlyphs(f, self.diacriticList)
+ log(">> Copying features")
+ f.ot_classes = self.ot_classes
+ copyFeatures(self.basefont,f)
+ fl.UpdateFont(index)
+ log(">> Decomposing")
+ for gname in self.decompose:
+ g = f[f.FindGlyph(gname)]
+ if g != None:
+ g.Decompose()
+ g.Decompose()
+
+ setNames(f, n, foundry=self.config.get('main','foundry'),
+ version=self.config.get('main','version'),
+ build=self.buildnumber)
+ cleanCurves(f)
+ deleteGlyphs(f,self.deleteList)
+ if kern:
+ generateFLKernClassesFromOTString(f,self.ot_kerningclasses)
+ log(">> Generating font files")
+ directoryName = n[0].replace(" ","")
+ directoryPath = "%s/%s/%sTTF"%(self.basedir,self.builddir,directoryName)
+ if not os.path.exists(directoryPath):
+ os.makedirs(directoryPath)
+ ttfName = "%s/%s.ttf"%(directoryPath,f.font_name)
+ fl.GenerateFont(fl.ifont,ftTRUETYPE,ttfName)
+ f.modified = 0
+ fl.Close(index)
+
+def transformGlyphMembers(g, m):
+ g.width = int(g.width * m.a)
+ g.Transform(m)
+ for a in g.anchors:
+ p = Point(a.p)
+ p.Transform(m)
+ a.p = p
+ for c in g.components:
+ # Assumes that components have also been individually transformed
+ p = Point(0,0)
+ d = Point(c.deltas[0])
+ d.Transform(m)
+ p.Transform(m)
+ d1 = d - p
+ c.deltas[0].x = d1.x
+ c.deltas[0].y = d1.y
+ s = Point(c.scale)
+ s.Transform(m)
+ #c.scale = s
+
+def swapGlyphs(f,gName1,gName2):
+ try:
+ g1 = f.glyphs[f.FindGlyph(gName1)]
+ g2 = f.glyphs[f.FindGlyph(gName2)]
+ except IndexError:
+ log("swapGlyphs failed for %s %s"%(gName1, gName2))
+ return
+ g3 = Glyph(g1)
+
+ g1.Clear()
+ g1.Insert(g2)
+ g1.SetMetrics(g2.GetMetrics())
+
+ g2.Clear()
+ g2.Insert(g3)
+ g2.SetMetrics(g3.GetMetrics())
+
+def log(msg):
+ print msg
+
+# def addOTFeatures(f):
+# f.ot_classes = ot_classes
+
+def copyFeatures(f1, f2):
+ for ft in f1.features:
+ t = Feature(ft.tag, ft.value)
+ f2.features.append(t)
+ #f2.ot_classes = f1.ot_classes
+ f2.classes = []
+ f2.classes = f1.classes
+
+def generateGlyphs(f, glyphNames):
+ log(">> Generating diacritics")
+ glyphnames = [gname for gname in glyphNames if not gname.startswith("#") and gname != ""]
+
+ for glyphName in glyphNames:
+ generateGlyph(f, glyphName)
+
+def cleanCurves(f):
+ log(">> Removing overlaps")
+ for g in f.glyphs:
+ g.UnselectAll()
+ g.RemoveOverlap()
+
+ log(">> Mitring sharp corners")
+ # for g in f.glyphs:
+ # mitreGlyph(g, 3., .7)
+
+ log(">> Converting curves to quadratic")
+ # for g in f.glyphs:
+ # glyphCurvesToQuadratic(g)
+
+def deleteGlyphs(f,deleteList):
+ fl.Unselect()
+ for name in deleteList:
+ glyphIndex = f.FindGlyph(name)
+ if glyphIndex != -1:
+ del f.glyphs[glyphIndex]
+ fl.UpdateFont()
diff --git a/scripts/lib/fontbuild/__init__.py b/scripts/lib/fontbuild/__init__.py
new file mode 100755
index 0000000..4ed7203
--- /dev/null
+++ b/scripts/lib/fontbuild/__init__.py
@@ -0,0 +1,6 @@
+"""
+fontbuild
+
+A collection of font production tools written for FontLab
+"""
+version = "0.1" \ No newline at end of file
diff --git a/scripts/lib/fontbuild/alignpoints.py b/scripts/lib/fontbuild/alignpoints.py
new file mode 100644
index 0000000..ed502ed
--- /dev/null
+++ b/scripts/lib/fontbuild/alignpoints.py
@@ -0,0 +1,148 @@
+
+
+import numpy as np
+from numpy.linalg import lstsq
+import math
+
+def alignCorners(glyph, va, subsegments):
+ out = va.copy()
+ # for i,c in enumerate(subsegments):
+ # segmentCount = len(glyph.contours[i].segments) - 1
+ # n = len(c)
+ # for j,s in enumerate(c):
+ # if j < segmentCount:
+ # seg = glyph.contours[i].segments[j]
+ # if seg.type == "line":
+ # subIndex = subsegmentIndex(i,j,subsegments)
+ # out[subIndex] = alignPoints(va[subIndex])
+
+ for i,c in enumerate(subsegments):
+ segmentCount = len(glyph.contours[i].segments)
+ n = len(c)
+ for j,s in enumerate(c):
+ if j < segmentCount - 1:
+ segType = glyph.contours[i].segments[j].type
+ segnextType = glyph.contours[i].segments[j+1].type
+ next = j+1
+ elif j == segmentCount -1 and s[1] > 3:
+ segType = glyph.contours[i].segments[j].type
+ segNextType = "line"
+ next = j+1
+ elif j == segmentCount:
+ segType = "line"
+ segnextType = glyph.contours[i].segments[1].type
+ if glyph.name == "J":
+ print s[1]
+ print segnextType
+ next = 1
+ else:
+ break
+ if segType == "line" and segnextType == "line":
+ subIndex = subsegmentIndex(i,j,subsegments)
+ pts = va[subIndex]
+ ptsnext = va[subsegmentIndex(i,next,subsegments)]
+ # out[subIndex[-1]] = (out[subIndex[-1]] - 500) * 3 + 500 #findCorner(pts, ptsnext)
+ # print subIndex[-1], subIndex, subsegmentIndex(i,next,subsegments)
+ try:
+ out[subIndex[-1]] = findCorner(pts, ptsnext)
+ except:
+ pass
+ # print glyph.name, "Can't find corner: parallel lines"
+ return out
+
+
+def subsegmentIndex(contourIndex, segmentIndex, subsegments):
+ # This whole thing is so dumb. Need a better data model for subsegments
+
+ contourOffset = 0
+ for i,c in enumerate(subsegments):
+ if i == contourIndex:
+ break
+ contourOffset += c[-1][0]
+ n = subsegments[contourIndex][-1][0]
+ # print contourIndex, contourOffset, n
+ startIndex = subsegments[contourIndex][segmentIndex-1][0]
+ segmentCount = subsegments[contourIndex][segmentIndex][1]
+ endIndex = (startIndex + segmentCount + 1) % (n)
+
+ indices = np.array([(startIndex + i) % (n) + contourOffset for i in range(segmentCount + 1)])
+ return indices
+
+def alignPoints(pts, start=None, end=None):
+ if start == None or end == None:
+ start, end = fitLine(pts)
+ out = pts.copy()
+ for i,p in enumerate(pts):
+ out[i] = nearestPoint(start, end, p)
+ return out
+
+def findCorner(pp, nn):
+ if len(pp) < 4 or len(nn) < 4:
+ assert 0, "line too short to fit"
+ pStart,pEnd = fitLine(pp)
+ nStart,nEnd = fitLine(nn)
+ prev = pEnd - pStart
+ next = nEnd - nStart
+ # print int(np.arctan2(prev[1],prev[0]) / math.pi * 180),
+ # print int(np.arctan2(next[1],next[0]) / math.pi * 180)
+ # if lines are parallel, return simple average of end and start points
+ if np.dot(prev / np.linalg.norm(prev),
+ next / np.linalg.norm(next)) > .999999:
+ # print "parallel lines", np.arctan2(prev[1],prev[0]), np.arctan2(next[1],next[0])
+ # print prev, next
+ assert 0, "parallel lines"
+ return lineIntersect(pStart, pEnd, nStart, nEnd)
+
+def lineIntersect((x1,y1),(x2,y2),(x3,y3),(x4,y4)):
+ x12 = x1 - x2
+ x34 = x3 - x4
+ y12 = y1 - y2
+ y34 = y3 - y4
+
+ det = x12 * y34 - y12 * x34
+ if det == 0:
+ print "parallel!"
+
+ a = x1 * y2 - y1 * x2
+ b = x3 * y4 - y3 * x4
+
+ x = (a * x34 - b * x12) / det
+ y = (a * y34 - b * y12) / det
+
+ return (x,y)
+
+def fitLineLSQ(pts):
+ "returns a line fit with least squares. Fails for vertical lines"
+ n = len(pts)
+ a = np.ones((n,2))
+ for i in range(n):
+ a[i,0] = pts[i,0]
+ line = lstsq(a,pts[:,1])[0]
+ return line
+
+def fitLine(pts):
+ """returns a start vector and direction vector
+ Assumes points segments that already form a somewhat smooth line
+ """
+ n = len(pts)
+ if n < 1:
+ return (0,0),(0,0)
+ a = np.zeros((n-1,2))
+ for i in range(n-1):
+ v = pts[i] - pts[i+1]
+ a[i] = v / np.linalg.norm(v)
+ direction = np.mean(a[1:-1], axis=0)
+ start = np.mean(pts[1:-1], axis=0)
+ return start, start+direction
+
+def nearestPoint(a,b,c):
+ "nearest point to point c on line a_b"
+ magnitude = np.linalg.norm(b-a)
+ if magnitude == 0:
+ raise Exception, "Line segment cannot be 0 length"
+ return (b-a) * np.dot((c-a) / magnitude, (b-a) / magnitude) + a
+
+# pts = np.array([[1,1],[2,2],[3,3],[4,4]])
+# pts2 = np.array([[1,0],[2,0],[3,0],[4,0]])
+# print alignPoints(pts2, start = pts[0], end = pts[0]+pts[0])
+# # print findCorner(pts,pts2) \ No newline at end of file
diff --git a/scripts/lib/fontbuild/anchors.py b/scripts/lib/fontbuild/anchors.py
new file mode 100755
index 0000000..7cc3869
--- /dev/null
+++ b/scripts/lib/fontbuild/anchors.py
@@ -0,0 +1,44 @@
+#import numpy as np
+from FL import *
+
+
+def getGlyph(gname,font):
+ index = font.FindGlyph(gname)
+ if index != -1:
+ return font.glyphs[index]
+ else:
+ return None
+
+def getComponentByName(f,g,componentName):
+ componentIndex = f.FindGlyph(componentName)
+ for c in g.components:
+ if c.index == componentIndex:
+ return c
+
+def getAnchorByName(g,anchorName):
+ for a in g.anchors:
+ if a.name == anchorName:
+ return a
+
+
+def alignComponentToAnchor(f,glyphName,baseName,accentName,anchorName):
+ g = getGlyph(glyphName,f)
+ base = getGlyph(baseName,f)
+ accent = getGlyph(accentName,f)
+ if g == None or base == None or accent == None:
+ return
+ a1 = getAnchorByName(base,anchorName)
+ a2 = getAnchorByName(accent,"_" + anchorName)
+ if a1 == None or a2 == None:
+ return
+ offset = a1.p - a2.p
+ c = getComponentByName(f,g,accentName)
+ c.deltas[0].x = offset.x
+ c.deltas[0].y = offset.y
+
+def alignComponentsToAnchors(f,glyphName,baseName,accentNames):
+ for a in accentNames:
+ if len(a) == 1:
+ continue
+ alignComponentToAnchor(f,glyphName,baseName,a[0],a[1])
+
diff --git a/scripts/lib/fontbuild/convertCurves.py b/scripts/lib/fontbuild/convertCurves.py
new file mode 100755
index 0000000..e900a73
--- /dev/null
+++ b/scripts/lib/fontbuild/convertCurves.py
@@ -0,0 +1,90 @@
+#! /usr/bin/env python
+
+"""
+Converts a cubic bezier curve to a quadratic spline with
+exactly two off curve points.
+
+"""
+
+import numpy
+from numpy import array,cross,dot
+from fontTools.misc import bezierTools
+from FL import *
+
+def calcIntersect(a,b,c,d):
+ numpy.seterr(all='raise')
+ e = b-a
+ f = d-c
+ p = array([-e[1], e[0]])
+ try:
+ h = dot((a-c),p) / dot(f,p)
+ except:
+ print a,b,c,d
+ raise
+ return c + dot(f,h)
+
+def simpleConvertToQuadratic(p0,p1,p2,p3):
+ p = [array(i.x,i.y) for i in [p0,p1,p2,p3]]
+ off = calcIntersect(p[0],p[1],p[2],p[3])
+
+# OFFCURVE_VECTOR_CORRECTION = -.015
+OFFCURVE_VECTOR_CORRECTION = 0
+
+def convertToQuadratic(p0,p1,p2,p3):
+ # TODO: test for accuracy and subdivide further if needed
+ p = [(i.x,i.y) for i in [p0,p1,p2,p3]]
+ # if p[0][0] == p[1][0] and p[0][0] == p[2][0] and p[0][0] == p[2][0] and p[0][0] == p[3][0]:
+ # return (p[0],p[1],p[2],p[3])
+ # if p[0][1] == p[1][1] and p[0][1] == p[2][1] and p[0][1] == p[2][1] and p[0][1] == p[3][1]:
+ # return (p[0],p[1],p[2],p[3])
+ seg1,seg2 = bezierTools.splitCubicAtT(p[0], p[1], p[2], p[3], .5)
+ pts1 = [array([i[0], i[1]]) for i in seg1]
+ pts2 = [array([i[0], i[1]]) for i in seg2]
+ on1 = seg1[0]
+ on2 = seg2[3]
+ try:
+ off1 = calcIntersect(pts1[0], pts1[1], pts1[2], pts1[3])
+ off2 = calcIntersect(pts2[0], pts2[1], pts2[2], pts2[3])
+ except:
+ return (p[0],p[1],p[2],p[3])
+ off1 = (on1 - off1) * OFFCURVE_VECTOR_CORRECTION + off1
+ off2 = (on2 - off2) * OFFCURVE_VECTOR_CORRECTION + off2
+ return (on1,off1,off2,on2)
+
+def cubicNodeToQuadratic(g,nid):
+
+ node = g.nodes[nid]
+ if (node.type != nCURVE):
+ print "Node type not curve"
+ return
+
+ #pNode,junk = getPrevAnchor(g,nid)
+ pNode = g.nodes[nid-1] #assumes that a nCURVE type will always be proceeded by another point on the same contour
+ points = convertToQuadratic(pNode[0],node[1],node[2],node[0])
+ points = [Point(p[0],p[1]) for p in points]
+ curve = [
+ Node(nOFF, points[1]),
+ Node(nOFF, points[2]),
+ Node(nLINE,points[3]) ]
+ return curve
+
+def glyphCurvesToQuadratic(g):
+
+ nodes = []
+ for i in range(len(g.nodes)):
+ n = g.nodes[i]
+ if n.type == nCURVE:
+ try:
+ newNodes = cubicNodeToQuadratic(g, i)
+ nodes = nodes + newNodes
+ except Exception:
+ print g.name, i
+ raise
+ else:
+ nodes.append(Node(g.nodes[i]))
+ g.Clear()
+ g.Insert(nodes)
+
+
+
+
diff --git a/scripts/lib/fontbuild/curveFitPen.py b/scripts/lib/fontbuild/curveFitPen.py
new file mode 100644
index 0000000..d050479
--- /dev/null
+++ b/scripts/lib/fontbuild/curveFitPen.py
@@ -0,0 +1,402 @@
+#! /opt/local/bin/pythonw2.7
+
+__all__ = ["SubsegmentPen","SubsegmentsToCurvesPen", "segmentGlyph", "fitGlyph"]
+
+from fontTools.pens.basePen import BasePen
+from fontTools.misc import bezierTools
+from robofab.pens.pointPen import AbstractPointPen
+from robofab.pens.adapterPens import PointToSegmentPen, GuessSmoothPointPen
+import numpy as np
+from numpy.linalg import norm
+from numpy import array as v
+from random import random
+
+from robofab.pens.pointPen import BasePointToSegmentPen
+class SubsegmentsToCurvesPointPen(BasePointToSegmentPen):
+ def __init__(self, glyph, subsegmentGlyph, subsegments):
+ BasePointToSegmentPen.__init__(self)
+ self.glyph = glyph
+ self.subPen = SubsegmentsToCurvesPen(None, glyph.getPen(), subsegmentGlyph, subsegments)
+
+ def setMatchTangents(self, b):
+ self.subPen.matchTangents = b
+
+ def _flushContour(self, segments):
+ #
+ # adapted from robofab.pens.adapterPens.rfUFOPointPen
+ #
+ assert len(segments) >= 1
+ # if we only have one point and it has a name, we must have an anchor
+ first = segments[0]
+ segmentType, points = first
+ pt, smooth, name, kwargs = points[0]
+ if len(segments) == 1 and name != None:
+ self.glyph.appendAnchor(name, pt)
+ return
+ else:
+ segmentType, points = segments[-1]
+ movePt, smooth, name, kwargs = points[-1]
+ if smooth:
+ # last point is smooth, set pen to start smooth
+ self.subPen.setLastSmooth(True)
+ if segmentType == 'line':
+ del segments[-1]
+
+ self.subPen.moveTo(movePt)
+
+ # do the rest of the segments
+ for segmentType, points in segments:
+ isSmooth = True in [smooth for pt, smooth, name, kwargs in points]
+ pp = [pt for pt, smooth, name, kwargs in points]
+ if segmentType == "line":
+ assert len(pp) == 1
+ if isSmooth:
+ self.subPen.smoothLineTo(pp[0])
+ else:
+ self.subPen.lineTo(pp[0])
+ elif segmentType == "curve":
+ assert len(pp) == 3
+ if isSmooth:
+ self.subPen.smoothCurveTo(*pp)
+ else:
+ self.subPen.curveTo(*pp)
+ elif segmentType == "qcurve":
+ assert 0, "qcurve not supported"
+ else:
+ assert 0, "illegal segmentType: %s" % segmentType
+ self.subPen.closePath()
+
+ def addComponent(self, glyphName, transform):
+ self.subPen.addComponent(glyphName, transform)
+
+class SubsegmentsToCurvesPen(BasePen):
+ def __init__(self, glyphSet, otherPen, subsegmentGlyph, subsegments):
+ BasePen.__init__(self, None)
+ self.otherPen = otherPen
+ self.ssglyph = subsegmentGlyph
+ self.subsegments = subsegments
+ self.contourIndex = -1
+ self.segmentIndex = -1
+ self.lastPoint = (0,0)
+ self.lastSmooth = False
+ self.nextSmooth = False
+
+ def setLastSmooth(self, b):
+ self.lastSmooth = b
+
+ def _moveTo(self, (x, y)):
+ self.contourIndex += 1
+ self.segmentIndex = 0
+ self.startPoint = (x,y)
+ p = self.ssglyph.contours[self.contourIndex][0].points[0]
+ self.otherPen.moveTo((p.x, p.y))
+ self.lastPoint = (x,y)
+
+ def _lineTo(self, (x, y)):
+ self.segmentIndex += 1
+ index = self.subsegments[self.contourIndex][self.segmentIndex][0]
+ p = self.ssglyph.contours[self.contourIndex][index].points[0]
+ self.otherPen.lineTo((p.x, p.y))
+ self.lastPoint = (x,y)
+ self.lastSmooth = False
+
+ def smoothLineTo(self, (x, y)):
+ self.lineTo((x,y))
+ self.lastSmooth = True
+
+ def smoothCurveTo(self, (x1, y1), (x2, y2), (x3, y3)):
+ self.nextSmooth = True
+ self.curveTo((x1, y1), (x2, y2), (x3, y3))
+ self.nextSmooth = False
+ self.lastSmooth = True
+
+ def _curveToOne(self, (x1, y1), (x2, y2), (x3, y3)):
+ self.segmentIndex += 1
+ c = self.ssglyph.contours[self.contourIndex]
+ n = len(c)
+ startIndex = (self.subsegments[self.contourIndex][self.segmentIndex-1][0])
+ segmentCount = (self.subsegments[self.contourIndex][self.segmentIndex][1])
+ endIndex = (startIndex + segmentCount + 1) % (n)
+
+ indices = [(startIndex + i) % (n) for i in range(segmentCount + 1)]
+ points = np.array([(c[i].points[0].x, c[i].points[0].y) for i in indices])
+ prevPoint = (c[(startIndex - 1)].points[0].x, c[(startIndex - 1)].points[0].y)
+ nextPoint = (c[(endIndex) % n].points[0].x, c[(endIndex) % n].points[0].y)
+ prevTangent = prevPoint - points[0]
+ nextTangent = nextPoint - points[-1]
+
+ tangent1 = points[1] - points[0]
+ tangent3 = points[-2] - points[-1]
+ prevTangent /= np.linalg.norm(prevTangent)
+ nextTangent /= np.linalg.norm(nextTangent)
+ tangent1 /= np.linalg.norm(tangent1)
+ tangent3 /= np.linalg.norm(tangent3)
+
+ tangent1, junk = self.smoothTangents(tangent1, prevTangent, self.lastSmooth)
+ tangent3, junk = self.smoothTangents(tangent3, nextTangent, self.nextSmooth)
+ if self.matchTangents == True:
+ cp = fitBezier(points, tangent1, tangent3)
+ cp[1] = norm(cp[1] - cp[0]) * tangent1 / norm(tangent1) + cp[0]
+ cp[2] = norm(cp[2] - cp[3]) * tangent3 / norm(tangent3) + cp[3]
+ else:
+ cp = fitBezier(points)
+ # if self.ssglyph.name == 'r':
+ # print "-----------"
+ # print self.lastSmooth, self.nextSmooth
+ # print "%i %i : %i %i \n %i %i : %i %i \n %i %i : %i %i"%(x1,y1, cp[1,0], cp[1,1], x2,y2, cp[2,0], cp[2,1], x3,y3, cp[3,0], cp[3,1])
+ self.otherPen.curveTo((cp[1,0], cp[1,1]), (cp[2,0], cp[2,1]), (cp[3,0], cp[3,1]))
+ self.lastPoint = (x3, y3)
+ self.lastSmooth = False
+
+ def smoothTangents(self,t1,t2,forceSmooth = False):
+ if forceSmooth or (abs(t1.dot(t2)) > .95 and norm(t1-t2) > 1):
+ # print t1,t2,
+ t1 = (t1 - t2) / 2
+ t2 = -t1
+ # print t1,t2
+ return t1 / norm(t1), t2 / norm(t2)
+
+
+ def _closePath(self):
+ self.otherPen.closePath()
+
+ def _endPath(self):
+ self.otherPen.endPath()
+
+
+ def addComponent(self, glyphName, transformation):
+ self.otherPen.addComponent(glyphName, transformation)
+
+
+class SubsegmentPointPen(BasePointToSegmentPen):
+ def __init__(self, glyph, resolution):
+ BasePointToSegmentPen.__init__(self)
+ self.glyph = glyph
+ self.resolution = resolution
+ self.subPen = SubsegmentPen(None, glyph.getPen())
+
+ def getSubsegments(self):
+ return self.subPen.subsegments[:]
+
+ def _flushContour(self, segments):
+ #
+ # adapted from robofab.pens.adapterPens.rfUFOPointPen
+ #
+ assert len(segments) >= 1
+ # if we only have one point and it has a name, we must have an anchor
+ first = segments[0]
+ segmentType, points = first
+ pt, smooth, name, kwargs = points[0]
+ if len(segments) == 1 and name != None:
+ self.glyph.appendAnchor(name, pt)
+ return
+ else:
+ segmentType, points = segments[-1]
+ movePt, smooth, name, kwargs = points[-1]
+ if segmentType == 'line':
+ del segments[-1]
+
+ self.subPen.moveTo(movePt)
+
+ # do the rest of the segments
+ for segmentType, points in segments:
+ points = [pt for pt, smooth, name, kwargs in points]
+ if segmentType == "line":
+ assert len(points) == 1
+ self.subPen.lineTo(points[0])
+ elif segmentType == "curve":
+ assert len(points) == 3
+ self.subPen.curveTo(*points)
+ elif segmentType == "qcurve":
+ assert 0, "qcurve not supported"
+ else:
+ assert 0, "illegal segmentType: %s" % segmentType
+ self.subPen.closePath()
+
+ def addComponent(self, glyphName, transform):
+ self.subPen.addComponent(glyphName, transform)
+
+class SubsegmentPen(BasePen):
+
+ def __init__(self, glyphSet, otherPen, resolution=25):
+ BasePen.__init__(self,glyphSet)
+ self.resolution = resolution
+ self.otherPen = otherPen
+ self.subsegments = []
+ self.startContour = (0,0)
+ self.contourIndex = -1
+
+ def _moveTo(self, (x, y)):
+ self.contourIndex += 1
+ self.segmentIndex = 0
+ self.subsegments.append([])
+ self.subsegmentCount = 0
+ self.subsegments[self.contourIndex].append([self.subsegmentCount, 0])
+ self.startContour = (x,y)
+ self.lastPoint = (x,y)
+ self.otherPen.moveTo((x,y))
+
+ def _lineTo(self, (x, y)):
+ count = self.stepsForSegment((x,y),self.lastPoint)
+ if count < 1:
+ count = 1
+ self.subsegmentCount += count
+ self.subsegments[self.contourIndex].append([self.subsegmentCount, count])
+ for i in range(1,count+1):
+ x1 = self.lastPoint[0] + (x - self.lastPoint[0]) * i/float(count)
+ y1 = self.lastPoint[1] + (y - self.lastPoint[1]) * i/float(count)
+ self.otherPen.lineTo((x1,y1))
+ self.lastPoint = (x,y)
+
+ def _curveToOne(self, (x1, y1), (x2, y2), (x3, y3)):
+ count = self.stepsForSegment((x3,y3),self.lastPoint)
+ if count < 2:
+ count = 2
+ self.subsegmentCount += count
+ self.subsegments[self.contourIndex].append([self.subsegmentCount,count])
+ x = self.renderCurve((self.lastPoint[0],x1,x2,x3),count)
+ y = self.renderCurve((self.lastPoint[1],y1,y2,y3),count)
+ assert len(x) == count
+ if (x3 == self.startContour[0] and y3 == self.startContour[1]):
+ count -= 1
+ for i in range(count):
+ self.otherPen.lineTo((x[i],y[i]))
+ self.lastPoint = (x3,y3)
+
+ def _closePath(self):
+ if not (self.lastPoint[0] == self.startContour[0] and self.lastPoint[1] == self.startContour[1]):
+ self._lineTo(self.startContour)
+ self.otherPen.closePath()
+
+ def _endPath(self):
+ self.otherPen.endPath()
+
+ def addComponent(self, glyphName, transformation):
+ self.otherPen.addComponent(glyphName, transformation)
+
+ def stepsForSegment(self, p1, p2):
+ dist = np.linalg.norm(v(p1) - v(p2))
+ out = int(dist / self.resolution)
+ return out
+
+ def renderCurve(self,p,count):
+ curvePoints = []
+ t = 1.0 / float(count)
+ temp = t * t
+
+ f = p[0]
+ fd = 3 * (p[1] - p[0]) * t
+ fdd_per_2 = 3 * (p[0] - 2 * p[1] + p[2]) * temp
+ fddd_per_2 = 3 * (3 * (p[1] - p[2]) + p[3] - p[0]) * temp * t
+
+ fddd = fddd_per_2 + fddd_per_2
+ fdd = fdd_per_2 + fdd_per_2
+ fddd_per_6 = fddd_per_2 * (1.0 / 3)
+
+ for i in range(count):
+ f = f + fd + fdd_per_2 + fddd_per_6
+ fd = fd + fdd + fddd_per_2
+ fdd = fdd + fddd
+ fdd_per_2 = fdd_per_2 + fddd_per_2
+ curvePoints.append(f)
+
+ return curvePoints
+
+
+
+def fitBezierSimple(pts):
+ T = [np.linalg.norm(pts[i]-pts[i-1]) for i in range(1,len(pts))]
+ tsum = np.sum(T)
+ T = [0] + T
+ T = [np.sum(T[0:i+1])/tsum for i in range(len(pts))]
+ T = [[t**3, t**2, t, 1] for t in T]
+ T = np.array(T)
+ M = np.array([[-1, 3, -3, 1],
+ [ 3, -6, 3, 0],
+ [-3, 3, 0, 0],
+ [ 1, 0, 0, 0]])
+ T = T.dot(M)
+ T = np.concatenate((T, np.array([[100,0,0,0], [0,0,0,100]])))
+ # pts = np.vstack((pts, pts[0] * 100, pts[-1] * 100))
+ C = np.linalg.lstsq(T, pts)
+ return C[0]
+
+
+def subdivideLineSegment(pts):
+ out = [pts[0]]
+ for i in range(1, len(pts)):
+ out.append(pts[i-1] + (pts[i] - pts[i-1]) * .5)
+ out.append(pts[i])
+ return np.array(out)
+
+
+def fitBezier(pts,tangent0=None,tangent3=None):
+ if len(pts < 4):
+ pts = subdivideLineSegment(pts)
+ T = [np.linalg.norm(pts[i]-pts[i-1]) for i in range(1,len(pts))]
+ tsum = np.sum(T)
+ T = [0] + T
+ T = [np.sum(T[0:i+1])/tsum for i in range(len(pts))]
+ T = [[t**3, t**2, t, 1] for t in T]
+ T = np.array(T)
+ M = np.array([[-1, 3, -3, 1],
+ [ 3, -6, 3, 0],
+ [-3, 3, 0, 0],
+ [ 1, 0, 0, 0]])
+ T = T.dot(M)
+ n = len(pts)
+ pout = pts.copy()
+ pout[:,0] -= (T[:,0] * pts[0,0]) + (T[:,3] * pts[-1,0])
+ pout[:,1] -= (T[:,0] * pts[0,1]) + (T[:,3] * pts[-1,1])
+
+ TT = np.zeros((n*2,4))
+ for i in range(n):
+ for j in range(2):
+ TT[i*2,j*2] = T[i,j+1]
+ TT[i*2+1,j*2+1] = T[i,j+1]
+ pout = pout.reshape((n*2,1),order="C")
+
+ if tangent0 != None and tangent3 != None:
+ tangentConstraintsT = np.array([
+ [tangent0[1], -tangent0[0], 0, 0],
+ [0, 0, tangent3[1], -tangent3[0]]
+ ])
+ tangentConstraintsP = np.array([
+ [pts[0][1] * -tangent0[0] + pts[0][0] * tangent0[1]],
+ [pts[-1][1] * -tangent3[0] + pts[-1][0] * tangent3[1]]
+ ])
+ TT = np.concatenate((TT, tangentConstraintsT * 1000))
+ pout = np.concatenate((pout, tangentConstraintsP * 1000))
+ C = np.linalg.lstsq(TT,pout)[0].reshape((2,2))
+ return np.array([pts[0], C[0], C[1], pts[-1]])
+
+
+def segmentGlyph(glyph,resolution=50):
+ g1 = glyph.copy()
+ g1.clear()
+ dp = SubsegmentPointPen(g1, resolution)
+ glyph.drawPoints(dp)
+ return g1, dp.getSubsegments()
+
+
+def fitGlyph(glyph, subsegmentGlyph, subsegmentIndices, matchTangents=True):
+ outGlyph = glyph.copy()
+ outGlyph.clear()
+ fitPen = SubsegmentsToCurvesPointPen(outGlyph, subsegmentGlyph, subsegmentIndices)
+ fitPen.setMatchTangents(matchTangents)
+ # smoothPen = GuessSmoothPointPen(fitPen)
+ glyph.drawPoints(fitPen)
+ outGlyph.width = subsegmentGlyph.width
+ return outGlyph
+
+
+if __name__ == '__main__':
+ p = SubsegmentPen(None, None)
+ pts = np.array([
+ [0,0],
+ [.5,.5],
+ [.5,.5],
+ [1,1]
+ ])
+ print np.array(p.renderCurve(pts,10)) * 10
+ \ No newline at end of file
diff --git a/scripts/lib/fontbuild/generateGlyph.py b/scripts/lib/fontbuild/generateGlyph.py
new file mode 100755
index 0000000..f2214f0
--- /dev/null
+++ b/scripts/lib/fontbuild/generateGlyph.py
@@ -0,0 +1,48 @@
+from anchors import alignComponentsToAnchors
+from FL import *
+
+def parseComposite(composite):
+ c = composite.split("=")
+ d = c[1].split("/")
+ glyphName = d[0]
+ if len(d) == 1:
+ offset = [0,0]
+ else:
+ offset = [int(i) for i in d[1].split(",")]
+ accentString = c[0]
+ accents = accentString.split("+")
+ baseName = accents.pop(0)
+ accentNames = [i.split(":") for i in accents ]
+ return (glyphName, baseName, accentNames, offset)
+
+def shiftGlyphMembers(g, x):
+ g.Shift(Point(x,0))
+ for c in g.components:
+ c.deltas[0].x = c.deltas[0].x + x
+
+def generateGlyph(f,gname):
+ if gname.find("_") != -1:
+ generateString = gname
+ g = f.GenerateGlyph(generateString)
+ if f.FindGlyph(g.name) == -1:
+ f.glyphs.append(g)
+ return g
+ else:
+ glyphName, baseName, accentNames, offset = parseComposite(gname)
+ components = [baseName] + [i[0] for i in accentNames]
+ if len(components) == 1:
+ components.append("NONE")
+ generateString = "%s=%s" %("+".join(components), glyphName)
+ g = f.GenerateGlyph(generateString)
+ if f.FindGlyph(g.name) == -1:
+ f.glyphs.append(g)
+ g1 = f.glyphs[f.FindGlyph(g.name)]
+ if len(accentNames) > 0:
+ alignComponentsToAnchors(f,glyphName,baseName,accentNames)
+ if (offset[0] != 0 or offset[1] != 0):
+ g1.width += offset[1] + offset[0]
+ shiftGlyphMembers(g1,offset[0])
+ return g
+
+# generateGlyph(fl.font,"A+ogonek=Aogonek")
+# fl.UpdateFont() \ No newline at end of file
diff --git a/scripts/lib/fontbuild/instanceNames.py b/scripts/lib/fontbuild/instanceNames.py
new file mode 100755
index 0000000..96827c8
--- /dev/null
+++ b/scripts/lib/fontbuild/instanceNames.py
@@ -0,0 +1,184 @@
+from datetime import date
+import re
+from random import randint
+import string
+
+class InstanceNames:
+ "Class that allows easy setting of FontLab name fields. TODO: Add proper italic flags"
+
+ foundry = ""
+ build = "0000"
+ version = "1.0"
+ year = date.today().year
+ designer = "Christian Robertson"
+ license = "Licensed under the Apache License, Version 2.0"
+ licenseURL = "http://www.apache.org/licenses/LICENSE-2.0"
+
+ def __init__(self,names):
+ if type(names) == type(" "):
+ names = names.split("/")
+
+ self.longfamily = names[0]
+ self.longstyle = names[1]
+ self.shortstyle = names[2]
+ self.subfamilyAbbrev = names[3]
+
+ self.width = self._getWidth()
+ self.italic = self._getItalic()
+ self.weight = self._getWeight()
+ self.fullname = "%s %s" %(self.longfamily, self.longstyle)
+ self.postscript = re.sub(' ','', self.longfamily) + "-" + re.sub(' ','',self.longstyle)
+
+ # if self.subfamilyAbbrev != "" and self.subfamilyAbbrev != None and self.subfamilyAbbrev != "Rg":
+ # self.shortfamily = "%s %s" %(self.longfamily, self.subfamilyAbbrev)
+ # else:
+ # self.shortfamily = self.longfamily
+ self.shortfamily = self.longfamily
+
+ def setRFNames(self,f, version=1, versionMinor=0):
+ f.info.familyName = self.longfamily
+ f.info.styleName = self.longstyle
+ f.info.styleMapFamilyName = self.shortfamily
+ f.info.styleMapStyleName = self.shortstyle.lower()
+ f.info.versionMajor = version
+ f.info.versionMinor = versionMinor
+ f.info.year = self.year
+ f.info.copyright = "Font data copyright %s %s" %(self.foundry, self.year)
+ f.info.trademark = "%s is a trademark of %s." %(self.longfamily, self.foundry)
+
+ f.info.openTypeNameDesigner = "Christian Robertson"
+ f.info.openTypeNameDesignerURL = self.foundry + ".com"
+ f.info.openTypeNameManufacturer = self.foundry
+ f.info.openTypeNameManufacturerURL = self.foundry + ".com"
+ f.info.openTypeNameLicense = self.license
+ f.info.openTypeNameLicenseURL = self.licenseURL
+ f.info.openTypeNameVersion = "%i.%i" %(version,versionMinor)
+ f.info.openTypeNameUniqueID = "%s:%s:%s" %(self.foundry, self.longfamily, self.year)
+ # f.info.openTypeNameDescription = ""
+ # f.info.openTypeNameCompatibleFullName = ""
+ # f.info.openTypeNameSampleText = ""
+ if (self.subfamilyAbbrev != "Rg"):
+ f.info.openTypeNamePreferredFamilyName = self.longfamily
+ f.info.openTypeNamePreferredSubfamilyName = self.longstyle
+
+ f.info.macintoshFONDName = re.sub(' ','',self.longfamily) + " " + re.sub(' ','',self.longstyle)
+
+
+ def setFLNames(self,flFont):
+
+ from FL import NameRecord
+
+ flFont.family_name = self.shortfamily
+ flFont.mac_compatible = self.fullname
+ flFont.style_name = self.longstyle
+ flFont.full_name = self.fullname
+ flFont.font_name = self.postscript
+ flFont.font_style = self._getStyleCode()
+ flFont.menu_name = self.shortfamily
+ flFont.apple_name = re.sub(' ','',self.longfamily) + " " + re.sub(' ','',self.longstyle)
+ flFont.fond_id = randint(1000,9999)
+ flFont.pref_family_name = self.longfamily
+ flFont.pref_style_name = self.longstyle
+ flFont.weight = self.weight
+ flFont.weight_code = self._getWeightCode(self.weight)
+ flFont.width = self.width
+
+ fn = flFont.fontnames
+ fn.clean()
+ fn.append(NameRecord(0,1,0,0, "Font data copyright %s %s" %(self.foundry, self.year) ))
+ fn.append(NameRecord(0,3,1,1033, "Font data copyright %s %s" %(self.foundry, self.year) ))
+ fn.append(NameRecord(1,1,0,0, self.longfamily ))
+ fn.append(NameRecord(1,3,1,1033, self.shortfamily ))
+ fn.append(NameRecord(2,1,0,0, self.longstyle ))
+ fn.append(NameRecord(2,3,1,1033, self.longstyle ))
+ fn.append(NameRecord(3,1,0,0, "%s:%s:%s" %(self.foundry, self.longfamily, self.year) ))
+ fn.append(NameRecord(3,3,1,1033, "%s:%s:%s" %(self.foundry, self.longfamily, self.year) ))
+ fn.append(NameRecord(4,1,0,0, self.fullname ))
+ fn.append(NameRecord(4,3,1,1033, self.fullname ))
+ fn.append(NameRecord(5,1,0,0, "Version %s%s; %s" %(self.version, self.build, self.year) ))
+ fn.append(NameRecord(5,3,1,1033, "Version %s%s; %s" %(self.version, self.build, self.year) ))
+ fn.append(NameRecord(6,1,0,0, self.postscript ))
+ fn.append(NameRecord(6,3,1,1033, self.postscript ))
+ fn.append(NameRecord(7,1,0,0, "%s is a trademark of %s." %(self.longfamily, self.foundry) ))
+ fn.append(NameRecord(7,3,1,1033, "%s is a trademark of %s." %(self.longfamily, self.foundry) ))
+ fn.append(NameRecord(9,1,0,0, self.foundry ))
+ fn.append(NameRecord(9,3,1,1033, self.foundry ))
+ fn.append(NameRecord(11,1,0,0, self.foundry + ".com" ))
+ fn.append(NameRecord(11,3,1,1033, self.foundry + ".com" ))
+ fn.append(NameRecord(12,1,0,0, self.designer ))
+ fn.append(NameRecord(12,3,1,1033, self.designer ))
+ fn.append(NameRecord(13,1,0,0, self.license ))
+ fn.append(NameRecord(13,3,1,1033, self.license ))
+ fn.append(NameRecord(14,1,0,0, self.licenseURL ))
+ fn.append(NameRecord(14,3,1,1033, self.licenseURL ))
+ if (self.subfamilyAbbrev != "Rg"):
+ fn.append(NameRecord(16,3,1,1033, self.longfamily ))
+ fn.append(NameRecord(17,3,1,1033, self.longstyle))
+ #else:
+ #fn.append(NameRecord(17,3,1,1033,""))
+ #fn.append(NameRecord(18,1,0,0, re.sub("Italic","It", self.fullname)))
+
+ def _getSubstyle(self, regex):
+ substyle = re.findall(regex, self.longstyle)
+ if len(substyle) > 0:
+ return substyle[0]
+ else:
+ return ""
+
+ def _getItalic(self):
+ return self._getSubstyle(r"Italic|Oblique|Obliq")
+
+ def _getWeight(self):
+ w = self._getSubstyle(r"Extrabold|Superbold|Super|Fat|Bold|Semibold|Demibold|Medium|Light|Thin")
+ if w == "":
+ w = "Regular"
+ return w
+
+ def _getWidth(self):
+ w = self._getSubstyle(r"Condensed|Extended|Narrow|Wide")
+ if w == "":
+ w = "Normal"
+ return w
+
+ def _getStyleCode(self):
+ styleCode = 0
+ if self.shortstyle == "Bold":
+ styleCode = 32
+ if self.shortstyle == "Italic":
+ styleCode = 1
+ if self.shortstyle == "Bold Italic":
+ styleCode = 33
+ if self.longstyle == "Regular":
+ styleCode = 64
+ return styleCode
+
+ def _getWeightCode(self,weight):
+ if weight == "Thin":
+ return 250
+ elif weight == "Light":
+ return 300
+ elif weight == "Bold":
+ return 700
+ elif weight == "Medium":
+ return 500
+ elif weight == "Semibold":
+ return 600
+ elif weight == "Black":
+ return 800
+ elif weight == "Fat":
+ return 900
+
+ return 400
+
+def setNames(f,names,foundry="",version="1.0",build="0000"):
+ InstanceNames.foundry = foundry
+ InstanceNames.version = version
+ InstanceNames.build = build
+ i = InstanceNames(names)
+ i.setFLNames(f)
+
+def setNamesRF(f,names,foundry=""):
+ InstanceNames.foundry = foundry
+ i = InstanceNames(names)
+ i.setRFNames(f)
+ \ No newline at end of file
diff --git a/scripts/lib/fontbuild/italics.py b/scripts/lib/fontbuild/italics.py
new file mode 100644
index 0000000..72178b4
--- /dev/null
+++ b/scripts/lib/fontbuild/italics.py
@@ -0,0 +1,267 @@
+from fontTools.misc.transform import Transform
+from robofab.world import CurrentFont
+from robofab.world import RFont
+from time import clock
+import numpy as np
+import math
+from alignpoints import alignCorners
+
+def italicizeGlyph(g, angle=10, stemWidth=185):
+ f = CurrentFont()
+ glyph = f[g.name]
+ slope = np.tanh([math.pi * angle / 180])
+
+ # determine how far on the x axis the glyph should slide
+ # to compensate for the slant. -600 is a magic number
+ # that assumes a 2048 unit em square
+ MEAN_YCENTER = -600
+ m = Transform(1, 0, slope, 1, 0, 0)
+ xoffset, junk = m.transformPoint((0, MEAN_YCENTER))
+ m = Transform(.97, 0, slope, 1, xoffset, 0)
+
+ if len(glyph) > 0:
+ g2 = italicize(f[g.name], angle, xoffset=xoffset, stemWidth=stemWidth)
+ f.insertGlyph(g2, g.name)
+
+ transformFLGlyphMembers(f[g.name], m)
+
+
+def italicize(glyph, angle=12, stemWidth=180, xoffset=-50):
+ CURVE_CORRECTION_WEIGHT = .03
+ CORNER_WEIGHT = 10
+ ga,subsegments = segmentGlyph(glyph,25)
+ va, e = glyphToMesh(ga)
+ n = len(va)
+ grad = mapEdges(lambda a,(p,n): normalize(p-a), va, e)
+ cornerWeights = mapEdges(lambda a,(p,n): normalize(p-a).dot(normalize(a-n)), grad, e)[:,0].reshape((-1,1))
+ smooth = np.ones((n,1)) * CURVE_CORRECTION_WEIGHT
+
+ controlPoints = findControlPointsInMesh(glyph, va, subsegments)
+ smooth[controlPoints > 0] = 1
+ smooth[cornerWeights < .6] = CORNER_WEIGHT
+ # smooth[cornerWeights >= .9999] = 1
+
+ out = va.copy()
+ hascurves = False
+ for c in glyph.contours:
+ for s in c.segments:
+ if s.type == "curve":
+ hascurves = True
+ break
+ if hascurves:
+ break
+ if stemWidth > 100:
+ outCorrected = skewMesh(recompose(skewMesh(out, angle * 1.6), grad, e, smooth=smooth), -angle * 1.6)
+ # out = copyMeshDetails(va, out, e, 6)
+ else:
+ outCorrected = out
+ normals = edgeNormals(out, e)
+ center = va + normals * stemWidth * .4
+ if stemWidth > 130:
+ center[:, 0] = va[:, 0] * .7 + center[:,0] * .3
+ centerSkew = skewMesh(center.dot(np.array([[.97,0],[0,1]])), angle * .9)
+ out = outCorrected + (centerSkew - center)
+ out[:,1] = outCorrected[:,1]
+
+ smooth = np.ones((n,1)) * .1
+ out = alignCorners(glyph, out, subsegments)
+ out = copyMeshDetails(skewMesh(va, angle), out, e, 7, smooth=smooth)
+ # grad = mapEdges(lambda a,(p,n): normalize(p-a), skewMesh(outCorrected, angle*.9), e)
+ # out = recompose(out, grad, e, smooth=smooth)
+
+ out = skewMesh(out, angle * .1)
+ out[:,0] += xoffset
+ # out[:,1] = outCorrected[:,1]
+ out[va[:,1] == 0, 1] = 0
+ gOut = meshToGlyph(out, ga)
+ # gOut.width *= .97
+ # gOut.width += 10
+ # return gOut
+ return fitGlyph(glyph, gOut, subsegments)
+
+def condenseGlyph(glyph, scale=.8, stemWidth=185):
+ ga, subsegments = segmentGlyph(glyph, 25)
+ va, e = glyphToMesh(ga)
+ n = len(va)
+
+ normals = edgeNormals(va,e)
+ cn = va.dot(np.array([[scale, 0],[0,1]]))
+ grad = mapEdges(lambda a,(p,n): normalize(p-a), cn, e)
+ # ograd = mapEdges(lambda a,(p,n): normalize(p-a), va, e)
+
+ cn[:,0] -= normals[:,0] * stemWidth * .5 * (1 - scale)
+ out = recompose(cn, grad, e, smooth=.5)
+ # out = recompose(out, grad, e, smooth=.1)
+ out = recompose(out, grad, e, smooth=.01)
+
+ # cornerWeights = mapEdges(lambda a,(p,n): normalize(p-a).dot(normalize(a-n)), grad, e)[:,0].reshape((-1,1))
+ # smooth = np.ones((n,1)) * .1
+ # smooth[cornerWeights < .6] = 10
+ #
+ # grad2 = quantizeGradient(grad).astype(float)
+ # grad2 = copyGradDetails(grad, grad2, e, scale=10)
+ # grad2 = mapEdges(lambda a,e: normalize(a), grad2, e)
+ # out = recompose(out, grad2, e, smooth=smooth)
+ out[:,0] += 15
+ out[:,1] = va[:,1]
+ # out = recompose(out, grad, e, smooth=.5)
+ gOut = meshToGlyph(out, ga)
+ gOut = fitGlyph(glyph, gOut, subsegments)
+ for i,seg in enumerate(gOut):
+ gOut[i].points[0].y = glyph[i].points[0].y
+ return gOut
+
+
+def transformFLGlyphMembers(g, m, transformAnchors = True):
+ # g.transform(m)
+ g.width = g.width * m[0]
+ p = m.transformPoint((0,0))
+ for c in g.components:
+ d = m.transformPoint(c.offset)
+ c.offset = (d[0] - p[0], d[1] - p[1])
+ if transformAnchors:
+ for a in g.anchors:
+ aa = m.transformPoint((a.x,a.y))
+ a.x = aa[0]
+ # a.x,a.y = (aa[0] - p[0], aa[1] - p[1])
+ # a.x = a.x - m[4]
+
+
+from curveFitPen import fitGlyph,segmentGlyph
+from numpy.linalg import norm
+from scipy.sparse.linalg import cg
+from scipy.ndimage.filters import gaussian_filter1d as gaussian
+from scipy.cluster.vq import vq, kmeans2, whiten
+
+def glyphToMesh(g):
+ points = []
+ edges = {}
+ offset = 0
+ for c in g.contours:
+ if len(c) < 2:
+ continue
+ for i,prev,next in rangePrevNext(len(c)):
+ points.append((c[i].points[0].x, c[i].points[0].y))
+ edges[i + offset] = np.array([prev + offset, next + offset], dtype=int)
+ offset += len(c)
+ return np.array(points), edges
+
+def meshToGlyph(points, g):
+ g1 = g.copy()
+ j = 0
+ for c in g1.contours:
+ if len(c) < 2:
+ continue
+ for i in range(len(c)):
+ c[i].points[0].x = points[j][0]
+ c[i].points[0].y = points[j][1]
+ j += 1
+ return g1
+
+def quantizeGradient(grad, book=None):
+ if book == None:
+ book = np.array([(1,0),(0,1),(0,-1),(-1,0)])
+ indexArray = vq(whiten(grad), book)[0]
+ out = book[indexArray]
+ for i,v in enumerate(out):
+ out[i] = normalize(v)
+ return out
+
+def findControlPointsInMesh(glyph, va, subsegments):
+ controlPointIndices = np.zeros((len(va),1))
+ index = 0
+ for i,c in enumerate(subsegments):
+ segmentCount = len(glyph.contours[i].segments) - 1
+ for j,s in enumerate(c):
+ if j < segmentCount:
+ if glyph.contours[i].segments[j].type == "line":
+ controlPointIndices[index] = 1
+ index += s[1]
+ return controlPointIndices
+
+
+
+def recompose(v, grad, e, smooth=1, P=None, distance=None):
+ n = len(v)
+ if distance == None:
+ distance = mapEdges(lambda a,(p,n): norm(p - a), v, e)
+ if (P == None):
+ P = mP(v,e)
+ P += np.identity(n) * smooth
+ f = v.copy()
+ for i,(prev,next) in e.iteritems():
+ f[i] = (grad[next] * distance[next] - grad[i] * distance[i])
+ out = v.copy()
+ f += v * smooth
+ for i in range(len(out[0,:])):
+ out[:,i] = cg(P, f[:,i])[0]
+ return out
+
+def mP(v,e):
+ n = len(v)
+ M = np.zeros((n,n))
+ for i, edges in e.iteritems():
+ w = -2 / float(len(edges))
+ for index in edges:
+ M[i,index] = w
+ M[i,i] = 2
+ return M
+
+def normalize(v):
+ n = np.linalg.norm(v)
+ if n == 0:
+ return v
+ return v/n
+
+def mapEdges(func,v,e,*args):
+ b = v.copy()
+ for i, edges in e.iteritems():
+ b[i] = func(v[i], [v[j] for j in edges], *args)
+ return b
+
+def getNormal(a,b,c):
+ "Assumes TT winding direction"
+ p = np.roll(normalize(b - a), 1)
+ n = -np.roll(normalize(c - a), 1)
+ p[1] *= -1
+ n[1] *= -1
+ # print p, n, normalize((p + n) * .5)
+ return normalize((p + n) * .5)
+
+def edgeNormals(v,e):
+ "Assumes a mesh where each vertex has exactly least two edges"
+ return mapEdges(lambda a,(p,n) : getNormal(a,p,n),v,e)
+
+def rangePrevNext(count):
+ c = np.arange(count,dtype=int)
+ r = np.vstack((c, np.roll(c, 1), np.roll(c, -1)))
+ return r.T
+
+def skewMesh(v,angle):
+ slope = np.tanh([math.pi * angle / 180])
+ return v.dot(np.array([[1,0],[slope,1]]))
+
+def labelConnected(e):
+ label = 0
+ labels = np.zeros((len(e),1))
+ for i,(prev,next) in e.iteritems():
+ labels[i] = label
+ if next <= i:
+ label += 1
+ return labels
+
+def copyGradDetails(a,b,e,scale=15):
+ n = len(a)
+ labels = labelConnected(e)
+ out = a.astype(float).copy()
+ for i in range(labels[-1]+1):
+ mask = (labels==i).flatten()
+ out[mask,:] = gaussian(b[mask,:], scale, mode="wrap", axis=0) + a[mask,:] - gaussian(a[mask,:], scale, mode="wrap", axis=0)
+ return out
+
+def copyMeshDetails(va,vb,e,scale=5,smooth=.01):
+ gradA = mapEdges(lambda a,(p,n): normalize(p-a), va, e)
+ gradB = mapEdges(lambda a,(p,n): normalize(p-a), vb, e)
+ grad = copyGradDetails(gradA, gradB, e, scale)
+ grad = mapEdges(lambda a,(p,n): normalize(a), grad, e)
+ return recompose(vb, grad, e, smooth=smooth) \ No newline at end of file
diff --git a/scripts/lib/fontbuild/italics2.py b/scripts/lib/fontbuild/italics2.py
new file mode 100644
index 0000000..7eee90b
--- /dev/null
+++ b/scripts/lib/fontbuild/italics2.py
@@ -0,0 +1,148 @@
+from curveFitPen import fitGlyph,segmentGlyph
+import numpy as np
+from numpy.linalg import norm
+import math
+from scipy.sparse.linalg import cg
+
+def glyphToMesh(g):
+ points = []
+ edges = {}
+ offset = 0
+ for c in g.contours:
+ if len(c) < 2:
+ continue
+ for i,prev,next in rangePrevNext(len(c)):
+ points.append((c[i].points[0].x, c[i].points[0].y))
+ edges[i + offset] = np.array([prev + offset, next + offset], dtype=int)
+ offset += len(c)
+ return np.array(points), edges
+
+def meshToGlyph(points, g):
+ g1 = g.copy()
+ j = 0
+ for c in g1.contours:
+ if len(c) < 2:
+ continue
+ for i in range(len(c)):
+ c[i].points[0].x = points[j][0]
+ c[i].points[0].y = points[j][1]
+ j += 1
+ return g1
+
+def italicize(glyph, angle=12, stemWidth=180, xoffset=-50):
+ ga,subsegments = segmentGlyph(glyph,25)
+ va, e = glyphToMesh(ga)
+ n = len(va)
+ grad = mapEdges(lambda a,(p,n): normalize(p-a), va, e)
+ cornerWeights = mapEdges(lambda a,(p,n): normalize(p-a).dot(normalize(a-n)), grad, e)[:,0].reshape((-1,1))
+ smooth = np.ones((n,1)) * .02
+ smooth[cornerWeights < .6] = 5
+ # smooth[cornerWeights >= .9999] = 2
+ out = va.copy()
+ if stemWidth > 100:
+ out = skewMesh(poisson(skewMesh(out, angle * 2), grad, e, smooth=smooth), -angle * 2)
+ out = copyMeshDetails(va, out, e, 6)
+ # return meshToGlyph(out,ga)
+ normals = edgeNormals(out, e)
+ center = va + normals * stemWidth * .4
+ if stemWidth > 100:
+ center[:, 0] = va[:, 0]
+ centerSkew = skewMesh(center.dot(np.array([[.97,0],[0,1]])), angle * .7)
+ # centerSkew = skewMesh(center, angle * .7)
+ out = out + (centerSkew - center)
+ out = copyMeshDetails(skewMesh(va, angle * .7), out, e, 12)
+ out = skewMesh(out, angle * .3)
+ out[:,0] += xoffset
+ # out[:,1] = va[:,1]
+ gOut = meshToGlyph(out, ga)
+ # gOut.width *= .97
+ gOut.width += 10
+ # return gOut
+ return fitGlyph(glyph, gOut, subsegments)
+
+def poisson(v, grad, e, smooth=1, P=None, distance=None):
+ n = len(v)
+ if distance == None:
+ distance = mapEdges(lambda a,(p,n): norm(p - a), v, e)
+ if (P == None):
+ P = mP(v,e)
+ P += np.identity(n) * smooth
+ f = v.copy()
+ for i,(prev,next) in e.iteritems():
+ f[i] = (grad[next] * distance[next] - grad[i] * distance[i])
+ out = v.copy()
+ f += v * smooth
+ for i in range(len(out[0,:])):
+ out[:,i] = cg(P, f[:,i])[0]
+ return out
+
+def mP(v,e):
+ n = len(v)
+ M = np.zeros((n,n))
+ for i, edges in e.iteritems():
+ w = -2 / float(len(edges))
+ for index in edges:
+ M[i,index] = w
+ M[i,i] = 2
+ return M
+
+def normalize(v):
+ n = np.linalg.norm(v)
+ if n == 0:
+ return v
+ return v/n
+
+def mapEdges(func,v,e,*args):
+ b = v.copy()
+ for i, edges in e.iteritems():
+ b[i] = func(v[i], [v[j] for j in edges], *args)
+ return b
+
+def getNormal(a,b,c):
+ "Assumes TT winding direction"
+ p = np.roll(normalize(b - a), 1)
+ n = -np.roll(normalize(c - a), 1)
+ p[1] *= -1
+ n[1] *= -1
+ # print p, n, normalize((p + n) * .5)
+ return normalize((p + n) * .5)
+
+def edgeNormals(v,e):
+ "Assumes a mesh where each vertex has exactly least two edges"
+ return mapEdges(lambda a,(p,n) : getNormal(a,p,n),v,e)
+
+def rangePrevNext(count):
+ c = np.arange(count,dtype=int)
+ r = np.vstack((c, np.roll(c, 1), np.roll(c, -1)))
+ return r.T
+
+def skewMesh(v,angle):
+ slope = np.tanh([math.pi * angle / 180])
+ return v.dot(np.array([[1,0],[slope,1]]))
+
+
+from scipy.ndimage.filters import gaussian_filter1d as gaussian
+
+def labelConnected(e):
+ label = 0
+ labels = np.zeros((len(e),1))
+ for i,(prev,next) in e.iteritems():
+ labels[i] = label
+ if next <= i:
+ label += 1
+ return labels
+
+def copyGradDetails(a,b,e,scale=15):
+ n = len(a)
+ labels = labelConnected(e)
+ out = a.astype(float).copy()
+ for i in range(labels[-1]+1):
+ mask = (labels==i).flatten()
+ out[mask,:] = gaussian(b[mask,:], scale, mode="wrap", axis=0) + a[mask,:] - gaussian(a[mask,:], scale, mode="wrap", axis=0)
+ return out
+
+def copyMeshDetails(va,vb,e,scale=5,smooth=.01):
+ gradA = mapEdges(lambda a,(p,n): normalize(p-a), va, e)
+ gradB = mapEdges(lambda a,(p,n): normalize(p-a), vb, e)
+ grad = copyGradDetails(gradA, gradB, e, scale)
+ return poisson(vb, grad, e, smooth=smooth) \ No newline at end of file
diff --git a/scripts/lib/fontbuild/kerning.py b/scripts/lib/fontbuild/kerning.py
new file mode 100755
index 0000000..cdece40
--- /dev/null
+++ b/scripts/lib/fontbuild/kerning.py
@@ -0,0 +1,26 @@
+import re
+from FL import *
+
+def markKernClassesLR(f):
+ for i in range(len(f.classes)):
+ classname = f.classes[i].split(':', 1).pop(0).strip()
+ if classname.startswith('_'):
+ l = 0
+ r = 0
+ if classname.endswith('_L'):
+ l = 1
+ elif classname.endswith('_R'):
+ r = 1
+ elif classname.endswith('_LR'):
+ l = 1
+ r = 1
+ f.SetClassFlags(i, l, r)
+ fl.UpdateFont()
+
+def generateFLKernClassesFromOTString(f,classString):
+ classString.replace("\r","\n")
+ rx = re.compile(r"@(_[\w]+)\s*=\s*\[\s*(\w+?)\s+(.*?)\]\s*;")
+ classes = ["%s : %s' %s" %(m[0],m[1],m[2]) for m in rx.findall(classString)]
+ f.classes = classes
+ markKernClassesLR(f)
+
diff --git a/scripts/lib/fontbuild/mitreGlyph.py b/scripts/lib/fontbuild/mitreGlyph.py
new file mode 100755
index 0000000..ab68e4e
--- /dev/null
+++ b/scripts/lib/fontbuild/mitreGlyph.py
@@ -0,0 +1,121 @@
+"""Mitre Glyph:
+
+mitreSize : Length of the segment created by the mitre. The default is 4.
+maxAngle : Maximum angle in radians at which nodes will be mitred. The default is .9 (about 50 degrees).
+ Works for both inside and outside angles
+
+"""
+
+from FL import *
+import math
+
+def getContours(g):
+ nLength = len(g.nodes)
+ contours = []
+ cid = -1
+ for i in range(nLength):
+ n = g.nodes[i]
+ if n.type == nMOVE:
+ cid += 1
+ contours.append([])
+ contours[cid].append(n)
+ return contours
+
+def getTangents(contours):
+ tmap = []
+ for c in contours:
+ clen = len(c)
+ for i in range(clen):
+ n = c[i]
+ p = Point(n.x, n.y)
+ nn = c[(i + 1) % clen]
+ pn = c[(clen + i - 1) % clen]
+ if nn.type == nCURVE:
+ np = Point(nn[1].x,nn[1].y)
+ else:
+ np = Point(nn.x,nn.y)
+ if n.type == nCURVE:
+ pp = Point(n[2].x,n[2].y)
+ else:
+ pp = Point(pn.x,pn.y)
+ nVect = Point(-p.x + np.x, -p.y + np.y)
+ pVect = Point(-p.x + pp.x, -p.y + pp.y)
+ tmap.append((pVect,nVect))
+ return tmap
+
+def normalizeVector(p):
+ m = getMagnitude(p);
+ if m != 0:
+ return p*(1/m)
+ else:
+ return Point(0,0)
+
+def getMagnitude(p):
+ return math.sqrt(p.x*p.x + p.y*p.y)
+
+def getDistance(v1,v2):
+ return getMagnitude(Point(v1.x - v2.x, v1.y - v2.y))
+
+def getAngle(v1,v2):
+ angle = math.atan2(v1.y,v1.x) - math.atan2(v2.y,v2.x)
+ return (angle + (2*math.pi)) % (2*math.pi)
+
+def angleDiff(a,b):
+ return math.pi - abs((abs(a - b) % (math.pi*2)) - math.pi)
+
+def getAngle2(v1,v2):
+ return abs(angleDiff(math.atan2(v1.y, v1.x), math.atan2(v2.y, v2.x)))
+
+def getMitreOffset(n,v1,v2,mitreSize=4,maxAngle=.9):
+
+ # dont mitre if segment is too short
+ if abs(getMagnitude(v1)) < mitreSize * 2 or abs(getMagnitude(v2)) < mitreSize * 2:
+ return
+ angle = getAngle2(v2,v1)
+ v1 = normalizeVector(v1)
+ v2 = normalizeVector(v2)
+ if v1.x == v2.x and v1.y == v2.y:
+ return
+
+
+ # only mitre corners sharper than maxAngle
+ if angle > maxAngle:
+ return
+
+ radius = mitreSize / abs(getDistance(v1,v2))
+ offset1 = Point(round(v1.x * radius), round(v1.y * radius))
+ offset2 = Point(round(v2.x * radius), round(v2.y * radius))
+ return offset1, offset2
+
+def mitreGlyph(g,mitreSize,maxAngle):
+ if g == None:
+ return
+
+ contours = getContours(g)
+ tangents = getTangents(contours)
+ nodes = []
+ needsMitring = False
+ nid = -1
+ for c in contours:
+ for n in c:
+ nid += 1
+ v1, v2 = tangents[nid]
+ off = getMitreOffset(n,v1,v2,mitreSize,maxAngle)
+ n1 = Node(n)
+ if off != None:
+ offset1, offset2 = off
+ n2 = Node(nLINE, Point(n.x + offset2.x, n.y + offset2.y))
+ n1[0].x += offset1.x
+ n1[0].y += offset1.y
+ nodes.append(n1)
+ nodes.append(n2)
+ needsMitring = True
+ else:
+ nodes.append(n1)
+ if needsMitring:
+ g.Clear()
+ g.Insert(nodes)
+
+fl.SetUndo()
+mitreGlyph(fl.glyph,8.,.9)
+fl.UpdateGlyph() \ No newline at end of file
diff --git a/scripts/lib/fontbuild/mix.py b/scripts/lib/fontbuild/mix.py
new file mode 100755
index 0000000..86bf4ed
--- /dev/null
+++ b/scripts/lib/fontbuild/mix.py
@@ -0,0 +1,334 @@
+from FL import *
+from numpy import array, append
+import copy
+
+class FFont:
+ "Font wrapper for floating point operations"
+
+ def __init__(self,f=None):
+ self.glyphs = {}
+ self.hstems = []
+ self.vstems = []
+ 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.glyphs:
+ self.glyphs[g.name] = FGlyph(g)
+ self.hstems = [s for s in f.stem_snap_h[0]]
+ self.vstems = [s for s in f.stem_snap_v[0]]
+
+
+ def copyToFont(self,f):
+ for g in f.glyphs:
+ try:
+ gF = self.glyphs[g.name]
+ gF.copyToGlyph(g)
+ except:
+ print "Copy to glyph failed for" + g.name
+ f.stem_snap_h[0] = self.hstems
+ f.stem_snap_v[0] = self.vstems
+
+ 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.nodes = []
+ self.width = 0.
+ self.components = []
+ self.kerning = []
+ 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.x)
+ valuesY.append(c.scale.y)
+ valuesX.append(c.delta.x)
+ valuesY.append(c.delta.y)
+
+ 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.nodes)):
+ self.nodes.append([])
+ for j in range (len(g.nodes[i])):
+ self.nodes[i].append( (len(valuesX), len(valuesY)) )
+ valuesX.append(g.nodes[i][j].x)
+ valuesY.append(g.nodes[i][j].y)
+
+ for k in g.kerning:
+ self.kerning.append(KerningPair(k))
+
+ 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.x = self._derefX( self.components[i][0] + 0)
+ g.components[i].scale.y = self._derefY( self.components[i][1] + 0)
+ g.components[i].deltas[0].x = self._derefX( self.components[i][0] + 1)
+ g.components[i].deltas[0].y = self._derefY( self.components[i][1] + 1)
+ g.kerning = []
+ 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 k in self.kerning:
+ g.kerning.append(KerningPair(k))
+ for i in range( len(g.nodes)) :
+ for j in range (len(g.nodes[i])):
+ g.nodes[i][j].x = self._derefX( self.nodes[i][j][0] )
+ g.nodes[i][j].y = self._derefY( self.nodes[i][j][1] )
+
+ def isCompatible(self,g):
+ return len(self.dataX) == len(g.dataX) and len(self.dataY) == len(g.dataY) and len(g.nodes) == len(self.nodes)
+
+ 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
+ gF.kerning = interpolateKerns(self,g,v)
+ return gF
+
+ def copy(self):
+ ng = FGlyph()
+ ng.nodes = list(self.nodes)
+ ng.width = self.width
+ ng.components = list(self.components)
+ ng.kerning = list(self.kerning)
+ 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,ifont=None, 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.ifont = ifont
+ self.ffont = FFont(font)
+ if isinstance(v,float) or isinstance(v,int):
+ self.v = Point(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):
+ fl.Open(path,True)
+ self.ifont = fl.ifont
+ self.font = fl.font
+ if overlayPath != None:
+ fl.Open(overlayPath,True)
+ ifont = self.ifont
+ font = self.font
+ overlayIfont = fl.ifont
+ overlayFont = fl.font
+
+ for overlayGlyph in overlayFont.glyphs:
+ glyphIndex = font.FindGlyph(overlayGlyph.name)
+ if glyphIndex != -1:
+ oldGlyph = Glyph(font.glyphs[glyphIndex])
+ kernlist = [KerningPair(k) for k in oldGlyph.kerning]
+ font.glyphs[glyphIndex] = Glyph(overlayGlyph)
+ font.glyphs[glyphIndex].kerning = kernlist
+ else:
+ font.glyphs.append(overlayGlyph)
+ fl.UpdateFont(ifont)
+ fl.Close(overlayIfont)
+ 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 = Point(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)
+ return ffont
+
+ def generateFont(self, baseFont):
+ newFont = Font(baseFont)
+ #self.mixStems(newFont) todo _ fix stems code
+ for g in newFont.glyphs:
+ 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"
+ 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 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(gA,gB,v):
+ kerns = []
+ for kA in gA.kerning:
+ key = kA.key
+ matchedKern = None
+ for kB in gA.kerning:
+ if key == kB.key:
+ matchedKern = kB
+ break
+ # if matchedkern == None:
+ # matchedkern = Kern(kA)
+ # matchedkern.value = 0
+ if matchedKern != None:
+ kernValue = interpolate(kA.value, matchedKern.value, v.x)
+ kerns.append(KerningPair(kA.key,kernValue))
+ return kerns \ No newline at end of file