[Snippets] Add symfont.py, for symbolic font statistics analysis
This commit is contained in:
parent
87d13888ce
commit
b01c41bcea
218
Snippets/symfont.py
Normal file
218
Snippets/symfont.py
Normal file
@ -0,0 +1,218 @@
|
||||
"""
|
||||
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 math
|
||||
from fontTools.pens.basePen import BasePen
|
||||
from functools import partial
|
||||
|
||||
n = 3 # Max Bezier degree; 3 for cubic, 2 for quadratic
|
||||
|
||||
t, x, y = sp.symbols('t x y', real=True)
|
||||
Psymbol = sp.symbols('P')
|
||||
|
||||
P = tuple(sp.symbols('P[:%d][:2]' % (n+1), real=True))
|
||||
P = tuple(P[2*i:2*(i+1)] for i in range(len(P) // 2))
|
||||
|
||||
# Cubic Berstein 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)
|
||||
|
||||
BersteinPolynomial = 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]*berstein for i,berstein in enumerate(bersteins))
|
||||
for j in range(2))
|
||||
for n,bersteins in enumerate(BersteinPolynomial))
|
||||
|
||||
def green(f, Bezier=BezierCurve[n]):
|
||||
f1 = sp.integrate(f, y)
|
||||
f2 = f1.replace(y, Bezier[1]).replace(x, Bezier[0])
|
||||
return sp.integrate(f2 * sp.diff(Bezier[0], t), (t, 0, 1))
|
||||
|
||||
def lambdify(f):
|
||||
return sp.lambdify(Psymbol, f)
|
||||
|
||||
class BezierFuncs(object):
|
||||
|
||||
def __init__(self, symfunc):
|
||||
self._symfunc = symfunc
|
||||
self._bezfuncs = {}
|
||||
|
||||
def __getitem__(self, i):
|
||||
if i not in self._bezfuncs:
|
||||
self._bezfuncs[i] = lambdify(green(self._symfunc, Bezier=BezierCurve[i]))
|
||||
return self._bezfuncs[i]
|
||||
|
||||
_BezierFuncs = {}
|
||||
|
||||
def getGreenBezierFuncs(func):
|
||||
func = sp.sympify(func)
|
||||
funcstr = str(func)
|
||||
global _BezierFuncs
|
||||
if not funcstr in _BezierFuncs:
|
||||
_BezierFuncs[funcstr] = BezierFuncs(func)
|
||||
return _BezierFuncs[funcstr]
|
||||
|
||||
class GreenPen(BasePen):
|
||||
|
||||
def __init__(self, glyphset, func, scale=1):
|
||||
BasePen.__init__(self, glyphset)
|
||||
self._funcs = getGreenBezierFuncs(func)
|
||||
self._scale = scale
|
||||
self.value = 0
|
||||
|
||||
def _segment(self, *P):
|
||||
scale = self._scale
|
||||
P = tuple((x/scale,y/scale) for x,y in P)
|
||||
self.value += self._funcs[len(P) - 1](P)
|
||||
|
||||
def _moveTo(self, p0):
|
||||
self._segment(p0)
|
||||
|
||||
def _lineTo(self, p1):
|
||||
p0 = self._getCurrentPoint()
|
||||
self._segment(p0,p1)
|
||||
|
||||
def _qCurveToOne(self, p1, p2):
|
||||
p0 = self._getCurrentPoint()
|
||||
self._segment(p0,p1,p2)
|
||||
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
p0 = self._getCurrentPoint()
|
||||
self._segment(p0,p1,p2,p3)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
class GlyphStatistics(object):
|
||||
|
||||
def __init__(self, glyph, glyphset=None, scale=1):
|
||||
self._glyph = glyph
|
||||
self._glyphset = glyphset
|
||||
self._scale = scale
|
||||
|
||||
def _penAttr(self, attr):
|
||||
internalName = '_'+attr
|
||||
if internalName not in self.__dict__:
|
||||
Pen = globals()[attr+'Pen']
|
||||
pen = Pen(self._glyphset, scale=self._scale)
|
||||
self._glyph.draw(pen)
|
||||
self.__dict__[internalName] = pen.value
|
||||
return self.__dict__[internalName]
|
||||
|
||||
Area = property(partial(_penAttr, attr='Area'))
|
||||
Moment1X = property(partial(_penAttr, attr='Moment1X'))
|
||||
Moment1Y = property(partial(_penAttr, attr='Moment1Y'))
|
||||
Moment2XX = property(partial(_penAttr, attr='Moment2XX'))
|
||||
Moment2YY = property(partial(_penAttr, attr='Moment2YY'))
|
||||
Moment2XY = property(partial(_penAttr, attr='Moment2XY'))
|
||||
|
||||
# TODO Memoize properties below
|
||||
|
||||
# Center of mass
|
||||
# https://en.wikipedia.org/wiki/Center_of_mass#A_continuous_volume
|
||||
@property
|
||||
def MeanX(self):
|
||||
return self.Moment1X / self.Area
|
||||
@property
|
||||
def MeanY(self):
|
||||
return self.Moment1Y / self.Area
|
||||
|
||||
# https://en.wikipedia.org/wiki/Second_moment_of_area
|
||||
|
||||
# Var(X) = E[X^2] - E[X]^2
|
||||
@property
|
||||
def VarianceX(self):
|
||||
return self.Moment2XX / self.Area - self.MeanX**2
|
||||
@property
|
||||
def VarianceY(self):
|
||||
return self.Moment2YY / self.Area - self.MeanY**2
|
||||
|
||||
@property
|
||||
def StdDevX(self):
|
||||
return self.VarianceX**.5
|
||||
@property
|
||||
def StdDevY(self):
|
||||
return self.VarianceY**.5
|
||||
|
||||
# Covariance(X,Y) = ( E[X.Y] - E[X]E[Y] )
|
||||
@property
|
||||
def Covariance(self):
|
||||
return self.Moment2XY / self.Area - self.MeanX*self.MeanY
|
||||
|
||||
@property
|
||||
def _CovarianceMatrix(self):
|
||||
cov = self.Covariance
|
||||
return ((self.VarianceX, cov), (cov, self.VarianceY))
|
||||
|
||||
@property
|
||||
def _Eigen(self):
|
||||
mat = self.CovarianceMatrix
|
||||
from numpy.linalg import eigh
|
||||
vals,vecs = eigh(mat)
|
||||
# Note: we return eigen-vectors row-major, unlike Matlab, et al
|
||||
return tuple(vals), tuple(tuple(row) for row in vecs)
|
||||
|
||||
# Correlation(X,Y) = Covariance(X,Y) / ( StdDev(X) * StdDev(Y)) )
|
||||
# https://en.wikipedia.org/wiki/Pearson_product-moment_correlation_coefficient
|
||||
@property
|
||||
def Correlation(self):
|
||||
corr = self.Covariance / (self.StdDevX * self.StdDevY)
|
||||
if abs(corr) < 1e-3: corr = 0
|
||||
return corr
|
||||
|
||||
@property
|
||||
def Slant(self):
|
||||
slant = self.Covariance / self.VarianceY
|
||||
if abs(slant) < 1e-3: slant = 0
|
||||
return slant
|
||||
|
||||
|
||||
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, glyphset, scale=upem)
|
||||
for item in dir(stats):
|
||||
if item[0] == '_': continue
|
||||
print ("%s: %g" % (item, getattr(stats, item)))
|
||||
|
||||
|
||||
def main(args):
|
||||
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)
|
||||
glyphset = font.getGlyphSet()
|
||||
test(font.getGlyphSet(), font['head'].unitsPerEm, glyphs)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
main(sys.argv[1:])
|
||||
|
||||
#areag = green(1)
|
||||
#area_f = lambdify(areag)
|
||||
#print("area", area_f)
|
Loading…
x
Reference in New Issue
Block a user