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