1"""Pen calculating area, center of mass, variance and standard-deviation, 2covariance and correlation, and slant, of glyph shapes.""" 3import math 4from fontTools.pens.momentsPen import MomentsPen 5 6__all__ = ["StatisticsPen"] 7 8 9class StatisticsPen(MomentsPen): 10 11 """Pen calculating area, center of mass, variance and 12 standard-deviation, covariance and correlation, and slant, 13 of glyph shapes. 14 15 Note that all the calculated values are 'signed'. Ie. if the 16 glyph shape is self-intersecting, the values are not correct 17 (but well-defined). As such, area will be negative if contour 18 directions are clockwise. Moreover, variance might be negative 19 if the shapes are self-intersecting in certain ways.""" 20 21 def __init__(self, glyphset=None): 22 MomentsPen.__init__(self, glyphset=glyphset) 23 self.__zero() 24 25 def _closePath(self): 26 MomentsPen._closePath(self) 27 self.__update() 28 29 def __zero(self): 30 self.meanX = 0 31 self.meanY = 0 32 self.varianceX = 0 33 self.varianceY = 0 34 self.stddevX = 0 35 self.stddevY = 0 36 self.covariance = 0 37 self.correlation = 0 38 self.slant = 0 39 40 def __update(self): 41 42 area = self.area 43 if not area: 44 self.__zero() 45 return 46 47 # Center of mass 48 # https://en.wikipedia.org/wiki/Center_of_mass#A_continuous_volume 49 self.meanX = meanX = self.momentX / area 50 self.meanY = meanY = self.momentY / area 51 52 # Var(X) = E[X^2] - E[X]^2 53 self.varianceX = varianceX = self.momentXX / area - meanX**2 54 self.varianceY = varianceY = self.momentYY / area - meanY**2 55 56 self.stddevX = stddevX = math.copysign(abs(varianceX)**.5, varianceX) 57 self.stddevY = stddevY = math.copysign(abs(varianceY)**.5, varianceY) 58 59 # Covariance(X,Y) = ( E[X.Y] - E[X]E[Y] ) 60 self.covariance = covariance = self.momentXY / area - meanX*meanY 61 62 # Correlation(X,Y) = Covariance(X,Y) / ( stddev(X) * stddev(Y) ) 63 # https://en.wikipedia.org/wiki/Pearson_product-moment_correlation_coefficient 64 correlation = covariance / (stddevX * stddevY) 65 self.correlation = correlation if abs(correlation) > 1e-3 else 0 66 67 slant = covariance / varianceY 68 self.slant = slant if abs(slant) > 1e-3 else 0 69 70 71def _test(glyphset, upem, glyphs): 72 from fontTools.pens.transformPen import TransformPen 73 from fontTools.misc.transform import Scale 74 75 print('upem', upem) 76 77 for glyph_name in glyphs: 78 print() 79 print("glyph:", glyph_name) 80 glyph = glyphset[glyph_name] 81 pen = StatisticsPen(glyphset=glyphset) 82 transformer = TransformPen(pen, Scale(1./upem)) 83 glyph.draw(transformer) 84 for item in ['area', 'momentX', 'momentY', 'momentXX', 'momentYY', 'momentXY', 'meanX', 'meanY', 'varianceX', 'varianceY', 'stddevX', 'stddevY', 'covariance', 'correlation', 'slant']: 85 if item[0] == '_': continue 86 print ("%s: %g" % (item, getattr(pen, item))) 87 88def main(args): 89 if not args: 90 return 91 filename, glyphs = args[0], args[1:] 92 if not glyphs: 93 glyphs = ['e', 'o', 'I', 'slash', 'E', 'zero', 'eight', 'minus', 'equal'] 94 from fontTools.ttLib import TTFont 95 font = TTFont(filename) 96 _test(font.getGlyphSet(), font['head'].unitsPerEm, glyphs) 97 98if __name__ == '__main__': 99 import sys 100 main(sys.argv[1:]) 101