| """Pen calculating area, center of mass, variance and standard-deviation, |
| covariance and correlation, and slant, of glyph shapes.""" |
| from __future__ import print_function, division, absolute_import |
| from fontTools.misc.py23 import * |
| import math |
| from fontTools.pens.momentsPen import MomentsPen |
| |
| __all__ = ["StatisticsPen"] |
| |
| |
| class StatisticsPen(MomentsPen): |
| |
| """Pen calculating area, center of mass, variance and |
| standard-deviation, covariance and correlation, and slant, |
| of glyph shapes. |
| |
| Note that all the calculated values are 'signed'. Ie. if the |
| glyph shape is self-intersecting, the values are not correct |
| (but well-defined). As such, area will be negative if contour |
| directions are clockwise. Moreover, variance might be negative |
| if the shapes are self-intersecting in certain ways.""" |
| |
| def __init__(self, glyphset=None): |
| MomentsPen.__init__(self, glyphset=glyphset) |
| self.__zero() |
| |
| def _closePath(self): |
| MomentsPen._closePath(self) |
| self.__update() |
| |
| def __zero(self): |
| 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 |
| |
| def __update(self): |
| |
| area = self.area |
| if not area: |
| self.__zero() |
| return |
| |
| # Center of mass |
| # https://en.wikipedia.org/wiki/Center_of_mass#A_continuous_volume |
| self.meanX = meanX = self.momentX / area |
| self.meanY = meanY = self.momentY / area |
| |
| # Var(X) = E[X^2] - E[X]^2 |
| self.varianceX = varianceX = self.momentXX / area - meanX**2 |
| self.varianceY = varianceY = self.momentYY / area - meanY**2 |
| |
| self.stddevX = stddevX = math.copysign(abs(varianceX)**.5, varianceX) |
| self.stddevY = stddevY = math.copysign(abs(varianceY)**.5, varianceY) |
| |
| # Covariance(X,Y) = ( E[X.Y] - E[X]E[Y] ) |
| self.covariance = covariance = self.momentXY / area - meanX*meanY |
| |
| # Correlation(X,Y) = Covariance(X,Y) / ( stddev(X) * stddev(Y) ) |
| # https://en.wikipedia.org/wiki/Pearson_product-moment_correlation_coefficient |
| correlation = covariance / (stddevX * stddevY) |
| self.correlation = correlation if abs(correlation) > 1e-3 else 0 |
| |
| slant = covariance / varianceY |
| self.slant = slant if abs(slant) > 1e-3 else 0 |
| |
| |
| def _test(glyphset, upem, glyphs): |
| from fontTools.pens.transformPen import TransformPen |
| from fontTools.misc.transform import Scale |
| |
| print('upem', upem) |
| |
| for glyph_name in glyphs: |
| print() |
| print("glyph:", glyph_name) |
| glyph = glyphset[glyph_name] |
| pen = StatisticsPen(glyphset=glyphset) |
| transformer = TransformPen(pen, Scale(1./upem)) |
| glyph.draw(transformer) |
| for item in ['area', 'momentX', 'momentY', 'momentXX', 'momentYY', 'momentXY', 'meanX', 'meanY', 'varianceX', 'varianceY', 'stddevX', 'stddevY', 'covariance', 'correlation', 'slant']: |
| if item[0] == '_': continue |
| print ("%s: %g" % (item, getattr(pen, 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:]) |