From b01c41bceae98d6dc986f56ce4ebe111a79c0ced Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Thu, 10 Mar 2016 06:13:18 -0800 Subject: [PATCH] [Snippets] Add symfont.py, for symbolic font statistics analysis --- Snippets/symfont.py | 218 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 Snippets/symfont.py diff --git a/Snippets/symfont.py b/Snippets/symfont.py new file mode 100644 index 000000000..7e3350330 --- /dev/null +++ b/Snippets/symfont.py @@ -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)