test_mathtext.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. from __future__ import annotations
  2. import io
  3. from pathlib import Path
  4. import platform
  5. import re
  6. from xml.etree import ElementTree as ET
  7. from typing import Any
  8. import numpy as np
  9. from packaging.version import parse as parse_version
  10. import pyparsing
  11. import pytest
  12. import matplotlib as mpl
  13. from matplotlib.testing.decorators import check_figures_equal, image_comparison
  14. import matplotlib.pyplot as plt
  15. from matplotlib import mathtext, _mathtext
  16. pyparsing_version = parse_version(pyparsing.__version__)
  17. # If test is removed, use None as placeholder
  18. math_tests = [
  19. r'$a+b+\dot s+\dot{s}+\ldots$',
  20. r'$x\hspace{-0.2}\doteq\hspace{-0.2}y$',
  21. r'\$100.00 $\alpha \_$',
  22. r'$\frac{\$100.00}{y}$',
  23. r'$x y$',
  24. r'$x+y\ x=y\ x<y\ x:y\ x,y\ x@y$',
  25. r'$100\%y\ x*y\ x/y x\$y$',
  26. r'$x\leftarrow y\ x\forall y\ x-y$',
  27. r'$x \sf x \bf x {\cal X} \rm x$',
  28. r'$x\ x\,x\;x\quad x\qquad x\!x\hspace{ 0.5 }y$',
  29. r'$\{ \rm braces \}$',
  30. r'$\left[\left\lfloor\frac{5}{\frac{\left(3\right)}{4}} y\right)\right]$',
  31. r'$\left(x\right)$',
  32. r'$\sin(x)$',
  33. r'$x_2$',
  34. r'$x^2$',
  35. r'$x^2_y$',
  36. r'$x_y^2$',
  37. (r'$\sum _{\genfrac{}{}{0}{}{0\leq i\leq m}{0<j<n}}f\left(i,j\right)'
  38. r'\mathcal{R}\prod_{i=\alpha_{i+1}}^\infty a_i \sin(2 \pi f x_i)'
  39. r"\sqrt[2]{\prod^\frac{x}{2\pi^2}_\infty}$"),
  40. r'$x = \frac{x+\frac{5}{2}}{\frac{y+3}{8}}$',
  41. r'$dz/dt = \gamma x^2 + {\rm sin}(2\pi y+\phi)$',
  42. r'Foo: $\alpha_{i+1}^j = {\rm sin}(2\pi f_j t_i) e^{-5 t_i/\tau}$',
  43. None,
  44. r'Variable $i$ is good',
  45. r'$\Delta_i^j$',
  46. r'$\Delta^j_{i+1}$',
  47. r'$\ddot{o}\acute{e}\grave{e}\hat{O}\breve{\imath}\tilde{n}\vec{q}$',
  48. r"$\arccos((x^i))$",
  49. r"$\gamma = \frac{x=\frac{6}{8}}{y} \delta$",
  50. r'$\limsup_{x\to\infty}$',
  51. None,
  52. r"$f'\quad f'''(x)\quad ''/\mathrm{yr}$",
  53. r'$\frac{x_2888}{y}$',
  54. r"$\sqrt[3]{\frac{X_2}{Y}}=5$",
  55. None,
  56. r"$\sqrt[3]{x}=5$",
  57. r'$\frac{X}{\frac{X}{Y}}$',
  58. 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]$",
  59. r'$\mathcal{H} = \int d \tau \left(\epsilon E^2 + \mu H^2\right)$',
  60. r'$\widehat{abc}\widetilde{def}$',
  61. '$\\Gamma \\Delta \\Theta \\Lambda \\Xi \\Pi \\Sigma \\Upsilon \\Phi \\Psi \\Omega$',
  62. '$\\alpha \\beta \\gamma \\delta \\epsilon \\zeta \\eta \\theta \\iota \\lambda \\mu \\nu \\xi \\pi \\kappa \\rho \\sigma \\tau \\upsilon \\phi \\chi \\psi$',
  63. # The following examples are from the MathML torture test here:
  64. # https://www-archive.mozilla.org/projects/mathml/demo/texvsmml.xhtml
  65. r'${x}^{2}{y}^{2}$',
  66. r'${}_{2}F_{3}$',
  67. r'$\frac{x+{y}^{2}}{k+1}$',
  68. r'$x+{y}^{\frac{2}{k+1}}$',
  69. r'$\frac{a}{b/2}$',
  70. r'${a}_{0}+\frac{1}{{a}_{1}+\frac{1}{{a}_{2}+\frac{1}{{a}_{3}+\frac{1}{{a}_{4}}}}}$',
  71. r'${a}_{0}+\frac{1}{{a}_{1}+\frac{1}{{a}_{2}+\frac{1}{{a}_{3}+\frac{1}{{a}_{4}}}}}$',
  72. r'$\binom{n}{k/2}$',
  73. r'$\binom{p}{2}{x}^{2}{y}^{p-2}-\frac{1}{1-x}\frac{1}{1-{x}^{2}}$',
  74. r'${x}^{2y}$',
  75. r'$\sum _{i=1}^{p}\sum _{j=1}^{q}\sum _{k=1}^{r}{a}_{ij}{b}_{jk}{c}_{ki}$',
  76. r'$\sqrt{1+\sqrt{1+\sqrt{1+\sqrt{1+\sqrt{1+\sqrt{1+\sqrt{1+x}}}}}}}$',
  77. r'$\left(\frac{{\partial }^{2}}{\partial {x}^{2}}+\frac{{\partial }^{2}}{\partial {y}^{2}}\right){|\varphi \left(x+iy\right)|}^{2}=0$',
  78. r'${2}^{{2}^{{2}^{x}}}$',
  79. r'${\int }_{1}^{x}\frac{\mathrm{dt}}{t}$',
  80. r'$\int {\int }_{D}\mathrm{dx} \mathrm{dy}$',
  81. # mathtex doesn't support array
  82. # '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}$',
  83. # mathtex doesn't support stackrel
  84. # 'mmltt19' : r'$\stackrel{\stackrel{k\text{times}}{\ufe37}}{x+...+x}$',
  85. r'${y}_{{x}^{2}}$',
  86. # mathtex doesn't support the "\text" command
  87. # 'mmltt21' : r'$\sum _{p\text{\prime}}f\left(p\right)={\int }_{t>1}f\left(t\right) d\pi \left(t\right)$',
  88. # mathtex doesn't support array
  89. # '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)$',
  90. # mathtex doesn't support array
  91. # '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$',
  92. r'${y}_{{x}_{2}}$',
  93. r'${x}_{92}^{31415}+\pi $',
  94. r'${x}_{{y}_{b}^{a}}^{{z}_{c}^{d}}$',
  95. r'${y}_{3}^{\prime \prime \prime }$',
  96. # End of the MathML torture tests.
  97. r"$\left( \xi \left( 1 - \xi \right) \right)$", # Bug 2969451
  98. r"$\left(2 \, a=b\right)$", # Sage bug #8125
  99. r"$? ! &$", # github issue #466
  100. None,
  101. None,
  102. r"$\left\Vert \frac{a}{b} \right\Vert \left\vert \frac{a}{b} \right\vert \left\| \frac{a}{b}\right\| \left| \frac{a}{b} \right| \Vert a \Vert \vert b \vert \| a \| | b |$",
  103. r'$\mathring{A} \AA$',
  104. r'$M \, M \thinspace M \/ M \> M \: M \; M \ M \enspace M \quad M \qquad M \! M$',
  105. r'$\Cap$ $\Cup$ $\leftharpoonup$ $\barwedge$ $\rightharpoonup$',
  106. r'$\hspace{-0.2}\dotplus\hspace{-0.2}$ $\hspace{-0.2}\doteq\hspace{-0.2}$ $\hspace{-0.2}\doteqdot\hspace{-0.2}$ $\ddots$',
  107. r'$xyz^kx_kx^py^{p-2} d_i^jb_jc_kd x^j_i E^0 E^0_u$', # github issue #4873
  108. 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$',
  109. 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$',
  110. r'testing$^{123}$',
  111. None,
  112. r'$6-2$; $-2$; $ -2$; ${-2}$; ${ -2}$; $20^{+3}_{-2}$',
  113. r'$\overline{\omega}^x \frac{1}{2}_0^x$', # github issue #5444
  114. r'$,$ $.$ $1{,}234{, }567{ , }890$ and $1,234,567,890$', # github issue 5799
  115. r'$\left(X\right)_{a}^{b}$', # github issue 7615
  116. r'$\dfrac{\$100.00}{y}$', # github issue #1888
  117. r'$a=-b-c$' # github issue #28180
  118. ]
  119. # 'svgastext' tests switch svg output to embed text as text (rather than as
  120. # paths).
  121. svgastext_math_tests = [
  122. r'$-$-',
  123. ]
  124. # 'lightweight' tests test only a single fontset (dejavusans, which is the
  125. # default) and only png outputs, in order to minimize the size of baseline
  126. # images.
  127. lightweight_math_tests = [
  128. r'$\sqrt[ab]{123}$', # github issue #8665
  129. r'$x \overset{f}{\rightarrow} \overset{f}{x} \underset{xx}{ff} \overset{xx}{ff} \underset{f}{x} \underset{f}{\leftarrow} x$', # github issue #18241
  130. r'$\sum x\quad\sum^nx\quad\sum_nx\quad\sum_n^nx\quad\prod x\quad\prod^nx\quad\prod_nx\quad\prod_n^nx$', # GitHub issue 18085
  131. r'$1.$ $2.$ $19680801.$ $a.$ $b.$ $mpl.$',
  132. r'$\text{text}_{\text{sub}}^{\text{sup}} + \text{\$foo\$} + \frac{\text{num}}{\mathbf{\text{den}}}\text{with space, curly brackets \{\}, and dash -}$',
  133. r'$\boldsymbol{abcde} \boldsymbol{+} \boldsymbol{\Gamma + \Omega} \boldsymbol{01234} \boldsymbol{\alpha * \beta}$',
  134. r'$\left\lbrace\frac{\left\lbrack A^b_c\right\rbrace}{\left\leftbrace D^e_f \right\rbrack}\right\rightbrace\ \left\leftparen\max_{x} \left\lgroup \frac{A}{B}\right\rgroup \right\rightparen$',
  135. r'$\left( a\middle. b \right)$ $\left( \frac{a}{b} \middle\vert x_i \in P^S \right)$ $\left[ 1 - \middle| a\middle| + \left( x - \left\lfloor \dfrac{a}{b}\right\rfloor \right) \right]$',
  136. r'$\sum_{\substack{k = 1\\ k \neq \lfloor n/2\rfloor}}^{n}P(i,j) \sum_{\substack{i \neq 0\\ -1 \leq i \leq 3\\ 1 \leq j \leq 5}} F^i(x,y) \sum_{\substack{\left \lfloor \frac{n}{2} \right\rfloor}} F(n)$',
  137. ]
  138. digits = "0123456789"
  139. uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  140. lowercase = "abcdefghijklmnopqrstuvwxyz"
  141. uppergreek = ("\\Gamma \\Delta \\Theta \\Lambda \\Xi \\Pi \\Sigma \\Upsilon \\Phi \\Psi "
  142. "\\Omega")
  143. lowergreek = ("\\alpha \\beta \\gamma \\delta \\epsilon \\zeta \\eta \\theta \\iota "
  144. "\\lambda \\mu \\nu \\xi \\pi \\kappa \\rho \\sigma \\tau \\upsilon "
  145. "\\phi \\chi \\psi")
  146. all = [digits, uppercase, lowercase, uppergreek, lowergreek]
  147. # Use stubs to reserve space if tests are removed
  148. # stub should be of the form (None, N) where N is the number of strings that
  149. # used to be tested
  150. # Add new tests at the end.
  151. font_test_specs: list[tuple[None | list[str], Any]] = [
  152. ([], all),
  153. (['mathrm'], all),
  154. (['mathbf'], all),
  155. (['mathit'], all),
  156. (['mathtt'], [digits, uppercase, lowercase]),
  157. (None, 3),
  158. (None, 3),
  159. (None, 3),
  160. (['mathbb'], [digits, uppercase, lowercase,
  161. r'\Gamma \Pi \Sigma \gamma \pi']),
  162. (['mathrm', 'mathbb'], [digits, uppercase, lowercase,
  163. r'\Gamma \Pi \Sigma \gamma \pi']),
  164. (['mathbf', 'mathbb'], [digits, uppercase, lowercase,
  165. r'\Gamma \Pi \Sigma \gamma \pi']),
  166. (['mathcal'], [uppercase]),
  167. (['mathfrak'], [uppercase, lowercase]),
  168. (['mathbf', 'mathfrak'], [uppercase, lowercase]),
  169. (['mathscr'], [uppercase, lowercase]),
  170. (['mathsf'], [digits, uppercase, lowercase]),
  171. (['mathrm', 'mathsf'], [digits, uppercase, lowercase]),
  172. (['mathbf', 'mathsf'], [digits, uppercase, lowercase]),
  173. (['mathbfit'], all),
  174. ]
  175. font_tests: list[None | str] = []
  176. for fonts, chars in font_test_specs:
  177. if fonts is None:
  178. font_tests.extend([None] * chars)
  179. else:
  180. wrapper = ''.join([
  181. ' '.join(fonts),
  182. ' $',
  183. *(r'\%s{' % font for font in fonts),
  184. '%s',
  185. *('}' for font in fonts),
  186. '$',
  187. ])
  188. for font_set in chars:
  189. font_tests.append(wrapper % font_set)
  190. @pytest.fixture
  191. def baseline_images(request, fontset, index, text):
  192. if text is None:
  193. pytest.skip("test has been removed")
  194. return ['%s_%s_%02d' % (request.param, fontset, index)]
  195. @pytest.mark.parametrize(
  196. 'index, text', enumerate(math_tests), ids=range(len(math_tests)))
  197. @pytest.mark.parametrize(
  198. 'fontset', ['cm', 'stix', 'stixsans', 'dejavusans', 'dejavuserif'])
  199. @pytest.mark.parametrize('baseline_images', ['mathtext'], indirect=True)
  200. @image_comparison(baseline_images=None,
  201. tol=0.011 if platform.machine() in ('ppc64le', 's390x') else 0)
  202. def test_mathtext_rendering(baseline_images, fontset, index, text):
  203. mpl.rcParams['mathtext.fontset'] = fontset
  204. fig = plt.figure(figsize=(5.25, 0.75))
  205. fig.text(0.5, 0.5, text,
  206. horizontalalignment='center', verticalalignment='center')
  207. @pytest.mark.parametrize('index, text', enumerate(svgastext_math_tests),
  208. ids=range(len(svgastext_math_tests)))
  209. @pytest.mark.parametrize('fontset', ['cm', 'dejavusans'])
  210. @pytest.mark.parametrize('baseline_images', ['mathtext0'], indirect=True)
  211. @image_comparison(
  212. baseline_images=None, extensions=['svg'],
  213. savefig_kwarg={'metadata': { # Minimize image size.
  214. 'Creator': None, 'Date': None, 'Format': None, 'Type': None}})
  215. def test_mathtext_rendering_svgastext(baseline_images, fontset, index, text):
  216. mpl.rcParams['mathtext.fontset'] = fontset
  217. mpl.rcParams['svg.fonttype'] = 'none' # Minimize image size.
  218. fig = plt.figure(figsize=(5.25, 0.75))
  219. fig.patch.set(visible=False) # Minimize image size.
  220. fig.text(0.5, 0.5, text,
  221. horizontalalignment='center', verticalalignment='center')
  222. @pytest.mark.parametrize('index, text', enumerate(lightweight_math_tests),
  223. ids=range(len(lightweight_math_tests)))
  224. @pytest.mark.parametrize('fontset', ['dejavusans'])
  225. @pytest.mark.parametrize('baseline_images', ['mathtext1'], indirect=True)
  226. @image_comparison(baseline_images=None, extensions=['png'])
  227. def test_mathtext_rendering_lightweight(baseline_images, fontset, index, text):
  228. fig = plt.figure(figsize=(5.25, 0.75))
  229. fig.text(0.5, 0.5, text, math_fontfamily=fontset,
  230. horizontalalignment='center', verticalalignment='center')
  231. @pytest.mark.parametrize(
  232. 'index, text', enumerate(font_tests), ids=range(len(font_tests)))
  233. @pytest.mark.parametrize(
  234. 'fontset', ['cm', 'stix', 'stixsans', 'dejavusans', 'dejavuserif'])
  235. @pytest.mark.parametrize('baseline_images', ['mathfont'], indirect=True)
  236. @image_comparison(baseline_images=None, extensions=['png'],
  237. tol=0.011 if platform.machine() in ('ppc64le', 's390x') else 0)
  238. def test_mathfont_rendering(baseline_images, fontset, index, text):
  239. mpl.rcParams['mathtext.fontset'] = fontset
  240. fig = plt.figure(figsize=(5.25, 0.75))
  241. fig.text(0.5, 0.5, text,
  242. horizontalalignment='center', verticalalignment='center')
  243. @check_figures_equal(extensions=["png"])
  244. def test_short_long_accents(fig_test, fig_ref):
  245. acc_map = _mathtext.Parser._accent_map
  246. short_accs = [s for s in acc_map if len(s) == 1]
  247. corresponding_long_accs = []
  248. for s in short_accs:
  249. l, = (l for l in acc_map if len(l) > 1 and acc_map[l] == acc_map[s])
  250. corresponding_long_accs.append(l)
  251. fig_test.text(0, .5, "$" + "".join(rf"\{s}a" for s in short_accs) + "$")
  252. fig_ref.text(
  253. 0, .5, "$" + "".join(fr"\{l} a" for l in corresponding_long_accs) + "$")
  254. def test_fontinfo():
  255. fontpath = mpl.font_manager.findfont("DejaVu Sans")
  256. font = mpl.ft2font.FT2Font(fontpath)
  257. table = font.get_sfnt_table("head")
  258. assert table is not None
  259. assert table['version'] == (1, 0)
  260. # See gh-26152 for more context on this xfail
  261. @pytest.mark.xfail(pyparsing_version.release == (3, 1, 0),
  262. reason="Error messages are incorrect for this version")
  263. @pytest.mark.parametrize(
  264. 'math, msg',
  265. [
  266. (r'$\hspace{}$', r'Expected \hspace{space}'),
  267. (r'$\hspace{foo}$', r'Expected \hspace{space}'),
  268. (r'$\sinx$', r'Unknown symbol: \sinx'),
  269. (r'$\dotx$', r'Unknown symbol: \dotx'),
  270. (r'$\frac$', r'Expected \frac{num}{den}'),
  271. (r'$\frac{}{}$', r'Expected \frac{num}{den}'),
  272. (r'$\binom$', r'Expected \binom{num}{den}'),
  273. (r'$\binom{}{}$', r'Expected \binom{num}{den}'),
  274. (r'$\genfrac$',
  275. r'Expected \genfrac{ldelim}{rdelim}{rulesize}{style}{num}{den}'),
  276. (r'$\genfrac{}{}{}{}{}{}$',
  277. r'Expected \genfrac{ldelim}{rdelim}{rulesize}{style}{num}{den}'),
  278. (r'$\sqrt$', r'Expected \sqrt{value}'),
  279. (r'$\sqrt f$', r'Expected \sqrt{value}'),
  280. (r'$\overline$', r'Expected \overline{body}'),
  281. (r'$\overline{}$', r'Expected \overline{body}'),
  282. (r'$\leftF$', r'Expected a delimiter'),
  283. (r'$\rightF$', r'Unknown symbol: \rightF'),
  284. (r'$\left(\right$', r'Expected a delimiter'),
  285. # PyParsing 2 uses double quotes, PyParsing 3 uses single quotes and an
  286. # extra backslash.
  287. (r'$\left($', re.compile(r'Expected ("|\'\\)\\right["\']')),
  288. (r'$\dfrac$', r'Expected \dfrac{num}{den}'),
  289. (r'$\dfrac{}{}$', r'Expected \dfrac{num}{den}'),
  290. (r'$\overset$', r'Expected \overset{annotation}{body}'),
  291. (r'$\underset$', r'Expected \underset{annotation}{body}'),
  292. (r'$\foo$', r'Unknown symbol: \foo'),
  293. (r'$a^2^2$', r'Double superscript'),
  294. (r'$a_2_2$', r'Double subscript'),
  295. (r'$a^2_a^2$', r'Double superscript'),
  296. (r'$a = {b$', r"Expected '}'"),
  297. ],
  298. ids=[
  299. 'hspace without value',
  300. 'hspace with invalid value',
  301. 'function without space',
  302. 'accent without space',
  303. 'frac without parameters',
  304. 'frac with empty parameters',
  305. 'binom without parameters',
  306. 'binom with empty parameters',
  307. 'genfrac without parameters',
  308. 'genfrac with empty parameters',
  309. 'sqrt without parameters',
  310. 'sqrt with invalid value',
  311. 'overline without parameters',
  312. 'overline with empty parameter',
  313. 'left with invalid delimiter',
  314. 'right with invalid delimiter',
  315. 'unclosed parentheses with sizing',
  316. 'unclosed parentheses without sizing',
  317. 'dfrac without parameters',
  318. 'dfrac with empty parameters',
  319. 'overset without parameters',
  320. 'underset without parameters',
  321. 'unknown symbol',
  322. 'double superscript',
  323. 'double subscript',
  324. 'super on sub without braces',
  325. 'unclosed group',
  326. ]
  327. )
  328. def test_mathtext_exceptions(math, msg):
  329. parser = mathtext.MathTextParser('agg')
  330. match = re.escape(msg) if isinstance(msg, str) else msg
  331. with pytest.raises(ValueError, match=match):
  332. parser.parse(math)
  333. def test_get_unicode_index_exception():
  334. with pytest.raises(ValueError):
  335. _mathtext.get_unicode_index(r'\foo')
  336. def test_single_minus_sign():
  337. fig = plt.figure()
  338. fig.text(0.5, 0.5, '$-$')
  339. fig.canvas.draw()
  340. t = np.asarray(fig.canvas.renderer.buffer_rgba())
  341. assert (t != 0xff).any() # assert that canvas is not all white.
  342. @check_figures_equal(extensions=["png"])
  343. def test_spaces(fig_test, fig_ref):
  344. fig_test.text(.5, .5, r"$1\,2\>3\ 4$")
  345. fig_ref.text(.5, .5, r"$1\/2\:3~4$")
  346. @check_figures_equal(extensions=["png"])
  347. def test_operator_space(fig_test, fig_ref):
  348. fig_test.text(0.1, 0.1, r"$\log 6$")
  349. fig_test.text(0.1, 0.2, r"$\log(6)$")
  350. fig_test.text(0.1, 0.3, r"$\arcsin 6$")
  351. fig_test.text(0.1, 0.4, r"$\arcsin|6|$")
  352. fig_test.text(0.1, 0.5, r"$\operatorname{op} 6$") # GitHub issue #553
  353. fig_test.text(0.1, 0.6, r"$\operatorname{op}[6]$")
  354. fig_test.text(0.1, 0.7, r"$\cos^2$")
  355. fig_test.text(0.1, 0.8, r"$\log_2$")
  356. fig_test.text(0.1, 0.9, r"$\sin^2 \cos$") # GitHub issue #17852
  357. fig_ref.text(0.1, 0.1, r"$\mathrm{log\,}6$")
  358. fig_ref.text(0.1, 0.2, r"$\mathrm{log}(6)$")
  359. fig_ref.text(0.1, 0.3, r"$\mathrm{arcsin\,}6$")
  360. fig_ref.text(0.1, 0.4, r"$\mathrm{arcsin}|6|$")
  361. fig_ref.text(0.1, 0.5, r"$\mathrm{op\,}6$")
  362. fig_ref.text(0.1, 0.6, r"$\mathrm{op}[6]$")
  363. fig_ref.text(0.1, 0.7, r"$\mathrm{cos}^2$")
  364. fig_ref.text(0.1, 0.8, r"$\mathrm{log}_2$")
  365. fig_ref.text(0.1, 0.9, r"$\mathrm{sin}^2 \mathrm{\,cos}$")
  366. @check_figures_equal(extensions=["png"])
  367. def test_inverted_delimiters(fig_test, fig_ref):
  368. fig_test.text(.5, .5, r"$\left)\right($", math_fontfamily="dejavusans")
  369. fig_ref.text(.5, .5, r"$)($", math_fontfamily="dejavusans")
  370. @check_figures_equal(extensions=["png"])
  371. def test_genfrac_displaystyle(fig_test, fig_ref):
  372. fig_test.text(0.1, 0.1, r"$\dfrac{2x}{3y}$")
  373. thickness = _mathtext.TruetypeFonts.get_underline_thickness(
  374. None, None, fontsize=mpl.rcParams["font.size"],
  375. dpi=mpl.rcParams["savefig.dpi"])
  376. fig_ref.text(0.1, 0.1, r"$\genfrac{}{}{%f}{0}{2x}{3y}$" % thickness)
  377. def test_mathtext_fallback_valid():
  378. for fallback in ['cm', 'stix', 'stixsans', 'None']:
  379. mpl.rcParams['mathtext.fallback'] = fallback
  380. def test_mathtext_fallback_invalid():
  381. for fallback in ['abc', '']:
  382. with pytest.raises(ValueError, match="not a valid fallback font name"):
  383. mpl.rcParams['mathtext.fallback'] = fallback
  384. @pytest.mark.parametrize(
  385. "fallback,fontlist",
  386. [("cm", ['DejaVu Sans', 'mpltest', 'STIXGeneral', 'cmr10', 'STIXGeneral']),
  387. ("stix", ['DejaVu Sans', 'mpltest', 'STIXGeneral', 'STIXGeneral', 'STIXGeneral'])])
  388. def test_mathtext_fallback(fallback, fontlist):
  389. mpl.font_manager.fontManager.addfont(
  390. str(Path(__file__).resolve().parent / 'mpltest.ttf'))
  391. mpl.rcParams["svg.fonttype"] = 'none'
  392. mpl.rcParams['mathtext.fontset'] = 'custom'
  393. mpl.rcParams['mathtext.rm'] = 'mpltest'
  394. mpl.rcParams['mathtext.it'] = 'mpltest:italic'
  395. mpl.rcParams['mathtext.bf'] = 'mpltest:bold'
  396. mpl.rcParams['mathtext.bfit'] = 'mpltest:italic:bold'
  397. mpl.rcParams['mathtext.fallback'] = fallback
  398. test_str = r'a$A\AA\breve\gimel$'
  399. buff = io.BytesIO()
  400. fig, ax = plt.subplots()
  401. fig.text(.5, .5, test_str, fontsize=40, ha='center')
  402. fig.savefig(buff, format="svg")
  403. tspans = (ET.fromstring(buff.getvalue())
  404. .findall(".//{http://www.w3.org/2000/svg}tspan[@style]"))
  405. char_fonts = [
  406. re.search(r"font-family: '([\w ]+)'", tspan.attrib["style"]).group(1)
  407. for tspan in tspans]
  408. assert char_fonts == fontlist, f'Expected {fontlist}, got {char_fonts}'
  409. mpl.font_manager.fontManager.ttflist.pop()
  410. def test_math_to_image(tmp_path):
  411. mathtext.math_to_image('$x^2$', tmp_path / 'example.png')
  412. mathtext.math_to_image('$x^2$', io.BytesIO())
  413. mathtext.math_to_image('$x^2$', io.BytesIO(), color='Maroon')
  414. @image_comparison(baseline_images=['math_fontfamily_image.png'],
  415. savefig_kwarg={'dpi': 40})
  416. def test_math_fontfamily():
  417. fig = plt.figure(figsize=(10, 3))
  418. fig.text(0.2, 0.7, r"$This\ text\ should\ have\ one\ font$",
  419. size=24, math_fontfamily='dejavusans')
  420. fig.text(0.2, 0.3, r"$This\ text\ should\ have\ another$",
  421. size=24, math_fontfamily='stix')
  422. def test_default_math_fontfamily():
  423. mpl.rcParams['mathtext.fontset'] = 'cm'
  424. test_str = r'abc$abc\alpha$'
  425. fig, ax = plt.subplots()
  426. text1 = fig.text(0.1, 0.1, test_str, font='Arial')
  427. prop1 = text1.get_fontproperties()
  428. assert prop1.get_math_fontfamily() == 'cm'
  429. text2 = fig.text(0.2, 0.2, test_str, fontproperties='Arial')
  430. prop2 = text2.get_fontproperties()
  431. assert prop2.get_math_fontfamily() == 'cm'
  432. fig.draw_without_rendering()
  433. def test_argument_order():
  434. mpl.rcParams['mathtext.fontset'] = 'cm'
  435. test_str = r'abc$abc\alpha$'
  436. fig, ax = plt.subplots()
  437. text1 = fig.text(0.1, 0.1, test_str,
  438. math_fontfamily='dejavusans', font='Arial')
  439. prop1 = text1.get_fontproperties()
  440. assert prop1.get_math_fontfamily() == 'dejavusans'
  441. text2 = fig.text(0.2, 0.2, test_str,
  442. math_fontfamily='dejavusans', fontproperties='Arial')
  443. prop2 = text2.get_fontproperties()
  444. assert prop2.get_math_fontfamily() == 'dejavusans'
  445. text3 = fig.text(0.3, 0.3, test_str,
  446. font='Arial', math_fontfamily='dejavusans')
  447. prop3 = text3.get_fontproperties()
  448. assert prop3.get_math_fontfamily() == 'dejavusans'
  449. text4 = fig.text(0.4, 0.4, test_str,
  450. fontproperties='Arial', math_fontfamily='dejavusans')
  451. prop4 = text4.get_fontproperties()
  452. assert prop4.get_math_fontfamily() == 'dejavusans'
  453. fig.draw_without_rendering()
  454. def test_mathtext_cmr10_minus_sign():
  455. # cmr10 does not contain a minus sign and used to issue a warning
  456. # RuntimeWarning: Glyph 8722 missing from current font.
  457. mpl.rcParams['font.family'] = 'cmr10'
  458. mpl.rcParams['axes.formatter.use_mathtext'] = True
  459. fig, ax = plt.subplots()
  460. ax.plot(range(-1, 1), range(-1, 1))
  461. # draw to make sure we have no warnings
  462. fig.canvas.draw()
  463. def test_mathtext_operators():
  464. test_str = r'''
  465. \increment \smallin \notsmallowns
  466. \smallowns \QED \rightangle
  467. \smallintclockwise \smallvarointclockwise
  468. \smallointctrcclockwise
  469. \ratio \minuscolon \dotsminusdots
  470. \sinewave \simneqq \nlesssim
  471. \ngtrsim \nlessgtr \ngtrless
  472. \cupleftarrow \oequal \rightassert
  473. \rightModels \hermitmatrix \barvee
  474. \measuredrightangle \varlrtriangle
  475. \equalparallel \npreccurlyeq \nsucccurlyeq
  476. \nsqsubseteq \nsqsupseteq \sqsubsetneq
  477. \sqsupsetneq \disin \varisins
  478. \isins \isindot \varisinobar
  479. \isinobar \isinvb \isinE
  480. \nisd \varnis \nis
  481. \varniobar \niobar \bagmember
  482. \triangle'''.split()
  483. fig = plt.figure()
  484. for x, i in enumerate(test_str):
  485. fig.text(0.5, (x + 0.5)/len(test_str), r'${%s}$' % i)
  486. fig.draw_without_rendering()
  487. @check_figures_equal(extensions=["png"])
  488. def test_boldsymbol(fig_test, fig_ref):
  489. fig_test.text(0.1, 0.2, r"$\boldsymbol{\mathrm{abc0123\alpha}}$")
  490. fig_ref.text(0.1, 0.2, r"$\mathrm{abc0123\alpha}$")