fonttools/Snippets/symfont.py
2017-02-20 12:33:12 -06:00

286 lines
7.7 KiB
Python
Executable File

#! /usr/bin/env python
"""
Pen to calculate geometrical glyph statistics.
When this is fully fleshed out, it will be moved to a more prominent
place, like fontTools.pens.
"""
from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
import sympy as sp
import sys
import math
from fontTools.pens.basePen import BasePen
from fontTools.pens.transformPen import TransformPen
from fontTools.pens.perimeterPen import PerimeterPen
from fontTools.pens.momentsPen import MomentsPen
from fontTools.pens.areaPen import AreaPen
from fontTools.misc.transform import Scale
from functools import partial
from itertools import count
n = 3 # Max Bezier degree; 3 for cubic, 2 for quadratic
t, x, y = sp.symbols('t x y', real=True)
c = sp.symbols('c', real=False) # Complex representation instead of x/y
P = tuple(zip(*(sp.symbols('%s:%d'%(w,n+1), real=True) for w in 'xy')))
C = tuple(sp.symbols('c:%d'%(n+1), real=False))
# Cubic Bernstein basis functions
BinomialCoefficient = [(1, 0)]
for i in range(1, n+1):
last = BinomialCoefficient[-1]
this = tuple(last[j-1]+last[j] for j in range(len(last)))+(0,)
BinomialCoefficient.append(this)
BinomialCoefficient = tuple(tuple(item[:-1]) for item in BinomialCoefficient)
del last, this
BernsteinPolynomial = tuple(
tuple(c * t**i * (1-t)**(n-i) for i,c in enumerate(coeffs))
for n,coeffs in enumerate(BinomialCoefficient))
BezierCurve = tuple(
tuple(sum(P[i][j]*bernstein for i,bernstein in enumerate(bernsteins))
for j in range(2))
for n,bernsteins in enumerate(BernsteinPolynomial))
BezierCurveC = tuple(
sum(C[i]*bernstein for i,bernstein in enumerate(bernsteins))
for n,bernsteins in enumerate(BernsteinPolynomial))
def green(f, curveXY, optimize=True):
f = -sp.integrate(sp.sympify(f), y)
f = f.subs({x:curveXY[0], y:curveXY[1]})
f = sp.integrate(f * sp.diff(curveXY[0], t), (t, 0, 1))
if optimize:
f = sp.gcd_terms(f.collect(sum(P,())))
return f
class BezierFuncs(object):
def __init__(self, symfunc):
self._symfunc = symfunc
self._bezfuncs = {}
def __getitem__(self, i):
if i not in self._bezfuncs:
args = []
for d in range(i+1):
args.append('x%d' % d)
args.append('y%d' % d)
self._bezfuncs[i] = sp.lambdify(args, green(self._symfunc, BezierCurve[i]))
return self._bezfuncs[i]
_BezierFuncs = {}
def getGreenBezierFuncs(func):
funcstr = str(func)
global _BezierFuncs
if not funcstr in _BezierFuncs:
_BezierFuncs[funcstr] = BezierFuncs(func)
return _BezierFuncs[funcstr]
def printCache(func, file=sys.stdout):
funcstr = str(func)
print("_BezierFuncs['%s'] = [" % funcstr, file=file)
for i in range(n+1):
print(' lambda P:', green(func, BezierCurve[i]), ',')
print(']', file=file)
def printPen(name, funcs, file=sys.stdout):
print(
'''from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
from fontTools.pens.basePen import BasePen
class {name}(BasePen):
def __init__(self, glyphset=None):
BasePen.__init__(self, glyphset)
'''.format(name=name), file=file)
for name,f in funcs:
print(' self.%s = 0' % name, file=file)
print('''
def _moveTo(self, p0):
self.__startPoint = p0
def _closePath(self):
p0 = self._getCurrentPoint()
if p0 != self.__startPoint:
p1 = self.__startPoint
self._lineTo(p1)''', file=file)
for n in (1, 2, 3):
if n == 1:
print('''
def _lineTo(self, p1):
x0,y0 = self._getCurrentPoint()
x1,y1 = p1
''', file=file)
elif n == 2:
print('''
def _qCurveToOne(self, p1, p2):
x0,y0 = self._getCurrentPoint()
x1,y1 = p1
x2,y2 = p2
''', file=file)
elif n == 3:
print('''
def _curveToOne(self, p1, p2, p3):
x0,y0 = self._getCurrentPoint()
x1,y1 = p1
x2,y2 = p2
x3,y3 = p3
''', file=file)
defs, exprs = sp.cse([green(f, BezierCurve[n]) for name,f in funcs],
optimizations='basic',
symbols=(sp.Symbol('r%d'%i) for i in count()))
for name,value in defs:
print(' %s = %s' % (name, value), file=file)
print(file=file)
for name,value in zip([f[0] for f in funcs], exprs):
print(' self.%s += %s' % (name, value), file=file)
#printPen('MomentsPen',
# [('area', 1),
# ('momentX', x),
# ('momentY', y),
# ('momentXX', x*x),
# ('momentXY', x*y),
# ('momentYY', y*y)])
class GreenPen(BasePen):
def __init__(self, func, glyphset=None):
BasePen.__init__(self, glyphset)
self._funcs = getGreenBezierFuncs(func)
self.value = 0
def _moveTo(self, p0):
self.__startPoint = p0
def _lineTo(self, p1):
p0 = self._getCurrentPoint()
self.value += self._funcs[1](p0[0],p0[1],p1[0],p1[1])
def _qCurveToOne(self, p1, p2):
p0 = self._getCurrentPoint()
self.value += self._funcs[2](p0[0],p0[1],p1[0],p1[1],p2[0],p2[1])
def _curveToOne(self, p1, p2, p3):
p0 = self._getCurrentPoint()
self.value += self._funcs[3](p0[0],p0[1],p1[0],p1[1],p2[0],p2[1],p3[0],p3[1])
def _closePath(self):
p0 = self._getCurrentPoint()
if p0 != self.__startPoint:
p1 = self.__startPoint
self.value += self._funcs[1](p0[0],p0[1],p1[0],p1[1])
#AreaPen = partial(GreenPen, func=1)
Moment1XPen = partial(GreenPen, func=x)
Moment1YPen = partial(GreenPen, func=y)
Moment2XXPen = partial(GreenPen, func=x*x)
Moment2YYPen = partial(GreenPen, func=y*y)
Moment2XYPen = partial(GreenPen, func=x*y)
#
# Glyph statistics object
#
class GlyphStatistics(object):
def __init__(self, glyph, transform=None, glyphset=None):
self._glyph = glyph
self._glyphset = glyphset
self._transform = transform
pen = transformer = PerimeterPen(glyphset=self._glyphset)
if self._transform:
transformer = TransformPen(pen, self._transform)
self._glyph.draw(transformer)
self.Perimeter = pen.value
Pen = MomentsPen
pen = transformer = Pen(glyphset=self._glyphset)
if self._transform:
transformer = TransformPen(pen, self._transform)
self._glyph.draw(transformer)
self.m = m = pen
self.Area = area = m.area
self.Moment1X = m.momentX
self.Moment1Y = m.momentY
self.Moment2XX = m.momentXX
self.Moment2XY = m.momentXY
self.Moment2YY = m.momentYY
if not area:
self.MeanX = 0.
self.MeanY = 0.
self.VarianceX = 0.
self.VarianceY = 0.
self.StdDevX = 0.
self.StdDevY = 0.
self.Covariance = 0.
self.Correlation = 0.
self.Slant = 0.
return
# Center of mass
# https://en.wikipedia.org/wiki/Center_of_mass#A_continuous_volume
self.MeanX = self.Moment1X / area
self.MeanY = self.Moment1Y / area
# Var(X) = E[X^2] - E[X]^2
self.VarianceX = self.Moment2XX / area - self.MeanX**2
self.VarianceY = self.Moment2YY / area - self.MeanY**2
self.StdDevX = math.copysign(abs(self.VarianceX)**.5, self.VarianceX)
self.StdDevY = math.copysign(abs(self.VarianceY)**.5, self.VarianceY)
# Covariance(X,Y) = ( E[X.Y] - E[X]E[Y] )
self.Covariance = self.Moment2XY / area - self.MeanX*self.MeanY
# Correlation(X,Y) = Covariance(X,Y) / ( StdDev(X) * StdDev(Y)) )
# https://en.wikipedia.org/wiki/Pearson_product-moment_correlation_coefficient
corr = self.Covariance / (self.StdDevX * self.StdDevY)
self.Correlation = corr if abs(corr) > 1e-3 else 0
slant = self.Covariance / self.VarianceY
self.Slant = slant if abs(slant) > 1e-3 else 0
def test(glyphset, upem, glyphs):
print('upem', upem)
for glyph_name in glyphs:
print()
print("glyph:", glyph_name)
glyph = glyphset[glyph_name]
stats = GlyphStatistics(glyph, transform=Scale(1./upem), glyphset=glyphset)
for item in dir(stats):
if item[0] == '_': continue
print ("%s: %g" % (item, getattr(stats, item)))
def main(args):
if not args:
return
filename, glyphs = args[0], args[1:]
if not glyphs:
glyphs = ['e', 'o', 'I', 'slash', 'E', 'zero', 'eight', 'minus', 'equal']
from fontTools.ttLib import TTFont
font = TTFont(filename)
test(font.getGlyphSet(), font['head'].unitsPerEm, glyphs)
if __name__ == '__main__':
import sys
main(sys.argv[1:])