Skip to content

[Bug]: usetex text drawn as paths (SVG, path effects) is narrower than its layout #32389

Description

@thiebes

Bug summary

TextToPath.get_glyphs_tex always runs LaTeX at the fixed FONT_SCALE of 100 pt and ignores the size in prop. TeX fonts have a separate design for each size, so all usetex text drawn as paths (e.g., all SVG output, and text with path_effects on any backend) uses the 17 pt design. At 6 to 12 pt the resulting text is 5-12% narrower than its layout and has lighter strokes. PNG and PDF output are correct.

Code for reproduction

import re

import matplotlib as mpl
import matplotlib.patheffects as pe
import matplotlib.pyplot as plt

mpl.rcParams["text.usetex"] = True
s = "Hamburgefonstiv"

# The same 6 pt text saved as SVG and as PDF.
fig = plt.figure(figsize=(2, 1))
fig.text(0.1, 0.5, s, fontsize=6)
fig.savefig("usetex.svg")
fig.savefig("usetex.pdf")
with open("usetex.svg", encoding="utf-8") as f:
    svg = f.read()
with open("usetex.pdf", "rb") as f:
    pdf = f.read()
print("SVG fonts:", sorted(set(re.findall(r'id="(CM[A-Z]+\d+)-', svg))))
print("PDF fonts:", sorted({m.decode() for m in
                            re.findall(rb"/BaseFont\s*/(?:[A-Z]{6}\+)?(\w+)", pdf)}))

# Right-aligned 8 pt text on Agg, without and with path effects.
fig = plt.figure(figsize=(1.6, 0.5), dpi=600)
fig.add_artist(plt.Line2D([0.93, 0.93], [0, 1], lw=0.3, color="tab:red"))
fig.text(0.93, 0.62, s, fontsize=8, ha="right", va="center")
fig.text(0.93, 0.25, s, fontsize=8, ha="right", va="center",
         path_effects=[pe.Normal()])
fig.savefig("usetex_path_effects.png")

Actual outcome

SVG fonts: ['CMSS17']
PDF fonts: ['CMSS8']

The same 6 pt text is drawn with the 17 pt design of Computer Modern Sans (CMSS17) in the SVG file, and with CMSS8, the smallest design, in the PDF file.
Two lines of usetex text, right-aligned on a red line; the lower line, with path effects, is narrower and ends short of the line

  • both lines are right-aligned on the red line, 8 pt
  • the lower line has path_effects=[pe.Normal()], so it is drawn through get_glyphs_tex.
  • the lower line narrower is narrower and lighter, and it ends short of the red line

The table below compares the width matplotlib lays the text out with (TexManager.get_text_width_height_descent) with the ink width as drawn by get_glyphs_tex, and with the ink width when LaTeX runs at the text size.

Size Layout width Ink width, drawn (SVG, path effects) Ink width, LaTeX at the text size
6 pt 44.87 pt 39.56 pt 44.76 pt
10 pt 70.43 pt 65.93 pt 70.27 pt
24 pt 158.55 pt 158.22 pt 158.22 pt

For 17 pt and larger CMSS17 is the correct design, so larger text is not affected. The layout width includes the side bearings of the first and last glyph, so it is slightly larger than the ink width even when both use the same design. The bug is the difference between the two ink columns, which closes at 17 pt and above.

Expected outcome

usetex text drawn as paths matches PNG and PDF output, with the design TeX selects for the text's size:

  • The SVG file uses CMSS8 for the 6 pt text, as the PDF file does.
    In the image, the lower line matches the upper line and ends at the red line.
  • In the table, the drawn ink width equals the ink width with LaTeX at the text size, at every size.

Additional information

Likely root cause:

get_glyphs_tex typesets the string with TexManager().make_dvi(s, self.FONT_SCALE), so LaTeX always runs at 100 pt, and its callers scale the result down to the text size. Everywhere else, LaTeX runs at the text's own size. i.e., in the draw_tex methods of the PDF, PS, and PGF backends (prop.get_size_in_points()), in Agg through dvipng, and in the layout measurement (TexManager.get_text_width_height_descent). Because TeX selects a font design by size, only the paths through get_glyphs_tex draw with a different design from the one matplotlib measured.

Affected paths:

The SVG backend does not override draw_tex, so RendererBase.draw_tex draws all usetex text through _draw_text_as_path and get_glyphs_tex, for any svg.fonttype. PathEffectRenderer inherits the same draw_tex, so usetex text with path effects is affected on every backend, including Agg. TextPath(..., usetex=True) also calls get_glyphs_tex. The Cairo backend inherits RendererBase.draw_tex as well.

Possible fix

-        dvifile = TexManager().make_dvi(s, self.FONT_SCALE)
+        size = prop.get_size_in_points()
+        scale = self.FONT_SCALE / size
+        dvifile = TexManager().make_dvi(s, size)
 ...
-            xpositions.append(text.x)
-            ypositions.append(text.y)
-            sizes.append(text.font_size / self.FONT_SCALE)
+            xpositions.append(text.x * scale)
+            ypositions.append(text.y * scale)
+            sizes.append(text.font_size / size)
 ...
         for ox, oy, h, w in page.boxes:
+            ox, oy, h, w = ox * scale, oy * scale, h * scale, w * scale

This is a safe solution because glyph outlines are still loaded at FONT_SCALE, and glyph keys include the font name, so the shared glyph_map of the SVG backend keeps separate entries for CMSS8 and CMSS17 when a figure has mixed sizes. The LaTeX run is the one where the layout measurement is already cached, so the change adds no extra LaTeX runs.

Tested so far as a local patch on 3.11.2:

Limitation for TextPath:

The fix takes the text size from prop, which is correct for the SVG backend and path effects, because they pass the font properties of the text. TextPath accepts its size separately and passes prop to get_text_path unchanged. So TextPath((0, 0), s, size=12, usetex=True) would run LaTeX at the default 10 pt in prop instead of at 12 pt. Passing a copy of prop with its size set to size in TextPath.__init__ would fix that.

Context

I maintain curved-text, a package listed in the third-party registry that draws text along curves from get_glyphs_tex outlines. I found this while adding usetex support to it. For now it works around the bug by setting FONT_SCALE to the text size on its own TextToPath instance, which fixes curved-text but not matplotlib's own SVG and path-effects output. I would be glad to open a PR with the change above and a test, if this direction is acceptable.

AI disclosure

I used an AI assistant (Claude) to trace the code paths, measure the widths, and test the patch. I ran the reproduction and the measurements myself and checked the results, and I reviewed the proposed change line by line.

Operating system

Windows 11

Matplotlib Version

3.11.2

Matplotlib Backend

Agg (image), SVG and PDF via savefig

Python version

3.13.3

Jupyter version

No response

Installation

pip

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions