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