Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
kc9jud opened this issue Nov 16, 2021 · 4 comments
Open

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

kc9jud opened this issue Nov 16, 2021 · 4 comments

Comments

@kc9jud
Copy link

kc9jud commented Nov 16, 2021

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

@anntzer
Copy link
Contributor

anntzer commented Nov 16, 2021

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

@kc9jud
Copy link
Author

kc9jud commented Nov 17, 2021

@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)

@kc9jud
Copy link
Author

kc9jud commented Nov 17, 2021

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

@anntzer
Copy link
Contributor

anntzer commented Nov 17, 2021

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...)

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

No branches or pull requests

3 participants