@@ -306,6 +306,12 @@ def render_rect_filled(self, output: Output,
306306 """
307307 output .rects .append ((x , y , w , h ))
308308
309+ def get_axis_height (self , font : str , fontsize : float , dpi : float ) -> float :
310+ """
311+ Get the axis height for the given *font* and *fontsize*.
312+ """
313+ raise NotImplementedError ()
314+
309315 def get_xheight (self , font : str , fontsize : float , dpi : float ) -> float :
310316 """
311317 Get the xheight for the given *font* and *fontsize*.
@@ -407,17 +413,19 @@ def _get_info(self, fontname: str, font_class: str, sym: str, fontsize: float,
407413 offset = offset
408414 )
409415
416+ def get_axis_height (self , fontname : str , fontsize : float , dpi : float ) -> float :
417+ # The fraction line (if present) must be aligned with the minus sign. Therefore,
418+ # the height of the latter from the baseline is the axis height.
419+ metrics = self .get_metrics (
420+ fontname , mpl .rcParams ['mathtext.default' ], '\u2212 ' , fontsize , dpi )
421+ return (metrics .ymax + metrics .ymin ) / 2
422+
410423 def get_xheight (self , fontname : str , fontsize : float , dpi : float ) -> float :
411- font = self ._get_font (fontname )
412- font .set_size (fontsize , dpi )
413- pclt = font .get_sfnt_table ('pclt' )
414- if pclt is None :
415- # Some fonts don't store the xHeight, so we do a poor man's xHeight
416- metrics = self .get_metrics (
417- fontname , mpl .rcParams ['mathtext.default' ], 'x' , fontsize , dpi )
418- return metrics .iceberg
419- x_height = (pclt ['xHeight' ] / 64 ) * (fontsize / 12 ) * (dpi / 100 )
420- return x_height
424+ # Some fonts report the wrong x-height, while some don't store it, so
425+ # we do a poor man's x-height.
426+ metrics = self .get_metrics (
427+ fontname , mpl .rcParams ['mathtext.default' ], 'x' , fontsize , dpi )
428+ return metrics .iceberg
421429
422430 def get_underline_thickness (self , font : str , fontsize : float , dpi : float ) -> float :
423431 # This function used to grab underline thickness from the font
@@ -895,7 +903,10 @@ class FontConstantsBase:
895903 # Percentage of x-height of additional horiz. space after sub/superscripts
896904 script_space : T .ClassVar [float ] = 0.05
897905
898- # Percentage of x-height that sub/superscripts drop below the baseline
906+ # Percentage of x-height that superscripts drop below the top of large box
907+ supdrop : T .ClassVar [float ] = 0.4
908+
909+ # Percentage of x-height that subscripts drop below the bottom of large box
899910 subdrop : T .ClassVar [float ] = 0.4
900911
901912 # Percentage of x-height that superscripts are raised from the baseline
@@ -921,16 +932,45 @@ class FontConstantsBase:
921932 # integrals
922933 delta_integral : T .ClassVar [float ] = 0.1
923934
935+ # Percentage of x-height the numerator is shifted up in display style.
936+ num1 : T .ClassVar [float ] = 1.4
937+
938+ # Percentage of x-height the numerator is shifted up in text, script and
939+ # scriptscript styles if there is a fraction line.
940+ num2 : T .ClassVar [float ] = 1.5
941+
942+ # Percentage of x-height the numerator is shifted up in text, script and
943+ # scriptscript styles if there is no fraction line.
944+ num3 : T .ClassVar [float ] = 1.3
945+
946+ # Percentage of x-height the denominator is shifted down in display style.
947+ denom1 : T .ClassVar [float ] = 1.3
948+
949+ # Percentage of x-height the denominator is shifted down in text, script
950+ # and scriptscript styles.
951+ denom2 : T .ClassVar [float ] = 1.1
952+
924953
925954class ComputerModernFontConstants (FontConstantsBase ):
926- script_space = 0.075
927- subdrop = 0.2
928- sup1 = 0.45
929- sub1 = 0.2
930- sub2 = 0.3
931- delta = 0.075
955+ # Previously, the x-height of Computer Modern was obtained from the font
956+ # table. However, that x-height was greater than the the actual (rendered)
957+ # x-height by a factor of 1.771484375 (at font size 12, DPI 100 and hinting
958+ # type 32). Now that we're using the rendered x-height, some font constants
959+ # have been increased by the same factor to compensate.
960+ script_space = 0.132861328125
961+ supdrop = 0.354296875
962+ subdrop = 0.354296875
963+ sup1 = 0.79716796875
964+ sub1 = 0.354296875
965+ sub2 = 0.5314453125
966+ delta = 0.132861328125
932967 delta_slanted = 0.3
933968 delta_integral = 0.3
969+ num1 = 1.5
970+ num2 = 1.5
971+ num3 = 1.5
972+ denom1 = 1.6
973+ denom2 = 1.2
934974
935975
936976class STIXFontConstants (FontConstantsBase ):
@@ -940,17 +980,27 @@ class STIXFontConstants(FontConstantsBase):
940980 delta = 0.05
941981 delta_slanted = 0.3
942982 delta_integral = 0.3
983+ num1 = 1.6
984+ num2 = 1.6
985+ num3 = 1.6
986+ denom1 = 1.6
943987
944988
945989class STIXSansFontConstants (FontConstantsBase ):
946990 script_space = 0.05
947991 sup1 = 0.8
948992 delta_slanted = 0.6
949993 delta_integral = 0.3
994+ num1 = 1.5
995+ num3 = 1.5
996+ denom1 = 1.5
950997
951998
952999class DejaVuSerifFontConstants (FontConstantsBase ):
953- pass
1000+ num1 = 1.5
1001+ num2 = 1.6
1002+ num3 = 1.4
1003+ denom1 = 1.4
9541004
9551005
9561006class DejaVuSansFontConstants (FontConstantsBase ):
@@ -1015,6 +1065,15 @@ def shrink(self) -> None:
10151065 def render (self , output : Output , x : float , y : float ) -> None :
10161066 """Render this node."""
10171067
1068+ def is_char_node (self ) -> bool :
1069+ # TeX defines a `char_node` as one which represents a single character,
1070+ # but also states that a `char_node` will never appear in a `Vlist`
1071+ # (node134). Further, nuclei made of one `Char` and nuclei made of
1072+ # multiple `Char`s have their superscripts and subscripts shifted by
1073+ # the same amount. In order to make Mathtext behave similarly, just
1074+ # check whether this node is a `Vlist` or has any `Vlist` descendants.
1075+ return True
1076+
10181077
10191078class Box (Node ):
10201079 """A node with a physical location."""
@@ -1204,6 +1263,10 @@ def __init__(self, elements: T.Sequence[Node], w: float = 0.0,
12041263 self .kern ()
12051264 self .hpack (w = w , m = m )
12061265
1266+ def is_char_node (self ) -> bool :
1267+ # See description in Node.is_char_node.
1268+ return all (map (lambda node : node .is_char_node (), self .children ))
1269+
12071270 def kern (self ) -> None :
12081271 """
12091272 Insert `Kern` nodes between `Char` nodes to set kerning.
@@ -1295,6 +1358,10 @@ def __init__(self, elements: T.Sequence[Node], h: float = 0.0,
12951358 super ().__init__ (elements )
12961359 self .vpack (h = h , m = m )
12971360
1361+ def is_char_node (self ) -> bool :
1362+ # See description in Node.is_char_node.
1363+ return False
1364+
12981365 def vpack (self , h : float = 0.0 ,
12991366 m : T .Literal ['additional' , 'exactly' ] = 'additional' ,
13001367 l : float = np .inf ) -> None :
@@ -2111,6 +2178,7 @@ def csnames(group: str, names: Iterable[str]) -> Regex:
21112178 | p .text
21122179 | p .boldsymbol
21132180 | p .substack
2181+ | p .auto_delim
21142182 )
21152183
21162184 mdelim = r"\middle" - (p .delim ("mdelim" ) | Error ("Expected a delimiter" ))
@@ -2440,8 +2508,7 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any:
24402508 state = self .get_state ()
24412509 rule_thickness = state .fontset .get_underline_thickness (
24422510 state .font , state .fontsize , state .dpi )
2443- x_height = state .fontset .get_xheight (
2444- state .font , state .fontsize , state .dpi )
2511+ x_height = state .fontset .get_xheight (state .font , state .fontsize , state .dpi )
24452512
24462513 if napostrophes :
24472514 if super is None :
@@ -2530,9 +2597,19 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any:
25302597 else :
25312598 subkern = 0
25322599
2600+ # Set the minimum shifts for the superscript and subscript (node756).
2601+ if nucleus .is_char_node ():
2602+ shift_up = 0.0
2603+ shift_down = 0.0
2604+ else :
2605+ shrunk_x_height = state .fontset .get_xheight (
2606+ state .font , state .fontsize * SHRINK_FACTOR , state .dpi )
2607+ shift_up = nucleus .height - consts .supdrop * shrunk_x_height
2608+ shift_down = nucleus .depth + consts .subdrop * shrunk_x_height
2609+
25332610 x : List
25342611 if super is None :
2535- # node757
2612+ # Align subscript without superscript ( node757).
25362613 # Note: One of super or sub must be a Node if we're in this function, but
25372614 # mypy can't know this, since it can't interpret pyparsing expressions,
25382615 # hence the cast.
@@ -2541,29 +2618,37 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any:
25412618 if self .is_dropsub (last_char ):
25422619 shift_down = lc_baseline + consts .subdrop * x_height
25432620 else :
2544- shift_down = consts .sub1 * x_height
2621+ shift_down = max (shift_down , consts .sub1 * x_height ,
2622+ x .height - x_height * 4 / 5 )
25452623 x .shift_amount = shift_down
25462624 else :
2625+ # Align superscript (node758).
25472626 x = Hlist ([Kern (superkern ), super ])
25482627 x .shrink ()
25492628 if self .is_dropsub (last_char ):
25502629 shift_up = lc_height - consts .subdrop * x_height
25512630 else :
2552- shift_up = consts .sup1 * x_height
2631+ shift_up = max ( shift_up , consts .sup1 * x_height , x . depth + x_height / 4 )
25532632 if sub is None :
25542633 x .shift_amount = - shift_up
2555- else : # Both sub and superscript
2634+ else :
2635+ # Align subscript with superscript (node759).
25562636 y = Hlist ([Kern (subkern ), sub ])
25572637 y .shrink ()
25582638 if self .is_dropsub (last_char ):
25592639 shift_down = lc_baseline + consts .subdrop * x_height
25602640 else :
2561- shift_down = consts .sub2 * x_height
2562- # If sub and superscript collide, move super up
2563- clr = (2 * rule_thickness -
2641+ shift_down = max (shift_down , consts .sub2 * x_height )
2642+ # If the subscript and superscript are too close to each other,
2643+ # move the subscript down.
2644+ clr = (4 * rule_thickness -
25642645 ((shift_up - x .depth ) - (y .height - shift_down )))
25652646 if clr > 0. :
2566- shift_up += clr
2647+ shift_down += clr
2648+ clr = x_height * 4 / 5 - shift_up + x .depth
2649+ if clr > 0 :
2650+ shift_up += clr
2651+ shift_down -= clr
25672652 x = Vlist ([
25682653 x ,
25692654 Kern ((shift_up - x .depth ) - (y .height - shift_down )),
@@ -2586,32 +2671,68 @@ def _genfrac(self, ldelim: str, rdelim: str, rule: float | None, style: _MathSty
25862671 state = self .get_state ()
25872672 thickness = state .get_current_underline_thickness ()
25882673
2674+ axis_height = state .fontset .get_axis_height (
2675+ state .font , state .fontsize , state .dpi )
2676+ consts = _get_font_constant_set (state )
2677+ x_height = state .fontset .get_xheight (state .font , state .fontsize , state .dpi )
2678+
25892679 for _ in range (style .value ):
2680+ x_height *= SHRINK_FACTOR
25902681 num .shrink ()
25912682 den .shrink ()
25922683 cnum = HCentered ([num ])
25932684 cden = HCentered ([den ])
25942685 width = max (num .width , den .width )
25952686 cnum .hpack (width , 'exactly' )
25962687 cden .hpack (width , 'exactly' )
2597- vlist = Vlist ([
2598- cnum , # numerator
2599- Vbox (0 , 2 * thickness ), # space
2600- Hrule (state , rule ), # rule
2601- Vbox (0 , 2 * thickness ), # space
2602- cden , # denominator
2603- ])
26042688
2605- # Shift so the fraction line sits in the middle of the
2606- # equals sign
2607- metrics = state .fontset .get_metrics (
2608- state .font , mpl .rcParams ['mathtext.default' ],
2609- '=' , state .fontsize , state .dpi )
2610- shift = (cden .height -
2611- ((metrics .ymax + metrics .ymin ) / 2 - 3 * thickness ))
2612- vlist .shift_amount = shift
2613-
2614- result : list [Box | Char | str ] = [Hlist ([vlist , Hbox (2 * thickness )])]
2689+ # Align the fraction with a fraction line (node743, node744 and node746).
2690+ if rule :
2691+ if style is self ._MathStyle .DISPLAYSTYLE :
2692+ num_shift_up = consts .num1 * x_height
2693+ den_shift_down = consts .denom1 * x_height
2694+ clr = 3 * rule # The minimum clearance.
2695+ else :
2696+ num_shift_up = consts .num2 * x_height
2697+ den_shift_down = consts .denom2 * x_height
2698+ clr = rule # The minimum clearance.
2699+ delta = rule / 2
2700+ num_clr = max ((num_shift_up - cnum .depth ) - (axis_height + delta ), clr )
2701+ den_clr = max ((axis_height - delta ) - (cden .height - den_shift_down ), clr )
2702+ # Possible bug in fraction rendering. See GitHub PR 22852 comments.
2703+ vlist = Vlist ([cnum , # numerator
2704+ Vbox (0 , num_clr - rule ), # space
2705+ Hrule (state , rule ), # rule
2706+ Vbox (0 , den_clr + rule ), # space
2707+ cden # denominator
2708+ ])
2709+ vlist .shift_amount = cden .height + den_clr + delta - axis_height
2710+
2711+ # Align the fraction without a fraction line (node743, node744 and node745).
2712+ else :
2713+ if style is self ._MathStyle .DISPLAYSTYLE :
2714+ num_shift_up = consts .num1 * x_height
2715+ den_shift_down = consts .denom1 * x_height
2716+ min_clr = 7 * thickness # The minimum clearance.
2717+ else :
2718+ num_shift_up = consts .num3 * x_height
2719+ den_shift_down = consts .denom2 * x_height
2720+ min_clr = 3 * thickness # The minimum clearance.
2721+ def_clr = (num_shift_up - cnum .depth ) - (cden .height - den_shift_down )
2722+ clr = max (def_clr , min_clr )
2723+ vlist = Vlist ([cnum , # numerator
2724+ Vbox (0 , clr ), # space
2725+ cden # denominator
2726+ ])
2727+ vlist .shift_amount = den_shift_down
2728+ if def_clr < min_clr :
2729+ vlist .shift_amount += (min_clr - def_clr ) / 2
2730+
2731+ result : list [Box | Char | str ] = [Hlist ([
2732+ Hbox (thickness ),
2733+ vlist ,
2734+ Hbox (thickness )
2735+ ])]
26152736 if ldelim or rdelim :
26162737 return self ._auto_sized_delimiter (ldelim or "." , result , rdelim or "." )
26172738 return result
0 commit comments