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. import shlex
  7. from xml.etree import ElementTree as ET
  8. from typing import Any
  9. import numpy as np
  10. from packaging.version import parse as parse_version
  11. import pyparsing
  12. import pytest
  13. import matplotlib as mpl
  14. from matplotlib.testing.decorators import check_figures_equal, image_comparison
  15. import matplotlib.pyplot as plt
  16. from matplotlib import mathtext, _mathtext
  17. pyparsing_version = parse_version(pyparsing.__version__)
  18. # If test is removed, use None as placeholder
  19. math_tests = [
  20. r'$a+b+\dot s+\dot{s}+\ldots$',
  21. r'$x\hspace{-0.2}\doteq\hspace{-0.2}y$',
  22. r'\$100.00 $\alpha \_$',
  23. r'$\frac{\$100.00}{y}$',
  24. r'$x y$',
  25. r'$x+y\ x=y\ x<y\ x:y\ x,y\ x@y$',
  26. r'$100\%y\ x*y\ x/y x\$y$',
  27. r'$x\leftarrow y\ x\forall y\ x-y$',
  28. r'$x \sf x \bf x {\cal X} \rm x$',
  29. r'$x\ x\,x\;x\quad x\qquad x\!x\hspace{ 0.5 }y$',
  30. r'$\{ \rm braces \}$',
  31. r'$\left[\left\lfloor\frac{5}{\frac{\left(3\right)}{4}} y\right)\right]$',
  32. r'$\left(x\right)$',
  33. r'$\sin(x)$',
  34. r'$x_2$',
  35. r'$x^2$',
  36. r'$x^2_y$',
  37. r'$x_y^2$',
  38. (r'$\sum _{\genfrac{}{}{0}{}{0\leq i\leq m}{0<j<n}}f\left(i,j\right)'
  39. r'\mathcal{R}\prod_{i=\alpha_{i+1}}^\infty a_i \sin(2 \pi f x_i)'
  40. r"\sqrt[2]{\prod^\frac{x}{2\pi^2}_\infty}$"),
  41. r'$x = \frac{x+\frac{5}{2}}{\frac{y+3}{8}}$',
  42. r'$dz/dt = \gamma x^2 + {\rm sin}(2\pi y+\phi)$',
  43. r'Foo: $\alpha_{i+1}^j = {\rm sin}(2\pi f_j t_i) e^{-5 t_i/\tau}$',
  44. None,
  45. r'Variable $i$ is good',
  46. r'$\Delta_i^j$',
  47. r'$\Delta^j_{i+1}$',
  48. r'$\ddot{o}\acute{e}\grave{e}\hat{O}\breve{\imath}\tilde{n}\vec{q}$',
  49. r"$\arccos((x^i))$",
  50. r"$\gamma = \frac{x=\frac{6}{8}}{y} \delta$",
  51. r'$\limsup_{x\to\infty}$',
  52. None,
  53. r"$f'\quad f'''(x)\quad ''/\mathrm{yr}$",
  54. r'$\frac{x_2888}{y}$',
  55. r"$\sqrt[3]{\frac{X_2}{Y}}=5$",
  56. None,
  57. r"$\sqrt[3]{x}=5$",
  58. r'$\frac{X}{\frac{X}{Y}}$',
  59. 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]$",
  60. r'$\mathcal{H} = \int d \tau \left(\epsilon E^2 + \mu H^2\right)$',
  61. r'$\widehat{abc}\widetilde{def}$',
  62. '$\\Gamma \\Delta \\Theta \\Lambda \\Xi \\Pi \\Sigma \\Upsilon \\Phi \\Psi \\Omega$',
  63. '$\\alpha \\beta \\gamma \\delta \\epsilon \\zeta \\eta \\theta \\iota \\lambda \\mu \\nu \\xi \\pi \\kappa \\rho \\sigma \\tau \\upsilon \\phi \\chi \\psi$',
  64. # The following examples are from the MathML torture test here:
  65. # https://www-archive.mozilla.org/projects/mathml/demo/texvsmml.xhtml
  66. r'${x}^{2}{y}^{2}$',
  67. r'${}_{2}F_{3}$',
  68. r'$\frac{x+{y}^{2}}{k+1}$',
  69. r'$x+{y}^{\frac{2}{k+1}}$',
  70. r'$\frac{a}{b/2}$',
  71. r'${a}_{0}+\frac{1}{{a}_{1}+\frac{1}{{a}_{2}+\frac{1}{{a}_{3}+\frac{1}{{a}_{4}}}}}$',
  72. r'${a}_{0}+\frac{1}{{a}_{1}+\frac{1}{{a}_{2}+\frac{1}{{a}_{3}+\frac{1}{{a}_{4}}}}}$',
  73. r'$\binom{n}{k/2}$',
  74. r'$\binom{p}{2}{x}^{2}{y}^{p-2}-\frac{1}{1-x}\frac{1}{1-{x}^{2}}$',
  75. r'${x}^{2y}$',
  76. r'$\sum _{i=1}^{p}\sum _{j=1}^{q}\sum _{k=1}^{r}{a}_{ij}{b}_{jk}{c}_{ki}$',
  77. r'$\sqrt{1+\sqrt{1+\sqrt{1+\sqrt{1+\sqrt{1+\sqrt{1+\sqrt{1+x}}}}}}}$',
  78. r'$\left(\frac{{\partial }^{2}}{\partial {x}^{2}}+\frac{{\partial }^{2}}{\partial {y}^{2}}\right){|\varphi \left(x+iy\right)|}^{2}=0$',
  79. r'${2}^{{2}^{{2}^{x}}}$',
  80. r'${\int }_{1}^{x}\frac{\mathrm{dt}}{t}$',
  81. r'$\int {\int }_{D}\mathrm{dx} \mathrm{dy}$',
  82. # mathtex doesn't support array
  83. # '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}$',
  84. # mathtex doesn't support stackrel
  85. # 'mmltt19' : r'$\stackrel{\stackrel{k\text{times}}{\ufe37}}{x+...+x}$',
  86. r'${y}_{{x}^{2}}$',
  87. # mathtex doesn't support the "\text" command
  88. # 'mmltt21' : r'$\sum _{p\text{\prime}}f\left(p\right)={\int }_{t>1}f\left(t\right) d\pi \left(t\right)$',
  89. # mathtex doesn't support array
  90. # '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)$',
  91. # mathtex doesn't support array
  92. # '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$',
  93. r'${y}_{{x}_{2}}$',
  94. r'${x}_{92}^{31415}+\pi $',
  95. r'${x}_{{y}_{b}^{a}}^{{z}_{c}^{d}}$',
  96. r'${y}_{3}^{\prime \prime \prime }$',
  97. # End of the MathML torture tests.
  98. r"$\left( \xi \left( 1 - \xi \right) \right)$", # Bug 2969451
  99. r"$\left(2 \, a=b\right)$", # Sage bug #8125
  100. r"$? ! &$", # github issue #466
  101. None,
  102. None,
  103. 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 |$",
  104. r'$\mathring{A} \AA$',
  105. r'$M \, M \thinspace M \/ M \> M \: M \; M \ M \enspace M \quad M \qquad M \! M$',
  106. r'$\Cap$ $\Cup$ $\leftharpoonup$ $\barwedge$ $\rightharpoonup$',
  107. r'$\hspace{-0.2}\dotplus\hspace{-0.2}$ $\hspace{-0.2}\doteq\hspace{-0.2}$ $\hspace{-0.2}\doteqdot\hspace{-0.2}$ $\ddots$',
  108. r'$xyz^kx_kx^py^{p-2} d_i^jb_jc_kd x^j_i E^0 E^0_u$', # github issue #4873
  109. 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$',
  110. 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$',
  111. r'testing$^{123}$',
  112. None,
  113. r'$6-2$; $-2$; $ -2$; ${-2}$; ${ -2}$; $20^{+3}_{-2}$',
  114. r'$\overline{\omega}^x \frac{1}{2}_0^x$', # github issue #5444
  115. r'$,$ $.$ $1{,}234{, }567{ , }890$ and $1,234,567,890$', # github issue 5799
  116. r'$\left(X\right)_{a}^{b}$', # github issue 7615
  117. r'$\dfrac{\$100.00}{y}$', # github issue #1888
  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 set in chars:
  189. font_tests.append(wrapper % 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'])])
  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. # Getting the last element of the style attrib is a close enough
  406. # approximation for parsing the font property.
  407. char_fonts = [shlex.split(tspan.attrib["style"])[-1] for tspan in tspans]
  408. assert char_fonts == fontlist
  409. mpl.font_manager.fontManager.ttflist.pop()
  410. def test_math_to_image(tmpdir):
  411. mathtext.math_to_image('$x^2$', str(tmpdir.join('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}$")