From 5f33bb8c8f9017a2af83629e7465e2cc7f1cbba4 Mon Sep 17 00:00:00 2001 From: Aman Kushwaha Date: Wed, 18 Mar 2026 06:53:25 +0530 Subject: [PATCH 1/8] 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 --- galleries/examples/misc/ftface_props.py | 129 +++++++++++++++++++----- 1 file changed, 105 insertions(+), 24 deletions(-) diff --git a/galleries/examples/misc/ftface_props.py b/galleries/examples/misc/ftface_props.py index ec26dff5bf6a..12d46292751a 100644 --- a/galleries/examples/misc/ftface_props.py +++ b/galleries/examples/misc/ftface_props.py @@ -10,13 +10,19 @@ import os +import matplotlib.pyplot as plt + import matplotlib +from matplotlib.font_manager import FontProperties import matplotlib.ft2font as ft -font = ft.FT2Font( - # Use a font shipped with Matplotlib. - os.path.join(matplotlib.get_data_path(), - 'fonts/ttf/DejaVuSans-Oblique.ttf')) +# Use a font shipped with Matplotlib. +font_path = os.path.join( + matplotlib.get_data_path(), + 'fonts/ttf/DejaVuSans-Oblique.ttf' +) + +font = ft.FT2Font(font_path) print('Num instances: ', font.num_named_instances) # number of named instances in file print('Num faces: ', font.num_faces) # number of faces in file @@ -26,26 +32,24 @@ print('PS name: ', font.postscript_name) # the postscript name print('Num fixed: ', font.num_fixed_sizes) # number of embedded bitmaps -# the following are only available if face.scalable -if font.scalable: - # the face global bounding box (xmin, ymin, xmax, ymax) - print('Bbox: ', font.bbox) - # number of font units covered by the EM - print('EM: ', font.units_per_EM) - # the ascender in 26.6 units - print('Ascender: ', font.ascender) - # the descender in 26.6 units - print('Descender: ', font.descender) - # the height in 26.6 units - print('Height: ', font.height) - # maximum horizontal cursor advance - print('Max adv width: ', font.max_advance_width) - # same for vertical layout - print('Max adv height: ', font.max_advance_height) - # vertical position of the underline bar - print('Underline pos: ', font.underline_position) - # vertical thickness of the underline - print('Underline thickness:', font.underline_thickness) +# the face global bounding box (xmin, ymin, xmax, ymax) +print('Bbox: ', font.bbox) +# number of font units covered by the EM +print('EM: ', font.units_per_EM) +# the ascender in 26.6 units +print('Ascender: ', font.ascender) +# the descender in 26.6 units +print('Descender: ', font.descender) +# the height in 26.6 units +print('Height: ', font.height) +# maximum horizontal cursor advance +print('Max adv width: ', font.max_advance_width) +# same for vertical layout +print('Max adv height: ', font.max_advance_height) +# vertical position of the underline bar +print('Underline pos: ', font.underline_position) +# vertical thickness of the underline +print('Underline thickness:', font.underline_thickness) for flag in ft.StyleFlags: name = flag.name.replace('_', ' ').title() + ':' @@ -54,3 +58,80 @@ for flag in ft.FaceFlags: name = flag.name.replace('_', ' ').title() + ':' print(f"{name:17}", flag in font.face_flags) + +# ── Visualise font metrics ──────────────────────────────────────────────────── +# Normalise all metrics to units_per_EM so values are in the range [-1, 1]. +# This figure is used by Sphinx Gallery to auto-generate the gallery thumbnail. +u = font.units_per_EM +asc = font.ascender / u +desc = font.descender / u +bbox_ymax = font.bbox[3] / u +bbox_ymin = font.bbox[1] / u +ul_pos = font.underline_position / u +ul_thick = font.underline_thickness / u + +fig, ax = plt.subplots(figsize=(8, 6)) + +# Metric lines drawn FIRST (lower zorder) so text renders on top of them. +metrics = [ + ("bbox top (ymax)", bbox_ymax, "tab:green"), + ("ascender", asc, "tab:blue"), + ("baseline (y=0)", 0, "black"), + ("underline_position", ul_pos, "tab:orange"), + ("descender", desc, "tab:red"), + ("bbox bottom (ymin)", bbox_ymin, "tab:purple"), +] + +# Lines span from left edge to 72% of axes width — crossing through the glyph. +# Labels sit at 75%, clearly to the right of the lines. +for label, y, color in metrics: + ax.plot( + [0.02, 0.72], [y, y], + color=color, linewidth=1.5, linestyle='--', alpha=0.9, zorder=2) + # default position + y_pos = y + + # adjust only bbox labels + if "bbox top" in label: + y_pos = y - 0.015 + elif "bbox bottom" in label: + y_pos = y + 0.015 + + ax.text( + 0.75, y_pos, label, color=color, va='center', + fontsize=9, fontweight='medium', ha='left', zorder=2) + +# Underline thickness — shaded band between underline_position and its lower edge. +ax.fill_between([0.02, 0.72], + ul_pos - ul_thick, + ul_pos, + color='tab:orange', + alpha=0.22, + label=f'underline_thickness = {font.underline_thickness}', + zorder=1) + +# Bounding box (font.bbox) as a rectangle. Drawn after lines, before text. +ax.add_patch(plt.Rectangle( + (0.02, bbox_ymin), 0.70, (bbox_ymax - bbox_ymin), + fill=False, edgecolor='black', linestyle='-', + linewidth=1.5, alpha=0.6, zorder=3, + label='font.bbox' +)) + +# Render "Ag" on top of everything — zorder=10 ensures no line covers the text. +# 'A' shows ascender/cap-height, 'g' shows descender. +fp = FontProperties(fname=font_path) +ax.text(0.30, 0.0, "Ag", fontproperties=fp, fontsize=150, + va='baseline', ha='center', color='black', zorder=10) + +ax.set_xlim(0, 1.35) +ax.set_ylim(bbox_ymin - 0.10, bbox_ymax + 0.15) +ax.set_title( + f"Font metrics — {font.family_name} {font.style_name}\n" + f"(values normalised to units_per_EM = {font.units_per_EM})", + fontsize=11.5, pad=15 +) +ax.legend(fontsize=8, loc='lower right', frameon=False) +ax.axis('off') +plt.tight_layout(pad=1.5) +plt.show() From 761fb91268b1f96c946aa4d414c37819823a7633 Mon Sep 17 00:00:00 2001 From: Aman Kushwaha Date: Sat, 28 Mar 2026 00:41:40 +0530 Subject: [PATCH 2/8] DOC: use TextPath for glyph so size is in data units, matching metric lines --- galleries/examples/misc/ftface_props.py | 157 ++++++++++++------------ 1 file changed, 77 insertions(+), 80 deletions(-) diff --git a/galleries/examples/misc/ftface_props.py b/galleries/examples/misc/ftface_props.py index 12d46292751a..c4ae3201ccee 100644 --- a/galleries/examples/misc/ftface_props.py +++ b/galleries/examples/misc/ftface_props.py @@ -10,10 +10,12 @@ import os -import matplotlib.pyplot as plt - import matplotlib +import matplotlib.pyplot as plt +import matplotlib.transforms from matplotlib.font_manager import FontProperties +from matplotlib.patches import PathPatch, Rectangle +from matplotlib.textpath import TextPath import matplotlib.ft2font as ft # Use a font shipped with Matplotlib. @@ -24,32 +26,22 @@ font = ft.FT2Font(font_path) -print('Num instances: ', font.num_named_instances) # number of named instances in file -print('Num faces: ', font.num_faces) # number of faces in file -print('Num glyphs: ', font.num_glyphs) # number of glyphs in the face -print('Family name: ', font.family_name) # face family name -print('Style name: ', font.style_name) # face style name -print('PS name: ', font.postscript_name) # the postscript name -print('Num fixed: ', font.num_fixed_sizes) # number of embedded bitmaps - -# the face global bounding box (xmin, ymin, xmax, ymax) -print('Bbox: ', font.bbox) -# number of font units covered by the EM -print('EM: ', font.units_per_EM) -# the ascender in 26.6 units -print('Ascender: ', font.ascender) -# the descender in 26.6 units -print('Descender: ', font.descender) -# the height in 26.6 units -print('Height: ', font.height) -# maximum horizontal cursor advance -print('Max adv width: ', font.max_advance_width) -# same for vertical layout -print('Max adv height: ', font.max_advance_height) -# vertical position of the underline bar -print('Underline pos: ', font.underline_position) -# vertical thickness of the underline -print('Underline thickness:', font.underline_thickness) +print("Num instances: ", font.num_named_instances) # number of named instances in file +print("Num faces: ", font.num_faces) # number of faces in file +print("Num glyphs: ", font.num_glyphs) # number of glyphs in the face +print("Family name: ", font.family_name) # face family name +print("Style name: ", font.style_name) # face style name +print("PS name: ", font.postscript_name) # the postscript name +print("Num fixed: ", font.num_fixed_sizes) # number of embedded bitmaps +print("Bbox: ", font.bbox) # the face global bounding box (xmin, ymin, xmax, ymax) +print("EM: ", font.units_per_EM) # number of font units covered by the EM +print("Ascender: ", font.ascender) # the ascender in 26.6 units +print("Descender: ", font.descender) # the descender in 26.6 units +print("Height: ", font.height) # the height in 26.6 units +print("Max adv width: ", font.max_advance_width) # maximum horizontal cursor advance +print("Max adv height: ", font.max_advance_height) # same for vertical layout +print("Underline pos: ", font.underline_position) # vertical position of the underline bar +print("Underline thickness:", font.underline_thickness) # vertical thickness of the underline for flag in ft.StyleFlags: name = flag.name.replace('_', ' ').title() + ':' @@ -59,79 +51,84 @@ name = flag.name.replace('_', ' ').title() + ':' print(f"{name:17}", flag in font.face_flags) -# ── Visualise font metrics ──────────────────────────────────────────────────── -# Normalise all metrics to units_per_EM so values are in the range [-1, 1]. -# This figure is used by Sphinx Gallery to auto-generate the gallery thumbnail. +# Normalise all vertical metrics to units_per_EM so all y-values sit in [-1, 1]. u = font.units_per_EM -asc = font.ascender / u -desc = font.descender / u +asc = font.ascender / u +desc = font.descender / u bbox_ymax = font.bbox[3] / u bbox_ymin = font.bbox[1] / u -ul_pos = font.underline_position / u -ul_thick = font.underline_thickness / u +ul_pos = font.underline_position / u +ul_thick = font.underline_thickness / u fig, ax = plt.subplots(figsize=(8, 6)) -# Metric lines drawn FIRST (lower zorder) so text renders on top of them. +fp = FontProperties(fname=font_path) +tp = TextPath((0, 0), "Ag", size=1, prop=fp) +text_bb = tp.get_extents() + +# Centre the glyph at a fixed x position, then read back where it actually lands. +GLYPH_CENTER_X = 0.70 +x_offset = GLYPH_CENTER_X - (text_bb.x0 + text_bb.width / 2) + +# True left/right edges of the rendered glyph in data coordinates. +glyph_x0 = text_bb.x0 + x_offset +glyph_x1 = text_bb.x1 + x_offset + +# Lines, rectangle and labels are all derived from these real glyph bounds. +H_MARGIN = 0.05 # horizontal padding around glyph +LINE_X0 = glyph_x0 - H_MARGIN # lines start here +LINE_X1 = glyph_x1 + H_MARGIN # lines end here (always past glyph edge) +LABEL_X = LINE_X1 + 0.08 # metric labels start here + metrics = [ ("bbox top (ymax)", bbox_ymax, "tab:green"), ("ascender", asc, "tab:blue"), - ("baseline (y=0)", 0, "black"), + ("y = 0 (origin)", 0, "black"), ("underline_position", ul_pos, "tab:orange"), ("descender", desc, "tab:red"), ("bbox bottom (ymin)", bbox_ymin, "tab:purple"), ] -# Lines span from left edge to 72% of axes width — crossing through the glyph. -# Labels sit at 75%, clearly to the right of the lines. for label, y, color in metrics: - ax.plot( - [0.02, 0.72], [y, y], - color=color, linewidth=1.5, linestyle='--', alpha=0.9, zorder=2) - # default position - y_pos = y - - # adjust only bbox labels - if "bbox top" in label: - y_pos = y - 0.015 - elif "bbox bottom" in label: - y_pos = y + 0.015 - - ax.text( - 0.75, y_pos, label, color=color, va='center', - fontsize=9, fontweight='medium', ha='left', zorder=2) - -# Underline thickness — shaded band between underline_position and its lower edge. -ax.fill_between([0.02, 0.72], - ul_pos - ul_thick, - ul_pos, - color='tab:orange', - alpha=0.22, + ax.plot([LINE_X0, LINE_X1], [y, y], + color=color, linewidth=1.5, linestyle='--', alpha=0.9, zorder=2) + # Nudge bbox-edge labels slightly away from the rectangle border. + y_text = (y - 0.015 if "bbox top" in label else + y + 0.015 if "bbox bottom" in label else y) + ax.text(LABEL_X, y_text, label, + color=color, va='center', ha='left', + fontsize=9, fontweight='medium', zorder=2) + +# Underline thickness: shaded band from (ul_pos − ul_thick) to ul_pos. +ax.fill_between([LINE_X0, LINE_X1], + ul_pos - ul_thick, ul_pos, + color='tab:orange', alpha=0.22, label=f'underline_thickness = {font.underline_thickness}', zorder=1) -# Bounding box (font.bbox) as a rectangle. Drawn after lines, before text. -ax.add_patch(plt.Rectangle( - (0.02, bbox_ymin), 0.70, (bbox_ymax - bbox_ymin), - fill=False, edgecolor='black', linestyle='-', - linewidth=1.5, alpha=0.6, zorder=3, - label='font.bbox' +# font.bbox visualised as a rectangle. x-span matches the line region so the +# box always contains the glyph and aligns with the metric lines exactly. +ax.add_patch(Rectangle( + (LINE_X0, bbox_ymin), LINE_X1 - LINE_X0, bbox_ymax - bbox_ymin, + fill=False, edgecolor='black', linewidth=1.5, linestyle='-', + alpha=0.6, zorder=3, label='font.bbox', )) -# Render "Ag" on top of everything — zorder=10 ensures no line covers the text. -# 'A' shows ascender/cap-height, 'g' shows descender. -fp = FontProperties(fname=font_path) -ax.text(0.30, 0.0, "Ag", fontproperties=fp, fontsize=150, - va='baseline', ha='center', color='black', zorder=10) +# Glyph path — translate only (scale = 1.0 implicit); high zorder so it sits +# on top of the reference lines. +ax.add_patch(PathPatch( + tp, + transform=matplotlib.transforms.Affine2D().translate(x_offset, 0) + ax.transData, + color='black', + zorder=10, +)) -ax.set_xlim(0, 1.35) +# x-limit: start at 0, end with enough room for the longest label. +ax.set_xlim(LINE_X0 - 0.05, LABEL_X + 0.75) ax.set_ylim(bbox_ymin - 0.10, bbox_ymax + 0.15) -ax.set_title( - f"Font metrics — {font.family_name} {font.style_name}\n" - f"(values normalised to units_per_EM = {font.units_per_EM})", - fontsize=11.5, pad=15 -) -ax.legend(fontsize=8, loc='lower right', frameon=False) +ax.set_title(f"Font metrics — {font.family_name} {font.style_name}", + fontsize=11.5, pad=15) +ax.legend(fontsize=8, loc='upper right', bbox_to_anchor=(1.02, 0.95), frameon=False) ax.axis('off') plt.tight_layout(pad=1.5) -plt.show() +plt.show() \ No newline at end of file From 08e2d319815d60b505a7339ef4227b8022768147 Mon Sep 17 00:00:00 2001 From: Aman Kushwaha Date: Sat, 28 Mar 2026 00:53:52 +0530 Subject: [PATCH 3/8] DOC: fix linting errors in ftface_props example --- galleries/examples/misc/ftface_props.py | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/galleries/examples/misc/ftface_props.py b/galleries/examples/misc/ftface_props.py index c4ae3201ccee..a613ee40b6d1 100644 --- a/galleries/examples/misc/ftface_props.py +++ b/galleries/examples/misc/ftface_props.py @@ -33,15 +33,15 @@ print("Style name: ", font.style_name) # face style name print("PS name: ", font.postscript_name) # the postscript name print("Num fixed: ", font.num_fixed_sizes) # number of embedded bitmaps -print("Bbox: ", font.bbox) # the face global bounding box (xmin, ymin, xmax, ymax) -print("EM: ", font.units_per_EM) # number of font units covered by the EM +print("Bbox: ", font.bbox) # global bounding box (xmin, ymin, xmax, ymax) +print("EM: ", font.units_per_EM) # font units per EM print("Ascender: ", font.ascender) # the ascender in 26.6 units print("Descender: ", font.descender) # the descender in 26.6 units print("Height: ", font.height) # the height in 26.6 units -print("Max adv width: ", font.max_advance_width) # maximum horizontal cursor advance +print("Max adv width: ", font.max_advance_width) # max horizontal advance print("Max adv height: ", font.max_advance_height) # same for vertical layout -print("Underline pos: ", font.underline_position) # vertical position of the underline bar -print("Underline thickness:", font.underline_thickness) # vertical thickness of the underline +print("Underline pos: ", font.underline_position) # underline bar position +print("Underline thickness:", font.underline_thickness) # underline thickness for flag in ft.StyleFlags: name = flag.name.replace('_', ' ').title() + ':' @@ -53,12 +53,12 @@ # Normalise all vertical metrics to units_per_EM so all y-values sit in [-1, 1]. u = font.units_per_EM -asc = font.ascender / u -desc = font.descender / u +asc = font.ascender / u +desc = font.descender / u bbox_ymax = font.bbox[3] / u bbox_ymin = font.bbox[1] / u -ul_pos = font.underline_position / u -ul_thick = font.underline_thickness / u +ul_pos = font.underline_position / u +ul_thick = font.underline_thickness / u fig, ax = plt.subplots(figsize=(8, 6)) @@ -76,9 +76,9 @@ # Lines, rectangle and labels are all derived from these real glyph bounds. H_MARGIN = 0.05 # horizontal padding around glyph -LINE_X0 = glyph_x0 - H_MARGIN # lines start here -LINE_X1 = glyph_x1 + H_MARGIN # lines end here (always past glyph edge) -LABEL_X = LINE_X1 + 0.08 # metric labels start here +LINE_X0 = glyph_x0 - H_MARGIN # lines start here +LINE_X1 = glyph_x1 + H_MARGIN # lines end here (always past glyph edge) +LABEL_X = LINE_X1 + 0.08 # metric labels start here metrics = [ ("bbox top (ymax)", bbox_ymax, "tab:green"), @@ -93,7 +93,7 @@ ax.plot([LINE_X0, LINE_X1], [y, y], color=color, linewidth=1.5, linestyle='--', alpha=0.9, zorder=2) # Nudge bbox-edge labels slightly away from the rectangle border. - y_text = (y - 0.015 if "bbox top" in label else + y_text = (y - 0.015 if "bbox top" in label else y + 0.015 if "bbox bottom" in label else y) ax.text(LABEL_X, y_text, label, color=color, va='center', ha='left', @@ -131,4 +131,4 @@ ax.legend(fontsize=8, loc='upper right', bbox_to_anchor=(1.02, 0.95), frameon=False) ax.axis('off') plt.tight_layout(pad=1.5) -plt.show() \ No newline at end of file +plt.show() From 0d8fe30e7af9d375bf632a6fee9259cc2fb97e92 Mon Sep 17 00:00:00 2001 From: Aman Kushwaha Date: Sat, 28 Mar 2026 01:09:38 +0530 Subject: [PATCH 4/8] STY: fix import order --- galleries/examples/misc/ftface_props.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/galleries/examples/misc/ftface_props.py b/galleries/examples/misc/ftface_props.py index a613ee40b6d1..ce76e82bc946 100644 --- a/galleries/examples/misc/ftface_props.py +++ b/galleries/examples/misc/ftface_props.py @@ -10,13 +10,14 @@ import os -import matplotlib import matplotlib.pyplot as plt -import matplotlib.transforms + +import matplotlib from matplotlib.font_manager import FontProperties +import matplotlib.ft2font as ft from matplotlib.patches import PathPatch, Rectangle from matplotlib.textpath import TextPath -import matplotlib.ft2font as ft +import matplotlib.transforms # Use a font shipped with Matplotlib. font_path = os.path.join( From e5a1d408fe8ea3cadb2db8c12b4921726e35f176 Mon Sep 17 00:00:00 2001 From: Aman Kushwaha Date: Sun, 29 Mar 2026 22:58:55 +0530 Subject: [PATCH 5/8] =?UTF-8?q?DOC:=20use=20=C3=81gua=20as=20sample=20text?= =?UTF-8?q?=20to=20show=20ascender=20and=20descender?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- galleries/examples/misc/ftface_props.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galleries/examples/misc/ftface_props.py b/galleries/examples/misc/ftface_props.py index ce76e82bc946..c6510bdce0b8 100644 --- a/galleries/examples/misc/ftface_props.py +++ b/galleries/examples/misc/ftface_props.py @@ -64,7 +64,7 @@ fig, ax = plt.subplots(figsize=(8, 6)) fp = FontProperties(fname=font_path) -tp = TextPath((0, 0), "Ag", size=1, prop=fp) +tp = TextPath((0, 0), "Água", size=1, prop=fp) text_bb = tp.get_extents() # Centre the glyph at a fixed x position, then read back where it actually lands. From 49a542cbab77df5467ea464d37f051886ee70f1f Mon Sep 17 00:00:00 2001 From: Aman Kushwaha Date: Sun, 29 Mar 2026 23:35:56 +0530 Subject: [PATCH 6/8] DOC: move legend to bottom to avoid label overlap --- galleries/examples/misc/ftface_props.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/galleries/examples/misc/ftface_props.py b/galleries/examples/misc/ftface_props.py index c6510bdce0b8..fc6e35e345af 100644 --- a/galleries/examples/misc/ftface_props.py +++ b/galleries/examples/misc/ftface_props.py @@ -61,7 +61,7 @@ ul_pos = font.underline_position / u ul_thick = font.underline_thickness / u -fig, ax = plt.subplots(figsize=(8, 6)) +fig, ax = plt.subplots(figsize=(9.8, 6)) fp = FontProperties(fname=font_path) tp = TextPath((0, 0), "Água", size=1, prop=fp) @@ -82,11 +82,11 @@ LABEL_X = LINE_X1 + 0.08 # metric labels start here metrics = [ - ("bbox top (ymax)", bbox_ymax, "tab:green"), - ("ascender", asc, "tab:blue"), - ("y = 0 (origin)", 0, "black"), - ("underline_position", ul_pos, "tab:orange"), - ("descender", desc, "tab:red"), + ("bbox top (ymax)", bbox_ymax, "tab:green"), + ("ascender", asc, "tab:blue"), + ("y = 0 (origin)", 0, "black"), + ("underline_position", ul_pos, "tab:orange"), + ("descender", desc, "tab:red"), ("bbox bottom (ymin)", bbox_ymin, "tab:purple"), ] @@ -129,7 +129,8 @@ ax.set_ylim(bbox_ymin - 0.10, bbox_ymax + 0.15) ax.set_title(f"Font metrics — {font.family_name} {font.style_name}", fontsize=11.5, pad=15) -ax.legend(fontsize=8, loc='upper right', bbox_to_anchor=(1.02, 0.95), frameon=False) +ax.legend(fontsize=8, loc='lower center', bbox_to_anchor=(0.5, -0.12), + frameon=False, ncol=2) ax.axis('off') plt.tight_layout(pad=1.5) plt.show() From 6b2ecab78fb924a0ebc3888ac69a3bfce20cec98 Mon Sep 17 00:00:00 2001 From: AMAN KUSHWAHA Date: Mon, 30 Mar 2026 23:24:35 +0530 Subject: [PATCH 7/8] Update galleries/examples/misc/ftface_props.py Co-authored-by: hannah --- galleries/examples/misc/ftface_props.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/galleries/examples/misc/ftface_props.py b/galleries/examples/misc/ftface_props.py index fc6e35e345af..fde8862fd786 100644 --- a/galleries/examples/misc/ftface_props.py +++ b/galleries/examples/misc/ftface_props.py @@ -91,13 +91,9 @@ ] for label, y, color in metrics: - ax.plot([LINE_X0, LINE_X1], [y, y], - color=color, linewidth=1.5, linestyle='--', alpha=0.9, zorder=2) + l, = ax.hline(y, color=color, linewidth=1.5, linestyle='--', alpha=0.9, zorder=2) # Nudge bbox-edge labels slightly away from the rectangle border. - y_text = (y - 0.015 if "bbox top" in label else - y + 0.015 if "bbox bottom" in label else y) - ax.text(LABEL_X, y_text, label, - color=color, va='center', ha='left', + ax.annotate (LABEL_X, y_text, label, (), xycoords = (1, .5), color=color, va='center', ha='left', fontsize=9, fontweight='medium', zorder=2) # Underline thickness: shaded band from (ul_pos − ul_thick) to ul_pos. From 5eb00ba246050e93fb040590127b7718d57cb96b Mon Sep 17 00:00:00 2001 From: Aman Kushwaha Date: Wed, 1 Apr 2026 15:27:48 +0530 Subject: [PATCH 8/8] Use true font.bbox bounds in ftface_props example --- galleries/examples/misc/ftface_props.py | 132 +++++++++++++++--------- 1 file changed, 81 insertions(+), 51 deletions(-) diff --git a/galleries/examples/misc/ftface_props.py b/galleries/examples/misc/ftface_props.py index fde8862fd786..3aeeb64b117f 100644 --- a/galleries/examples/misc/ftface_props.py +++ b/galleries/examples/misc/ftface_props.py @@ -20,10 +20,7 @@ import matplotlib.transforms # Use a font shipped with Matplotlib. -font_path = os.path.join( - matplotlib.get_data_path(), - 'fonts/ttf/DejaVuSans-Oblique.ttf' -) +font_path = os.path.join(matplotlib.get_data_path(), "fonts/ttf/DejaVuSans-Oblique.ttf") font = ft.FT2Font(font_path) @@ -36,20 +33,20 @@ print("Num fixed: ", font.num_fixed_sizes) # number of embedded bitmaps print("Bbox: ", font.bbox) # global bounding box (xmin, ymin, xmax, ymax) print("EM: ", font.units_per_EM) # font units per EM -print("Ascender: ", font.ascender) # the ascender in 26.6 units -print("Descender: ", font.descender) # the descender in 26.6 units -print("Height: ", font.height) # the height in 26.6 units +print("Ascender: ", font.ascender) # the ascender in 26.6 units +print("Descender: ", font.descender) # the descender in 26.6 units +print("Height: ", font.height) # the height in 26.6 units print("Max adv width: ", font.max_advance_width) # max horizontal advance print("Max adv height: ", font.max_advance_height) # same for vertical layout print("Underline pos: ", font.underline_position) # underline bar position print("Underline thickness:", font.underline_thickness) # underline thickness for flag in ft.StyleFlags: - name = flag.name.replace('_', ' ').title() + ':' + name = flag.name.replace("_", " ").title() + ":" print(f"{name:17}", flag in font.style_flags) for flag in ft.FaceFlags: - name = flag.name.replace('_', ' ').title() + ':' + name = flag.name.replace("_", " ").title() + ":" print(f"{name:17}", flag in font.face_flags) # Normalise all vertical metrics to units_per_EM so all y-values sit in [-1, 1]. @@ -58,6 +55,8 @@ desc = font.descender / u bbox_ymax = font.bbox[3] / u bbox_ymin = font.bbox[1] / u +bbox_xmin = font.bbox[0] / u +bbox_xmax = font.bbox[2] / u ul_pos = font.underline_position / u ul_thick = font.underline_thickness / u @@ -67,19 +66,13 @@ tp = TextPath((0, 0), "Água", size=1, prop=fp) text_bb = tp.get_extents() -# Centre the glyph at a fixed x position, then read back where it actually lands. -GLYPH_CENTER_X = 0.70 -x_offset = GLYPH_CENTER_X - (text_bb.x0 + text_bb.width / 2) - -# True left/right edges of the rendered glyph in data coordinates. -glyph_x0 = text_bb.x0 + x_offset -glyph_x1 = text_bb.x1 + x_offset +# Align the glyph's left edge with bbox_xmin. +x_offset = bbox_xmin - text_bb.x0 -# Lines, rectangle and labels are all derived from these real glyph bounds. -H_MARGIN = 0.05 # horizontal padding around glyph -LINE_X0 = glyph_x0 - H_MARGIN # lines start here -LINE_X1 = glyph_x1 + H_MARGIN # lines end here (always past glyph edge) -LABEL_X = LINE_X1 + 0.08 # metric labels start here +# Lines, rectangle and labels are derived from the true font bbox. +LINE_X0 = bbox_xmin +LINE_X1 = bbox_xmax +LABEL_X = LINE_X1 + 0.08 # metric labels start here metrics = [ ("bbox top (ymax)", bbox_ymax, "tab:green"), @@ -91,42 +84,79 @@ ] for label, y, color in metrics: - l, = ax.hline(y, color=color, linewidth=1.5, linestyle='--', alpha=0.9, zorder=2) + ax.plot( + [LINE_X0, LINE_X1], + [y, y], + color=color, + linewidth=1.5, + linestyle="--", + alpha=0.9, + zorder=2, + ) # Nudge bbox-edge labels slightly away from the rectangle border. - ax.annotate (LABEL_X, y_text, label, (), xycoords = (1, .5), color=color, va='center', ha='left', - fontsize=9, fontweight='medium', zorder=2) + y_text = ( + y - 0.015 if "bbox top" in label else y + 0.015 if "bbox bottom" in label else y + ) + ax.text( + LABEL_X, + y_text, + label, + color=color, + va="center", + ha="left", + fontsize=9, + fontweight="medium", + zorder=2, + ) # Underline thickness: shaded band from (ul_pos − ul_thick) to ul_pos. -ax.fill_between([LINE_X0, LINE_X1], - ul_pos - ul_thick, ul_pos, - color='tab:orange', alpha=0.22, - label=f'underline_thickness = {font.underline_thickness}', - zorder=1) - -# font.bbox visualised as a rectangle. x-span matches the line region so the -# box always contains the glyph and aligns with the metric lines exactly. -ax.add_patch(Rectangle( - (LINE_X0, bbox_ymin), LINE_X1 - LINE_X0, bbox_ymax - bbox_ymin, - fill=False, edgecolor='black', linewidth=1.5, linestyle='-', - alpha=0.6, zorder=3, label='font.bbox', -)) +ax.fill_between( + [LINE_X0, LINE_X1], + ul_pos - ul_thick, + ul_pos, + color="tab:orange", + alpha=0.22, + label=f"underline_thickness = {font.underline_thickness}", + zorder=1, +) + +# font.bbox visualised as a rectangle using the true font x/y bounds. +ax.add_patch( + Rectangle( + (bbox_xmin, bbox_ymin), + bbox_xmax - bbox_xmin, + bbox_ymax - bbox_ymin, + fill=False, + edgecolor="black", + linewidth=1.5, + linestyle="-", + alpha=0.6, + zorder=3, + label="font.bbox", + ) +) # Glyph path — translate only (scale = 1.0 implicit); high zorder so it sits # on top of the reference lines. -ax.add_patch(PathPatch( - tp, - transform=matplotlib.transforms.Affine2D().translate(x_offset, 0) + ax.transData, - color='black', - zorder=10, -)) - -# x-limit: start at 0, end with enough room for the longest label. -ax.set_xlim(LINE_X0 - 0.05, LABEL_X + 0.75) +ax.add_patch( + PathPatch( + tp, + transform=matplotlib.transforms.Affine2D().translate(x_offset, 0) + + ax.transData, + color="black", + zorder=10, + ) +) + +# x-limit: start at the bbox left edge and leave room for labels. +ax.set_xlim(LINE_X0, LABEL_X + 0.75) ax.set_ylim(bbox_ymin - 0.10, bbox_ymax + 0.15) -ax.set_title(f"Font metrics — {font.family_name} {font.style_name}", - fontsize=11.5, pad=15) -ax.legend(fontsize=8, loc='lower center', bbox_to_anchor=(0.5, -0.12), - frameon=False, ncol=2) -ax.axis('off') +ax.set_title( + f"Font metrics — {font.family_name} {font.style_name}", fontsize=11.5, pad=15 +) +ax.legend( + fontsize=8, loc="lower center", bbox_to_anchor=(0.5, -0.12), frameon=False, ncol=2 +) +ax.axis("off") plt.tight_layout(pad=1.5) plt.show()