Repository navigation
Description
Currently, we set a minimum line height by measuring the height and depth of the "lp" string. Line spacing is then some multiple of the line height (by default 1.2×).
matplotlib/lib/matplotlib/text.py
Lines 446 to 451 in 47874b4
| # Full vertical extent of font, including ascenders and descenders: | |
| _, lp_h, lp_d = _get_text_metrics_with_cache( | |
| renderer, "lp", self._fontproperties, | |
| ismath="TeX" if self.get_usetex() else False, | |
| dpi=self.get_figure(root=True).dpi) | |
| min_dy = (lp_h - lp_d) * self._linespacing |
Looking back into the history, this used to be "Tglp":
matplotlib/lib/matplotlib/text.py
Lines 254 to 256 in 30a5d33
| # Find full vertical extent, including ascenders and descenders: | |
| tmp, heightt = renderer.get_text_width_height( | |
| 'Tglp', self._fontproperties, ismath=False) |
and before that just a "T" was used for line height:
matplotlib/lib/matplotlib/text.py
Lines 251 to 252 in 6f2a3f1
| tmp, heightt = renderer.get_text_width_height( | |
| 'T', self._fontproperties, ismath=False) |
and before that, the "T" was used only for empty lines:
matplotlib/lib/matplotlib/text.py
Lines 183 to 186 in 2e3f321
| if not len(line) and not self.is_math_text(): | |
| # approx the height of empty line with tall char | |
| tmp, h = renderer.get_text_width_height( | |
| 'T', self._fontproperties, ismath=False) |
Now that we are supporting more fonts and languages, this may not be ideal. For example, for the test in #30725, I only specified the OpenMoji font. This font contains only emoji and no Latin characters of any kind. This causes warnings that both l and p are not found even though they aren't used in any user strings. We then end up with the ascender/descender values for the Last Resort font. I haven't found a specific example, but I can imagine a user may be setting some language-specific font that contains no Latin characters and no fallback, and would trigger this warning as well, which might be a bit mysterious.
Microsoft's recommendation is to use the ascender value to measure from top to baseline and descender value from baseline to bottom, plus a line gap to produce the interline spacing (suggested to add up to 120% the height similar to our default.) Of course, fonts being fonts, there are 2 other sets of vertical metrics and various strategies for dealing with them.
Regardless of the multiple values to try, from the history, I don't think we went with the measurement strategy because it was thought that the embedded values were unreliable. Rather, it seems to have been from necessity of not having a font parser.
So the question now goes (with some of my thoughts in parentheses):
- Should we move to font metrics instead? (I think so.)
- If so, which strategy to use? (probably the webfont or Google ones.)
- Should we still try to measure
lpand set some combined minimum or just trust the fonts aren't broken? (hopefully not required.) - What do we do when fallbacks are specified? Currently we get the measurement from the first to match the "lp" string. Should we instead look at all the specified fonts and take a minimum of all of them (even if not used in the text)? Or measure all the fonts used in a line? Or measure all the fonts used in an entire text object? (probably not the first option, unsure whether the second or third.)
Metadata
Metadata
Assignees
Type
Projects
- StatusShow more project fieldsDone
Activity
anntzer commentedon Mar 1, 2026
1. yes, 2. webfont or Google looks good (from reading your link, not from any original research), 3. let's hope not, but I guess this should be reasonably pluggable? 4. probably all fonts actually used in a text object, to have consistent interline spacing?
QuLogic commentedon Mar 2, 2026
I suppose the troublesome part here is figuring out the API to assess this across the backends. Currently, the measurement is done by the backend and the actual ft2font objects are not exposed to
Text(or possibly may not exist for some backends that embed the text directly.)For font metrics, we could extend
get_text_width_height_descentto acceptNoneas the string to measure and have that return the global metrics. It's a bit of a type mixing, but at least arguable, though I'm not sure it's so easy to inspect/fallback for other backends.Trying to figure out the used fonts seems like a whole new API, or some kind of change to the measurements.
Perhaps, it would be better to extend the backend API to process multiple lines itself instead of in
Text, and it could decide how to handle everything? On the other hand, there's also multi-line alignment to consider and maybe we don't want to push all that work to the backend.While trying to implement this, I do think that we need to work out some API for the backends, and this might require some design work or trial-and-error. Perhaps as a stop-gap for now, we can capture the warnings about
l/pand if that occurs, set the minimums to 0? This does mean that multiple lines may not be consistent, but at least they won't be spaced according to the (mostly invisible to the user) Last Resort font, and users won't get a mysterious warning. Then we can spend some time working out how exactly to implement the desired behaviour.anntzer commentedon Mar 2, 2026
Good points. It looks like we're headed towards just passing a Text instance to the backend and let it do whatever it needs with it (
renderer.draw_text_instance(textobj, ...)where it may be good to think carefully about what extra arguments (if any) we want to pass in..., given that the implementation ofdraw_text_instancecan already access alltextobjproperties). This is something we already discussed in #29695, and already how the pgf backend handles alignment (#1081). I was a bit reluctant to go that way, but perhaps we should indeed push all the work to the backends...tacaswell commentedon Mar 3, 2026
How long will this take to sort out? 3.11 is already way later than we wanted and I'm worried about adding another big re-design step to it. We have a history of letting the perfect be the enemy of the good.
If this is less than a week, then lets do it, if more, then what is the cost of leaving this as-is for now and then re-doing this in a year? If it is "regenerate a dozen images" lets delay, if it is "regenerate every image with text" lets do it now.
QuLogic commentedon Mar 3, 2026
I made a silly attempt of just multiplying
lp_handlp_dby 2:which causes 1798 test failures, mostly caused by all ticks moving vertically to re-align. Of course, this is an extreme change and not reflective of what will really happen, but should be the upper bound of how many images could be affected.
QuLogic commentedon Mar 3, 2026
I put together a small test with #31226:
which produces:
So the ascent for
lis slightly bigger than the design size, but the descent forpis noticeably smaller than the design size by over half a pixel.As a first pass, I have taken those metrics and applied them using:
This bypasses the backend for measurement, but maybe that's okay until we can do further API refinement.
Effectively, this will measure the first font, but could be extended to take the maximum of all requested fonts easily. I have not applied the line gap measurement as I haven't yet determined how best to square that with our line spacing setting.
With this change, we get about 720 test image failures. These are all mostly subtle changes of half-pixel alignment, bounding boxes being a bit larger (affecting some
FancyBboxPatchtests and most legend boxes), and more obviously any test with multiline text.QuLogic commentedon Mar 3, 2026
Also an interesting thing I found is that most of the mathtext images changed, even the ones that are purely Computer Modern. It turns out since we don't check that text is pure math before hand, we don't measure the math font, only the default (DejaVu Sans).
Of course, using the font metrics would change Computer Modern as well, had it been used before:
but it's slightly different from what DejaVu Sans would have been.
QuLogic commentedon Mar 12, 2026
I've spent some time reading CSS Inline Layout and CSS Fonts. We aren't of course using CSS or have all the same configuration, but it's probably the closest to an open standard that we can follow. A short summary of the relevant bits of the algorithm are:
sTypoAscenderandsTypoDescenderfrom theOS/2table, with fallback toAscent/Descentfrom thehheatable.sTypoLineGapfromOS/2orlineGapfromhhea.x, they suggest using the total height of anoto measure it. There aren't any suggestions for ascent/descent though.line-heightproperty:leadingvalue for thetext-edgeproperty.)In contrast, what we seem to be doing seems to correspond to the following CSS options, with some differences:
text-heightdefaults to 1.2 (but we call itlinespacing.) However, unlike CSS' use of the first available font, we actually look at all glyphs, which would be more in line with it's 'normal' setting.text-edgeis set totextinstead ofleading(except instead of the metrics, we look at the ascent/descent of the actual glyphs used, or "lp" in the first font.)If we want to be more consistent with CSS, we would have to:
linespacing, and make it the default.linespacingcalculations.QuLogic commentedon Apr 10, 2026
The
text-overhaulbranch is now merged tomain.