Initial public commit
This commit is contained in:
commit
3b1fffade1
6648 changed files with 363948 additions and 0 deletions
300
misc/pylib/fontbuild/Build.py
Normal file
300
misc/pylib/fontbuild/Build.py
Normal 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)
|
||||
201
misc/pylib/fontbuild/LICENSE
Normal file
201
misc/pylib/fontbuild/LICENSE
Normal 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.
|
||||
1
misc/pylib/fontbuild/ORIGIN.txt
Normal file
1
misc/pylib/fontbuild/ORIGIN.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
https://github.com/google/roboto/tree/master/scripts/lib/fontbuild
|
||||
6
misc/pylib/fontbuild/__init__.py
Normal file
6
misc/pylib/fontbuild/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"""
|
||||
fontbuild
|
||||
|
||||
A collection of font production tools written for FontLab
|
||||
"""
|
||||
version = "0.1"
|
||||
173
misc/pylib/fontbuild/alignpoints.py
Normal file
173
misc/pylib/fontbuild/alignpoints.py
Normal 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)
|
||||
77
misc/pylib/fontbuild/anchors.py
Normal file
77
misc/pylib/fontbuild/anchors.py
Normal 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])
|
||||
|
||||
102
misc/pylib/fontbuild/convertCurves.py
Normal file
102
misc/pylib/fontbuild/convertCurves.py
Normal 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)
|
||||
422
misc/pylib/fontbuild/curveFitPen.py
Normal file
422
misc/pylib/fontbuild/curveFitPen.py
Normal 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
|
||||
23
misc/pylib/fontbuild/decomposeGlyph.py
Normal file
23
misc/pylib/fontbuild/decomposeGlyph.py
Normal 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
189
misc/pylib/fontbuild/features.py
Executable 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()
|
||||
97
misc/pylib/fontbuild/generateGlyph.py
Normal file
97
misc/pylib/fontbuild/generateGlyph.py
Normal 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)
|
||||
232
misc/pylib/fontbuild/instanceNames.py
Normal file
232
misc/pylib/fontbuild/instanceNames.py
Normal 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)
|
||||
308
misc/pylib/fontbuild/italics.py
Normal file
308
misc/pylib/fontbuild/italics.py
Normal 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
|
||||
55
misc/pylib/fontbuild/markFeature.py
Executable file
55
misc/pylib/fontbuild/markFeature.py
Executable 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$"
|
||||
111
misc/pylib/fontbuild/mitreGlyph.py
Normal file
111
misc/pylib/fontbuild/mitreGlyph.py
Normal 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
360
misc/pylib/fontbuild/mix.py
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
# Copyright 2015 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
from numpy import array, append
|
||||
import copy
|
||||
import json
|
||||
from robofab.objects.objectsRF import RPoint, RGlyph
|
||||
from robofab.world import OpenFont
|
||||
from decomposeGlyph import decomposeGlyph
|
||||
|
||||
|
||||
class FFont:
|
||||
"Font wrapper for floating point operations"
|
||||
|
||||
def __init__(self,f=None):
|
||||
self.glyphs = {}
|
||||
self.hstems = []
|
||||
self.vstems = []
|
||||
self.kerning = {}
|
||||
if isinstance(f,FFont):
|
||||
#self.glyphs = [g.copy() for g in f.glyphs]
|
||||
for key,g in f.glyphs.iteritems():
|
||||
self.glyphs[key] = g.copy()
|
||||
self.hstems = list(f.hstems)
|
||||
self.vstems = list(f.vstems)
|
||||
self.kerning = dict(f.kerning)
|
||||
elif f != None:
|
||||
self.copyFromFont(f)
|
||||
|
||||
def copyFromFont(self, f):
|
||||
for g in f:
|
||||
self.glyphs[g.name] = FGlyph(g)
|
||||
self.hstems = [s for s in f.info.postscriptStemSnapH]
|
||||
self.vstems = [s for s in f.info.postscriptStemSnapV]
|
||||
self.kerning = f.kerning.asDict()
|
||||
|
||||
|
||||
def copyToFont(self, f):
|
||||
for g in f:
|
||||
try:
|
||||
gF = self.glyphs[g.name]
|
||||
gF.copyToGlyph(g)
|
||||
except:
|
||||
print "Copy to glyph failed for" + g.name
|
||||
f.info.postscriptStemSnapH = self.hstems
|
||||
f.info.postscriptStemSnapV = self.vstems
|
||||
for pair in self.kerning:
|
||||
f.kerning[pair] = self.kerning[pair]
|
||||
|
||||
def getGlyph(self, gname):
|
||||
try:
|
||||
return self.glyphs[gname]
|
||||
except:
|
||||
return None
|
||||
|
||||
def setGlyph(self, gname, glyph):
|
||||
self.glyphs[gname] = glyph
|
||||
|
||||
def addDiff(self,b,c):
|
||||
newFont = FFont(self)
|
||||
for key,g in newFont.glyphs.iteritems():
|
||||
gB = b.getGlyph(key)
|
||||
gC = c.getGlyph(key)
|
||||
try:
|
||||
newFont.glyphs[key] = g.addDiff(gB,gC)
|
||||
except:
|
||||
print "Add diff failed for '%s'" %key
|
||||
return newFont
|
||||
|
||||
class FGlyph:
|
||||
"provides a temporary floating point compatible glyph data structure"
|
||||
|
||||
def __init__(self, g=None):
|
||||
self.contours = []
|
||||
self.width = 0.
|
||||
self.components = []
|
||||
self.anchors = []
|
||||
if g != None:
|
||||
self.copyFromGlyph(g)
|
||||
|
||||
def copyFromGlyph(self,g):
|
||||
self.name = g.name
|
||||
valuesX = []
|
||||
valuesY = []
|
||||
self.width = len(valuesX)
|
||||
valuesX.append(g.width)
|
||||
for c in g.components:
|
||||
self.components.append((len(valuesX), len(valuesY)))
|
||||
valuesX.append(c.scale[0])
|
||||
valuesY.append(c.scale[1])
|
||||
valuesX.append(c.offset[0])
|
||||
valuesY.append(c.offset[1])
|
||||
|
||||
for a in g.anchors:
|
||||
self.anchors.append((len(valuesX), len(valuesY)))
|
||||
valuesX.append(a.x)
|
||||
valuesY.append(a.y)
|
||||
|
||||
for i in range(len(g)):
|
||||
self.contours.append([])
|
||||
for j in range (len(g[i].points)):
|
||||
self.contours[i].append((len(valuesX), len(valuesY)))
|
||||
valuesX.append(g[i].points[j].x)
|
||||
valuesY.append(g[i].points[j].y)
|
||||
|
||||
self.dataX = array(valuesX, dtype=float)
|
||||
self.dataY = array(valuesY, dtype=float)
|
||||
|
||||
def copyToGlyph(self,g):
|
||||
g.width = self._derefX(self.width)
|
||||
if len(g.components) == len(self.components):
|
||||
for i in range(len(self.components)):
|
||||
g.components[i].scale = (self._derefX(self.components[i][0] + 0, asInt=False),
|
||||
self._derefY(self.components[i][1] + 0, asInt=False))
|
||||
g.components[i].offset = (self._derefX(self.components[i][0] + 1),
|
||||
self._derefY(self.components[i][1] + 1))
|
||||
if len(g.anchors) == len(self.anchors):
|
||||
for i in range(len(self.anchors)):
|
||||
g.anchors[i].x = self._derefX( self.anchors[i][0])
|
||||
g.anchors[i].y = self._derefY( self.anchors[i][1])
|
||||
for i in range(len(g)) :
|
||||
for j in range (len(g[i].points)):
|
||||
g[i].points[j].x = self._derefX(self.contours[i][j][0])
|
||||
g[i].points[j].y = self._derefY(self.contours[i][j][1])
|
||||
|
||||
def isCompatible(self, g):
|
||||
return (len(self.dataX) == len(g.dataX) and
|
||||
len(self.dataY) == len(g.dataY) and
|
||||
len(g.contours) == len(self.contours))
|
||||
|
||||
def __add__(self,g):
|
||||
if self.isCompatible(g):
|
||||
newGlyph = self.copy()
|
||||
newGlyph.dataX = self.dataX + g.dataX
|
||||
newGlyph.dataY = self.dataY + g.dataY
|
||||
return newGlyph
|
||||
else:
|
||||
print "Add failed for '%s'" %(self.name)
|
||||
raise Exception
|
||||
|
||||
def __sub__(self,g):
|
||||
if self.isCompatible(g):
|
||||
newGlyph = self.copy()
|
||||
newGlyph.dataX = self.dataX - g.dataX
|
||||
newGlyph.dataY = self.dataY - g.dataY
|
||||
return newGlyph
|
||||
else:
|
||||
print "Subtract failed for '%s'" %(self.name)
|
||||
raise Exception
|
||||
|
||||
def __mul__(self,scalar):
|
||||
newGlyph = self.copy()
|
||||
newGlyph.dataX = self.dataX * scalar
|
||||
newGlyph.dataY = self.dataY * scalar
|
||||
return newGlyph
|
||||
|
||||
def scaleX(self,scalar):
|
||||
newGlyph = self.copy()
|
||||
if len(self.dataX) > 0:
|
||||
newGlyph.dataX = self.dataX * scalar
|
||||
for i in range(len(newGlyph.components)):
|
||||
newGlyph.dataX[newGlyph.components[i][0]] = self.dataX[newGlyph.components[i][0]]
|
||||
return newGlyph
|
||||
|
||||
def shift(self,ammount):
|
||||
newGlyph = self.copy()
|
||||
newGlyph.dataX = self.dataX + ammount
|
||||
for i in range(len(newGlyph.components)):
|
||||
newGlyph.dataX[newGlyph.components[i][0]] = self.dataX[newGlyph.components[i][0]]
|
||||
return newGlyph
|
||||
|
||||
def interp(self, g, v):
|
||||
gF = self.copy()
|
||||
if not self.isCompatible(g):
|
||||
print "Interpolate failed for '%s'; outlines incompatible" %(self.name)
|
||||
raise Exception
|
||||
|
||||
gF.dataX += (g.dataX - gF.dataX) * v.x
|
||||
gF.dataY += (g.dataY - gF.dataY) * v.y
|
||||
return gF
|
||||
|
||||
def copy(self):
|
||||
ng = FGlyph()
|
||||
ng.contours = list(self.contours)
|
||||
ng.width = self.width
|
||||
ng.components = list(self.components)
|
||||
ng.anchors = list(self.anchors)
|
||||
ng.dataX = self.dataX.copy()
|
||||
ng.dataY = self.dataY.copy()
|
||||
ng.name = self.name
|
||||
return ng
|
||||
|
||||
def _derefX(self,id, asInt=True):
|
||||
val = self.dataX[id]
|
||||
return int(round(val)) if asInt else val
|
||||
|
||||
def _derefY(self,id, asInt=True):
|
||||
val = self.dataY[id]
|
||||
return int(round(val)) if asInt else val
|
||||
|
||||
def addDiff(self,gB,gC):
|
||||
newGlyph = self + (gB - gC)
|
||||
return newGlyph
|
||||
|
||||
|
||||
|
||||
class Master:
|
||||
|
||||
def __init__(self, font=None, v=0, kernlist=None, overlay=None):
|
||||
if isinstance(font, FFont):
|
||||
self.font = None
|
||||
self.ffont = font
|
||||
elif isinstance(font,str):
|
||||
self.openFont(font,overlay)
|
||||
elif isinstance(font,Mix):
|
||||
self.font = font
|
||||
else:
|
||||
self.font = font
|
||||
self.ffont = FFont(font)
|
||||
if isinstance(v,float) or isinstance(v,int):
|
||||
self.v = RPoint(v, v)
|
||||
else:
|
||||
self.v = v
|
||||
if kernlist != None:
|
||||
kerns = [i.strip().split() for i in open(kernlist).readlines()]
|
||||
|
||||
self.kernlist = [{'left':k[0], 'right':k[1], 'value': k[2]}
|
||||
for k in kerns
|
||||
if not k[0].startswith("#")
|
||||
and not k[0] == ""]
|
||||
#TODO implement class based kerning / external kerning file
|
||||
|
||||
def openFont(self, path, overlayPath=None):
|
||||
self.font = OpenFont(path)
|
||||
for g in self.font:
|
||||
size = len(g)
|
||||
csize = len(g.components)
|
||||
if (size > 0 and csize > 0):
|
||||
decomposeGlyph(self.font, self.font[g.name])
|
||||
|
||||
if overlayPath != None:
|
||||
overlayFont = OpenFont(overlayPath)
|
||||
font = self.font
|
||||
for overlayGlyph in overlayFont:
|
||||
font.insertGlyph(overlayGlyph)
|
||||
|
||||
self.ffont = FFont(self.font)
|
||||
|
||||
|
||||
class Mix:
|
||||
def __init__(self,masters,v):
|
||||
self.masters = masters
|
||||
if isinstance(v,float) or isinstance(v,int):
|
||||
self.v = RPoint(v,v)
|
||||
else:
|
||||
self.v = v
|
||||
|
||||
def getFGlyph(self, master, gname):
|
||||
if isinstance(master.font, Mix):
|
||||
return font.mixGlyphs(gname)
|
||||
return master.ffont.getGlyph(gname)
|
||||
|
||||
def getGlyphMasters(self,gname):
|
||||
masters = self.masters
|
||||
if len(masters) <= 2:
|
||||
return self.getFGlyph(masters[0], gname), self.getFGlyph(masters[-1], gname)
|
||||
|
||||
def generateFFont(self):
|
||||
ffont = FFont(self.masters[0].ffont)
|
||||
for key,g in ffont.glyphs.iteritems():
|
||||
ffont.glyphs[key] = self.mixGlyphs(key)
|
||||
ffont.kerning = self.mixKerns()
|
||||
return ffont
|
||||
|
||||
def generateFont(self, baseFont):
|
||||
newFont = baseFont.copy()
|
||||
#self.mixStems(newFont) todo _ fix stems code
|
||||
for g in newFont:
|
||||
gF = self.mixGlyphs(g.name)
|
||||
if gF == None:
|
||||
g.mark = True
|
||||
elif isinstance(gF, RGlyph):
|
||||
newFont[g.name] = gF.copy()
|
||||
else:
|
||||
gF.copyToGlyph(g)
|
||||
|
||||
newFont.kerning.clear()
|
||||
newFont.kerning.update(self.mixKerns() or {})
|
||||
return newFont
|
||||
|
||||
def mixGlyphs(self,gname):
|
||||
gA,gB = self.getGlyphMasters(gname)
|
||||
try:
|
||||
return gA.interp(gB,self.v)
|
||||
except:
|
||||
print "mixglyph failed for %s" %(gname)
|
||||
if gA != None:
|
||||
return gA.copy()
|
||||
|
||||
def getKerning(self, master):
|
||||
if isinstance(master.font, Mix):
|
||||
return master.font.mixKerns()
|
||||
return master.ffont.kerning
|
||||
|
||||
def mixKerns(self):
|
||||
masters = self.masters
|
||||
kA, kB = self.getKerning(masters[0]), self.getKerning(masters[-1])
|
||||
return interpolateKerns(kA, kB, self.v)
|
||||
|
||||
|
||||
def narrowFLGlyph(g, gThin, factor=.75):
|
||||
gF = FGlyph(g)
|
||||
if not isinstance(gThin,FGlyph):
|
||||
gThin = FGlyph(gThin)
|
||||
gCondensed = gThin.scaleX(factor)
|
||||
try:
|
||||
gNarrow = gF + (gCondensed - gThin)
|
||||
gNarrow.copyToGlyph(g)
|
||||
except:
|
||||
print "No dice for: " + g.name
|
||||
|
||||
def interpolate(a,b,v,e=0):
|
||||
if e == 0:
|
||||
return a+(b-a)*v
|
||||
qe = (b-a)*v*v*v + a #cubic easing
|
||||
le = a+(b-a)*v # linear easing
|
||||
return le + (qe-le) * e
|
||||
|
||||
def interpolateKerns(kA, kB, v):
|
||||
# to yield correct kerning for Roboto output, we must emulate the behavior
|
||||
# of old versions of this code; namely, take the kerning values of the first
|
||||
# master instead of actually interpolating.
|
||||
# old code:
|
||||
# https://github.com/google/roboto/blob/7f083ac31241cc86d019ea6227fa508b9fcf39a6/scripts/lib/fontbuild/mix.py
|
||||
# bug:
|
||||
# https://github.com/google/roboto/issues/213
|
||||
# return dict(kA)
|
||||
|
||||
kerns = {}
|
||||
for pair, val in kA.items():
|
||||
kerns[pair] = interpolate(val, kB.get(pair, 0), v.x)
|
||||
for pair, val in kB.items():
|
||||
lerped_val = interpolate(val, kA.get(pair, 0), 1 - v.x)
|
||||
if pair in kerns:
|
||||
assert abs(kerns[pair] - lerped_val) < 1e-6
|
||||
else:
|
||||
kerns[pair] = lerped_val
|
||||
return kerns
|
||||
Reference in a new issue