-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
[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
Comments
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.) 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 |
@anntzer That patch almost fixes my original problem as well... 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) |
It's possible that the slight (1-2px) issue is actually the same as #14177 (comment) ("wiggly baseline") |
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...) |
Bug summary
When including LaTeX with a superscript, the bbox is computed incorrectly.
Code for reproduction
Actual outcome
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
The text was updated successfully, but these errors were encountered: