Cython is used to compile some hot paths into native Python extensions. These hot paths were identified through running ufocompile with the hotshot profiler and then converting file by file to Cython, starting with the "hottest" paths and continuing until returns were deminishing. This means that only a few Python files were converted to Cython. Closes #23 Closes #20 (really this time)
300 lines
10 KiB
Cython
300 lines
10 KiB
Cython
# 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]))
|
|
if isinstance( mix, Mix):
|
|
log(">> Mixing masters")
|
|
f = mix.generateFont(self.basefont)
|
|
else:
|
|
f = mix.copy()
|
|
|
|
if italic == True:
|
|
log(">> Italicizing")
|
|
i = 0
|
|
for g in f:
|
|
decomposeGlyph(f, g)
|
|
removeGlyphOverlap(g)
|
|
|
|
for g in f:
|
|
i += 1
|
|
if i % 10 == 0: print g.name
|
|
|
|
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(">> Reading features")
|
|
# readFeatureFile(f, f.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'),
|
|
'italicAngle': float(getcfg('italicAngle', '-12')),
|
|
})
|
|
|
|
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)
|