Skip to content

Commit 02dff6a

Browse files
committed
Implemented TeX's fraction and script alignment.
As described in *TeX: the Program* by Don Knuth. New font constants are set to the nearest integral multiples of 0.1 for which numerators and denominators containing normal text do not have to be shifted beyond their default shift amounts at font size 30. To better process superscripts and subscripts, the x-height is now always calculated instead of being retrieved from the font table (which was the case for Computer Modern); the affected font constants have been changed. Mathtext tests which failed as a result of these changes have been moved from `math_tests` to `svgastext_math_tests`. A duplicate test was also fixed in the process.
1 parent f06c2c3 commit 02dff6a

File tree

843 files changed

+3838
-58531
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

843 files changed

+3838
-58531
lines changed

lib/matplotlib/_mathtext.py

+150-42
Original file line numberDiff line numberDiff line change
@@ -249,16 +249,11 @@ def _get_info(self, fontname, font_class, sym, fontsize, dpi):
249249
)
250250

251251
def get_xheight(self, fontname, fontsize, dpi):
252-
font = self._get_font(fontname)
253-
font.set_size(fontsize, dpi)
254-
pclt = font.get_sfnt_table('pclt')
255-
if pclt is None:
256-
# Some fonts don't store the xHeight, so we do a poor man's xHeight
257-
metrics = self.get_metrics(
258-
fontname, mpl.rcParams['mathtext.default'], 'x', fontsize, dpi)
259-
return metrics.iceberg
260-
xHeight = (pclt['xHeight'] / 64.0) * (fontsize / 12.0) * (dpi / 100.0)
261-
return xHeight
252+
# Some fonts report the wrong x-height, while some don't store it, so
253+
# we do a poor man's x-height.
254+
metrics = self.get_metrics(
255+
fontname, mpl.rcParams['mathtext.default'], 'x', fontsize, dpi)
256+
return metrics.iceberg
262257

263258
def get_underline_thickness(self, font, fontsize, dpi):
264259
# This function used to grab underline thickness from the font
@@ -759,16 +754,44 @@ class FontConstantsBase:
759754
# integrals
760755
delta_integral = 0.1
761756

757+
# Percentage of x-height the numerator is shifted up in display style.
758+
num1 = 1.4
759+
760+
# Percentage of x-height the numerator is shifted up in text, script and
761+
# scriptscript styles if there is a fraction line.
762+
num2 = 1.5
763+
764+
# Percentage of x-height the numerator is shifted up in text, script and
765+
# scriptscript styles if there is no fraction line.
766+
num3 = 1.3
767+
768+
# Percentage of x-height the denominator is shifted down in display style.
769+
denom1 = 1.3
770+
771+
# Percentage of x-height the denominator is shifted down in text, script
772+
# and scriptscript styles.
773+
denom2 = 1.1
774+
762775

763776
class ComputerModernFontConstants(FontConstantsBase):
764-
script_space = 0.075
765-
subdrop = 0.2
766-
sup1 = 0.45
767-
sub1 = 0.2
768-
sub2 = 0.3
769-
delta = 0.075
777+
# Previously, the x-height of Computer Modern was obtained from the font
778+
# table. However, that x-height was greater than the the actual (rendered)
779+
# x-height by a factor of 1.771484375 (at font size 12, DPI 100 and hinting
780+
# type 32). Now that we're using the rendered x-height, some font constants
781+
# have been increased by the same factor to compensate.
782+
script_space = 0.132861328125
783+
subdrop = 0.354296875
784+
sup1 = 0.79716796875
785+
sub1 = 0.354296875
786+
sub2 = 0.5314453125
787+
delta = 0.132861328125
770788
delta_slanted = 0.3
771789
delta_integral = 0.3
790+
num1 = 1.5
791+
num2 = 1.5
792+
num3 = 1.5
793+
denom1 = 1.6
794+
denom2 = 1.2
772795

773796

774797
class STIXFontConstants(FontConstantsBase):
@@ -778,17 +801,27 @@ class STIXFontConstants(FontConstantsBase):
778801
delta = 0.05
779802
delta_slanted = 0.3
780803
delta_integral = 0.3
804+
num1 = 1.6
805+
num2 = 1.6
806+
num3 = 1.6
807+
denom1 = 1.6
781808

782809

783810
class STIXSansFontConstants(FontConstantsBase):
784811
script_space = 0.05
785812
sup1 = 0.8
786813
delta_slanted = 0.6
787814
delta_integral = 0.3
815+
num1 = 1.5
816+
num3 = 1.5
817+
denom1 = 1.5
788818

789819

790820
class DejaVuSerifFontConstants(FontConstantsBase):
791-
pass
821+
num1 = 1.5
822+
num2 = 1.6
823+
num3 = 1.4
824+
denom1 = 1.4
792825

793826

794827
class DejaVuSansFontConstants(FontConstantsBase):
@@ -1832,6 +1865,7 @@ def set_names_and_parse_actions():
18321865
| p.underset
18331866
| p.sqrt
18341867
| p.overline
1868+
| p.auto_delim
18351869
)
18361870

18371871
p.simple <<= (
@@ -2156,6 +2190,19 @@ def is_slanted(self, nucleus):
21562190
def is_between_brackets(self, s, loc):
21572191
return False
21582192

2193+
def is_char_node(self, node):
2194+
# TeX defines a `char_node` as one which represents a single character,
2195+
# but also states that a `char_node` will never appear in a `Vlist`
2196+
# (node134). Further, nuclei made of one `Char` and nuclei made of
2197+
# multiple `Char`s have their superscripts and subscripts shifted by
2198+
# the same amount. In order to make Mathtext behave similarly, just
2199+
# check whether this node is a `Vlist` or has any `Vlist` descendants.
2200+
if isinstance(node, Vlist):
2201+
return False
2202+
if not isinstance(node, Hlist):
2203+
return True
2204+
return all(map(self.is_char_node, node.children))
2205+
21592206
def subsuper(self, s, loc, toks):
21602207
nucleus = toks.get("nucleus", Hbox(0))
21612208
subsuper = toks.get("subsuper", [])
@@ -2269,36 +2316,56 @@ def subsuper(self, s, loc, toks):
22692316
else:
22702317
subkern = 0
22712318

2319+
# Set the minimum shifts for the superscript and subscript (node756).
2320+
if self.is_char_node(nucleus):
2321+
shift_up = 0
2322+
shift_down = 0
2323+
else:
2324+
drop_amount = constants.subdrop * xHeight * SHRINK_FACTOR
2325+
shift_up = nucleus.height - drop_amount
2326+
shift_down = nucleus.depth + drop_amount
2327+
22722328
if super is None:
2273-
# node757
2329+
# Align subscript without superscript (node757).
22742330
x = Hlist([Kern(subkern), sub])
22752331
x.shrink()
22762332
if self.is_dropsub(last_char):
22772333
shift_down = lc_baseline + constants.subdrop * xHeight
22782334
else:
2279-
shift_down = constants.sub1 * xHeight
2335+
shift_down = max(shift_down,
2336+
constants.sub1 * xHeight,
2337+
x.height - xHeight * 4 / 5)
22802338
x.shift_amount = shift_down
22812339
else:
2340+
# Align superscript (node758).
22822341
x = Hlist([Kern(superkern), super])
22832342
x.shrink()
22842343
if self.is_dropsub(last_char):
22852344
shift_up = lc_height - constants.subdrop * xHeight
22862345
else:
2287-
shift_up = constants.sup1 * xHeight
2346+
shift_up = max(shift_up,
2347+
constants.sup1 * xHeight,
2348+
x.depth + xHeight / 4)
22882349
if sub is None:
22892350
x.shift_amount = -shift_up
2290-
else: # Both sub and superscript
2351+
else:
2352+
# Align subscript with superscript (node759).
22912353
y = Hlist([Kern(subkern), sub])
22922354
y.shrink()
22932355
if self.is_dropsub(last_char):
22942356
shift_down = lc_baseline + constants.subdrop * xHeight
22952357
else:
2296-
shift_down = constants.sub2 * xHeight
2297-
# If sub and superscript collide, move super up
2298-
clr = (2.0 * rule_thickness -
2358+
shift_down = max(shift_down, constants.sub2 * xHeight)
2359+
# If the subscript and superscript are too close to each other,
2360+
# move the subscript down.
2361+
clr = (4 * rule_thickness -
22992362
((shift_up - x.depth) - (y.height - shift_down)))
23002363
if clr > 0.:
2301-
shift_up += clr
2364+
shift_down += clr
2365+
clr = xHeight * 4 / 5 - shift_up + x.depth
2366+
if clr > 0:
2367+
shift_up += clr
2368+
shift_down -= clr
23022369
x = Vlist([
23032370
x,
23042371
Kern((shift_up - x.depth) - (y.height - shift_down)),
@@ -2322,32 +2389,73 @@ def _genfrac(self, ldelim, rdelim, rule, style, num, den):
23222389
state = self.get_state()
23232390
thickness = state.get_current_underline_thickness()
23242391

2392+
# The fraction line (if present) must be aligned with the minus sign.
2393+
# Therefore, the height of the latter from the baseline is the axis
2394+
# height.
2395+
metrics = state.font_output.get_metrics(
2396+
state.font, mpl.rcParams['mathtext.default'],
2397+
'\u2212', state.fontsize, state.dpi)
2398+
axis_height = (metrics.ymax + metrics.ymin) / 2
2399+
constants = _get_font_constant_set(state)
2400+
xHeight = state.font_output.get_xheight(
2401+
state.font, state.fontsize, state.dpi)
2402+
23252403
for _ in range(style.value):
2404+
xHeight *= SHRINK_FACTOR
23262405
num.shrink()
23272406
den.shrink()
23282407
cnum = HCentered([num])
23292408
cden = HCentered([den])
23302409
width = max(num.width, den.width)
23312410
cnum.hpack(width, 'exactly')
23322411
cden.hpack(width, 'exactly')
2333-
vlist = Vlist([cnum, # numerator
2334-
Vbox(0, thickness * 2.0), # space
2335-
Hrule(state, rule), # rule
2336-
Vbox(0, thickness * 2.0), # space
2337-
cden # denominator
2338-
])
2339-
2340-
# Shift so the fraction line sits in the middle of the
2341-
# equals sign
2342-
metrics = state.font_output.get_metrics(
2343-
state.font, mpl.rcParams['mathtext.default'],
2344-
'=', state.fontsize, state.dpi)
2345-
shift = (cden.height -
2346-
((metrics.ymax + metrics.ymin) / 2 -
2347-
thickness * 3.0))
2348-
vlist.shift_amount = shift
23492412

2350-
result = [Hlist([vlist, Hbox(thickness * 2.)])]
2413+
# Align the fraction with a fraction line (node743, node744 and
2414+
# node746).
2415+
if rule:
2416+
if style is self._MathStyle.DISPLAYSTYLE:
2417+
num_shift_up = constants.num1 * xHeight
2418+
den_shift_down = constants.denom1 * xHeight
2419+
min_clr = 3 * rule
2420+
else:
2421+
num_shift_up = constants.num2 * xHeight
2422+
den_shift_down = constants.denom2 * xHeight
2423+
min_clr = rule
2424+
num_clr = max(
2425+
num_shift_up - cnum.depth - axis_height - rule / 2, min_clr)
2426+
den_clr = max(
2427+
axis_height - rule / 2 + den_shift_down - cden.height, min_clr)
2428+
# Possible bug in fraction rendering. See GitHub PR 22852 comments.
2429+
vlist = Vlist([cnum, # numerator
2430+
Vbox(0, num_clr - rule), # space
2431+
Hrule(state, rule), # rule
2432+
Vbox(0, den_clr + rule), # space
2433+
cden # denominator
2434+
])
2435+
vlist.shift_amount = cden.height + den_clr + rule / 2 - axis_height
2436+
2437+
# Align the fraction without a fraction line (node743, node744 and
2438+
# node745).
2439+
else:
2440+
if style is self._MathStyle.DISPLAYSTYLE:
2441+
num_shift_up = constants.num1 * xHeight
2442+
den_shift_down = constants.denom1 * xHeight
2443+
min_clr = 7 * thickness
2444+
else:
2445+
num_shift_up = constants.num3 * xHeight
2446+
den_shift_down = constants.denom2 * xHeight
2447+
min_clr = 3 * thickness
2448+
def_clr = num_shift_up - cnum.depth + den_shift_down - cden.height
2449+
clr = max(def_clr, min_clr)
2450+
vlist = Vlist([cnum, # numerator
2451+
Vbox(0, clr), # space
2452+
cden # denominator
2453+
])
2454+
vlist.shift_amount = den_shift_down
2455+
if def_clr < min_clr:
2456+
vlist.shift_amount += (min_clr - def_clr) / 2
2457+
2458+
result = [Hlist([Hbox(thickness), vlist, Hbox(thickness)])]
23512459
if ldelim or rdelim:
23522460
if ldelim == '':
23532461
ldelim = '.'

0 commit comments

Comments
 (0)