Spaces:
Sleeping
Sleeping
"""Pen calculating area, center of mass, variance and standard-deviation, | |
covariance and correlation, and slant, of glyph shapes.""" | |
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) ** 0.5, varianceX) | |
self.stddevY = stddevY = math.copysign(abs(varianceY) ** 0.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 | |
if stddevX * stddevY == 0: | |
correlation = float("NaN") | |
else: | |
correlation = covariance / (stddevX * stddevY) | |
self.correlation = correlation if abs(correlation) > 1e-3 else 0 | |
slant = covariance / varianceY if varianceY != 0 else float("NaN") | |
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.0 / upem)) | |
glyph.draw(transformer) | |
for item in [ | |
"area", | |
"momentX", | |
"momentY", | |
"momentXX", | |
"momentYY", | |
"momentXY", | |
"meanX", | |
"meanY", | |
"varianceX", | |
"varianceY", | |
"stddevX", | |
"stddevY", | |
"covariance", | |
"correlation", | |
"slant", | |
]: | |
print("%s: %g" % (item, getattr(pen, item))) | |
def main(args): | |
if not args: | |
return | |
filename, glyphs = args[0], args[1:] | |
from fontTools.ttLib import TTFont | |
font = TTFont(filename) | |
if not glyphs: | |
glyphs = font.getGlyphOrder() | |
_test(font.getGlyphSet(), font["head"].unitsPerEm, glyphs) | |
if __name__ == "__main__": | |
import sys | |
main(sys.argv[1:]) | |