Skip to content

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

Open

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

  1. AishikDas500 commented on Sep 24, 2026

    @AishikDas500

    Are you planning to open a PR for this, or is it open for someone else to take?

  2. thiebes commented on Sep 25, 2026

    @thiebes
    Author

    Are you planning to open a PR for this, or is it open for someone else to take?

    I won't be able to work on this, @AishikDas500, so please go ahead if you'd like to take it.

  3. AishikDas500 commented on Sep 25, 2026

    @AishikDas500

    Are you planning to open a PR for this, or is it open for someone else to take?

    I won't be able to work on this, @AishikDas500, so please go ahead if you'd like to take it.

    Thank You very much sir, I would like to handle this matter. I will start looking right into it.

  4. krishsinghhura commented on Oct 10, 2026

    @krishsinghhura

    Hi @thiebes and @AishikDas500, I opened #32392 to address this issue. It includes the optical-sizing fix, handling for TextPath resizing, and regression tests, and I’ve addressed the review feedback received so far. Sharing it here so you’re aware of the existing PR. @thiebes, if you have time to try it against your original reproduction or curved-text use case, your feedback would be helpful. Thanks!

  5. thiebes commented on Oct 11, 2026

    @thiebes
    Author

    @krishsinghhura thanks for picking this up. I tried the PR against my original reproduction and against curved-text, and it fixes what I saw.

    I tested by dropping the PR's textpath.py into a 3.11.2 install (Windows 11, MiKTeX), since the PR only touches that file. Before and after:

    Check 3.11.2 PR
    SVG font for 6 pt text CMSS17 CMSS8 (matches PDF)
    Right-aligned 8 pt text with path_effects ends short of the line ends on the line
    Design from get_glyphs_tex at 6/8/10/12/24 pt CMSS17 at all sizes CMSS8/8/10/12/17
    Drawn ink width at 6 pt (layout width 44.87 pt) 39.03 pt 44.20 pt

    TextPath(..., size=12, usetex=True) now gets the 12 pt design too.

    For curved-text, the test suite passes with the PR, and the outlines from the shared converter now match the ones from my workaround (a TextToPath with FONT_SCALE set to the text size) to within 1e-4 pt at every size I tried. So I'll be able to drop the workaround once this is released. I didn't test the resize case raised in review.

    I used an AI assistant (Claude) to set up the test environment and run the comparisons; I checked the results.

  6. krishsinghhura commented on Oct 11, 2026

    @krishsinghhura

    @thiebes glad it fixes your use case! Would you be able to review #32392 as well and approve if you’re comfortable with the changes?

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