xref: /aosp_15_r20/external/fonttools/Snippets/statShape.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1"""Draw statistical shape of a glyph as an ellipse."""
2
3from fontTools.ttLib import TTFont
4from fontTools.pens.recordingPen import RecordingPen
5from fontTools.pens.cairoPen import CairoPen
6from fontTools.pens.statisticsPen import StatisticsPen
7import cairo
8import math
9import sys
10
11
12font = TTFont(sys.argv[1])
13unicode = sys.argv[2]
14
15cmap = font["cmap"].getBestCmap()
16gid = cmap[ord(unicode)]
17
18hhea = font["hhea"]
19glyphset = font.getGlyphSet()
20with cairo.SVGSurface(
21    "example.svg", hhea.advanceWidthMax, hhea.ascent - hhea.descent
22) as surface:
23    context = cairo.Context(surface)
24    context.translate(0, +font["hhea"].ascent)
25    context.scale(1, -1)
26
27    glyph = glyphset[gid]
28
29    recording = RecordingPen()
30    glyph.draw(recording)
31
32    context.translate((hhea.advanceWidthMax - glyph.width) * 0.5, 0)
33
34    pen = CairoPen(glyphset, context)
35    glyph.draw(pen)
36    context.fill()
37
38    stats = StatisticsPen(glyphset)
39    glyph.draw(stats)
40
41    # https://cookierobotics.com/007/
42    a = stats.varianceX
43    b = stats.covariance
44    c = stats.varianceY
45    delta = (((a - c) * 0.5) ** 2 + b * b) ** 0.5
46    lambda1 = (a + c) * 0.5 + delta  # Major eigenvalue
47    lambda2 = (a + c) * 0.5 - delta  # Minor eigenvalue
48    theta = math.atan2(lambda1 - a, b) if b != 0 else (math.pi * 0.5 if a < c else 0)
49    mult = 4  # Empirical by drawing '.'
50    transform = cairo.Matrix()
51    transform.translate(stats.meanX, stats.meanY)
52    transform.rotate(theta)
53    transform.scale(math.sqrt(lambda1), math.sqrt(lambda2))
54    transform.scale(mult, mult)
55
56    ellipse_area = math.sqrt(lambda1) * math.sqrt(lambda2) * math.pi / 4 * mult * mult
57
58    if stats.area:
59        context.save()
60        context.set_line_cap(cairo.LINE_CAP_ROUND)
61        context.transform(transform)
62        context.move_to(0, 0)
63        context.line_to(0, 0)
64        context.set_line_width(1)
65        context.set_source_rgba(1, 0, 0, abs(stats.area / ellipse_area))
66        context.stroke()
67        context.restore()
68
69        context.save()
70        context.set_line_cap(cairo.LINE_CAP_ROUND)
71        context.set_source_rgb(0.8, 0, 0)
72        context.translate(stats.meanX, stats.meanY)
73
74        context.move_to(0, 0)
75        context.line_to(0, 0)
76        context.set_line_width(15)
77        context.stroke()
78
79        context.transform(cairo.Matrix(1, 0, stats.slant, 1, 0, 0))
80        context.move_to(0, -stats.meanY + font["hhea"].ascent)
81        context.line_to(0, -stats.meanY + font["hhea"].descent)
82        context.set_line_width(5)
83        context.stroke()
84
85        context.restore()
86