Skip to content

Commit 47c30d4

Browse files
committed
DOC: Make ftface_props example generate a font metrics figure
The example only printed to stdout with no visual output, so Sphinx Gallery could not auto-generate a thumbnail. Added a matplotlib figure that visualises the font metrics (ascender, descender, bbox, underline position/thickness) normalised to units_per_EM using the same font loaded in the example. Closes #17479
1 parent e6a833f commit 47c30d4

File tree

1 file changed

+104
-24
lines changed

1 file changed

+104
-24
lines changed

galleries/examples/misc/ftface_props.py

Lines changed: 104 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,16 @@
1212

1313
import matplotlib
1414
import matplotlib.ft2font as ft
15+
import matplotlib.pyplot as plt
16+
from matplotlib.font_manager import FontProperties
1517

16-
font = ft.FT2Font(
17-
# Use a font shipped with Matplotlib.
18-
os.path.join(matplotlib.get_data_path(),
19-
'fonts/ttf/DejaVuSans-Oblique.ttf'))
18+
# Use a font shipped with Matplotlib.
19+
font_path = os.path.join(
20+
matplotlib.get_data_path(),
21+
'fonts/ttf/DejaVuSans-Oblique.ttf'
22+
)
23+
24+
font = ft.FT2Font(font_path)
2025

2126
print('Num instances: ', font.num_named_instances) # number of named instances in file
2227
print('Num faces: ', font.num_faces) # number of faces in file
@@ -26,26 +31,24 @@
2631
print('PS name: ', font.postscript_name) # the postscript name
2732
print('Num fixed: ', font.num_fixed_sizes) # number of embedded bitmaps
2833

29-
# the following are only available if face.scalable
30-
if font.scalable:
31-
# the face global bounding box (xmin, ymin, xmax, ymax)
32-
print('Bbox: ', font.bbox)
33-
# number of font units covered by the EM
34-
print('EM: ', font.units_per_EM)
35-
# the ascender in 26.6 units
36-
print('Ascender: ', font.ascender)
37-
# the descender in 26.6 units
38-
print('Descender: ', font.descender)
39-
# the height in 26.6 units
40-
print('Height: ', font.height)
41-
# maximum horizontal cursor advance
42-
print('Max adv width: ', font.max_advance_width)
43-
# same for vertical layout
44-
print('Max adv height: ', font.max_advance_height)
45-
# vertical position of the underline bar
46-
print('Underline pos: ', font.underline_position)
47-
# vertical thickness of the underline
48-
print('Underline thickness:', font.underline_thickness)
34+
# the face global bounding box (xmin, ymin, xmax, ymax)
35+
print('Bbox: ', font.bbox)
36+
# number of font units covered by the EM
37+
print('EM: ', font.units_per_EM)
38+
# the ascender in 26.6 units
39+
print('Ascender: ', font.ascender)
40+
# the descender in 26.6 units
41+
print('Descender: ', font.descender)
42+
# the height in 26.6 units
43+
print('Height: ', font.height)
44+
# maximum horizontal cursor advance
45+
print('Max adv width: ', font.max_advance_width)
46+
# same for vertical layout
47+
print('Max adv height: ', font.max_advance_height)
48+
# vertical position of the underline bar
49+
print('Underline pos: ', font.underline_position)
50+
# vertical thickness of the underline
51+
print('Underline thickness:', font.underline_thickness)
4952

5053
for flag in ft.StyleFlags:
5154
name = flag.name.replace('_', ' ').title() + ':'
@@ -54,3 +57,80 @@
5457
for flag in ft.FaceFlags:
5558
name = flag.name.replace('_', ' ').title() + ':'
5659
print(f"{name:17}", flag in font.face_flags)
60+
61+
# ── Visualise font metrics ────────────────────────────────────────────────────
62+
# Normalise all metrics to units_per_EM so values are in the range [-1, 1].
63+
# This figure is used by Sphinx Gallery to auto-generate the gallery thumbnail.
64+
u = font.units_per_EM
65+
asc = font.ascender / u
66+
desc = font.descender / u
67+
bbox_ymax = font.bbox[3] / u
68+
bbox_ymin = font.bbox[1] / u
69+
ul_pos = font.underline_position / u
70+
ul_thick = font.underline_thickness / u
71+
72+
fig, ax = plt.subplots(figsize=(8, 6))
73+
74+
# Metric lines drawn FIRST (lower zorder) so text renders on top of them.
75+
metrics = [
76+
("bbox top (ymax)", bbox_ymax, "tab:green"),
77+
("ascender", asc, "tab:blue"),
78+
("baseline (y=0)", 0, "black"),
79+
("underline_position", ul_pos, "tab:orange"),
80+
("descender", desc, "tab:red"),
81+
("bbox bottom (ymin)", bbox_ymin, "tab:purple"),
82+
]
83+
84+
# Lines span from left edge to 72% of axes width — crossing through the glyph.
85+
# Labels sit at 75%, clearly to the right of the lines.
86+
for label, y, color in metrics:
87+
ax.plot(
88+
[0.02, 0.72], [y, y],
89+
color=color, linewidth=1.5, linestyle='--', alpha=0.9, zorder=2)
90+
# default position
91+
y_pos = y
92+
93+
# adjust only bbox labels
94+
if "bbox top" in label:
95+
y_pos = y - 0.015
96+
elif "bbox bottom" in label:
97+
y_pos = y + 0.015
98+
99+
ax.text(
100+
0.75, y_pos, label, color=color, va='center',
101+
fontsize=9, fontweight='medium', ha='left', zorder=2)
102+
103+
# Underline thickness — shaded band between underline_position and its lower edge.
104+
ax.fill_between([0.02, 0.72],
105+
ul_pos - ul_thick,
106+
ul_pos,
107+
color='tab:orange',
108+
alpha=0.22,
109+
label=f'underline_thickness = {font.underline_thickness}',
110+
zorder=1)
111+
112+
# Bounding box (font.bbox) as a rectangle. Drawn after lines, before text.
113+
ax.add_patch(plt.Rectangle(
114+
(0.02, bbox_ymin), 0.70, (bbox_ymax - bbox_ymin),
115+
fill=False, edgecolor='black', linestyle='-',
116+
linewidth=1.5, alpha=0.6, zorder=3,
117+
label='font.bbox'
118+
))
119+
120+
# Render "Ag" on top of everything — zorder=10 ensures no line covers the text.
121+
# 'A' shows ascender/cap-height, 'g' shows descender.
122+
fp = FontProperties(fname=font_path)
123+
ax.text(0.30, 0.0, "Ag", fontproperties=fp, fontsize=150,
124+
va='baseline', ha='center', color='black', zorder=10)
125+
126+
ax.set_xlim(0, 1.35)
127+
ax.set_ylim(bbox_ymin - 0.10, bbox_ymax + 0.15)
128+
ax.set_title(
129+
f"Font metrics — {font.family_name} {font.style_name}\n"
130+
f"(values normalised to units_per_EM = {font.units_per_EM})",
131+
fontsize=11.5, pad=15
132+
)
133+
ax.legend(fontsize=8, loc='lower right', frameon=False)
134+
ax.axis('off')
135+
plt.tight_layout(pad=1.5)
136+
plt.show()

0 commit comments

Comments
 (0)