summaryrefslogtreecommitdiff
path: root/misc/pylib
diff options
context:
space:
mode:
authorRasmus Andersson <rasmus@notion.se>2017-08-22 10:05:20 +0300
committerRasmus Andersson <rasmus@notion.se>2017-08-22 12:23:08 +0300
commit3b1fffade1473f20f2558733fbd218f4580fc7c3 (patch)
treeea4f80b43b08744d493bb86ab646444ec04ddc7f /misc/pylib
downloadinter-3b1fffade1473f20f2558733fbd218f4580fc7c3.tar.xz
Initial public commitv1.0
Diffstat (limited to 'misc/pylib')
-rw-r--r--misc/pylib/fontbuild/Build.py300
-rw-r--r--misc/pylib/fontbuild/LICENSE201
-rw-r--r--misc/pylib/fontbuild/ORIGIN.txt1
-rw-r--r--misc/pylib/fontbuild/__init__.py6
-rw-r--r--misc/pylib/fontbuild/alignpoints.py173
-rw-r--r--misc/pylib/fontbuild/anchors.py77
-rw-r--r--misc/pylib/fontbuild/convertCurves.py102
-rw-r--r--misc/pylib/fontbuild/curveFitPen.py422
-rw-r--r--misc/pylib/fontbuild/decomposeGlyph.py23
-rwxr-xr-xmisc/pylib/fontbuild/features.py189
-rw-r--r--misc/pylib/fontbuild/generateGlyph.py97
-rw-r--r--misc/pylib/fontbuild/instanceNames.py232
-rw-r--r--misc/pylib/fontbuild/italics.py308
-rwxr-xr-xmisc/pylib/fontbuild/markFeature.py55
-rw-r--r--misc/pylib/fontbuild/mitreGlyph.py111
-rw-r--r--misc/pylib/fontbuild/mix.py360
16 files changed, 2657 insertions, 0 deletions
diff --git a/misc/pylib/fontbuild/Build.py b/misc/pylib/fontbuild/Build.py
new file mode 100644
index 000000000..5046f9f91
--- /dev/null
+++ b/misc/pylib/fontbuild/Build.py
@@ -0,0 +1,300 @@
+# 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.
+
+
+import ConfigParser
+import os
+import sys
+
+from booleanOperations import BooleanOperationManager
+from cu2qu.ufo import fonts_to_quadratic
+from fontTools.misc.transform import Transform
+from robofab.world import OpenFont
+from ufo2ft import compileOTF, compileTTF
+
+from fontbuild.decomposeGlyph import decomposeGlyph
+from fontbuild.features import readFeatureFile, writeFeatureFile
+from fontbuild.generateGlyph import generateGlyph
+from fontbuild.instanceNames import setInfoRF
+from fontbuild.italics import italicizeGlyph
+from fontbuild.markFeature import RobotoFeatureCompiler, RobotoKernWriter
+from fontbuild.mitreGlyph import mitreGlyph
+from fontbuild.mix import Mix,Master,narrowFLGlyph
+
+
+class FontProject:
+
+ def __init__(self, basefont, basedir, configfile, buildTag=''):
+ self.basefont = basefont
+ self.basedir = basedir
+ self.config = ConfigParser.RawConfigParser()
+ self.configfile = os.path.join(self.basedir, configfile)
+ self.config.read(self.configfile)
+ self.buildTag = buildTag
+
+ self.diacriticList = [
+ line.strip() for line in self.openResource("diacriticfile")
+ if not line.startswith("#")]
+ self.adobeGlyphList = dict(
+ line.split(";") for line in self.openResource("agl_glyphlistfile")
+ if not line.startswith("#"))
+ self.glyphOrder = self.openResource("glyphorder")
+
+ # map exceptional glyph names in Roboto to names in the AGL
+ roboNames = (
+ ('Obar', 'Ocenteredtilde'), ('obar', 'obarred'),
+ ('eturn', 'eturned'), ('Iota1', 'Iotaafrican'))
+ for roboName, aglName in roboNames:
+ self.adobeGlyphList[roboName] = self.adobeGlyphList[aglName]
+
+ 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.noItalic = self.config.get("glyphs","noitalic").split()
+
+ self.buildOTF = False
+ self.compatible = False
+ self.generatedFonts = []
+
+ def openResource(self, name):
+ with open(os.path.join(
+ self.basedir, self.config.get("res", name))) as resourceFile:
+ resource = resourceFile.read()
+ return resource.splitlines()
+
+ def generateOutputPath(self, font, ext):
+ family = font.info.familyName.replace(" ", "")
+ style = font.info.styleName.replace(" ", "")
+ path = os.path.join(self.basedir, self.builddir, family + ext.upper())
+ if not os.path.exists(path):
+ os.makedirs(path)
+ return os.path.join(path, "%s-%s.%s" % (family, style, ext))
+
+ def generateFont(self, mix, names, italic=False, swapSuffixes=None, stemWidth=185,
+ italicMeanYCenter=-825, italicNarrowAmount=1):
+
+ 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 = mix.copy()
+
+ if italic == True:
+ log(">> Italicizing")
+ i = 0
+ for g in f:
+ i += 1
+ if i % 10 == 0: print g.name
+
+ if g.name == "uniFFFD":
+ continue
+
+ decomposeGlyph(f, g)
+ removeGlyphOverlap(g)
+
+ if g.name in self.lessItalic:
+ italicizeGlyph(f, g, 9, stemWidth=stemWidth,
+ meanYCenter=italicMeanYCenter,
+ narrowAmount=italicNarrowAmount)
+ elif g.name not in self.noItalic:
+ italicizeGlyph(f, g, 10, stemWidth=stemWidth,
+ meanYCenter=italicMeanYCenter,
+ narrowAmount=italicNarrowAmount)
+ if g.width != 0:
+ g.width += 10
+
+ # set the oblique flag in fsSelection
+ f.info.openTypeOS2Selection.append(9)
+
+ if swapSuffixes != None:
+ for swap in swapSuffixes:
+ swapList = [g.name for g in f if g.name.endswith(swap)]
+ for gname in swapList:
+ print gname
+ swapContours(f, gname.replace(swap,""), gname)
+ for gname in self.predecompose:
+ if f.has_key(gname):
+ decomposeGlyph(f, f[gname])
+
+ log(">> Generating glyphs")
+ generateGlyphs(f, self.diacriticList, self.adobeGlyphList)
+ log(">> Copying features")
+ readFeatureFile(f, self.basefont.features.text)
+ log(">> Decomposing")
+ for g in f:
+ if len(g.components) > 0:
+ decomposeGlyph(f, g)
+ # for gname in self.decompose:
+ # if f.has_key(gname):
+ # decomposeGlyph(f, f[gname])
+
+ copyrightHolderName = ''
+ if self.config.has_option('main', 'copyrightHolderName'):
+ copyrightHolderName = self.config.get('main', 'copyrightHolderName')
+
+ def getcfg(name, fallback=''):
+ if self.config.has_option('main', name):
+ return self.config.get('main', name)
+ else:
+ return fallback
+
+ setInfoRF(f, n, {
+ 'foundry': getcfg('foundry'),
+ 'foundryURL': getcfg('foundryURL'),
+ 'designer': getcfg('designer'),
+ 'copyrightHolderName': getcfg('copyrightHolderName'),
+ 'build': self.buildTag,
+ 'version': getcfg('version'),
+ 'license': getcfg('license'),
+ 'licenseURL': getcfg('licenseURL'),
+ })
+
+ if not self.compatible:
+ cleanCurves(f)
+ deleteGlyphs(f, self.deleteList)
+
+ log(">> Generating font files")
+ ufoName = self.generateOutputPath(f, "ufo")
+ f.save(ufoName)
+ self.generatedFonts.append(ufoName)
+
+ if self.buildOTF:
+ log(">> Generating OTF file")
+ newFont = OpenFont(ufoName)
+ otfName = self.generateOutputPath(f, "otf")
+ saveOTF(newFont, otfName, self.glyphOrder)
+
+ def generateTTFs(self):
+ """Build TTF for each font generated since last call to generateTTFs."""
+
+ fonts = [OpenFont(ufo) for ufo in self.generatedFonts]
+ self.generatedFonts = []
+
+ log(">> Converting curves to quadratic")
+ # using a slightly higher max error (e.g. 0.0025 em), dots will have
+ # fewer control points and look noticeably different
+ max_err = 0.001
+ if self.compatible:
+ fonts_to_quadratic(fonts, max_err_em=max_err, dump_stats=True, reverse_direction=True)
+ else:
+ for font in fonts:
+ fonts_to_quadratic([font], max_err_em=max_err, dump_stats=True, reverse_direction=True)
+
+ log(">> Generating TTF files")
+ for font in fonts:
+ ttfName = self.generateOutputPath(font, "ttf")
+ log(os.path.basename(ttfName))
+ saveOTF(font, ttfName, self.glyphOrder, truetype=True)
+
+
+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 swapContours(f,gName1,gName2):
+ try:
+ g1 = f[gName1]
+ g2 = f[gName2]
+ except KeyError:
+ log("swapGlyphs failed for %s %s" % (gName1, gName2))
+ return
+ g3 = g1.copy()
+
+ while g1.contours:
+ g1.removeContour(0)
+ for contour in g2.contours:
+ g1.appendContour(contour)
+ g1.width = g2.width
+
+ while g2.contours:
+ g2.removeContour(0)
+ for contour in g3.contours:
+ g2.appendContour(contour)
+ g2.width = g3.width
+
+
+def log(msg):
+ print msg
+
+
+def generateGlyphs(f, glyphNames, glyphList={}):
+ log(">> Generating diacritics")
+ glyphnames = [gname for gname in glyphNames if not gname.startswith("#") and gname != ""]
+
+ for glyphName in glyphNames:
+ generateGlyph(f, glyphName, glyphList)
+
+def cleanCurves(f):
+ log(">> Removing overlaps")
+ for g in f:
+ removeGlyphOverlap(g)
+
+ # log(">> Mitring sharp corners")
+ # for g in f:
+ # mitreGlyph(g, 3., .7)
+
+ # log(">> Converting curves to quadratic")
+ # for g in f:
+ # glyphCurvesToQuadratic(g)
+
+
+def deleteGlyphs(f, deleteList):
+ for name in deleteList:
+ if f.has_key(name):
+ f.removeGlyph(name)
+
+
+def removeGlyphOverlap(glyph):
+ """Remove overlaps in contours from a glyph."""
+ #TODO(jamesgk) verify overlaps exist first, as per library's recommendation
+ manager = BooleanOperationManager()
+ contours = glyph.contours
+ glyph.clearContours()
+ manager.union(contours, glyph.getPointPen())
+
+
+def saveOTF(font, destFile, glyphOrder, truetype=False):
+ """Save a RoboFab font as an OTF binary using ufo2fdk."""
+
+ if truetype:
+ otf = compileTTF(font, featureCompilerClass=RobotoFeatureCompiler,
+ kernWriter=RobotoKernWriter, glyphOrder=glyphOrder,
+ convertCubics=False,
+ useProductionNames=False)
+ else:
+ otf = compileOTF(font, featureCompilerClass=RobotoFeatureCompiler,
+ kernWriter=RobotoKernWriter, glyphOrder=glyphOrder,
+ useProductionNames=False)
+ otf.save(destFile)
diff --git a/misc/pylib/fontbuild/LICENSE b/misc/pylib/fontbuild/LICENSE
new file mode 100644
index 000000000..261eeb9e9
--- /dev/null
+++ b/misc/pylib/fontbuild/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/misc/pylib/fontbuild/ORIGIN.txt b/misc/pylib/fontbuild/ORIGIN.txt
new file mode 100644
index 000000000..1b0a3cf79
--- /dev/null
+++ b/misc/pylib/fontbuild/ORIGIN.txt
@@ -0,0 +1 @@
+https://github.com/google/roboto/tree/master/scripts/lib/fontbuild
diff --git a/misc/pylib/fontbuild/__init__.py b/misc/pylib/fontbuild/__init__.py
new file mode 100644
index 000000000..4ed720308
--- /dev/null
+++ b/misc/pylib/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/misc/pylib/fontbuild/alignpoints.py b/misc/pylib/fontbuild/alignpoints.py
new file mode 100644
index 000000000..f49f24d95
--- /dev/null
+++ b/misc/pylib/fontbuild/alignpoints.py
@@ -0,0 +1,173 @@
+# 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.
+
+
+import math
+
+import numpy as np
+from numpy.linalg import lstsq
+
+
+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"
+ if glyph.name is None:
+ # Never happens, but here to fix a bug in Python 2.7 with -OO
+ print ''
+ 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)
diff --git a/misc/pylib/fontbuild/anchors.py b/misc/pylib/fontbuild/anchors.py
new file mode 100644
index 000000000..a617b2f51
--- /dev/null
+++ b/misc/pylib/fontbuild/anchors.py
@@ -0,0 +1,77 @@
+# 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.
+
+
+def getGlyph(gname, font):
+ return font[gname] if font.has_key(gname) else None
+
+
+def getComponentByName(f, g, componentName):
+ for c in g.components:
+ if c.baseGlyph == componentName:
+ return c
+
+def getAnchorByName(g,anchorName):
+ for a in g.anchors:
+ if a.name == anchorName:
+ return a
+
+def moveMarkAnchors(f, g, anchorName, accentName, dx, dy):
+ if "top"==anchorName:
+ anchors = f[accentName].anchors
+ for anchor in anchors:
+ if "mkmktop_acc" == anchor.name:
+ for anc in g.anchors:
+ if anc.name == "top":
+ g.removeAnchor(anc)
+ break
+ g.appendAnchor("top", (anchor.x + int(dx), anchor.y + int(dy)))
+
+ elif anchorName in ["bottom", "bottomu"]:
+ anchors = f[accentName].anchors
+ for anchor in anchors:
+ if "mkmkbottom_acc" == anchor.name:
+ for anc in g.anchors:
+ if anc.name == "bottom":
+ g.removeAnchor(anc)
+ break
+ x = anchor.x + int(dx)
+ for anc in anchors:
+ if "top" == anc.name:
+ x = anc.x + int(dx)
+ g.appendAnchor("bottom", (x, anchor.y + int(dy)))
+
+
+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.x - a2.x, a1.y - a2.y)
+ c = getComponentByName(f, g, accentName)
+ c.offset = offset
+ moveMarkAnchors(f, g, anchorName, accentName, offset[0], offset[1])
+
+
+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/misc/pylib/fontbuild/convertCurves.py b/misc/pylib/fontbuild/convertCurves.py
new file mode 100644
index 000000000..b6efd5ca2
--- /dev/null
+++ b/misc/pylib/fontbuild/convertCurves.py
@@ -0,0 +1,102 @@
+#! /usr/bin/env python
+#
+# 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.
+
+"""
+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 robofab.objects.objectsRF import RSegment
+
+def replaceSegments(contour, segments):
+ while len(contour):
+ contour.removeSegment(0)
+ for s in segments:
+ contour.appendSegment(s.type, [(p.x, p.y) for p in s.points], s.smooth)
+
+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 cubicSegmentToQuadratic(c,sid):
+
+ segment = c[sid]
+ if (segment.type != "curve"):
+ print "Segment type not curve"
+ return
+
+ #pSegment,junk = getPrevAnchor(c,sid)
+ pSegment = c[sid-1] #assumes that a curve type will always be proceeded by another point on the same contour
+ points = convertToQuadratic(pSegment.points[-1],segment.points[0],
+ segment.points[1],segment.points[2])
+ return RSegment(
+ 'qcurve', [[int(i) for i in p] for p in points[1:]], segment.smooth)
+
+def glyphCurvesToQuadratic(g):
+
+ for c in g:
+ segments = []
+ for i in range(len(c)):
+ s = c[i]
+ if s.type == "curve":
+ try:
+ segments.append(cubicSegmentToQuadratic(c, i))
+ except Exception:
+ print g.name, i
+ raise
+ else:
+ segments.append(s)
+ replaceSegments(c, segments)
diff --git a/misc/pylib/fontbuild/curveFitPen.py b/misc/pylib/fontbuild/curveFitPen.py
new file mode 100644
index 000000000..f7c0caed9
--- /dev/null
+++ b/misc/pylib/fontbuild/curveFitPen.py
@@ -0,0 +1,422 @@
+#! /opt/local/bin/pythonw2.7
+#
+# 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.
+
+
+__all__ = ["SubsegmentPen","SubsegmentsToCurvesPen", "segmentGlyph", "fitGlyph"]
+
+
+from fontTools.pens.basePen import BasePen
+import numpy as np
+from numpy import array as v
+from numpy.linalg import norm
+from robofab.pens.adapterPens import GuessSmoothPointPen
+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)
+
+ # round values used by otherPen (a RoboFab SegmentToPointPen) to decide
+ # whether to delete duplicate points at start and end of contour
+ #TODO(jamesgk) figure out why we have to do this hack, then remove it
+ c = self.otherPen.contour
+ for i in [0, -1]:
+ c[i] = [[round(n, 5) for n in c[i][0]]] + list(c[i][1:])
+
+ 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
diff --git a/misc/pylib/fontbuild/decomposeGlyph.py b/misc/pylib/fontbuild/decomposeGlyph.py
new file mode 100644
index 000000000..0470fa60b
--- /dev/null
+++ b/misc/pylib/fontbuild/decomposeGlyph.py
@@ -0,0 +1,23 @@
+def decomposeGlyph(font, glyph):
+ """Moves the components of a glyph to its outline."""
+ if len(glyph.components):
+ deepCopyContours(font, glyph, glyph, (0, 0), (1, 1))
+ glyph.clearComponents()
+
+
+def deepCopyContours(font, parent, component, offset, scale):
+ """Copy contours to parent from component, including nested components."""
+
+ for nested in component.components:
+ deepCopyContours(
+ font, parent, font[nested.baseGlyph],
+ (offset[0] + nested.offset[0], offset[1] + nested.offset[1]),
+ (scale[0] * nested.scale[0], scale[1] * nested.scale[1]))
+
+ if component == parent:
+ return
+ for contour in component:
+ contour = contour.copy()
+ contour.scale(scale)
+ contour.move(offset)
+ parent.appendContour(contour)
diff --git a/misc/pylib/fontbuild/features.py b/misc/pylib/fontbuild/features.py
new file mode 100755
index 000000000..fe6eca012
--- /dev/null
+++ b/misc/pylib/fontbuild/features.py
@@ -0,0 +1,189 @@
+# 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.
+
+
+import re
+
+from feaTools import parser
+from feaTools.writers.fdkSyntaxWriter import FDKSyntaxFeatureWriter
+
+
+class FilterFeatureWriter(FDKSyntaxFeatureWriter):
+ """Feature writer to detect invalid references and duplicate definitions."""
+
+ def __init__(self, refs=set(), name=None, isFeature=False):
+ """Initializes the set of known references, empty by default."""
+ self.refs = refs
+ self.featureNames = set()
+ self.lookupNames = set()
+ self.tableNames = set()
+ self.languageSystems = set()
+ super(FilterFeatureWriter, self).__init__(
+ name=name, isFeature=isFeature)
+
+ # error to print when undefined reference is found in glyph class
+ self.classErr = ('Undefined reference "%s" removed from glyph class '
+ 'definition %s.')
+
+ # error to print when undefined reference is found in sub or pos rule
+ subErr = ['Substitution rule with undefined reference "%s" removed']
+ if self._name:
+ subErr.append(" from ")
+ subErr.append("feature" if self._isFeature else "lookup")
+ subErr.append(' "%s"' % self._name)
+ subErr.append(".")
+ self.subErr = "".join(subErr)
+ self.posErr = self.subErr.replace("Substitution", "Positioning")
+
+ def _subwriter(self, name, isFeature):
+ """Use this class for nested expressions e.g. in feature definitions."""
+ return FilterFeatureWriter(self.refs, name, isFeature)
+
+ def _flattenRefs(self, refs, flatRefs):
+ """Flatten a list of references."""
+ for ref in refs:
+ if type(ref) == list:
+ self._flattenRefs(ref, flatRefs)
+ elif ref != "'": # ignore contextual class markings
+ flatRefs.append(ref)
+
+ def _checkRefs(self, refs, errorMsg):
+ """Check a list of references found in a sub or pos rule."""
+ flatRefs = []
+ self._flattenRefs(refs, flatRefs)
+ for ref in flatRefs:
+ # trailing apostrophes should be ignored
+ if ref[-1] == "'":
+ ref = ref[:-1]
+ if ref not in self.refs:
+ print errorMsg % ref
+ # insert an empty instruction so that we can't end up with an
+ # empty block, which is illegal syntax
+ super(FilterFeatureWriter, self).rawText(";")
+ return False
+ return True
+
+ def classDefinition(self, name, contents):
+ """Check that contents are valid, then add name to known references."""
+ if name in self.refs:
+ return
+ newContents = []
+ for ref in contents:
+ if ref not in self.refs and ref != "-":
+ print self.classErr % (ref, name)
+ else:
+ newContents.append(ref)
+ self.refs.add(name)
+ super(FilterFeatureWriter, self).classDefinition(name, newContents)
+
+ def gsubType1(self, target, replacement):
+ """Check a sub rule with one-to-one replacement."""
+ if self._checkRefs([target, replacement], self.subErr):
+ super(FilterFeatureWriter, self).gsubType1(target, replacement)
+
+ def gsubType4(self, target, replacement):
+ """Check a sub rule with many-to-one replacement."""
+ if self._checkRefs([target, replacement], self.subErr):
+ super(FilterFeatureWriter, self).gsubType4(target, replacement)
+
+ def gsubType6(self, precedingContext, target, trailingContext, replacement):
+ """Check a sub rule with contextual replacement."""
+ refs = [precedingContext, target, trailingContext, replacement]
+ if self._checkRefs(refs, self.subErr):
+ super(FilterFeatureWriter, self).gsubType6(
+ precedingContext, target, trailingContext, replacement)
+
+ def gposType1(self, target, value):
+ """Check a single positioning rule."""
+ if self._checkRefs([target], self.posErr):
+ super(FilterFeatureWriter, self).gposType1(target, value)
+
+ def gposType2(self, target, value, needEnum=False):
+ """Check a pair positioning rule."""
+ if self._checkRefs(target, self.posErr):
+ super(FilterFeatureWriter, self).gposType2(target, value, needEnum)
+
+ # these rules may contain references, but they aren't present in Roboto
+ def gsubType3(self, target, replacement):
+ raise NotImplementedError
+
+ def feature(self, name):
+ """Adds a feature definition only once."""
+ if name not in self.featureNames:
+ self.featureNames.add(name)
+ return super(FilterFeatureWriter, self).feature(name)
+ # we must return a new writer even if we don't add it to this one
+ return FDKSyntaxFeatureWriter(name, True)
+
+ def lookup(self, name):
+ """Adds a lookup block only once."""
+ if name not in self.lookupNames:
+ self.lookupNames.add(name)
+ return super(FilterFeatureWriter, self).lookup(name)
+ # we must return a new writer even if we don't add it to this one
+ return FDKSyntaxFeatureWriter(name, False)
+
+ def languageSystem(self, langTag, scriptTag):
+ """Adds a language system instruction only once."""
+ system = (langTag, scriptTag)
+ if system not in self.languageSystems:
+ self.languageSystems.add(system)
+ super(FilterFeatureWriter, self).languageSystem(langTag, scriptTag)
+
+ def table(self, name, data):
+ """Adds a table only once."""
+ if name in self.tableNames:
+ return
+ self.tableNames.add(name)
+ self._instructions.append("table %s {" % name)
+ self._instructions.extend([" %s %s;" % line for line in data])
+ self._instructions.append("} %s;" % name)
+
+
+def compileFeatureRE(name):
+ """Compiles a feature-matching regex."""
+
+ # this is the pattern used internally by feaTools:
+ # https://github.com/typesupply/feaTools/blob/master/Lib/feaTools/parser.py
+ featureRE = list(parser.featureContentRE)
+ featureRE.insert(2, name)
+ featureRE.insert(6, name)
+ return re.compile("".join(featureRE))
+
+
+def updateFeature(font, name, value):
+ """Add a feature definition, or replace existing one."""
+ featureRE = compileFeatureRE(name)
+ if featureRE.search(font.features.text):
+ font.features.text = featureRE.sub(value, font.features.text)
+ else:
+ font.features.text += "\n" + value
+
+
+def readFeatureFile(font, text, prepend=True):
+ """Incorporate valid definitions from feature text into font."""
+ writer = FilterFeatureWriter(set(font.keys()))
+ if prepend:
+ text += font.features.text
+ else:
+ text = font.features.text + text
+ parser.parseFeatures(writer, text)
+ font.features.text = writer.write()
+
+
+def writeFeatureFile(font, path):
+ """Write the font's features to an external file."""
+ fout = open(path, "w")
+ fout.write(font.features.text)
+ fout.close()
diff --git a/misc/pylib/fontbuild/generateGlyph.py b/misc/pylib/fontbuild/generateGlyph.py
new file mode 100644
index 000000000..465f940a9
--- /dev/null
+++ b/misc/pylib/fontbuild/generateGlyph.py
@@ -0,0 +1,97 @@
+# 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.
+
+
+import re
+from string import find
+
+from anchors import alignComponentsToAnchors, getAnchorByName
+
+
+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 copyMarkAnchors(f, g, srcname, width):
+ for anchor in f[srcname].anchors:
+ if anchor.name in ("top_dd", "bottom_dd", "top0315"):
+ g.appendAnchor(anchor.name, (anchor.x + width, anchor.y))
+
+ if ("top" == anchor.name and
+ not any(a.name == "parent_top" for a in g.anchors)):
+ g.appendAnchor("parent_top", anchor.position)
+
+ if ("bottom" == anchor.name and
+ not any(a.name == "bottom" for a in g.anchors)):
+ g.appendAnchor("bottom", anchor.position)
+
+ if any(a.name == "top" for a in g.anchors):
+ return
+
+ anchor_parent_top = getAnchorByName(g, "parent_top")
+ if anchor_parent_top is not None:
+ g.appendAnchor("top", anchor_parent_top.position)
+
+
+def generateGlyph(f,gname,glyphList={}):
+ glyphName, baseName, accentNames, offset = parseComposite(gname)
+ if f.has_key(glyphName):
+ print('Existing glyph "%s" found in font, ignoring composition rule '
+ '"%s"' % (glyphName, gname))
+ return
+
+ if baseName.find("_") != -1:
+ g = f.newGlyph(glyphName)
+ for componentName in baseName.split("_"):
+ g.appendComponent(componentName, (g.width, 0))
+ g.width += f[componentName].width
+ setUnicodeValue(g, glyphList)
+
+ else:
+ try:
+ f.compileGlyph(glyphName, baseName, accentNames)
+ except KeyError as e:
+ print('KeyError raised for composition rule "%s", likely "%s" '
+ 'anchor not found in glyph "%s"' % (gname, e, baseName))
+ return
+ g = f[glyphName]
+ setUnicodeValue(g, glyphList)
+ copyMarkAnchors(f, g, baseName, offset[1] + offset[0])
+ if len(accentNames) > 0:
+ alignComponentsToAnchors(f, glyphName, baseName, accentNames)
+ if offset[0] != 0 or offset[1] != 0:
+ g.width += offset[1] + offset[0]
+ g.move((offset[0], 0), anchors=False)
+
+
+def setUnicodeValue(glyph, glyphList):
+ """Try to ensure glyph has a unicode value -- used by FDK to make OTFs."""
+
+ if glyph.name in glyphList:
+ glyph.unicode = int(glyphList[glyph.name], 16)
+ else:
+ uvNameMatch = re.match("uni([\dA-F]{4})$", glyph.name)
+ if uvNameMatch:
+ glyph.unicode = int(uvNameMatch.group(1), 16)
diff --git a/misc/pylib/fontbuild/instanceNames.py b/misc/pylib/fontbuild/instanceNames.py
new file mode 100644
index 000000000..cf87ba719
--- /dev/null
+++ b/misc/pylib/fontbuild/instanceNames.py
@@ -0,0 +1,232 @@
+# 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 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 = ""
+ foundryURL = ""
+ copyrightHolderName = ""
+ build = ""
+ version = "1.0"
+ year = date.today().year
+ designer = ""
+ designerURL = ""
+ license = ""
+ licenseURL = ""
+
+ def __init__(self,names):
+ if type(names) == type(" "):
+ names = names.split("/")
+ #print names
+ 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.longstyle.split()[0])
+ else:
+ 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
+ if len(self.copyrightHolderName) > 0:
+ f.info.copyright = "Copyright %s %s" % (self.year, self.copyrightHolderName)
+ f.info.trademark = "%s is a trademark of %s." %(self.longfamily, self.foundry.rstrip('.'))
+
+ if len(self.designer) > 0:
+ f.info.openTypeNameDesigner = self.designer
+ if len(self.designerURL) > 0:
+ f.info.openTypeNameDesignerURL = self.designerURL
+ f.info.openTypeNameManufacturer = self.foundry
+ f.info.openTypeNameManufacturerURL = self.foundryURL
+ f.info.openTypeNameLicense = self.license
+ f.info.openTypeNameLicenseURL = self.licenseURL
+ f.info.openTypeNameVersion = "Version %i.%i" %(version, versionMinor)
+
+ if self.build is not None and len(self.build):
+ f.info.openTypeNameUniqueID = "%s:%s:%s" %(self.fullname, self.build, self.year)
+ else:
+ f.info.openTypeNameUniqueID = "%s:%s" %(self.fullname, 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.openTypeOS2WeightClass = self._getWeightCode(self.weight)
+ f.info.macintoshFONDName = re.sub(' ','',self.longfamily) + " " + re.sub(' ','',self.longstyle)
+ f.info.postscriptFontName = f.info.macintoshFONDName.replace(" ", "-")
+ if self.italic:
+ f.info.italicAngle = -12.0
+
+
+ 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
+ if len(self.italic):
+ flFont.italic_angle = -12
+
+ 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) ))
+ copyrightHolderName = self.copyrightHolderName if len(self.copyrightHolderName) > 0 else self.foundry
+ fn.append(NameRecord(0,1,0,0, "Copyright %s %s" %(self.year, copyrightHolderName) ))
+ fn.append(NameRecord(0,3,1,1033, "Copyright %s %s" %(self.year, copyrightHolderName) ))
+ 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(3,1,0,0, "%s:%s:%s" %(self.foundry, self.fullname, self.year) ))
+ fn.append(NameRecord(3,3,1,1033, "%s:%s:%s" %(self.foundry, self.fullname, self.year) ))
+ fn.append(NameRecord(4,1,0,0, self.fullname ))
+ fn.append(NameRecord(4,3,1,1033, self.fullname ))
+ if len(self.build) > 0:
+ 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) ))
+ else:
+ fn.append(NameRecord(5,1,0,0, "Version %s; %s" %(self.version, self.year) ))
+ fn.append(NameRecord(5,3,1,1033, "Version %s; %s" %(self.version, 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.foundryURL ))
+ fn.append(NameRecord(11,3,1,1033, self.foundryURL ))
+ 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|Black|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):
+ #print "shortstyle:", self.shortstyle
+ 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 900
+ elif weight == "Fat":
+ return 900
+
+ return 400
+
+def setNames(f,names,foundry="",version="1.0",build=""):
+ InstanceNames.foundry = foundry
+ InstanceNames.version = version
+ InstanceNames.build = build
+ i = InstanceNames(names)
+ i.setFLNames(f)
+
+
+def setInfoRF(f, names, attrs={}):
+ i = InstanceNames(names)
+ version, versionMinor = (1, 0)
+ for k,v in attrs.iteritems():
+ if k == 'version':
+ if v.find('.') != -1:
+ version, versionMinor = [int(num) for num in v.split(".")]
+ else:
+ version = int(v)
+ setattr(i, k, v)
+ i.setRFNames(f, version=version, versionMinor=versionMinor)
diff --git a/misc/pylib/fontbuild/italics.py b/misc/pylib/fontbuild/italics.py
new file mode 100644
index 000000000..91e658c74
--- /dev/null
+++ b/misc/pylib/fontbuild/italics.py
@@ -0,0 +1,308 @@
+# 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.
+
+
+import math
+
+from fontTools.misc.transform import Transform
+import numpy as np
+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, whiten
+
+from fontbuild.alignpoints import alignCorners
+from fontbuild.curveFitPen import fitGlyph, segmentGlyph
+
+
+def italicizeGlyph(f, g, angle=10, stemWidth=185, meanYCenter=-825, narrowAmount=1):
+ unic = g.unicode #save unicode
+
+ 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.
+ # meanYCenter:
+ # -600 is a magic number that assumes a 2048 unit em square,
+ # and -825 for a 2816 unit em square. (UPM*0.29296875)
+ m = Transform(1, 0, slope, 1, 0, 0)
+ xoffset, junk = m.transformPoint((0, meanYCenter))
+ m = Transform(narrowAmount, 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)
+
+ if unic > 0xFFFF: #restore unicode
+ g.unicode = unic
+
+
+def italicize(glyph, angle=12, stemWidth=180, xoffset=-50):
+ CURVE_CORRECTION_WEIGHT = .03
+ CORNER_WEIGHT = 10
+
+ # decompose the glyph into smaller segments
+ 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
+
+ # create a transform for italicizing
+ 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)
+
+ # apply the transform
+ out = outCorrected + (centerSkew - center)
+ out[:,1] = outCorrected[:,1]
+
+ # make some corrections
+ 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
+
+ # recompose the glyph into original segments
+ return fitGlyph(glyph, gOut, subsegments)
+
+
+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]
+
+
+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)
+
+
+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
diff --git a/misc/pylib/fontbuild/markFeature.py b/misc/pylib/fontbuild/markFeature.py
new file mode 100755
index 000000000..42cafe4c7
--- /dev/null
+++ b/misc/pylib/fontbuild/markFeature.py
@@ -0,0 +1,55 @@
+# 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 ufo2ft.kernFeatureWriter import KernFeatureWriter
+from ufo2ft.makeotfParts import FeatureOTFCompiler
+
+
+class RobotoFeatureCompiler(FeatureOTFCompiler):
+ def precompile(self):
+ self.overwriteFeatures = True
+
+ def setupAnchorPairs(self):
+ self.anchorPairs = [
+ ["top", "_marktop"],
+ ["bottom", "_markbottom"],
+ ["top_dd", "_marktop_dd"],
+ ["bottom_dd", "_markbottom_dd"],
+ ["rhotichook", "_markrhotichook"],
+ ["top0315", "_marktop0315"],
+ ["parent_top", "_markparent_top"],
+ ["parenthesses.w1", "_markparenthesses.w1"],
+ ["parenthesses.w2", "_markparenthesses.w2"],
+ ["parenthesses.w3", "_markparenthesses.w3"]]
+
+ self.mkmkAnchorPairs = [
+ ["mkmktop", "_marktop"],
+ ["mkmkbottom_acc", "_markbottom"],
+
+ # By providing a pair with accent anchor _bottom and no base anchor,
+ # we designate all glyphs with _bottom as accents (so that they will
+ # be used as base glyphs for mkmk features) without generating any
+ # positioning rules actually using this anchor (which is instead
+ # used to generate composite glyphs). This is all for consistency
+ # with older roboto versions.
+ ["", "_bottom"],
+ ]
+
+ self.ligaAnchorPairs = []
+
+
+class RobotoKernWriter(KernFeatureWriter):
+ leftFeaClassRe = r"@_(.+)_L$"
+ rightFeaClassRe = r"@_(.+)_R$"
diff --git a/misc/pylib/fontbuild/mitreGlyph.py b/misc/pylib/fontbuild/mitreGlyph.py
new file mode 100644
index 000000000..d0834ed84
--- /dev/null
+++ b/misc/pylib/fontbuild/mitreGlyph.py
@@ -0,0 +1,111 @@
+# 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.
+
+
+"""Mitre Glyph:
+
+mitreSize : Length of the segment created by the mitre. The default is 4.
+maxAngle : Maximum angle in radians at which segments will be mitred. The default is .9 (about 50 degrees).
+ Works for both inside and outside angles
+
+"""
+
+import math
+from robofab.objects.objectsRF import RPoint, RSegment
+from fontbuild.convertCurves import replaceSegments
+
+def getTangents(contours):
+ tmap = []
+ for c in contours:
+ clen = len(c)
+ for i in range(clen):
+ s = c[i]
+ p = s.points[-1]
+ ns = c[(i + 1) % clen]
+ ps = c[(clen + i - 1) % clen]
+ np = ns.points[1] if ns.type == 'curve' else ns.points[-1]
+ pp = s.points[2] if s.type == 'curve' else ps.points[-1]
+ tmap.append((pp - p, np - p))
+ return tmap
+
+def normalizeVector(p):
+ m = getMagnitude(p);
+ if m != 0:
+ return p*(1/m)
+ else:
+ return RPoint(0,0)
+
+def getMagnitude(p):
+ return math.sqrt(p.x*p.x + p.y*p.y)
+
+def getDistance(v1,v2):
+ return getMagnitude(RPoint(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 = RPoint(round(v1.x * radius), round(v1.y * radius))
+ offset2 = RPoint(round(v2.x * radius), round(v2.y * radius))
+ return offset1, offset2
+
+def mitreGlyph(g,mitreSize,maxAngle):
+ if g == None:
+ return
+
+ tangents = getTangents(g.contours)
+ sid = -1
+ for c in g.contours:
+ segments = []
+ needsMitring = False
+ for s in c:
+ sid += 1
+ v1, v2 = tangents[sid]
+ off = getMitreOffset(s,v1,v2,mitreSize,maxAngle)
+ s1 = s.copy()
+ if off != None:
+ offset1, offset2 = off
+ p2 = s.points[-1] + offset2
+ s2 = RSegment('line', [(p2.x, p2.y)])
+ s1.points[0] += offset1
+ segments.append(s1)
+ segments.append(s2)
+ needsMitring = True
+ else:
+ segments.append(s1)
+ if needsMitring:
+ replaceSegments(c, segments)
diff --git a/misc/pylib/fontbuild/mix.py b/misc/pylib/fontbuild/mix.py
new file mode 100644
index 000000000..5e5388b3e
--- /dev/null
+++ b/misc/pylib/fontbuild/mix.py
@@ -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 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