@@ -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,40 +932,109 @@ 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+ delta = 0.132861328125
932962 delta_slanted = 0.3
933963 delta_integral = 0.3
964+ _x_height = 451470
965+ # These all come from the cmsy10.tfm metrics, divided by the design xheight from
966+ # there, since we multiply these values by the scaled xheight later.
967+ supdrop = 404864 / _x_height
968+ subdrop = 52429 / _x_height
969+ sup1 = 432949 / _x_height
970+ sub1 = 157286 / _x_height
971+ sub2 = 259226 / _x_height
972+ num1 = 709370 / _x_height
973+ num2 = 412858 / _x_height
974+ num3 = 465286 / _x_height
975+ denom1 = 719272 / _x_height
976+ denom2 = 361592 / _x_height
934977
935978
936979class STIXFontConstants (FontConstantsBase ):
937980 script_space = 0.1
938- sup1 = 0.8
939- sub2 = 0.6
940981 delta = 0.05
941982 delta_slanted = 0.3
942983 delta_integral = 0.3
943-
944-
945- class STIXSansFontConstants (FontConstantsBase ):
984+ # These values are extracted from the TeX table of STIXGeneral.ttf using FreeType,
985+ # and then divided by design xheight, since we multiply these values by the scaled
986+ # xheight later.
987+ _x_height = 450
988+ supdrop = 386 / _x_height
989+ subdrop = 50.0002 / _x_height
990+ sup1 = 413 / _x_height
991+ sub1 = 150 / _x_height
992+ sub2 = 309 / _x_height
993+ num1 = 747 / _x_height
994+ num2 = 424 / _x_height
995+ num3 = 474 / _x_height
996+ denom1 = 756 / _x_height
997+ denom2 = 375 / _x_height
998+
999+
1000+ class STIXSansFontConstants (STIXFontConstants ):
9461001 script_space = 0.05
947- sup1 = 0.8
9481002 delta_slanted = 0.6
9491003 delta_integral = 0.3
9501004
9511005
9521006class DejaVuSerifFontConstants (FontConstantsBase ):
953- pass
1007+ # These values are extracted from the TeX table of DejaVuSerif.ttf using FreeType,
1008+ # and then divided by design xheight, since we multiply these values by the scaled
1009+ # xheight later.
1010+ _x_height = 1063
1011+ supdrop = 790.527 / _x_height
1012+ subdrop = 102.4 / _x_height
1013+ sup1 = 845.824 / _x_height
1014+ sub1 = 307.199 / _x_height
1015+ sub2 = 632.832 / _x_height
1016+ num1 = 1529.86 / _x_height
1017+ num2 = 868.352 / _x_height
1018+ num3 = 970.752 / _x_height
1019+ denom1 = 1548.29 / _x_height
1020+ denom2 = 768 / _x_height
9541021
9551022
9561023class DejaVuSansFontConstants (FontConstantsBase ):
957- pass
1024+ # These values are extracted from the TeX table of DejaVuSans.ttf using FreeType,
1025+ # and then divided by design xheight, since we multiply these values by the scaled
1026+ # xheight later.
1027+ _x_height = 1120
1028+ supdrop = 790.527 / _x_height
1029+ subdrop = 102.4 / _x_height
1030+ sup1 = 845.824 / _x_height
1031+ sub1 = 307.199 / _x_height
1032+ sub2 = 632.832 / _x_height
1033+ num1 = 1529.86 / _x_height
1034+ num2 = 868.352 / _x_height
1035+ num3 = 970.752 / _x_height
1036+ denom1 = 1548.29 / _x_height
1037+ denom2 = 768 / _x_height
9581038
9591039
9601040# Maps font family names to the FontConstantBase subclass to use
@@ -1015,6 +1095,15 @@ def shrink(self) -> None:
10151095 def render (self , output : Output , x : float , y : float ) -> None :
10161096 """Render this node."""
10171097
1098+ def is_char_node (self ) -> bool :
1099+ # TeX defines a `char_node` as one which represents a single character,
1100+ # but also states that a `char_node` will never appear in a `Vlist`
1101+ # (node134). Further, nuclei made of one `Char` and nuclei made of
1102+ # multiple `Char`s have their superscripts and subscripts shifted by
1103+ # the same amount. In order to make Mathtext behave similarly, just
1104+ # check whether this node is a `Vlist` or has any `Vlist` descendants.
1105+ return True
1106+
10181107
10191108class Box (Node ):
10201109 """A node with a physical location."""
@@ -1204,6 +1293,10 @@ def __init__(self, elements: T.Sequence[Node], w: float = 0.0,
12041293 self .kern ()
12051294 self .hpack (w = w , m = m )
12061295
1296+ def is_char_node (self ) -> bool :
1297+ # See description in Node.is_char_node.
1298+ return all (map (lambda node : node .is_char_node (), self .children ))
1299+
12071300 def kern (self ) -> None :
12081301 """
12091302 Insert `Kern` nodes between `Char` nodes to set kerning.
@@ -1295,6 +1388,10 @@ def __init__(self, elements: T.Sequence[Node], h: float = 0.0,
12951388 super ().__init__ (elements )
12961389 self .vpack (h = h , m = m )
12971390
1391+ def is_char_node (self ) -> bool :
1392+ # See description in Node.is_char_node.
1393+ return False
1394+
12981395 def vpack (self , h : float = 0.0 ,
12991396 m : T .Literal ['additional' , 'exactly' ] = 'additional' ,
13001397 l : float = np .inf ) -> None :
@@ -1386,7 +1483,7 @@ def __init__(self, width: float, height: float, depth: float, state: ParserState
13861483
13871484 def render (self , output : Output , # type: ignore[override]
13881485 x : float , y : float , w : float , h : float ) -> None :
1389- self .fontset .render_rect_filled (output , x , y , w , h )
1486+ self .fontset .render_rect_filled (output , x , y - h , w , h )
13901487
13911488
13921489class Hrule (Rule ):
@@ -2111,6 +2208,7 @@ def csnames(group: str, names: Iterable[str]) -> Regex:
21112208 | p .text
21122209 | p .boldsymbol
21132210 | p .substack
2211+ | p .auto_delim
21142212 )
21152213
21162214 mdelim = r"\middle" - (p .delim ("mdelim" ) | Error ("Expected a delimiter" ))
@@ -2440,8 +2538,7 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any:
24402538 state = self .get_state ()
24412539 rule_thickness = state .fontset .get_underline_thickness (
24422540 state .font , state .fontsize , state .dpi )
2443- x_height = state .fontset .get_xheight (
2444- state .font , state .fontsize , state .dpi )
2541+ x_height = state .fontset .get_xheight (state .font , state .fontsize , state .dpi )
24452542
24462543 if napostrophes :
24472544 if super is None :
@@ -2530,9 +2627,19 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any:
25302627 else :
25312628 subkern = 0
25322629
2630+ # Set the minimum shifts for the superscript and subscript (node756).
2631+ if nucleus .is_char_node ():
2632+ shift_up = 0.0
2633+ shift_down = 0.0
2634+ else :
2635+ shrunk_x_height = state .fontset .get_xheight (
2636+ state .font , state .fontsize * SHRINK_FACTOR , state .dpi )
2637+ shift_up = nucleus .height - consts .supdrop * shrunk_x_height
2638+ shift_down = nucleus .depth + consts .subdrop * shrunk_x_height
2639+
25332640 x : List
25342641 if super is None :
2535- # node757
2642+ # Align subscript without superscript ( node757).
25362643 # Note: One of super or sub must be a Node if we're in this function, but
25372644 # mypy can't know this, since it can't interpret pyparsing expressions,
25382645 # hence the cast.
@@ -2541,29 +2648,37 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any:
25412648 if self .is_dropsub (last_char ):
25422649 shift_down = lc_baseline + consts .subdrop * x_height
25432650 else :
2544- shift_down = consts .sub1 * x_height
2651+ shift_down = max (shift_down , consts .sub1 * x_height ,
2652+ x .height - x_height * 4 / 5 )
25452653 x .shift_amount = shift_down
25462654 else :
2655+ # Align superscript (node758).
25472656 x = Hlist ([Kern (superkern ), super ])
25482657 x .shrink ()
25492658 if self .is_dropsub (last_char ):
25502659 shift_up = lc_height - consts .subdrop * x_height
25512660 else :
2552- shift_up = consts .sup1 * x_height
2661+ shift_up = max ( shift_up , consts .sup1 * x_height , x . depth + x_height / 4 )
25532662 if sub is None :
25542663 x .shift_amount = - shift_up
2555- else : # Both sub and superscript
2664+ else :
2665+ # Align subscript with superscript (node759).
25562666 y = Hlist ([Kern (subkern ), sub ])
25572667 y .shrink ()
25582668 if self .is_dropsub (last_char ):
25592669 shift_down = lc_baseline + consts .subdrop * x_height
25602670 else :
2561- shift_down = consts .sub2 * x_height
2562- # If sub and superscript collide, move super up
2563- clr = (2 * rule_thickness -
2671+ shift_down = max (shift_down , consts .sub2 * x_height )
2672+ # If the subscript and superscript are too close to each other,
2673+ # move the subscript down.
2674+ clr = (4 * rule_thickness -
25642675 ((shift_up - x .depth ) - (y .height - shift_down )))
25652676 if clr > 0. :
2566- shift_up += clr
2677+ shift_down += clr
2678+ clr = x_height * 4 / 5 - shift_up + x .depth
2679+ if clr > 0 :
2680+ shift_up += clr
2681+ shift_down -= clr
25672682 x = Vlist ([
25682683 x ,
25692684 Kern ((shift_up - x .depth ) - (y .height - shift_down )),
@@ -2586,32 +2701,67 @@ def _genfrac(self, ldelim: str, rdelim: str, rule: float | None, style: _MathSty
25862701 state = self .get_state ()
25872702 thickness = state .get_current_underline_thickness ()
25882703
2704+ axis_height = state .fontset .get_axis_height (
2705+ state .font , state .fontsize , state .dpi )
2706+ consts = _get_font_constant_set (state )
2707+ x_height = state .fontset .get_xheight (state .font , state .fontsize , state .dpi )
2708+
25892709 for _ in range (style .value ):
2710+ x_height *= SHRINK_FACTOR
25902711 num .shrink ()
25912712 den .shrink ()
25922713 cnum = HCentered ([num ])
25932714 cden = HCentered ([den ])
25942715 width = max (num .width , den .width )
25952716 cnum .hpack (width , 'exactly' )
25962717 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- ])
26042718
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 )])]
2719+ # Align the fraction with a fraction line (node743, node744 and node746).
2720+ if rule :
2721+ if style is self ._MathStyle .DISPLAYSTYLE :
2722+ num_shift_up = consts .num1 * x_height
2723+ den_shift_down = consts .denom1 * x_height
2724+ clr = 3 * rule # The minimum clearance.
2725+ else :
2726+ num_shift_up = consts .num2 * x_height
2727+ den_shift_down = consts .denom2 * x_height
2728+ clr = rule # The minimum clearance.
2729+ delta = rule / 2
2730+ num_clr = max ((num_shift_up - cnum .depth ) - (axis_height + delta ), clr )
2731+ den_clr = max ((axis_height - delta ) - (cden .height - den_shift_down ), clr )
2732+ vlist = Vlist ([cnum , # numerator
2733+ Vbox (0 , num_clr ), # space
2734+ Hrule (state , rule ), # rule
2735+ Vbox (0 , den_clr ), # space
2736+ cden # denominator
2737+ ])
2738+ vlist .shift_amount = cden .height + den_clr + delta - axis_height
2739+
2740+ # Align the fraction without a fraction line (node743, node744 and node745).
2741+ else :
2742+ if style is self ._MathStyle .DISPLAYSTYLE :
2743+ num_shift_up = consts .num1 * x_height
2744+ den_shift_down = consts .denom1 * x_height
2745+ min_clr = 7 * thickness # The minimum clearance.
2746+ else :
2747+ num_shift_up = consts .num3 * x_height
2748+ den_shift_down = consts .denom2 * x_height
2749+ min_clr = 3 * thickness # The minimum clearance.
2750+ def_clr = (num_shift_up - cnum .depth ) - (cden .height - den_shift_down )
2751+ clr = max (def_clr , min_clr )
2752+ vlist = Vlist ([cnum , # numerator
2753+ Vbox (0 , clr ), # space
2754+ cden # denominator
2755+ ])
2756+ vlist .shift_amount = den_shift_down
2757+ if def_clr < min_clr :
2758+ vlist .shift_amount += (min_clr - def_clr ) / 2
2759+
2760+ result : list [Box | Char | str ] = [Hlist ([
2761+ Hbox (thickness ),
2762+ vlist ,
2763+ Hbox (thickness )
2764+ ])]
26152765 if ldelim or rdelim :
26162766 return self ._auto_sized_delimiter (ldelim or "." , result , rdelim or "." )
26172767 return result
0 commit comments