xref: /aosp_15_r20/external/fonttools/Lib/fontTools/pens/statisticsPen.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughes"""Pen calculating area, center of mass, variance and standard-deviation,
2*e1fe3e4aSElliott Hughescovariance and correlation, and slant, of glyph shapes."""
3*e1fe3e4aSElliott Hughes
4*e1fe3e4aSElliott Hughesfrom math import sqrt, degrees, atan
5*e1fe3e4aSElliott Hughesfrom fontTools.pens.basePen import BasePen, OpenContourError
6*e1fe3e4aSElliott Hughesfrom fontTools.pens.momentsPen import MomentsPen
7*e1fe3e4aSElliott Hughes
8*e1fe3e4aSElliott Hughes__all__ = ["StatisticsPen", "StatisticsControlPen"]
9*e1fe3e4aSElliott Hughes
10*e1fe3e4aSElliott Hughes
11*e1fe3e4aSElliott Hughesclass StatisticsBase:
12*e1fe3e4aSElliott Hughes    def __init__(self):
13*e1fe3e4aSElliott Hughes        self._zero()
14*e1fe3e4aSElliott Hughes
15*e1fe3e4aSElliott Hughes    def _zero(self):
16*e1fe3e4aSElliott Hughes        self.area = 0
17*e1fe3e4aSElliott Hughes        self.meanX = 0
18*e1fe3e4aSElliott Hughes        self.meanY = 0
19*e1fe3e4aSElliott Hughes        self.varianceX = 0
20*e1fe3e4aSElliott Hughes        self.varianceY = 0
21*e1fe3e4aSElliott Hughes        self.stddevX = 0
22*e1fe3e4aSElliott Hughes        self.stddevY = 0
23*e1fe3e4aSElliott Hughes        self.covariance = 0
24*e1fe3e4aSElliott Hughes        self.correlation = 0
25*e1fe3e4aSElliott Hughes        self.slant = 0
26*e1fe3e4aSElliott Hughes
27*e1fe3e4aSElliott Hughes    def _update(self):
28*e1fe3e4aSElliott Hughes        # XXX The variance formulas should never produce a negative value,
29*e1fe3e4aSElliott Hughes        # but due to reasons I don't understand, both of our pens do.
30*e1fe3e4aSElliott Hughes        # So we take the absolute value here.
31*e1fe3e4aSElliott Hughes        self.varianceX = abs(self.varianceX)
32*e1fe3e4aSElliott Hughes        self.varianceY = abs(self.varianceY)
33*e1fe3e4aSElliott Hughes
34*e1fe3e4aSElliott Hughes        self.stddevX = stddevX = sqrt(self.varianceX)
35*e1fe3e4aSElliott Hughes        self.stddevY = stddevY = sqrt(self.varianceY)
36*e1fe3e4aSElliott Hughes
37*e1fe3e4aSElliott Hughes        # Correlation(X,Y) = Covariance(X,Y) / ( stddev(X) * stddev(Y) )
38*e1fe3e4aSElliott Hughes        # https://en.wikipedia.org/wiki/Pearson_product-moment_correlation_coefficient
39*e1fe3e4aSElliott Hughes        if stddevX * stddevY == 0:
40*e1fe3e4aSElliott Hughes            correlation = float("NaN")
41*e1fe3e4aSElliott Hughes        else:
42*e1fe3e4aSElliott Hughes            # XXX The above formula should never produce a value outside
43*e1fe3e4aSElliott Hughes            # the range [-1, 1], but due to reasons I don't understand,
44*e1fe3e4aSElliott Hughes            # (probably the same issue as above), it does. So we clamp.
45*e1fe3e4aSElliott Hughes            correlation = self.covariance / (stddevX * stddevY)
46*e1fe3e4aSElliott Hughes            correlation = max(-1, min(1, correlation))
47*e1fe3e4aSElliott Hughes        self.correlation = correlation if abs(correlation) > 1e-3 else 0
48*e1fe3e4aSElliott Hughes
49*e1fe3e4aSElliott Hughes        slant = (
50*e1fe3e4aSElliott Hughes            self.covariance / self.varianceY if self.varianceY != 0 else float("NaN")
51*e1fe3e4aSElliott Hughes        )
52*e1fe3e4aSElliott Hughes        self.slant = slant if abs(slant) > 1e-3 else 0
53*e1fe3e4aSElliott Hughes
54*e1fe3e4aSElliott Hughes
55*e1fe3e4aSElliott Hughesclass StatisticsPen(StatisticsBase, MomentsPen):
56*e1fe3e4aSElliott Hughes    """Pen calculating area, center of mass, variance and
57*e1fe3e4aSElliott Hughes    standard-deviation, covariance and correlation, and slant,
58*e1fe3e4aSElliott Hughes    of glyph shapes.
59*e1fe3e4aSElliott Hughes
60*e1fe3e4aSElliott Hughes    Note that if the glyph shape is self-intersecting, the values
61*e1fe3e4aSElliott Hughes    are not correct (but well-defined). Moreover, area will be
62*e1fe3e4aSElliott Hughes    negative if contour directions are clockwise."""
63*e1fe3e4aSElliott Hughes
64*e1fe3e4aSElliott Hughes    def __init__(self, glyphset=None):
65*e1fe3e4aSElliott Hughes        MomentsPen.__init__(self, glyphset=glyphset)
66*e1fe3e4aSElliott Hughes        StatisticsBase.__init__(self)
67*e1fe3e4aSElliott Hughes
68*e1fe3e4aSElliott Hughes    def _closePath(self):
69*e1fe3e4aSElliott Hughes        MomentsPen._closePath(self)
70*e1fe3e4aSElliott Hughes        self._update()
71*e1fe3e4aSElliott Hughes
72*e1fe3e4aSElliott Hughes    def _update(self):
73*e1fe3e4aSElliott Hughes        area = self.area
74*e1fe3e4aSElliott Hughes        if not area:
75*e1fe3e4aSElliott Hughes            self._zero()
76*e1fe3e4aSElliott Hughes            return
77*e1fe3e4aSElliott Hughes
78*e1fe3e4aSElliott Hughes        # Center of mass
79*e1fe3e4aSElliott Hughes        # https://en.wikipedia.org/wiki/Center_of_mass#A_continuous_volume
80*e1fe3e4aSElliott Hughes        self.meanX = meanX = self.momentX / area
81*e1fe3e4aSElliott Hughes        self.meanY = meanY = self.momentY / area
82*e1fe3e4aSElliott Hughes
83*e1fe3e4aSElliott Hughes        # Var(X) = E[X^2] - E[X]^2
84*e1fe3e4aSElliott Hughes        self.varianceX = self.momentXX / area - meanX * meanX
85*e1fe3e4aSElliott Hughes        self.varianceY = self.momentYY / area - meanY * meanY
86*e1fe3e4aSElliott Hughes
87*e1fe3e4aSElliott Hughes        # Covariance(X,Y) = (E[X.Y] - E[X]E[Y])
88*e1fe3e4aSElliott Hughes        self.covariance = self.momentXY / area - meanX * meanY
89*e1fe3e4aSElliott Hughes
90*e1fe3e4aSElliott Hughes        StatisticsBase._update(self)
91*e1fe3e4aSElliott Hughes
92*e1fe3e4aSElliott Hughes
93*e1fe3e4aSElliott Hughesclass StatisticsControlPen(StatisticsBase, BasePen):
94*e1fe3e4aSElliott Hughes    """Pen calculating area, center of mass, variance and
95*e1fe3e4aSElliott Hughes    standard-deviation, covariance and correlation, and slant,
96*e1fe3e4aSElliott Hughes    of glyph shapes, using the control polygon only.
97*e1fe3e4aSElliott Hughes
98*e1fe3e4aSElliott Hughes    Note that if the glyph shape is self-intersecting, the values
99*e1fe3e4aSElliott Hughes    are not correct (but well-defined). Moreover, area will be
100*e1fe3e4aSElliott Hughes    negative if contour directions are clockwise."""
101*e1fe3e4aSElliott Hughes
102*e1fe3e4aSElliott Hughes    def __init__(self, glyphset=None):
103*e1fe3e4aSElliott Hughes        BasePen.__init__(self, glyphset)
104*e1fe3e4aSElliott Hughes        StatisticsBase.__init__(self)
105*e1fe3e4aSElliott Hughes        self._nodes = []
106*e1fe3e4aSElliott Hughes
107*e1fe3e4aSElliott Hughes    def _moveTo(self, pt):
108*e1fe3e4aSElliott Hughes        self._nodes.append(complex(*pt))
109*e1fe3e4aSElliott Hughes
110*e1fe3e4aSElliott Hughes    def _lineTo(self, pt):
111*e1fe3e4aSElliott Hughes        self._nodes.append(complex(*pt))
112*e1fe3e4aSElliott Hughes
113*e1fe3e4aSElliott Hughes    def _qCurveToOne(self, pt1, pt2):
114*e1fe3e4aSElliott Hughes        for pt in (pt1, pt2):
115*e1fe3e4aSElliott Hughes            self._nodes.append(complex(*pt))
116*e1fe3e4aSElliott Hughes
117*e1fe3e4aSElliott Hughes    def _curveToOne(self, pt1, pt2, pt3):
118*e1fe3e4aSElliott Hughes        for pt in (pt1, pt2, pt3):
119*e1fe3e4aSElliott Hughes            self._nodes.append(complex(*pt))
120*e1fe3e4aSElliott Hughes
121*e1fe3e4aSElliott Hughes    def _closePath(self):
122*e1fe3e4aSElliott Hughes        self._update()
123*e1fe3e4aSElliott Hughes
124*e1fe3e4aSElliott Hughes    def _endPath(self):
125*e1fe3e4aSElliott Hughes        p0 = self._getCurrentPoint()
126*e1fe3e4aSElliott Hughes        if p0 != self.__startPoint:
127*e1fe3e4aSElliott Hughes            raise OpenContourError("Glyph statistics not defined on open contours.")
128*e1fe3e4aSElliott Hughes
129*e1fe3e4aSElliott Hughes    def _update(self):
130*e1fe3e4aSElliott Hughes        nodes = self._nodes
131*e1fe3e4aSElliott Hughes        n = len(nodes)
132*e1fe3e4aSElliott Hughes
133*e1fe3e4aSElliott Hughes        # Triangle formula
134*e1fe3e4aSElliott Hughes        self.area = (
135*e1fe3e4aSElliott Hughes            sum(
136*e1fe3e4aSElliott Hughes                (p0.real * p1.imag - p1.real * p0.imag)
137*e1fe3e4aSElliott Hughes                for p0, p1 in zip(nodes, nodes[1:] + nodes[:1])
138*e1fe3e4aSElliott Hughes            )
139*e1fe3e4aSElliott Hughes            / 2
140*e1fe3e4aSElliott Hughes        )
141*e1fe3e4aSElliott Hughes
142*e1fe3e4aSElliott Hughes        # Center of mass
143*e1fe3e4aSElliott Hughes        # https://en.wikipedia.org/wiki/Center_of_mass#A_system_of_particles
144*e1fe3e4aSElliott Hughes        sumNodes = sum(nodes)
145*e1fe3e4aSElliott Hughes        self.meanX = meanX = sumNodes.real / n
146*e1fe3e4aSElliott Hughes        self.meanY = meanY = sumNodes.imag / n
147*e1fe3e4aSElliott Hughes
148*e1fe3e4aSElliott Hughes        if n > 1:
149*e1fe3e4aSElliott Hughes            # Var(X) = (sum[X^2] - sum[X]^2 / n) / (n - 1)
150*e1fe3e4aSElliott Hughes            # https://www.statisticshowto.com/probability-and-statistics/descriptive-statistics/sample-variance/
151*e1fe3e4aSElliott Hughes            self.varianceX = varianceX = (
152*e1fe3e4aSElliott Hughes                sum(p.real * p.real for p in nodes)
153*e1fe3e4aSElliott Hughes                - (sumNodes.real * sumNodes.real) / n
154*e1fe3e4aSElliott Hughes            ) / (n - 1)
155*e1fe3e4aSElliott Hughes            self.varianceY = varianceY = (
156*e1fe3e4aSElliott Hughes                sum(p.imag * p.imag for p in nodes)
157*e1fe3e4aSElliott Hughes                - (sumNodes.imag * sumNodes.imag) / n
158*e1fe3e4aSElliott Hughes            ) / (n - 1)
159*e1fe3e4aSElliott Hughes
160*e1fe3e4aSElliott Hughes            # Covariance(X,Y) = (sum[X.Y] - sum[X].sum[Y] / n) / (n - 1)
161*e1fe3e4aSElliott Hughes            self.covariance = covariance = (
162*e1fe3e4aSElliott Hughes                sum(p.real * p.imag for p in nodes)
163*e1fe3e4aSElliott Hughes                - (sumNodes.real * sumNodes.imag) / n
164*e1fe3e4aSElliott Hughes            ) / (n - 1)
165*e1fe3e4aSElliott Hughes        else:
166*e1fe3e4aSElliott Hughes            self.varianceX = varianceX = 0
167*e1fe3e4aSElliott Hughes            self.varianceY = varianceY = 0
168*e1fe3e4aSElliott Hughes            self.covariance = covariance = 0
169*e1fe3e4aSElliott Hughes
170*e1fe3e4aSElliott Hughes        StatisticsBase._update(self)
171*e1fe3e4aSElliott Hughes
172*e1fe3e4aSElliott Hughes
173*e1fe3e4aSElliott Hughesdef _test(glyphset, upem, glyphs, quiet=False, *, control=False):
174*e1fe3e4aSElliott Hughes    from fontTools.pens.transformPen import TransformPen
175*e1fe3e4aSElliott Hughes    from fontTools.misc.transform import Scale
176*e1fe3e4aSElliott Hughes
177*e1fe3e4aSElliott Hughes    wght_sum = 0
178*e1fe3e4aSElliott Hughes    wght_sum_perceptual = 0
179*e1fe3e4aSElliott Hughes    wdth_sum = 0
180*e1fe3e4aSElliott Hughes    slnt_sum = 0
181*e1fe3e4aSElliott Hughes    slnt_sum_perceptual = 0
182*e1fe3e4aSElliott Hughes    for glyph_name in glyphs:
183*e1fe3e4aSElliott Hughes        glyph = glyphset[glyph_name]
184*e1fe3e4aSElliott Hughes        if control:
185*e1fe3e4aSElliott Hughes            pen = StatisticsControlPen(glyphset=glyphset)
186*e1fe3e4aSElliott Hughes        else:
187*e1fe3e4aSElliott Hughes            pen = StatisticsPen(glyphset=glyphset)
188*e1fe3e4aSElliott Hughes        transformer = TransformPen(pen, Scale(1.0 / upem))
189*e1fe3e4aSElliott Hughes        glyph.draw(transformer)
190*e1fe3e4aSElliott Hughes
191*e1fe3e4aSElliott Hughes        area = abs(pen.area)
192*e1fe3e4aSElliott Hughes        width = glyph.width
193*e1fe3e4aSElliott Hughes        wght_sum += area
194*e1fe3e4aSElliott Hughes        wght_sum_perceptual += pen.area * width
195*e1fe3e4aSElliott Hughes        wdth_sum += width
196*e1fe3e4aSElliott Hughes        slnt_sum += pen.slant
197*e1fe3e4aSElliott Hughes        slnt_sum_perceptual += pen.slant * width
198*e1fe3e4aSElliott Hughes
199*e1fe3e4aSElliott Hughes        if quiet:
200*e1fe3e4aSElliott Hughes            continue
201*e1fe3e4aSElliott Hughes
202*e1fe3e4aSElliott Hughes        print()
203*e1fe3e4aSElliott Hughes        print("glyph:", glyph_name)
204*e1fe3e4aSElliott Hughes
205*e1fe3e4aSElliott Hughes        for item in [
206*e1fe3e4aSElliott Hughes            "area",
207*e1fe3e4aSElliott Hughes            "momentX",
208*e1fe3e4aSElliott Hughes            "momentY",
209*e1fe3e4aSElliott Hughes            "momentXX",
210*e1fe3e4aSElliott Hughes            "momentYY",
211*e1fe3e4aSElliott Hughes            "momentXY",
212*e1fe3e4aSElliott Hughes            "meanX",
213*e1fe3e4aSElliott Hughes            "meanY",
214*e1fe3e4aSElliott Hughes            "varianceX",
215*e1fe3e4aSElliott Hughes            "varianceY",
216*e1fe3e4aSElliott Hughes            "stddevX",
217*e1fe3e4aSElliott Hughes            "stddevY",
218*e1fe3e4aSElliott Hughes            "covariance",
219*e1fe3e4aSElliott Hughes            "correlation",
220*e1fe3e4aSElliott Hughes            "slant",
221*e1fe3e4aSElliott Hughes        ]:
222*e1fe3e4aSElliott Hughes            print("%s: %g" % (item, getattr(pen, item)))
223*e1fe3e4aSElliott Hughes
224*e1fe3e4aSElliott Hughes    if not quiet:
225*e1fe3e4aSElliott Hughes        print()
226*e1fe3e4aSElliott Hughes        print("font:")
227*e1fe3e4aSElliott Hughes
228*e1fe3e4aSElliott Hughes    print("weight: %g" % (wght_sum * upem / wdth_sum))
229*e1fe3e4aSElliott Hughes    print("weight (perceptual): %g" % (wght_sum_perceptual / wdth_sum))
230*e1fe3e4aSElliott Hughes    print("width:  %g" % (wdth_sum / upem / len(glyphs)))
231*e1fe3e4aSElliott Hughes    slant = slnt_sum / len(glyphs)
232*e1fe3e4aSElliott Hughes    print("slant:  %g" % slant)
233*e1fe3e4aSElliott Hughes    print("slant angle:  %g" % -degrees(atan(slant)))
234*e1fe3e4aSElliott Hughes    slant_perceptual = slnt_sum_perceptual / wdth_sum
235*e1fe3e4aSElliott Hughes    print("slant (perceptual):  %g" % slant_perceptual)
236*e1fe3e4aSElliott Hughes    print("slant (perceptual) angle:  %g" % -degrees(atan(slant_perceptual)))
237*e1fe3e4aSElliott Hughes
238*e1fe3e4aSElliott Hughes
239*e1fe3e4aSElliott Hughesdef main(args):
240*e1fe3e4aSElliott Hughes    """Report font glyph shape geometricsl statistics"""
241*e1fe3e4aSElliott Hughes
242*e1fe3e4aSElliott Hughes    if args is None:
243*e1fe3e4aSElliott Hughes        import sys
244*e1fe3e4aSElliott Hughes
245*e1fe3e4aSElliott Hughes        args = sys.argv[1:]
246*e1fe3e4aSElliott Hughes
247*e1fe3e4aSElliott Hughes    import argparse
248*e1fe3e4aSElliott Hughes
249*e1fe3e4aSElliott Hughes    parser = argparse.ArgumentParser(
250*e1fe3e4aSElliott Hughes        "fonttools pens.statisticsPen",
251*e1fe3e4aSElliott Hughes        description="Report font glyph shape geometricsl statistics",
252*e1fe3e4aSElliott Hughes    )
253*e1fe3e4aSElliott Hughes    parser.add_argument("font", metavar="font.ttf", help="Font file.")
254*e1fe3e4aSElliott Hughes    parser.add_argument("glyphs", metavar="glyph-name", help="Glyph names.", nargs="*")
255*e1fe3e4aSElliott Hughes    parser.add_argument(
256*e1fe3e4aSElliott Hughes        "-y",
257*e1fe3e4aSElliott Hughes        metavar="<number>",
258*e1fe3e4aSElliott Hughes        help="Face index into a collection to open. Zero based.",
259*e1fe3e4aSElliott Hughes    )
260*e1fe3e4aSElliott Hughes    parser.add_argument(
261*e1fe3e4aSElliott Hughes        "-c",
262*e1fe3e4aSElliott Hughes        "--control",
263*e1fe3e4aSElliott Hughes        action="store_true",
264*e1fe3e4aSElliott Hughes        help="Use the control-box pen instead of the Green therem.",
265*e1fe3e4aSElliott Hughes    )
266*e1fe3e4aSElliott Hughes    parser.add_argument(
267*e1fe3e4aSElliott Hughes        "-q", "--quiet", action="store_true", help="Only report font-wide statistics."
268*e1fe3e4aSElliott Hughes    )
269*e1fe3e4aSElliott Hughes    parser.add_argument(
270*e1fe3e4aSElliott Hughes        "--variations",
271*e1fe3e4aSElliott Hughes        metavar="AXIS=LOC",
272*e1fe3e4aSElliott Hughes        default="",
273*e1fe3e4aSElliott Hughes        help="List of space separated locations. A location consist in "
274*e1fe3e4aSElliott Hughes        "the name of a variation axis, followed by '=' and a number. E.g.: "
275*e1fe3e4aSElliott Hughes        "wght=700 wdth=80. The default is the location of the base master.",
276*e1fe3e4aSElliott Hughes    )
277*e1fe3e4aSElliott Hughes
278*e1fe3e4aSElliott Hughes    options = parser.parse_args(args)
279*e1fe3e4aSElliott Hughes
280*e1fe3e4aSElliott Hughes    glyphs = options.glyphs
281*e1fe3e4aSElliott Hughes    fontNumber = int(options.y) if options.y is not None else 0
282*e1fe3e4aSElliott Hughes
283*e1fe3e4aSElliott Hughes    location = {}
284*e1fe3e4aSElliott Hughes    for tag_v in options.variations.split():
285*e1fe3e4aSElliott Hughes        fields = tag_v.split("=")
286*e1fe3e4aSElliott Hughes        tag = fields[0].strip()
287*e1fe3e4aSElliott Hughes        v = int(fields[1])
288*e1fe3e4aSElliott Hughes        location[tag] = v
289*e1fe3e4aSElliott Hughes
290*e1fe3e4aSElliott Hughes    from fontTools.ttLib import TTFont
291*e1fe3e4aSElliott Hughes
292*e1fe3e4aSElliott Hughes    font = TTFont(options.font, fontNumber=fontNumber)
293*e1fe3e4aSElliott Hughes    if not glyphs:
294*e1fe3e4aSElliott Hughes        glyphs = font.getGlyphOrder()
295*e1fe3e4aSElliott Hughes    _test(
296*e1fe3e4aSElliott Hughes        font.getGlyphSet(location=location),
297*e1fe3e4aSElliott Hughes        font["head"].unitsPerEm,
298*e1fe3e4aSElliott Hughes        glyphs,
299*e1fe3e4aSElliott Hughes        quiet=options.quiet,
300*e1fe3e4aSElliott Hughes        control=options.control,
301*e1fe3e4aSElliott Hughes    )
302*e1fe3e4aSElliott Hughes
303*e1fe3e4aSElliott Hughes
304*e1fe3e4aSElliott Hughesif __name__ == "__main__":
305*e1fe3e4aSElliott Hughes    import sys
306*e1fe3e4aSElliott Hughes
307*e1fe3e4aSElliott Hughes    main(sys.argv[1:])
308