# Source code for fontTools.pens.statisticsPen

    """Pen calculating area, center of mass, variance and standard-deviation,
    covariance and correlation, and slant, of glyph shapes."""

    from math import sqrt, degrees, atan
    from fontTools.pens.basePen import BasePen, OpenContourError
    from fontTools.pens.momentsPen import MomentsPen

    __all__ = ["StatisticsPen", "StatisticsControlPen"]

    class StatisticsBase:
        def __init__(self):
            self._zero()

        def _zero(self):
            self.area = 0
            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):
            # XXX The variance formulas should never produce a negative value,
            # but due to reasons I don't understand, both of our pens do.
            # So we take the absolute value here.
            self.varianceX = abs(self.varianceX)
            self.varianceY = abs(self.varianceY)

            self.stddevX = stddevX = sqrt(self.varianceX)
            self.stddevY = stddevY = sqrt(self.varianceY)

            # 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:
                # XXX The above formula should never produce a value outside
                # the range [-1, 1], but due to reasons I don't understand,
                # (probably the same issue as above), it does. So we clamp.
                correlation = self.covariance / (stddevX * stddevY)
                correlation = max(-1, min(1, correlation))
            self.correlation = correlation if abs(correlation) > 1e-3 else 0

            slant = (
                self.covariance / self.varianceY if self.varianceY != 0 else float("NaN")
            )
            self.slant = slant if abs(slant) > 1e-3 else 0

    class StatisticsPen(StatisticsBase, MomentsPen):
        """Pen calculating area, center of mass, variance and
        standard-deviation, covariance and correlation, and slant,
        of glyph shapes.

        Note that if the glyph shape is self-intersecting, the values
        are not correct (but well-defined). Moreover, area will be
        negative if contour directions are clockwise."""

        def __init__(self, glyphset=None):
            MomentsPen.__init__(self, glyphset=glyphset)
            StatisticsBase.__init__(self)

        def _closePath(self):
            MomentsPen._closePath(self)
            self._update()

        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 = self.momentXX / area - meanX * meanX
            self.varianceY = self.momentYY / area - meanY * meanY

            # Covariance(X,Y) = (E[X.Y] - E[X]E[Y])
            self.covariance = self.momentXY / area - meanX * meanY

            StatisticsBase._update(self)

    class StatisticsControlPen(StatisticsBase, BasePen):
        """Pen calculating area, center of mass, variance and
        standard-deviation, covariance and correlation, and slant,
        of glyph shapes, using the control polygon only.

        Note that if the glyph shape is self-intersecting, the values
        are not correct (but well-defined). Moreover, area will be
        negative if contour directions are clockwise."""

        def __init__(self, glyphset=None):
            BasePen.__init__(self, glyphset)
            StatisticsBase.__init__(self)
            self._nodes = []

        def _moveTo(self, pt):
            self._nodes.append(complex(*pt))
            self._startPoint = pt

        def _lineTo(self, pt):
            self._nodes.append(complex(*pt))

        def _qCurveToOne(self, pt1, pt2):
            for pt in (pt1, pt2):
                self._nodes.append(complex(*pt))

        def _curveToOne(self, pt1, pt2, pt3):
            for pt in (pt1, pt2, pt3):
                self._nodes.append(complex(*pt))

        def _closePath(self):
            p0 = self._getCurrentPoint()
            if p0 != self._startPoint:
                self._lineTo(self._startPoint)
            self._update()

        def _endPath(self):
            p0 = self._getCurrentPoint()
            if p0 != self._startPoint:
                raise OpenContourError("Glyph statistics not defined on open contours.")
            self._update()

        def _update(self):
            nodes = self._nodes
            n = len(nodes)

            # Triangle formula
            self.area = (
                sum(
                    (p0.real * p1.imag - p1.real * p0.imag)
                    for p0, p1 in zip(nodes, nodes[1:] + nodes[:1])
                )
                / 2
            )

            # Center of mass
            # https://en.wikipedia.org/wiki/Center_of_mass#A_system_of_particles
            sumNodes = sum(nodes)
            self.meanX = meanX = sumNodes.real / n
            self.meanY = meanY = sumNodes.imag / n

            if n > 1:
                # Var(X) = (sum[X^2] - sum[X]^2 / n) / (n - 1)
                # https://www.statisticshowto.com/probability-and-statistics/descriptive-statistics/sample-variance/
                self.varianceX = varianceX = (
                    sum(p.real * p.real for p in nodes)
                    - (sumNodes.real * sumNodes.real) / n
                ) / (n - 1)
                self.varianceY = varianceY = (
                    sum(p.imag * p.imag for p in nodes)
                    - (sumNodes.imag * sumNodes.imag) / n
                ) / (n - 1)

                # Covariance(X,Y) = (sum[X.Y] - sum[X].sum[Y] / n) / (n - 1)
                self.covariance = covariance = (
                    sum(p.real * p.imag for p in nodes)
                    - (sumNodes.real * sumNodes.imag) / n
                ) / (n - 1)
            else:
                self.varianceX = varianceX = 0
                self.varianceY = varianceY = 0
                self.covariance = covariance = 0

            StatisticsBase._update(self)

    def _test(glyphset, upem, glyphs, quiet=False, *, control=False):
        from fontTools.pens.transformPen import TransformPen
        from fontTools.misc.transform import Scale

        wght_sum = 0
        wght_sum_perceptual = 0
        wdth_sum = 0
        slnt_sum = 0
        slnt_sum_perceptual = 0
        for glyph_name in glyphs:
            glyph = glyphset[glyph_name]
            if control:
                pen = StatisticsControlPen(glyphset=glyphset)
            else:
                pen = StatisticsPen(glyphset=glyphset)
            transformer = TransformPen(pen, Scale(1.0 / upem))
            glyph.draw(transformer)

            area = abs(pen.area)
            width = glyph.width
            wght_sum += area
            wght_sum_perceptual += pen.area * width
            wdth_sum += width
            slnt_sum += pen.slant
            slnt_sum_perceptual += pen.slant * width

            if quiet:
                continue

            print()
            print("glyph:", glyph_name)

            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)))

        if not quiet:
            print()
            print("font:")

        print("weight: %g" % (wght_sum * upem / wdth_sum))
        print("weight (perceptual): %g" % (wght_sum_perceptual / wdth_sum))
        print("width:  %g" % (wdth_sum / upem / len(glyphs)))
        slant = slnt_sum / len(glyphs)
        print("slant:  %g" % slant)
        print("slant angle:  %g" % -degrees(atan(slant)))
        slant_perceptual = slnt_sum_perceptual / wdth_sum
        print("slant (perceptual):  %g" % slant_perceptual)
        print("slant (perceptual) angle:  %g" % -degrees(atan(slant_perceptual)))

    def main(args):
        """Report font glyph shape geometricsl statistics"""

        if args is None:
            import sys

            args = sys.argv[1:]

        import argparse

        parser = argparse.ArgumentParser(
            "fonttools pens.statisticsPen",
            description="Report font glyph shape geometricsl statistics",
        )
        parser.add_argument("font", metavar="font.ttf", help="Font file.")
        parser.add_argument("glyphs", metavar="glyph-name", help="Glyph names.", nargs="*")
        parser.add_argument(
            "-y",
            metavar="<number>",
            help="Face index into a collection to open. Zero based.",
        )
        parser.add_argument(
            "-c",
            "--control",
            action="store_true",
            help="Use the control-box pen instead of the Green therem.",
        )
        parser.add_argument(
            "-q", "--quiet", action="store_true", help="Only report font-wide statistics."
        )
        parser.add_argument(
            "--variations",
            metavar="AXIS=LOC",
            default="",
            help="List of space separated locations. A location consist in "
            "the name of a variation axis, followed by '=' and a number. E.g.: "
            "wght=700 wdth=80. The default is the location of the base master.",
        )

        options = parser.parse_args(args)

        glyphs = options.glyphs
        fontNumber = int(options.y) if options.y is not None else 0

        location = {}
        for tag_v in options.variations.split():
            fields = tag_v.split("=")
            tag = fields[0].strip()
            v = int(fields[1])
            location[tag] = v

        from fontTools.ttLib import TTFont

        font = TTFont(options.font, fontNumber=fontNumber)
        if not glyphs:
            glyphs = font.getGlyphOrder()
        _test(
            font.getGlyphSet(location=location),
            font["head"].unitsPerEm,
            glyphs,
            quiet=options.quiet,
            control=options.control,
        )

    if __name__ == "__main__":
        import sys

        main(sys.argv[1:])