1import io 2import os 3import re 4 5import numpy as np 6import pytest 7 8import matplotlib as mpl 9from matplotlib.testing.decorators import check_figures_equal, image_comparison 10import matplotlib.pyplot as plt 11from matplotlib import _api, mathtext 12 13 14# If test is removed, use None as placeholder 15math_tests = [ 16 r'$a+b+\dot s+\dot{s}+\ldots$', 17 r'$x \doteq y$', 18 r'\$100.00 $\alpha \_$', 19 r'$\frac{\$100.00}{y}$', 20 r'$x y$', 21 r'$x+y\ x=y\ x<y\ x:y\ x,y\ x@y$', 22 r'$100\%y\ x*y\ x/y x\$y$', 23 r'$x\leftarrow y\ x\forall y\ x-y$', 24 r'$x \sf x \bf x {\cal X} \rm x$', 25 r'$x\ x\,x\;x\quad x\qquad x\!x\hspace{ 0.5 }y$', 26 r'$\{ \rm braces \}$', 27 r'$\left[\left\lfloor\frac{5}{\frac{\left(3\right)}{4}} y\right)\right]$', 28 r'$\left(x\right)$', 29 r'$\sin(x)$', 30 r'$x_2$', 31 r'$x^2$', 32 r'$x^2_y$', 33 r'$x_y^2$', 34 (r'$\sum _{\genfrac{}{}{0}{}{0\leq i\leq m}{0<j<n}}f\left(i,j\right)' 35 r'\mathcal{R}\prod_{i=\alpha_{i+1}}^\infty a_i \sin(2 \pi f x_i)' 36 r"\sqrt[2]{\prod^\frac{x}{2\pi^2}_\infty}$"), 37 r'$x = \frac{x+\frac{5}{2}}{\frac{y+3}{8}}$', 38 r'$dz/dt = \gamma x^2 + {\rm sin}(2\pi y+\phi)$', 39 r'Foo: $\alpha_{i+1}^j = {\rm sin}(2\pi f_j t_i) e^{-5 t_i/\tau}$', 40 None, 41 r'Variable $i$ is good', 42 r'$\Delta_i^j$', 43 r'$\Delta^j_{i+1}$', 44 r'$\ddot{o}\acute{e}\grave{e}\hat{O}\breve{\imath}\tilde{n}\vec{q}$', 45 r"$\arccos((x^i))$", 46 r"$\gamma = \frac{x=\frac{6}{8}}{y} \delta$", 47 r'$\limsup_{x\to\infty}$', 48 r'$\oint^\infty_0$', 49 r"$f'\quad f'''(x)\quad ''/\mathrm{yr}$", 50 r'$\frac{x_2888}{y}$', 51 r"$\sqrt[3]{\frac{X_2}{Y}}=5$", 52 None, 53 r"$\sqrt[3]{x}=5$", 54 r'$\frac{X}{\frac{X}{Y}}$', 55 r"$W^{3\beta}_{\delta_1 \rho_1 \sigma_2} = U^{3\beta}_{\delta_1 \rho_1} + \frac{1}{8 \pi 2} \int^{\alpha_2}_{\alpha_2} d \alpha^\prime_2 \left[\frac{ U^{2\beta}_{\delta_1 \rho_1} - \alpha^\prime_2U^{1\beta}_{\rho_1 \sigma_2} }{U^{0\beta}_{\rho_1 \sigma_2}}\right]$", 56 r'$\mathcal{H} = \int d \tau \left(\epsilon E^2 + \mu H^2\right)$', 57 r'$\widehat{abc}\widetilde{def}$', 58 '$\\Gamma \\Delta \\Theta \\Lambda \\Xi \\Pi \\Sigma \\Upsilon \\Phi \\Psi \\Omega$', 59 '$\\alpha \\beta \\gamma \\delta \\epsilon \\zeta \\eta \\theta \\iota \\lambda \\mu \\nu \\xi \\pi \\kappa \\rho \\sigma \\tau \\upsilon \\phi \\chi \\psi$', 60 61 # The examples prefixed by 'mmltt' are from the MathML torture test here: 62 # https://developer.mozilla.org/en-US/docs/Mozilla/MathML_Project/MathML_Torture_Test 63 r'${x}^{2}{y}^{2}$', 64 r'${}_{2}F_{3}$', 65 r'$\frac{x+{y}^{2}}{k+1}$', 66 r'$x+{y}^{\frac{2}{k+1}}$', 67 r'$\frac{a}{b/2}$', 68 r'${a}_{0}+\frac{1}{{a}_{1}+\frac{1}{{a}_{2}+\frac{1}{{a}_{3}+\frac{1}{{a}_{4}}}}}$', 69 r'${a}_{0}+\frac{1}{{a}_{1}+\frac{1}{{a}_{2}+\frac{1}{{a}_{3}+\frac{1}{{a}_{4}}}}}$', 70 r'$\binom{n}{k/2}$', 71 r'$\binom{p}{2}{x}^{2}{y}^{p-2}-\frac{1}{1-x}\frac{1}{1-{x}^{2}}$', 72 r'${x}^{2y}$', 73 r'$\sum _{i=1}^{p}\sum _{j=1}^{q}\sum _{k=1}^{r}{a}_{ij}{b}_{jk}{c}_{ki}$', 74 r'$\sqrt{1+\sqrt{1+\sqrt{1+\sqrt{1+\sqrt{1+\sqrt{1+\sqrt{1+x}}}}}}}$', 75 r'$\left(\frac{{\partial }^{2}}{\partial {x}^{2}}+\frac{{\partial }^{2}}{\partial {y}^{2}}\right){|\varphi \left(x+iy\right)|}^{2}=0$', 76 r'${2}^{{2}^{{2}^{x}}}$', 77 r'${\int }_{1}^{x}\frac{\mathrm{dt}}{t}$', 78 r'$\int {\int }_{D}\mathrm{dx} \mathrm{dy}$', 79 # mathtex doesn't support array 80 # 'mmltt18' : r'$f\left(x\right)=\left\{\begin{array}{cc}\hfill 1/3\hfill & \text{if_}0\le x\le 1;\hfill \\ \hfill 2/3\hfill & \hfill \text{if_}3\le x\le 4;\hfill \\ \hfill 0\hfill & \text{elsewhere.}\hfill \end{array}$', 81 # mathtex doesn't support stackrel 82 # 'mmltt19' : r'$\stackrel{\stackrel{k\text{times}}{\ufe37}}{x+...+x}$', 83 r'${y}_{{x}^{2}}$', 84 # mathtex doesn't support the "\text" command 85 # 'mmltt21' : r'$\sum _{p\text{\prime}}f\left(p\right)={\int }_{t>1}f\left(t\right) d\pi \left(t\right)$', 86 # mathtex doesn't support array 87 # 'mmltt23' : r'$\left(\begin{array}{cc}\hfill \left(\begin{array}{cc}\hfill a\hfill & \hfill b\hfill \\ \hfill c\hfill & \hfill d\hfill \end{array}\right)\hfill & \hfill \left(\begin{array}{cc}\hfill e\hfill & \hfill f\hfill \\ \hfill g\hfill & \hfill h\hfill \end{array}\right)\hfill \\ \hfill 0\hfill & \hfill \left(\begin{array}{cc}\hfill i\hfill & \hfill j\hfill \\ \hfill k\hfill & \hfill l\hfill \end{array}\right)\hfill \end{array}\right)$', 88 # mathtex doesn't support array 89 # 'mmltt24' : r'$det|\begin{array}{ccccc}\hfill {c}_{0}\hfill & \hfill {c}_{1}\hfill & \hfill {c}_{2}\hfill & \hfill \dots \hfill & \hfill {c}_{n}\hfill \\ \hfill {c}_{1}\hfill & \hfill {c}_{2}\hfill & \hfill {c}_{3}\hfill & \hfill \dots \hfill & \hfill {c}_{n+1}\hfill \\ \hfill {c}_{2}\hfill & \hfill {c}_{3}\hfill & \hfill {c}_{4}\hfill & \hfill \dots \hfill & \hfill {c}_{n+2}\hfill \\ \hfill \u22ee\hfill & \hfill \u22ee\hfill & \hfill \u22ee\hfill & \hfill \hfill & \hfill \u22ee\hfill \\ \hfill {c}_{n}\hfill & \hfill {c}_{n+1}\hfill & \hfill {c}_{n+2}\hfill & \hfill \dots \hfill & \hfill {c}_{2n}\hfill \end{array}|>0$', 90 r'${y}_{{x}_{2}}$', 91 r'${x}_{92}^{31415}+\pi $', 92 r'${x}_{{y}_{b}^{a}}^{{z}_{c}^{d}}$', 93 r'${y}_{3}^{\prime \prime \prime }$', 94 # End of the MathML torture tests. 95 96 r"$\left( \xi \left( 1 - \xi \right) \right)$", # Bug 2969451 97 r"$\left(2 \, a=b\right)$", # Sage bug #8125 98 r"$? ! &$", # github issue #466 99 None, 100 None, 101 r"$\left\Vert a \right\Vert \left\vert b \right\vert \left| a \right| \left\| b\right\| \Vert a \Vert \vert b \vert$", 102 r'$\mathring{A} \AA$', 103 r'$M \, M \thinspace M \/ M \> M \: M \; M \ M \enspace M \quad M \qquad M \! M$', 104 r'$\Cup$ $\Cap$ $\leftharpoonup$ $\barwedge$ $\rightharpoonup$', 105 r'$\dotplus$ $\doteq$ $\doteqdot$ $\ddots$', 106 r'$xyz^kx_kx^py^{p-2} d_i^jb_jc_kd x^j_i E^0 E^0_u$', # github issue #4873 107 r'${xyz}^k{x}_{k}{x}^{p}{y}^{p-2} {d}_{i}^{j}{b}_{j}{c}_{k}{d} {x}^{j}_{i}{E}^{0}{E}^0_u$', 108 r'${\int}_x^x x\oint_x^x x\int_{X}^{X}x\int_x x \int^x x \int_{x} x\int^{x}{\int}_{x} x{\int}^{x}_{x}x$', 109 r'testing$^{123}$', 110 ' '.join('$\\' + p + '$' for p in sorted(mathtext.Parser._accentprefixed)), 111 r'$6-2$; $-2$; $ -2$; ${-2}$; ${ -2}$; $20^{+3}_{-2}$', 112 r'$\overline{\omega}^x \frac{1}{2}_0^x$', # github issue #5444 113 r'$,$ $.$ $1{,}234{, }567{ , }890$ and $1,234,567,890$', # github issue 5799 114 r'$\left(X\right)_{a}^{b}$', # github issue 7615 115 r'$\dfrac{\$100.00}{y}$', # github issue #1888 116] 117# 'Lightweight' tests test only a single fontset (dejavusans, which is the 118# default) and only png outputs, in order to minimize the size of baseline 119# images. 120lightweight_math_tests = [ 121 r'$\sqrt[ab]{123}$', # github issue #8665 122 r'$x \overset{f}{\rightarrow} \overset{f}{x} \underset{xx}{ff} \overset{xx}{ff} \underset{f}{x} \underset{f}{\leftarrow} x$', # github issue #18241 123] 124 125digits = "0123456789" 126uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 127lowercase = "abcdefghijklmnopqrstuvwxyz" 128uppergreek = ("\\Gamma \\Delta \\Theta \\Lambda \\Xi \\Pi \\Sigma \\Upsilon \\Phi \\Psi " 129 "\\Omega") 130lowergreek = ("\\alpha \\beta \\gamma \\delta \\epsilon \\zeta \\eta \\theta \\iota " 131 "\\lambda \\mu \\nu \\xi \\pi \\kappa \\rho \\sigma \\tau \\upsilon " 132 "\\phi \\chi \\psi") 133all = [digits, uppercase, lowercase, uppergreek, lowergreek] 134 135# Use stubs to reserve space if tests are removed 136# stub should be of the form (None, N) where N is the number of strings that 137# used to be tested 138# Add new tests at the end. 139font_test_specs = [ 140 ([], all), 141 (['mathrm'], all), 142 (['mathbf'], all), 143 (['mathit'], all), 144 (['mathtt'], [digits, uppercase, lowercase]), 145 (None, 3), 146 (None, 3), 147 (None, 3), 148 (['mathbb'], [digits, uppercase, lowercase, 149 r'\Gamma \Pi \Sigma \gamma \pi']), 150 (['mathrm', 'mathbb'], [digits, uppercase, lowercase, 151 r'\Gamma \Pi \Sigma \gamma \pi']), 152 (['mathbf', 'mathbb'], [digits, uppercase, lowercase, 153 r'\Gamma \Pi \Sigma \gamma \pi']), 154 (['mathcal'], [uppercase]), 155 (['mathfrak'], [uppercase, lowercase]), 156 (['mathbf', 'mathfrak'], [uppercase, lowercase]), 157 (['mathscr'], [uppercase, lowercase]), 158 (['mathsf'], [digits, uppercase, lowercase]), 159 (['mathrm', 'mathsf'], [digits, uppercase, lowercase]), 160 (['mathbf', 'mathsf'], [digits, uppercase, lowercase]) 161 ] 162 163font_tests = [] 164for fonts, chars in font_test_specs: 165 if fonts is None: 166 font_tests.extend([None] * chars) 167 else: 168 wrapper = ''.join([ 169 ' '.join(fonts), 170 ' $', 171 *(r'\%s{' % font for font in fonts), 172 '%s', 173 *('}' for font in fonts), 174 '$', 175 ]) 176 for set in chars: 177 font_tests.append(wrapper % set) 178 179 180@pytest.fixture 181def baseline_images(request, fontset, index, text): 182 if text is None: 183 pytest.skip("test has been removed") 184 return ['%s_%s_%02d' % (request.param, fontset, index)] 185 186 187@pytest.mark.parametrize( 188 'index, text', enumerate(math_tests), ids=range(len(math_tests))) 189@pytest.mark.parametrize( 190 'fontset', ['cm', 'stix', 'stixsans', 'dejavusans', 'dejavuserif']) 191@pytest.mark.parametrize('baseline_images', ['mathtext'], indirect=True) 192@image_comparison(baseline_images=None) 193def test_mathtext_rendering(baseline_images, fontset, index, text): 194 mpl.rcParams['mathtext.fontset'] = fontset 195 fig = plt.figure(figsize=(5.25, 0.75)) 196 fig.text(0.5, 0.5, text, 197 horizontalalignment='center', verticalalignment='center') 198 199 200@pytest.mark.parametrize('index, text', enumerate(lightweight_math_tests), 201 ids=range(len(lightweight_math_tests))) 202@pytest.mark.parametrize('fontset', ['dejavusans']) 203@pytest.mark.parametrize('baseline_images', ['mathtext1'], indirect=True) 204@image_comparison(baseline_images=None, extensions=['png']) 205def test_mathtext_rendering_lightweight(baseline_images, fontset, index, text): 206 fig = plt.figure(figsize=(5.25, 0.75)) 207 fig.text(0.5, 0.5, text, math_fontfamily=fontset, 208 horizontalalignment='center', verticalalignment='center') 209 210 211@pytest.mark.parametrize( 212 'index, text', enumerate(font_tests), ids=range(len(font_tests))) 213@pytest.mark.parametrize( 214 'fontset', ['cm', 'stix', 'stixsans', 'dejavusans', 'dejavuserif']) 215@pytest.mark.parametrize('baseline_images', ['mathfont'], indirect=True) 216@image_comparison(baseline_images=None, extensions=['png']) 217def test_mathfont_rendering(baseline_images, fontset, index, text): 218 mpl.rcParams['mathtext.fontset'] = fontset 219 fig = plt.figure(figsize=(5.25, 0.75)) 220 fig.text(0.5, 0.5, text, 221 horizontalalignment='center', verticalalignment='center') 222 223 224def test_fontinfo(): 225 fontpath = mpl.font_manager.findfont("DejaVu Sans") 226 font = mpl.ft2font.FT2Font(fontpath) 227 table = font.get_sfnt_table("head") 228 assert table['version'] == (1, 0) 229 230 231@pytest.mark.parametrize( 232 'math, msg', 233 [ 234 (r'$\hspace{}$', r'Expected \hspace{n}'), 235 (r'$\hspace{foo}$', r'Expected \hspace{n}'), 236 (r'$\frac$', r'Expected \frac{num}{den}'), 237 (r'$\frac{}{}$', r'Expected \frac{num}{den}'), 238 (r'$\binom$', r'Expected \binom{num}{den}'), 239 (r'$\binom{}{}$', r'Expected \binom{num}{den}'), 240 (r'$\genfrac$', 241 r'Expected \genfrac{ldelim}{rdelim}{rulesize}{style}{num}{den}'), 242 (r'$\genfrac{}{}{}{}{}{}$', 243 r'Expected \genfrac{ldelim}{rdelim}{rulesize}{style}{num}{den}'), 244 (r'$\sqrt$', r'Expected \sqrt{value}'), 245 (r'$\sqrt f$', r'Expected \sqrt{value}'), 246 (r'$\overline$', r'Expected \overline{value}'), 247 (r'$\overline{}$', r'Expected \overline{value}'), 248 (r'$\leftF$', r'Expected a delimiter'), 249 (r'$\rightF$', r'Unknown symbol: \rightF'), 250 (r'$\left(\right$', r'Expected a delimiter'), 251 (r'$\left($', r'Expected "\right"'), 252 (r'$\dfrac$', r'Expected \dfrac{num}{den}'), 253 (r'$\dfrac{}{}$', r'Expected \dfrac{num}{den}'), 254 (r'$\overset$', r'Expected \overset{body}{annotation}'), 255 (r'$\underset$', r'Expected \underset{body}{annotation}'), 256 ], 257 ids=[ 258 'hspace without value', 259 'hspace with invalid value', 260 'frac without parameters', 261 'frac with empty parameters', 262 'binom without parameters', 263 'binom with empty parameters', 264 'genfrac without parameters', 265 'genfrac with empty parameters', 266 'sqrt without parameters', 267 'sqrt with invalid value', 268 'overline without parameters', 269 'overline with empty parameter', 270 'left with invalid delimiter', 271 'right with invalid delimiter', 272 'unclosed parentheses with sizing', 273 'unclosed parentheses without sizing', 274 'dfrac without parameters', 275 'dfrac with empty parameters', 276 'overset without parameters', 277 'underset without parameters', 278 ] 279) 280def test_mathtext_exceptions(math, msg): 281 parser = mathtext.MathTextParser('agg') 282 283 with pytest.raises(ValueError, match=re.escape(msg)): 284 parser.parse(math) 285 286 287def test_single_minus_sign(): 288 plt.figure(figsize=(0.3, 0.3)) 289 plt.text(0.5, 0.5, '$-$') 290 plt.gca().spines[:].set_visible(False) 291 plt.gca().set_xticks([]) 292 plt.gca().set_yticks([]) 293 294 buff = io.BytesIO() 295 plt.savefig(buff, format="rgba", dpi=1000) 296 array = np.frombuffer(buff.getvalue(), dtype=np.uint8) 297 298 # If this fails, it would be all white 299 assert not np.all(array == 0xff) 300 301 302@check_figures_equal(extensions=["png"]) 303def test_spaces(fig_test, fig_ref): 304 fig_test.subplots().set_title(r"$1\,2\>3\ 4$") 305 fig_ref.subplots().set_title(r"$1\/2\:3~4$") 306 307 308@check_figures_equal(extensions=["png"]) 309def test_operator_space(fig_test, fig_ref): 310 fig_test.text(0.1, 0.1, r"$\log 6$") 311 fig_test.text(0.1, 0.2, r"$\log(6)$") 312 fig_test.text(0.1, 0.3, r"$\arcsin 6$") 313 fig_test.text(0.1, 0.4, r"$\arcsin|6|$") 314 fig_test.text(0.1, 0.5, r"$\operatorname{op} 6$") # GitHub issue #553 315 fig_test.text(0.1, 0.6, r"$\operatorname{op}[6]$") 316 fig_test.text(0.1, 0.7, r"$\cos^2$") 317 fig_test.text(0.1, 0.8, r"$\log_2$") 318 319 fig_ref.text(0.1, 0.1, r"$\mathrm{log\,}6$") 320 fig_ref.text(0.1, 0.2, r"$\mathrm{log}(6)$") 321 fig_ref.text(0.1, 0.3, r"$\mathrm{arcsin\,}6$") 322 fig_ref.text(0.1, 0.4, r"$\mathrm{arcsin}|6|$") 323 fig_ref.text(0.1, 0.5, r"$\mathrm{op\,}6$") 324 fig_ref.text(0.1, 0.6, r"$\mathrm{op}[6]$") 325 fig_ref.text(0.1, 0.7, r"$\mathrm{cos}^2$") 326 fig_ref.text(0.1, 0.8, r"$\mathrm{log}_2$") 327 328 329def test_mathtext_fallback_valid(): 330 for fallback in ['cm', 'stix', 'stixsans', 'None']: 331 mpl.rcParams['mathtext.fallback'] = fallback 332 333 334def test_mathtext_fallback_invalid(): 335 for fallback in ['abc', '']: 336 with pytest.raises(ValueError, match="not a valid fallback font name"): 337 mpl.rcParams['mathtext.fallback'] = fallback 338 339 340def test_mathtext_fallback_to_cm_invalid(): 341 for fallback in [True, False]: 342 with pytest.warns(_api.MatplotlibDeprecationWarning): 343 mpl.rcParams['mathtext.fallback_to_cm'] = fallback 344 345 346@pytest.mark.parametrize( 347 "fallback,fontlist", 348 [("cm", ['DejaVu Sans', 'mpltest', 'STIXGeneral', 'cmr10', 'STIXGeneral']), 349 ("stix", ['DejaVu Sans', 'mpltest', 'STIXGeneral'])]) 350def test_mathtext_fallback(fallback, fontlist): 351 mpl.font_manager.fontManager.addfont( 352 os.path.join((os.path.dirname(os.path.realpath(__file__))), 'mpltest.ttf')) 353 mpl.rcParams["svg.fonttype"] = 'none' 354 mpl.rcParams['mathtext.fontset'] = 'custom' 355 mpl.rcParams['mathtext.rm'] = 'mpltest' 356 mpl.rcParams['mathtext.it'] = 'mpltest:italic' 357 mpl.rcParams['mathtext.bf'] = 'mpltest:bold' 358 mpl.rcParams['mathtext.fallback'] = fallback 359 360 test_str = r'a$A\AA\breve\gimel$' 361 362 buff = io.BytesIO() 363 fig, ax = plt.subplots() 364 fig.text(.5, .5, test_str, fontsize=40, ha='center') 365 fig.savefig(buff, format="svg") 366 char_fonts = [ 367 line.split("font-family:")[-1].split(";")[0] 368 for line in str(buff.getvalue()).split(r"\n") if "tspan" in line 369 ] 370 assert char_fonts == fontlist 371 mpl.font_manager.fontManager.ttflist = mpl.font_manager.fontManager.ttflist[:-1] 372 373 374def test_math_to_image(tmpdir): 375 mathtext.math_to_image('$x^2$', str(tmpdir.join('example.png'))) 376 mathtext.math_to_image('$x^2$', io.BytesIO()) 377 378 379def test_mathtext_to_png(tmpdir): 380 with _api.suppress_matplotlib_deprecation_warning(): 381 mt = mathtext.MathTextParser('bitmap') 382 mt.to_png(str(tmpdir.join('example.png')), '$x^2$') 383 mt.to_png(io.BytesIO(), '$x^2$') 384 385 386@image_comparison(baseline_images=['math_fontfamily_image.png'], 387 savefig_kwarg={'dpi': 40}) 388def test_math_fontfamily(): 389 fig = plt.figure(figsize=(10, 3)) 390 fig.text(0.2, 0.7, r"$This\ text\ should\ have\ one\ font$", 391 size=24, math_fontfamily='dejavusans') 392 fig.text(0.2, 0.3, r"$This\ text\ should\ have\ another$", 393 size=24, math_fontfamily='stix') 394