test_mathtext.py 14 KB

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