GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — [Bug]: bbox computed incorrectly when using a superscript · Issue #21653 · matplotlib/matplotlib

[Bug]: bbox computed incorrectly when using a superscript #21653

New issue
Copy link
New issue
Copy link
Closed
Closed
[Bug]: bbox computed incorrectly when using a superscript#21653
Copy link
Labels
topic: text/usetex
Milestone
v3.11.0

Description

@kc9jud
kc9jud
opened on Nov 16, 2021
Issue body actions

Bug summary

When including LaTeX with a superscript, the bbox is computed incorrectly.

Code for reproduction

import matplotlib.pyplot as plt

kw = dict(
    usetex=True,
    fontsize=40,
    bbox=dict(boxstyle='round,pad=0', fc='lightsteelblue', ec='none')
)

plt.axhline(y=0.7)
plt.annotate('A', xy=(0.3, 0.7), **kw)
plt.annotate(r'$A$', xy=(0.4, 0.7), **kw)
plt.annotate(r'$^{\tiny 1}A$', xy=(0.5, 0.7), **kw)
plt.annotate(r'$^{\tiny 1j}A$', xy=(0.65, 0.7), **kw)

plt.axhline(y=0.4)
plt.annotate('A', xy=(0.3, 0.4), va='bottom', **kw)
plt.annotate(r'$A$', xy=(0.4, 0.4), va='bottom', **kw)
plt.annotate(r'$^{1}A$', xy=(0.5, 0.4), va='bottom', **kw)
plt.annotate(r'$^{1j}A$', xy=(0.65, 0.4), va='bottom', **kw)

plt.show()

Actual outcome

Figure_1

Expected outcome

The bounding box should correctly surround the superscript.

Additional information

Related bugs: #7075 #14177

Operating system

Ubuntu

Matplotlib Version

3.5.0

Matplotlib Backend

No response

Python version

3.8.10

Jupyter version

No response

Installation

pip

Activity

  1. jklymak
    added
    topic: text/usetex
    on Nov 16, 2021
  2. anntzer commented on Nov 16, 2021

    @anntzer
    anntzer
    on Nov 16, 2021
    Contributor
    More actions

    As it turns out, the previous metrics handling was, let's say, not optimal. (The main problem was the use of glyph height (=ascent+descent) and glyph descent, rather than ascent and descent, and some adjustments to descent were not propagated back to the height.)
    The following patch seems to resolve the issue here, although many test baselines also get slightly shifted :( (likely related to the "wiggly baseline" problem in nonhinted text alluded to elsewhere).

    diff --git i/lib/matplotlib/text.py w/lib/matplotlib/text.py
    index b2b2195837..02f34ddf7e 100644
    --- i/lib/matplotlib/text.py
    +++ w/lib/matplotlib/text.py
    @@ -62,23 +62,23 @@ def _get_textbox(text, renderer):
         # called within the _get_textbox. So, it would better to move this
         # function as a method with some refactoring of _get_layout method.
     
    -    projected_xs = []
    -    projected_ys = []
    +    projected_xys = []
     
         theta = np.deg2rad(text.get_rotation())
         tr = Affine2D().rotate(-theta)
     
    -    _, parts, d = text._get_layout(renderer)
    +    _, parts, _ = text._get_layout(renderer)
     
    -    for t, wh, x, y in parts:
    -        w, h = wh
    -
    -        xt1, yt1 = tr.transform((x, y))
    -        yt1 -= d
    -        xt2, yt2 = xt1 + w, yt1 + h
    -
    -        projected_xs.extend([xt1, xt2])
    -        projected_ys.extend([yt1, yt2])
    +    for i, (t, wad, x, y) in enumerate(parts):
    +        w, a, d = wad
    +        xt, yt = tr.transform((x, y))
    +        projected_xys.extend([
    +            (xt, yt + a),
    +            (xt, yt - d),
    +            (xt + w, yt + a),
    +            (xt + w, yt - d),
    +        ])
    +    projected_xs, projected_ys = zip(*projected_xys)
     
         xt_box, yt_box = min(projected_xs), min(projected_ys)
         w_box, h_box = max(projected_xs) - xt_box, max(projected_ys) - yt_box
    @@ -300,8 +300,7 @@ class Text(Artist):
             thisx, thisy = 0.0, 0.0
             lines = self.get_text().split("\n")  # Ensures lines is not empty.
     
    -        ws = []
    -        hs = []
    +        wads = []  # widths, ascents above baseline, descents below baseline.
             xs = []
             ys = []
     
    @@ -309,7 +308,8 @@ class Text(Artist):
             _, lp_h, lp_d = renderer.get_text_width_height_descent(
                 "lp", self._fontproperties,
                 ismath="TeX" if self.get_usetex() else False)
    -        min_dy = (lp_h - lp_d) * self._linespacing
    +        lp_a = lp_h - lp_d  # ascent, i.e. height above-the-baseline.
    +        min_dy = lp_a * self._linespacing
     
             for i, line in enumerate(lines):
                 clean_line, ismath = self._preprocess_math(line)
    @@ -318,26 +318,25 @@ class Text(Artist):
                         clean_line, self._fontproperties, ismath=ismath)
                 else:
                     w = h = d = 0
    -
    -            # For multiline text, increase the line spacing when the text
    -            # net-height (excluding baseline) is larger than that of a "l"
    -            # (e.g., use of superscripts), which seems what TeX does.
    -            h = max(h, lp_h)
    +            a = h - d
    +            # Pretend that the ascent of all lines is at least as large as "l",
    +            # to ensure good linespacing.  This seems similar to what TeX does.
    +            a = max(a, lp_a)
    +            # Pretend that the descent of all lines is at least as large as
    +            # "p", to ensure good linespacing.
                 d = max(d, lp_d)
    +            # Ideally, a should not be adjusted on the first line and d should
    +            # not be adjusted on the last line (they don't participate in
    +            # linespacing), but this would break all baseline images.
     
    -            ws.append(w)
    -            hs.append(h)
    +            baseline = a - thisy  # Last line metrics; needed later.
     
    -            # Metrics of the last line that are needed later:
    -            baseline = (h - d) - thisy
    -
    -            if i == 0:
    -                # position at baseline
    -                thisy = -(h - d)
    -            else:
    -                # put baseline a good distance from bottom of previous line
    -                thisy -= max(min_dy, (h - d) * self._linespacing)
    +            if i == 0:  # position at baseline
    +                thisy = -a
    +            else:  # put baseline a good distance from bottom of previous line
    +                thisy -= max(min_dy, a * self._linespacing)
     
    +            wads.append((w, a, d))
                 xs.append(thisx)  # == 0.
                 ys.append(thisy)
     
    @@ -347,6 +346,7 @@ class Text(Artist):
             descent = d
     
             # Bounding box definition:
    +        ws = [w for w, a, d in wads]
             width = max(ws)
             xmin = 0
             xmax = width
    @@ -440,7 +440,7 @@ class Text(Artist):
             # now rotate the positions around the first (x, y) position
             xys = M.transform(offset_layout) - (offsetx, offsety)
     
    -        ret = bbox, list(zip(lines, zip(ws, hs), *xys.T)), descent
    +        ret = bbox, list(zip(lines, wads, *xys.T)), descent
             self._cached[key] = ret
             return ret
     
    @@ -709,7 +709,7 @@ class Text(Artist):
     
                 angle = self.get_rotation()
     
    -            for line, wh, x, y in info:
    +            for line, wad, x, y in info:
     
                     mtext = self if len(info) == 1 else None
                     x = x + posx
  3. kc9jud commented on Nov 17, 2021

    @kc9jud
    kc9jud
    on Nov 17, 2021
    Author
    More actions

    @anntzer That patch almost fixes my original problem as well...

    before:
    test1

    after:
    test2

    You can see that the superscript still isn't quite within the bbox, but for my current purpose it's good enough.


    edit to add: Here's the code for generating that new test case:

    plt.axhline(y=0.05)
    plt.annotate(r"$^6\mathrm{He}$ $0^+$" " \n " r"$n 0s_{1/2}$", xy=(0.01, 0.05), va='bottom', **kw)
  4. kc9jud commented on Nov 17, 2021

    @kc9jud
    kc9jud
    on Nov 17, 2021
    Author
    More actions

    It's possible that the slight (1-2px) issue is actually the same as #14177 (comment) ("wiggly baseline")

  5. anntzer commented on Nov 17, 2021

    @anntzer
    anntzer
    on Nov 17, 2021
    Contributor
    More actions

    Actually this is likely something else, because it occurs even if you set a huge dpi. My uneducated guess is that the tfm metrics (used when parsing the dpi file) are in fact "incorrect", because they are designed to get proper glyph alignment (see https://tex.stackexchange.com/questions/526103/why-does-cmsy10-tfm-give-the-minus-sign-a-positive-depth for a similar case), rather than to correctly give rasterization bounding boxes. In other words, the metrics file claims that the glyph is shorter than it really is.

    If that is correct (to be checked...), the solution would be to not use the tfm files in dviread, and directly load the Type1 fonts and use their metrics. (That would also require a decent bit of surgery...)

  6. anntzer
    added this to the v3.11.0 milestone on Sep 5, 2025
  7. anntzer commented on Sep 5, 2025

    @anntzer
    anntzer
    on Sep 5, 2025
    Contributor
    More actions

    Milestoning as 3.11 to try to get something along the lines of #21653 (comment) into the text-overhaul branch, although I can't guarantee any timing to look into it.

  8. anntzer
    added this to Font and text overhaulon Sep 5, 2025
  9. github-project-automation
    moved this to Waiting for other PR in Font and text overhaulon Sep 5, 2025
  10. QuLogic commented on Feb 6, 2026

    @QuLogic
    QuLogic
    on Feb 6, 2026
    Member
    More actions

    @anntzer do you have plans to come back to this soon?

  11. anntzer commented on Feb 6, 2026

    @anntzer
    anntzer
    on Feb 6, 2026
    Contributor
    More actions

    The patch I gave above still works and is still (I believe) technically better (because it doesn't confuse height and ascent), with the same limitations as noted previously (the metrics are still slightly off because they are tfm metrics rather than real ones). I guess we can at least get that in for now, and always improve on that by switching to real metrics.
    Made a PR for that.

  12. QuLogic commented on May 15, 2026

    @QuLogic
    QuLogic
    on May 15, 2026
    Member
    More actions

    This looks like it's only off by a pixel at most now?

    Image
  13. tacaswell commented on May 15, 2026

    @tacaswell
    tacaswell
    on May 15, 2026
    Member
    More actions

    I'm going to make the call that this is close enough (from current main):

    Image
  14. tacaswell
    closed this as completedon May 15, 2026
  15. github-project-automation
    moved this from Waiting for other PR to Done in Font and text overhaulon May 15, 2026
  16. QuLogic commented on May 15, 2026

    @QuLogic
    QuLogic
    on May 15, 2026
    Member
    More actions

    And at very large zoom, we can see that the rectangle is actually antialiased a bit, so while it looks 2 pixels off, it's maybe only within 1 pixel:

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    topic: text/usetex

    Type

    No type

    Projects

    • Font and text overhaul
      Status
      Done
      Show more project fields

    Milestone

    • v3.11.0

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    You are visitor 01402503 — GeezerVison does not store the pages you look up.
    Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.