|
20 | 20 | import matplotlib.transforms |
21 | 21 |
|
22 | 22 | # Use a font shipped with Matplotlib. |
23 | | -font_path = os.path.join( |
24 | | - matplotlib.get_data_path(), |
25 | | - 'fonts/ttf/DejaVuSans-Oblique.ttf' |
26 | | -) |
| 23 | +font_path = os.path.join(matplotlib.get_data_path(), "fonts/ttf/DejaVuSans-Oblique.ttf") |
27 | 24 |
|
28 | 25 | font = ft.FT2Font(font_path) |
29 | 26 |
|
|
36 | 33 | print("Num fixed: ", font.num_fixed_sizes) # number of embedded bitmaps |
37 | 34 | print("Bbox: ", font.bbox) # global bounding box (xmin, ymin, xmax, ymax) |
38 | 35 | print("EM: ", font.units_per_EM) # font units per EM |
39 | | -print("Ascender: ", font.ascender) # the ascender in 26.6 units |
40 | | -print("Descender: ", font.descender) # the descender in 26.6 units |
41 | | -print("Height: ", font.height) # the height in 26.6 units |
| 36 | +print("Ascender: ", font.ascender) # the ascender in 26.6 units |
| 37 | +print("Descender: ", font.descender) # the descender in 26.6 units |
| 38 | +print("Height: ", font.height) # the height in 26.6 units |
42 | 39 | print("Max adv width: ", font.max_advance_width) # max horizontal advance |
43 | 40 | print("Max adv height: ", font.max_advance_height) # same for vertical layout |
44 | 41 | print("Underline pos: ", font.underline_position) # underline bar position |
45 | 42 | print("Underline thickness:", font.underline_thickness) # underline thickness |
46 | 43 |
|
47 | 44 | for flag in ft.StyleFlags: |
48 | | - name = flag.name.replace('_', ' ').title() + ':' |
| 45 | + name = flag.name.replace("_", " ").title() + ":" |
49 | 46 | print(f"{name:17}", flag in font.style_flags) |
50 | 47 |
|
51 | 48 | for flag in ft.FaceFlags: |
52 | | - name = flag.name.replace('_', ' ').title() + ':' |
| 49 | + name = flag.name.replace("_", " ").title() + ":" |
53 | 50 | print(f"{name:17}", flag in font.face_flags) |
54 | 51 |
|
55 | 52 | # Normalise all vertical metrics to units_per_EM so all y-values sit in [-1, 1]. |
|
58 | 55 | desc = font.descender / u |
59 | 56 | bbox_ymax = font.bbox[3] / u |
60 | 57 | bbox_ymin = font.bbox[1] / u |
| 58 | +bbox_xmin = font.bbox[0] / u |
| 59 | +bbox_xmax = font.bbox[2] / u |
61 | 60 | ul_pos = font.underline_position / u |
62 | 61 | ul_thick = font.underline_thickness / u |
63 | 62 |
|
|
67 | 66 | tp = TextPath((0, 0), "Água", size=1, prop=fp) |
68 | 67 | text_bb = tp.get_extents() |
69 | 68 |
|
70 | | -# Centre the glyph at a fixed x position, then read back where it actually lands. |
71 | | -GLYPH_CENTER_X = 0.70 |
72 | | -x_offset = GLYPH_CENTER_X - (text_bb.x0 + text_bb.width / 2) |
73 | | - |
74 | | -# True left/right edges of the rendered glyph in data coordinates. |
75 | | -glyph_x0 = text_bb.x0 + x_offset |
76 | | -glyph_x1 = text_bb.x1 + x_offset |
| 69 | +# Align the glyph's left edge with bbox_xmin. |
| 70 | +x_offset = bbox_xmin - text_bb.x0 |
77 | 71 |
|
78 | | -# Lines, rectangle and labels are all derived from these real glyph bounds. |
79 | | -H_MARGIN = 0.05 # horizontal padding around glyph |
80 | | -LINE_X0 = glyph_x0 - H_MARGIN # lines start here |
81 | | -LINE_X1 = glyph_x1 + H_MARGIN # lines end here (always past glyph edge) |
82 | | -LABEL_X = LINE_X1 + 0.08 # metric labels start here |
| 72 | +# Lines, rectangle and labels are derived from the true font bbox. |
| 73 | +LINE_X0 = bbox_xmin |
| 74 | +LINE_X1 = bbox_xmax |
| 75 | +LABEL_X = LINE_X1 + 0.08 # metric labels start here |
83 | 76 |
|
84 | 77 | metrics = [ |
85 | 78 | ("bbox top (ymax)", bbox_ymax, "tab:green"), |
|
91 | 84 | ] |
92 | 85 |
|
93 | 86 | for label, y, color in metrics: |
94 | | - l, = ax.hline(y, color=color, linewidth=1.5, linestyle='--', alpha=0.9, zorder=2) |
| 87 | + ax.plot( |
| 88 | + [LINE_X0, LINE_X1], |
| 89 | + [y, y], |
| 90 | + color=color, |
| 91 | + linewidth=1.5, |
| 92 | + linestyle="--", |
| 93 | + alpha=0.9, |
| 94 | + zorder=2, |
| 95 | + ) |
95 | 96 | # Nudge bbox-edge labels slightly away from the rectangle border. |
96 | | - ax.annotate (LABEL_X, y_text, label, (), xycoords = (1, .5), color=color, va='center', ha='left', |
97 | | - fontsize=9, fontweight='medium', zorder=2) |
| 97 | + y_text = ( |
| 98 | + y - 0.015 if "bbox top" in label else y + 0.015 if "bbox bottom" in label else y |
| 99 | + ) |
| 100 | + ax.text( |
| 101 | + LABEL_X, |
| 102 | + y_text, |
| 103 | + label, |
| 104 | + color=color, |
| 105 | + va="center", |
| 106 | + ha="left", |
| 107 | + fontsize=9, |
| 108 | + fontweight="medium", |
| 109 | + zorder=2, |
| 110 | + ) |
98 | 111 |
|
99 | 112 | # Underline thickness: shaded band from (ul_pos − ul_thick) to ul_pos. |
100 | | -ax.fill_between([LINE_X0, LINE_X1], |
101 | | - ul_pos - ul_thick, ul_pos, |
102 | | - color='tab:orange', alpha=0.22, |
103 | | - label=f'underline_thickness = {font.underline_thickness}', |
104 | | - zorder=1) |
105 | | - |
106 | | -# font.bbox visualised as a rectangle. x-span matches the line region so the |
107 | | -# box always contains the glyph and aligns with the metric lines exactly. |
108 | | -ax.add_patch(Rectangle( |
109 | | - (LINE_X0, bbox_ymin), LINE_X1 - LINE_X0, bbox_ymax - bbox_ymin, |
110 | | - fill=False, edgecolor='black', linewidth=1.5, linestyle='-', |
111 | | - alpha=0.6, zorder=3, label='font.bbox', |
112 | | -)) |
| 113 | +ax.fill_between( |
| 114 | + [LINE_X0, LINE_X1], |
| 115 | + ul_pos - ul_thick, |
| 116 | + ul_pos, |
| 117 | + color="tab:orange", |
| 118 | + alpha=0.22, |
| 119 | + label=f"underline_thickness = {font.underline_thickness}", |
| 120 | + zorder=1, |
| 121 | +) |
| 122 | + |
| 123 | +# font.bbox visualised as a rectangle using the true font x/y bounds. |
| 124 | +ax.add_patch( |
| 125 | + Rectangle( |
| 126 | + (bbox_xmin, bbox_ymin), |
| 127 | + bbox_xmax - bbox_xmin, |
| 128 | + bbox_ymax - bbox_ymin, |
| 129 | + fill=False, |
| 130 | + edgecolor="black", |
| 131 | + linewidth=1.5, |
| 132 | + linestyle="-", |
| 133 | + alpha=0.6, |
| 134 | + zorder=3, |
| 135 | + label="font.bbox", |
| 136 | + ) |
| 137 | +) |
113 | 138 |
|
114 | 139 | # Glyph path — translate only (scale = 1.0 implicit); high zorder so it sits |
115 | 140 | # on top of the reference lines. |
116 | | -ax.add_patch(PathPatch( |
117 | | - tp, |
118 | | - transform=matplotlib.transforms.Affine2D().translate(x_offset, 0) + ax.transData, |
119 | | - color='black', |
120 | | - zorder=10, |
121 | | -)) |
122 | | - |
123 | | -# x-limit: start at 0, end with enough room for the longest label. |
124 | | -ax.set_xlim(LINE_X0 - 0.05, LABEL_X + 0.75) |
| 141 | +ax.add_patch( |
| 142 | + PathPatch( |
| 143 | + tp, |
| 144 | + transform=matplotlib.transforms.Affine2D().translate(x_offset, 0) |
| 145 | + + ax.transData, |
| 146 | + color="black", |
| 147 | + zorder=10, |
| 148 | + ) |
| 149 | +) |
| 150 | + |
| 151 | +# x-limit: start at the bbox left edge and leave room for labels. |
| 152 | +ax.set_xlim(LINE_X0, LABEL_X + 0.75) |
125 | 153 | ax.set_ylim(bbox_ymin - 0.10, bbox_ymax + 0.15) |
126 | | -ax.set_title(f"Font metrics — {font.family_name} {font.style_name}", |
127 | | - fontsize=11.5, pad=15) |
128 | | -ax.legend(fontsize=8, loc='lower center', bbox_to_anchor=(0.5, -0.12), |
129 | | - frameon=False, ncol=2) |
130 | | -ax.axis('off') |
| 154 | +ax.set_title( |
| 155 | + f"Font metrics — {font.family_name} {font.style_name}", fontsize=11.5, pad=15 |
| 156 | +) |
| 157 | +ax.legend( |
| 158 | + fontsize=8, loc="lower center", bbox_to_anchor=(0.5, -0.12), frameon=False, ncol=2 |
| 159 | +) |
| 160 | +ax.axis("off") |
131 | 161 | plt.tight_layout(pad=1.5) |
132 | 162 | plt.show() |
0 commit comments