Initial public commit

This commit is contained in:
Rasmus Andersson 2017-08-22 00:05:20 -07:00
commit 3b1fffade1
6648 changed files with 363948 additions and 0 deletions

View file

@ -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)

View file

@ -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.

View file

@ -0,0 +1 @@
https://github.com/google/roboto/tree/master/scripts/lib/fontbuild

View file

@ -0,0 +1,6 @@
"""
fontbuild
A collection of font production tools written for FontLab
"""
version = "0.1"

View file

@ -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)

View file

@ -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])

View file

@ -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)

View file

@ -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

View file

@ -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)

189
misc/pylib/fontbuild/features.py Executable file
View file

@ -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()

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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$"

View file

@ -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)

360
misc/pylib/fontbuild/mix.py Normal file
View file

@ -0,0 +1,360 @@
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from numpy import array, append
import copy
import json
from robofab.objects.objectsRF import RPoint, RGlyph
from robofab.world import OpenFont
from decomposeGlyph import decomposeGlyph
class FFont:
"Font wrapper for floating point operations"
def __init__(self,f=None):
self.glyphs = {}
self.hstems = []
self.vstems = []
self.kerning = {}
if isinstance(f,FFont):
#self.glyphs = [g.copy() for g in f.glyphs]
for key,g in f.glyphs.iteritems():
self.glyphs[key] = g.copy()
self.hstems = list(f.hstems)
self.vstems = list(f.vstems)
self.kerning = dict(f.kerning)
elif f != None:
self.copyFromFont(f)
def copyFromFont(self, f):
for g in f:
self.glyphs[g.name] = FGlyph(g)
self.hstems = [s for s in f.info.postscriptStemSnapH]
self.vstems = [s for s in f.info.postscriptStemSnapV]
self.kerning = f.kerning.asDict()
def copyToFont(self, f):
for g in f:
try:
gF = self.glyphs[g.name]
gF.copyToGlyph(g)
except:
print "Copy to glyph failed for" + g.name
f.info.postscriptStemSnapH = self.hstems
f.info.postscriptStemSnapV = self.vstems
for pair in self.kerning:
f.kerning[pair] = self.kerning[pair]
def getGlyph(self, gname):
try:
return self.glyphs[gname]
except:
return None
def setGlyph(self, gname, glyph):
self.glyphs[gname] = glyph
def addDiff(self,b,c):
newFont = FFont(self)
for key,g in newFont.glyphs.iteritems():
gB = b.getGlyph(key)
gC = c.getGlyph(key)
try:
newFont.glyphs[key] = g.addDiff(gB,gC)
except:
print "Add diff failed for '%s'" %key
return newFont
class FGlyph:
"provides a temporary floating point compatible glyph data structure"
def __init__(self, g=None):
self.contours = []
self.width = 0.
self.components = []
self.anchors = []
if g != None:
self.copyFromGlyph(g)
def copyFromGlyph(self,g):
self.name = g.name
valuesX = []
valuesY = []
self.width = len(valuesX)
valuesX.append(g.width)
for c in g.components:
self.components.append((len(valuesX), len(valuesY)))
valuesX.append(c.scale[0])
valuesY.append(c.scale[1])
valuesX.append(c.offset[0])
valuesY.append(c.offset[1])
for a in g.anchors:
self.anchors.append((len(valuesX), len(valuesY)))
valuesX.append(a.x)
valuesY.append(a.y)
for i in range(len(g)):
self.contours.append([])
for j in range (len(g[i].points)):
self.contours[i].append((len(valuesX), len(valuesY)))
valuesX.append(g[i].points[j].x)
valuesY.append(g[i].points[j].y)
self.dataX = array(valuesX, dtype=float)
self.dataY = array(valuesY, dtype=float)
def copyToGlyph(self,g):
g.width = self._derefX(self.width)
if len(g.components) == len(self.components):
for i in range(len(self.components)):
g.components[i].scale = (self._derefX(self.components[i][0] + 0, asInt=False),
self._derefY(self.components[i][1] + 0, asInt=False))
g.components[i].offset = (self._derefX(self.components[i][0] + 1),
self._derefY(self.components[i][1] + 1))
if len(g.anchors) == len(self.anchors):
for i in range(len(self.anchors)):
g.anchors[i].x = self._derefX( self.anchors[i][0])
g.anchors[i].y = self._derefY( self.anchors[i][1])
for i in range(len(g)) :
for j in range (len(g[i].points)):
g[i].points[j].x = self._derefX(self.contours[i][j][0])
g[i].points[j].y = self._derefY(self.contours[i][j][1])
def isCompatible(self, g):
return (len(self.dataX) == len(g.dataX) and
len(self.dataY) == len(g.dataY) and
len(g.contours) == len(self.contours))
def __add__(self,g):
if self.isCompatible(g):
newGlyph = self.copy()
newGlyph.dataX = self.dataX + g.dataX
newGlyph.dataY = self.dataY + g.dataY
return newGlyph
else:
print "Add failed for '%s'" %(self.name)
raise Exception
def __sub__(self,g):
if self.isCompatible(g):
newGlyph = self.copy()
newGlyph.dataX = self.dataX - g.dataX
newGlyph.dataY = self.dataY - g.dataY
return newGlyph
else:
print "Subtract failed for '%s'" %(self.name)
raise Exception
def __mul__(self,scalar):
newGlyph = self.copy()
newGlyph.dataX = self.dataX * scalar
newGlyph.dataY = self.dataY * scalar
return newGlyph
def scaleX(self,scalar):
newGlyph = self.copy()
if len(self.dataX) > 0:
newGlyph.dataX = self.dataX * scalar
for i in range(len(newGlyph.components)):
newGlyph.dataX[newGlyph.components[i][0]] = self.dataX[newGlyph.components[i][0]]
return newGlyph
def shift(self,ammount):
newGlyph = self.copy()
newGlyph.dataX = self.dataX + ammount
for i in range(len(newGlyph.components)):
newGlyph.dataX[newGlyph.components[i][0]] = self.dataX[newGlyph.components[i][0]]
return newGlyph
def interp(self, g, v):
gF = self.copy()
if not self.isCompatible(g):
print "Interpolate failed for '%s'; outlines incompatible" %(self.name)
raise Exception
gF.dataX += (g.dataX - gF.dataX) * v.x
gF.dataY += (g.dataY - gF.dataY) * v.y
return gF
def copy(self):
ng = FGlyph()
ng.contours = list(self.contours)
ng.width = self.width
ng.components = list(self.components)
ng.anchors = list(self.anchors)
ng.dataX = self.dataX.copy()
ng.dataY = self.dataY.copy()
ng.name = self.name
return ng
def _derefX(self,id, asInt=True):
val = self.dataX[id]
return int(round(val)) if asInt else val
def _derefY(self,id, asInt=True):
val = self.dataY[id]
return int(round(val)) if asInt else val
def addDiff(self,gB,gC):
newGlyph = self + (gB - gC)
return newGlyph
class Master:
def __init__(self, font=None, v=0, kernlist=None, overlay=None):
if isinstance(font, FFont):
self.font = None
self.ffont = font
elif isinstance(font,str):
self.openFont(font,overlay)
elif isinstance(font,Mix):
self.font = font
else:
self.font = font
self.ffont = FFont(font)
if isinstance(v,float) or isinstance(v,int):
self.v = RPoint(v, v)
else:
self.v = v
if kernlist != None:
kerns = [i.strip().split() for i in open(kernlist).readlines()]
self.kernlist = [{'left':k[0], 'right':k[1], 'value': k[2]}
for k in kerns
if not k[0].startswith("#")
and not k[0] == ""]
#TODO implement class based kerning / external kerning file
def openFont(self, path, overlayPath=None):
self.font = OpenFont(path)
for g in self.font:
size = len(g)
csize = len(g.components)
if (size > 0 and csize > 0):
decomposeGlyph(self.font, self.font[g.name])
if overlayPath != None:
overlayFont = OpenFont(overlayPath)
font = self.font
for overlayGlyph in overlayFont:
font.insertGlyph(overlayGlyph)
self.ffont = FFont(self.font)
class Mix:
def __init__(self,masters,v):
self.masters = masters
if isinstance(v,float) or isinstance(v,int):
self.v = RPoint(v,v)
else:
self.v = v
def getFGlyph(self, master, gname):
if isinstance(master.font, Mix):
return 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