1515import matplotlib as mpl
1616from . import _api , artist , cbook , _docstring , colors as mcolors
1717from .artist import Artist
18- from .font_manager import FontProperties
18+ from .font_manager import FontProperties , fontManager , get_font
1919from .patches import FancyArrowPatch , FancyBboxPatch , Rectangle
2020from .textpath import TextPath , TextToPath # noqa # Logically located here
2121from .transforms import (
@@ -241,7 +241,7 @@ def _reset_visual_defaults(
241241 self ._bbox_patch = None # a FancyBboxPatch instance
242242 self ._renderer = None
243243 if linespacing is None :
244- linespacing = 1.2 # Maybe use rcParam later.
244+ linespacing = 'normal' # Maybe use rcParam later.
245245 self .set_linespacing (linespacing )
246246 self .set_rotation_mode (rotation_mode )
247247 self .set_antialiased (mpl ._val_or_rc (antialiased , 'text.antialiased' ))
@@ -433,15 +433,40 @@ def _get_layout(self, renderer):
433433 xs = []
434434 ys = []
435435
436- # Full vertical extent of font, including ascenders and descenders:
437- _ , lp_h , lp_d = _get_text_metrics_with_cache (
438- renderer , "lp" , self ._fontproperties ,
439- ismath = "TeX" if self .get_usetex () else False ,
440- dpi = self .get_figure (root = True ).dpi )
441- lp_a = lp_h - lp_d
442- min_dy = lp_a * self ._linespacing
443-
444- for i , line in enumerate (lines ):
436+ min_ascent = min_descent = line_gap = None
437+ dpi = self .get_figure (root = True ).dpi
438+ # Determine full vertical extent of font, including ascenders and descenders:
439+ if not self .get_usetex ():
440+ font = get_font (fontManager ._find_fonts_by_props (self ._fontproperties ))
441+ possible_metrics = [
442+ ('OS/2' , 'sTypoLineGap' , 'sTypoAscender' , 'sTypoDescender' ),
443+ ('hhea' , 'lineGap' , 'ascent' , 'descent' )
444+ ]
445+ for table_name , linegap_key , ascent_key , descent_key in possible_metrics :
446+ table = font .get_sfnt_table (table_name )
447+ if table is None :
448+ continue
449+ # Rescale to font size/DPI if the metrics were available.
450+ fontsize = self ._fontproperties .get_size_in_points ()
451+ units_per_em = font .get_sfnt_table ('head' )['unitsPerEm' ]
452+ line_gap = table [linegap_key ] / units_per_em * fontsize * dpi / 72
453+ min_ascent = table [ascent_key ] / units_per_em * fontsize * dpi / 72
454+ min_descent = - table [descent_key ] / units_per_em * fontsize * dpi / 72
455+ break
456+ if None in (min_ascent , min_descent ):
457+ # Fallback to font measurement.
458+ _ , h , min_descent = _get_text_metrics_with_cache (
459+ renderer , "lp" , self ._fontproperties ,
460+ ismath = "TeX" if self .get_usetex () else False ,
461+ dpi = dpi )
462+ min_ascent = h - min_descent
463+ line_gap = 0
464+
465+ # Don't increase text height too much if it's not multiple lines.
466+ if len (lines ) == 1 :
467+ line_gap = 0
468+
469+ for line in lines :
445470 clean_line , ismath = self ._preprocess_math (line )
446471 if clean_line :
447472 w , h , d = _get_text_metrics_with_cache (
@@ -451,18 +476,24 @@ def _get_layout(self, renderer):
451476 w = h = d = 0
452477
453478 a = h - d
454- # To ensure good linespacing, pretend that the ascent (resp.
455- # descent) of all lines is at least as large as "l" (resp. "p").
456- a = max (a , lp_a )
457- d = max (d , lp_d )
479+
480+ if self .get_usetex () or self ._linespacing == 'normal' :
481+ # To ensure good linespacing, pretend that the ascent / descent of all
482+ # lines is at least as large as the measured sizes.
483+ a = max (a , min_ascent ) + line_gap / 2
484+ d = max (d , min_descent ) + line_gap / 2
485+ else :
486+ # If using a fixed line spacing, then every line's spacing will be
487+ # determined by the font metrics of the first available font.
488+ line_height = self ._linespacing * (min_ascent + min_descent )
489+ leading = line_height - (a + d )
490+ a += leading / 2
491+ d += leading / 2
458492
459493 # Metrics of the last line that are needed later:
460494 baseline = a - thisy
461495
462- if i == 0 : # position at baseline
463- thisy = - a
464- else : # put baseline a good distance from bottom of previous line
465- thisy -= max (min_dy , a * self ._linespacing )
496+ thisy -= a
466497
467498 wads .append ((w , a , d ))
468499 xs .append (thisx ) # == 0.
@@ -1122,18 +1153,26 @@ def set_multialignment(self, align):
11221153
11231154 def set_linespacing (self , spacing ):
11241155 """
1125- Set the line spacing as a multiple of the font size.
1126-
1127- The default line spacing is 1.2.
1156+ Set the line spacing.
11281157
11291158 Parameters
11301159 ----------
1131- spacing : float (multiple of font size)
1160+ spacing : 'normal' or float, default: 'normal'
1161+ If 'normal', then the line spacing is automatically determined by font
1162+ metrics for each line individually.
1163+
1164+ If a float, then line spacing will be fixed to this multiple of the font
1165+ size for every line.
11321166 """
1133- _api .check_isinstance (Real , spacing = spacing )
1167+ if not cbook ._str_equal (spacing , 'normal' ):
1168+ _api .check_isinstance (Real , spacing = spacing )
11341169 self ._linespacing = spacing
11351170 self .stale = True
11361171
1172+ def get_linespacing (self ):
1173+ """Get the line spacing."""
1174+ return self ._linespacing
1175+
11371176 def set_fontfamily (self , fontname ):
11381177 """
11391178 Set the font family. Can be either a single string, or a list of
0 commit comments