1010
1111import os
1212
13- import matplotlib .pyplot as plt
14-
1513import matplotlib
14+ import matplotlib .pyplot as plt
15+ import matplotlib .transforms
1616from matplotlib .font_manager import FontProperties
17+ from matplotlib .patches import PathPatch , Rectangle
18+ from matplotlib .textpath import TextPath
1719import matplotlib .ft2font as ft
1820
1921# Use a font shipped with Matplotlib.
2426
2527font = ft .FT2Font (font_path )
2628
27- print ('Num instances: ' , font .num_named_instances ) # number of named instances in file
28- print ('Num faces: ' , font .num_faces ) # number of faces in file
29- print ('Num glyphs: ' , font .num_glyphs ) # number of glyphs in the face
30- print ('Family name: ' , font .family_name ) # face family name
31- print ('Style name: ' , font .style_name ) # face style name
32- print ('PS name: ' , font .postscript_name ) # the postscript name
33- print ('Num fixed: ' , font .num_fixed_sizes ) # number of embedded bitmaps
34-
35- # the face global bounding box (xmin, ymin, xmax, ymax)
36- print ('Bbox: ' , font .bbox )
37- # number of font units covered by the EM
38- print ('EM: ' , font .units_per_EM )
39- # the ascender in 26.6 units
40- print ('Ascender: ' , font .ascender )
41- # the descender in 26.6 units
42- print ('Descender: ' , font .descender )
43- # the height in 26.6 units
44- print ('Height: ' , font .height )
45- # maximum horizontal cursor advance
46- print ('Max adv width: ' , font .max_advance_width )
47- # same for vertical layout
48- print ('Max adv height: ' , font .max_advance_height )
49- # vertical position of the underline bar
50- print ('Underline pos: ' , font .underline_position )
51- # vertical thickness of the underline
52- print ('Underline thickness:' , font .underline_thickness )
29+ print ("Num instances: " , font .num_named_instances ) # number of named instances in file
30+ print ("Num faces: " , font .num_faces ) # number of faces in file
31+ print ("Num glyphs: " , font .num_glyphs ) # number of glyphs in the face
32+ print ("Family name: " , font .family_name ) # face family name
33+ print ("Style name: " , font .style_name ) # face style name
34+ print ("PS name: " , font .postscript_name ) # the postscript name
35+ print ("Num fixed: " , font .num_fixed_sizes ) # number of embedded bitmaps
36+ print ("Bbox: " , font .bbox ) # the face global bounding box (xmin, ymin, xmax, ymax)
37+ print ("EM: " , font .units_per_EM ) # number of font units covered by the EM
38+ print ("Ascender: " , font .ascender ) # the ascender in 26.6 units
39+ print ("Descender: " , font .descender ) # the descender in 26.6 units
40+ print ("Height: " , font .height ) # the height in 26.6 units
41+ print ("Max adv width: " , font .max_advance_width ) # maximum horizontal cursor advance
42+ print ("Max adv height: " , font .max_advance_height ) # same for vertical layout
43+ print ("Underline pos: " , font .underline_position ) # vertical position of the underline bar
44+ print ("Underline thickness:" , font .underline_thickness ) # vertical thickness of the underline
5345
5446for flag in ft .StyleFlags :
5547 name = flag .name .replace ('_' , ' ' ).title () + ':'
5951 name = flag .name .replace ('_' , ' ' ).title () + ':'
6052 print (f"{ name :17} " , flag in font .face_flags )
6153
62- # ── Visualise font metrics ────────────────────────────────────────────────────
63- # Normalise all metrics to units_per_EM so values are in the range [-1, 1].
64- # This figure is used by Sphinx Gallery to auto-generate the gallery thumbnail.
54+ # Normalise all vertical metrics to units_per_EM so all y-values sit in [-1, 1].
6555u = font .units_per_EM
66- asc = font .ascender / u
67- desc = font .descender / u
56+ asc = font .ascender / u
57+ desc = font .descender / u
6858bbox_ymax = font .bbox [3 ] / u
6959bbox_ymin = font .bbox [1 ] / u
70- ul_pos = font .underline_position / u
71- ul_thick = font .underline_thickness / u
60+ ul_pos = font .underline_position / u
61+ ul_thick = font .underline_thickness / u
7262
7363fig , ax = plt .subplots (figsize = (8 , 6 ))
7464
75- # Metric lines drawn FIRST (lower zorder) so text renders on top of them.
65+ fp = FontProperties (fname = font_path )
66+ tp = TextPath ((0 , 0 ), "Ag" , size = 1 , prop = fp )
67+ text_bb = tp .get_extents ()
68+
69+ # Centre the glyph at a fixed x position, then read back where it actually lands.
70+ GLYPH_CENTER_X = 0.70
71+ x_offset = GLYPH_CENTER_X - (text_bb .x0 + text_bb .width / 2 )
72+
73+ # True left/right edges of the rendered glyph in data coordinates.
74+ glyph_x0 = text_bb .x0 + x_offset
75+ glyph_x1 = text_bb .x1 + x_offset
76+
77+ # Lines, rectangle and labels are all derived from these real glyph bounds.
78+ H_MARGIN = 0.05 # horizontal padding around glyph
79+ LINE_X0 = glyph_x0 - H_MARGIN # lines start here
80+ LINE_X1 = glyph_x1 + H_MARGIN # lines end here (always past glyph edge)
81+ LABEL_X = LINE_X1 + 0.08 # metric labels start here
82+
7683metrics = [
7784 ("bbox top (ymax)" , bbox_ymax , "tab:green" ),
7885 ("ascender" , asc , "tab:blue" ),
79- ("baseline (y=0 )" , 0 , "black" ),
86+ ("y = 0 (origin )" , 0 , "black" ),
8087 ("underline_position" , ul_pos , "tab:orange" ),
8188 ("descender" , desc , "tab:red" ),
8289 ("bbox bottom (ymin)" , bbox_ymin , "tab:purple" ),
8390]
8491
85- # Lines span from left edge to 72% of axes width — crossing through the glyph.
86- # Labels sit at 75%, clearly to the right of the lines.
8792for label , y , color in metrics :
88- ax .plot (
89- [0.02 , 0.72 ], [y , y ],
90- color = color , linewidth = 1.5 , linestyle = '--' , alpha = 0.9 , zorder = 2 )
91- # default position
92- y_pos = y
93-
94- # adjust only bbox labels
95- if "bbox top" in label :
96- y_pos = y - 0.015
97- elif "bbox bottom" in label :
98- y_pos = y + 0.015
99-
100- ax .text (
101- 0.75 , y_pos , label , color = color , va = 'center' ,
102- fontsize = 9 , fontweight = 'medium' , ha = 'left' , zorder = 2 )
103-
104- # Underline thickness — shaded band between underline_position and its lower edge.
105- ax .fill_between ([0.02 , 0.72 ],
106- ul_pos - ul_thick ,
107- ul_pos ,
108- color = 'tab:orange' ,
109- alpha = 0.22 ,
93+ ax .plot ([LINE_X0 , LINE_X1 ], [y , y ],
94+ color = color , linewidth = 1.5 , linestyle = '--' , alpha = 0.9 , zorder = 2 )
95+ # Nudge bbox-edge labels slightly away from the rectangle border.
96+ y_text = (y - 0.015 if "bbox top" in label else
97+ y + 0.015 if "bbox bottom" in label else y )
98+ ax .text (LABEL_X , y_text , label ,
99+ color = color , va = 'center' , ha = 'left' ,
100+ fontsize = 9 , fontweight = 'medium' , zorder = 2 )
101+
102+ # Underline thickness: shaded band from (ul_pos − ul_thick) to ul_pos.
103+ ax .fill_between ([LINE_X0 , LINE_X1 ],
104+ ul_pos - ul_thick , ul_pos ,
105+ color = 'tab:orange' , alpha = 0.22 ,
110106 label = f'underline_thickness = { font .underline_thickness } ' ,
111107 zorder = 1 )
112108
113- # Bounding box ( font.bbox) as a rectangle. Drawn after lines, before text.
114- ax . add_patch ( plt . Rectangle (
115- ( 0.02 , bbox_ymin ), 0.70 , ( bbox_ymax - bbox_ymin ),
116- fill = False , edgecolor = 'black' , linestyle = '-' ,
117- linewidth = 1.5 , alpha = 0.6 , zorder = 3 ,
118- label = 'font.bbox'
109+ # font.bbox visualised as a rectangle. x-span matches the line region so the
110+ # box always contains the glyph and aligns with the metric lines exactly.
111+ ax . add_patch ( Rectangle (
112+ ( LINE_X0 , bbox_ymin ), LINE_X1 - LINE_X0 , bbox_ymax - bbox_ymin ,
113+ fill = False , edgecolor = 'black' , linewidth = 1.5 , linestyle = '-' ,
114+ alpha = 0.6 , zorder = 3 , label = 'font.bbox' ,
119115))
120116
121- # Render "Ag" on top of everything — zorder=10 ensures no line covers the text.
122- # 'A' shows ascender/cap-height, 'g' shows descender.
123- fp = FontProperties (fname = font_path )
124- ax .text (0.30 , 0.0 , "Ag" , fontproperties = fp , fontsize = 150 ,
125- va = 'baseline' , ha = 'center' , color = 'black' , zorder = 10 )
117+ # Glyph path — translate only (scale = 1.0 implicit); high zorder so it sits
118+ # on top of the reference lines.
119+ ax .add_patch (PathPatch (
120+ tp ,
121+ transform = matplotlib .transforms .Affine2D ().translate (x_offset , 0 ) + ax .transData ,
122+ color = 'black' ,
123+ zorder = 10 ,
124+ ))
126125
127- ax .set_xlim (0 , 1.35 )
126+ # x-limit: start at 0, end with enough room for the longest label.
127+ ax .set_xlim (LINE_X0 - 0.05 , LABEL_X + 0.75 )
128128ax .set_ylim (bbox_ymin - 0.10 , bbox_ymax + 0.15 )
129- ax .set_title (
130- f"Font metrics — { font .family_name } { font .style_name } \n "
131- f"(values normalised to units_per_EM = { font .units_per_EM } )" ,
132- fontsize = 11.5 , pad = 15
133- )
134- ax .legend (fontsize = 8 , loc = 'lower right' , frameon = False )
129+ ax .set_title (f"Font metrics — { font .family_name } { font .style_name } " ,
130+ fontsize = 11.5 , pad = 15 )
131+ ax .legend (fontsize = 8 , loc = 'upper right' , bbox_to_anchor = (1.02 , 0.95 ), frameon = False )
135132ax .axis ('off' )
136133plt .tight_layout (pad = 1.5 )
137- plt .show ()
134+ plt .show ()
0 commit comments