test_backend_ps.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. from collections import Counter
  2. from pathlib import Path
  3. import io
  4. import re
  5. import tempfile
  6. import numpy as np
  7. import pytest
  8. from matplotlib import cbook, path, patheffects, font_manager as fm
  9. from matplotlib.figure import Figure
  10. from matplotlib.patches import Ellipse
  11. from matplotlib.testing._markers import needs_ghostscript, needs_usetex
  12. from matplotlib.testing.decorators import check_figures_equal, image_comparison
  13. import matplotlib as mpl
  14. import matplotlib.collections as mcollections
  15. import matplotlib.colors as mcolors
  16. import matplotlib.pyplot as plt
  17. # This tests tends to hit a TeX cache lock on AppVeyor.
  18. @pytest.mark.flaky(reruns=3)
  19. @pytest.mark.parametrize('papersize', ['letter', 'figure'])
  20. @pytest.mark.parametrize('orientation', ['portrait', 'landscape'])
  21. @pytest.mark.parametrize('format, use_log, rcParams', [
  22. ('ps', False, {}),
  23. ('ps', False, {'ps.usedistiller': 'ghostscript'}),
  24. ('ps', False, {'ps.usedistiller': 'xpdf'}),
  25. ('ps', False, {'text.usetex': True}),
  26. ('eps', False, {}),
  27. ('eps', True, {'ps.useafm': True}),
  28. ('eps', False, {'text.usetex': True}),
  29. ], ids=[
  30. 'ps',
  31. 'ps with distiller=ghostscript',
  32. 'ps with distiller=xpdf',
  33. 'ps with usetex',
  34. 'eps',
  35. 'eps afm',
  36. 'eps with usetex'
  37. ])
  38. def test_savefig_to_stringio(format, use_log, rcParams, orientation, papersize):
  39. mpl.rcParams.update(rcParams)
  40. if mpl.rcParams["ps.usedistiller"] == "ghostscript":
  41. try:
  42. mpl._get_executable_info("gs")
  43. except mpl.ExecutableNotFoundError as exc:
  44. pytest.skip(str(exc))
  45. elif mpl.rcParams["ps.usedistiller"] == "xpdf":
  46. try:
  47. mpl._get_executable_info("gs") # Effectively checks for ps2pdf.
  48. mpl._get_executable_info("pdftops")
  49. except mpl.ExecutableNotFoundError as exc:
  50. pytest.skip(str(exc))
  51. fig, ax = plt.subplots()
  52. with io.StringIO() as s_buf, io.BytesIO() as b_buf:
  53. if use_log:
  54. ax.set_yscale('log')
  55. ax.plot([1, 2], [1, 2])
  56. title = "Déjà vu"
  57. if not mpl.rcParams["text.usetex"]:
  58. title += " \N{MINUS SIGN}\N{EURO SIGN}"
  59. ax.set_title(title)
  60. allowable_exceptions = []
  61. if mpl.rcParams["text.usetex"]:
  62. allowable_exceptions.append(RuntimeError)
  63. if mpl.rcParams["ps.useafm"]:
  64. allowable_exceptions.append(mpl.MatplotlibDeprecationWarning)
  65. try:
  66. fig.savefig(s_buf, format=format, orientation=orientation,
  67. papertype=papersize)
  68. fig.savefig(b_buf, format=format, orientation=orientation,
  69. papertype=papersize)
  70. except tuple(allowable_exceptions) as exc:
  71. pytest.skip(str(exc))
  72. assert not s_buf.closed
  73. assert not b_buf.closed
  74. s_val = s_buf.getvalue().encode('ascii')
  75. b_val = b_buf.getvalue()
  76. if format == 'ps':
  77. # Default figsize = (8, 6) inches = (576, 432) points = (203.2, 152.4) mm.
  78. # Landscape orientation will swap dimensions.
  79. if mpl.rcParams["ps.usedistiller"] == "xpdf":
  80. # Some versions specifically show letter/203x152, but not all,
  81. # so we can only use this simpler test.
  82. if papersize == 'figure':
  83. assert b'letter' not in s_val.lower()
  84. else:
  85. assert b'letter' in s_val.lower()
  86. elif mpl.rcParams["ps.usedistiller"] or mpl.rcParams["text.usetex"]:
  87. width = b'432.0' if orientation == 'landscape' else b'576.0'
  88. wanted = (b'-dDEVICEWIDTHPOINTS=' + width if papersize == 'figure'
  89. else b'-sPAPERSIZE')
  90. assert wanted in s_val
  91. else:
  92. if papersize == 'figure':
  93. assert b'%%DocumentPaperSizes' not in s_val
  94. else:
  95. assert b'%%DocumentPaperSizes' in s_val
  96. # Strip out CreationDate: ghostscript and cairo don't obey
  97. # SOURCE_DATE_EPOCH, and that environment variable is already tested in
  98. # test_determinism.
  99. s_val = re.sub(b"(?<=\n%%CreationDate: ).*", b"", s_val)
  100. b_val = re.sub(b"(?<=\n%%CreationDate: ).*", b"", b_val)
  101. assert s_val == b_val.replace(b'\r\n', b'\n')
  102. def test_patheffects():
  103. mpl.rcParams['path.effects'] = [
  104. patheffects.withStroke(linewidth=4, foreground='w')]
  105. fig, ax = plt.subplots()
  106. ax.plot([1, 2, 3])
  107. with io.BytesIO() as ps:
  108. fig.savefig(ps, format='ps')
  109. @needs_usetex
  110. @needs_ghostscript
  111. def test_tilde_in_tempfilename(tmp_path):
  112. # Tilde ~ in the tempdir path (e.g. TMPDIR, TMP or TEMP on windows
  113. # when the username is very long and windows uses a short name) breaks
  114. # latex before https://github.com/matplotlib/matplotlib/pull/5928
  115. base_tempdir = tmp_path / "short-1"
  116. base_tempdir.mkdir()
  117. # Change the path for new tempdirs, which is used internally by the ps
  118. # backend to write a file.
  119. with cbook._setattr_cm(tempfile, tempdir=str(base_tempdir)):
  120. # usetex results in the latex call, which does not like the ~
  121. mpl.rcParams['text.usetex'] = True
  122. plt.plot([1, 2, 3, 4])
  123. plt.xlabel(r'\textbf{time} (s)')
  124. # use the PS backend to write the file...
  125. plt.savefig(base_tempdir / 'tex_demo.eps', format="ps")
  126. @image_comparison(["empty.eps"])
  127. def test_transparency():
  128. fig, ax = plt.subplots()
  129. ax.set_axis_off()
  130. ax.plot([0, 1], color="r", alpha=0)
  131. ax.text(.5, .5, "foo", color="r", alpha=0)
  132. @needs_usetex
  133. @image_comparison(["empty.eps"])
  134. def test_transparency_tex():
  135. mpl.rcParams['text.usetex'] = True
  136. fig, ax = plt.subplots()
  137. ax.set_axis_off()
  138. ax.plot([0, 1], color="r", alpha=0)
  139. ax.text(.5, .5, "foo", color="r", alpha=0)
  140. def test_bbox():
  141. fig, ax = plt.subplots()
  142. with io.BytesIO() as buf:
  143. fig.savefig(buf, format='eps')
  144. buf = buf.getvalue()
  145. bb = re.search(b'^%%BoundingBox: (.+) (.+) (.+) (.+)$', buf, re.MULTILINE)
  146. assert bb
  147. hibb = re.search(b'^%%HiResBoundingBox: (.+) (.+) (.+) (.+)$', buf,
  148. re.MULTILINE)
  149. assert hibb
  150. for i in range(1, 5):
  151. # BoundingBox must use integers, and be ceil/floor of the hi res.
  152. assert b'.' not in bb.group(i)
  153. assert int(bb.group(i)) == pytest.approx(float(hibb.group(i)), 1)
  154. @needs_usetex
  155. def test_failing_latex():
  156. """Test failing latex subprocess call"""
  157. mpl.rcParams['text.usetex'] = True
  158. # This fails with "Double subscript"
  159. plt.xlabel("$22_2_2$")
  160. with pytest.raises(RuntimeError):
  161. plt.savefig(io.BytesIO(), format="ps")
  162. @needs_usetex
  163. def test_partial_usetex(caplog):
  164. caplog.set_level("WARNING")
  165. plt.figtext(.1, .1, "foo", usetex=True)
  166. plt.figtext(.2, .2, "bar", usetex=True)
  167. plt.savefig(io.BytesIO(), format="ps")
  168. record, = caplog.records # asserts there's a single record.
  169. assert "as if usetex=False" in record.getMessage()
  170. @needs_usetex
  171. def test_usetex_preamble(caplog):
  172. mpl.rcParams.update({
  173. "text.usetex": True,
  174. # Check that these don't conflict with the packages loaded by default.
  175. "text.latex.preamble": r"\usepackage{color,graphicx,textcomp}",
  176. })
  177. plt.figtext(.5, .5, "foo")
  178. plt.savefig(io.BytesIO(), format="ps")
  179. @image_comparison(["useafm.eps"])
  180. def test_useafm():
  181. mpl.rcParams["ps.useafm"] = True
  182. fig, ax = plt.subplots()
  183. ax.set_axis_off()
  184. ax.axhline(.5)
  185. ax.text(.5, .5, "qk")
  186. @image_comparison(["type3.eps"])
  187. def test_type3_font():
  188. plt.figtext(.5, .5, "I/J")
  189. @image_comparison(["coloredhatcheszerolw.eps"])
  190. def test_colored_hatch_zero_linewidth():
  191. ax = plt.gca()
  192. ax.add_patch(Ellipse((0, 0), 1, 1, hatch='/', facecolor='none',
  193. edgecolor='r', linewidth=0))
  194. ax.add_patch(Ellipse((0.5, 0.5), 0.5, 0.5, hatch='+', facecolor='none',
  195. edgecolor='g', linewidth=0.2))
  196. ax.add_patch(Ellipse((1, 1), 0.3, 0.8, hatch='\\', facecolor='none',
  197. edgecolor='b', linewidth=0))
  198. ax.set_axis_off()
  199. @check_figures_equal(extensions=["eps"])
  200. def test_text_clip(fig_test, fig_ref):
  201. ax = fig_test.add_subplot()
  202. # Fully clipped-out text should not appear.
  203. ax.text(0, 0, "hello", transform=fig_test.transFigure, clip_on=True)
  204. fig_ref.add_subplot()
  205. @needs_ghostscript
  206. def test_d_glyph(tmp_path):
  207. # Ensure that we don't have a procedure defined as /d, which would be
  208. # overwritten by the glyph definition for "d".
  209. fig = plt.figure()
  210. fig.text(.5, .5, "def")
  211. out = tmp_path / "test.eps"
  212. fig.savefig(out)
  213. mpl.testing.compare.convert(out, cache=False) # Should not raise.
  214. @image_comparison(["type42_without_prep.eps"], style='mpl20')
  215. def test_type42_font_without_prep():
  216. # Test whether Type 42 fonts without prep table are properly embedded
  217. mpl.rcParams["ps.fonttype"] = 42
  218. mpl.rcParams["mathtext.fontset"] = "stix"
  219. plt.figtext(0.5, 0.5, "Mass $m$")
  220. @pytest.mark.parametrize('fonttype', ["3", "42"])
  221. def test_fonttype(fonttype):
  222. mpl.rcParams["ps.fonttype"] = fonttype
  223. fig, ax = plt.subplots()
  224. ax.text(0.25, 0.5, "Forty-two is the answer to everything!")
  225. buf = io.BytesIO()
  226. fig.savefig(buf, format="ps")
  227. test = b'/FontType ' + bytes(f"{fonttype}", encoding='utf-8') + b' def'
  228. assert re.search(test, buf.getvalue(), re.MULTILINE)
  229. def test_linedash():
  230. """Test that dashed lines do not break PS output"""
  231. fig, ax = plt.subplots()
  232. ax.plot([0, 1], linestyle="--")
  233. buf = io.BytesIO()
  234. fig.savefig(buf, format="ps")
  235. assert buf.tell() > 0
  236. def test_empty_line():
  237. # Smoke-test for gh#23954
  238. figure = Figure()
  239. figure.text(0.5, 0.5, "\nfoo\n\n")
  240. buf = io.BytesIO()
  241. figure.savefig(buf, format='eps')
  242. figure.savefig(buf, format='ps')
  243. def test_no_duplicate_definition():
  244. fig = Figure()
  245. axs = fig.subplots(4, 4, subplot_kw=dict(projection="polar"))
  246. for ax in axs.flat:
  247. ax.set(xticks=[], yticks=[])
  248. ax.plot([1, 2])
  249. fig.suptitle("hello, world")
  250. buf = io.StringIO()
  251. fig.savefig(buf, format='eps')
  252. buf.seek(0)
  253. wds = [ln.partition(' ')[0] for
  254. ln in buf.readlines()
  255. if ln.startswith('/')]
  256. assert max(Counter(wds).values()) == 1
  257. @image_comparison(["multi_font_type3.eps"], tol=0.51)
  258. def test_multi_font_type3():
  259. fp = fm.FontProperties(family=["WenQuanYi Zen Hei"])
  260. if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc":
  261. pytest.skip("Font may be missing")
  262. plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27)
  263. plt.rc('ps', fonttype=3)
  264. fig = plt.figure()
  265. fig.text(0.15, 0.475, "There are 几个汉字 in between!")
  266. @image_comparison(["multi_font_type42.eps"], tol=1.6)
  267. def test_multi_font_type42():
  268. fp = fm.FontProperties(family=["WenQuanYi Zen Hei"])
  269. if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc":
  270. pytest.skip("Font may be missing")
  271. plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27)
  272. plt.rc('ps', fonttype=42)
  273. fig = plt.figure()
  274. fig.text(0.15, 0.475, "There are 几个汉字 in between!")
  275. @image_comparison(["scatter.eps"])
  276. def test_path_collection():
  277. rng = np.random.default_rng(19680801)
  278. xvals = rng.uniform(0, 1, 10)
  279. yvals = rng.uniform(0, 1, 10)
  280. sizes = rng.uniform(30, 100, 10)
  281. fig, ax = plt.subplots()
  282. ax.scatter(xvals, yvals, sizes, edgecolor=[0.9, 0.2, 0.1], marker='<')
  283. ax.set_axis_off()
  284. paths = [path.Path.unit_regular_polygon(i) for i in range(3, 7)]
  285. offsets = rng.uniform(0, 200, 20).reshape(10, 2)
  286. sizes = [0.02, 0.04]
  287. pc = mcollections.PathCollection(paths, sizes, zorder=-1,
  288. facecolors='yellow', offsets=offsets)
  289. ax.add_collection(pc)
  290. ax.set_xlim(0, 1)
  291. @image_comparison(["colorbar_shift.eps"], savefig_kwarg={"bbox_inches": "tight"},
  292. style="mpl20")
  293. def test_colorbar_shift(tmp_path):
  294. cmap = mcolors.ListedColormap(["r", "g", "b"])
  295. norm = mcolors.BoundaryNorm([-1, -0.5, 0.5, 1], cmap.N)
  296. plt.scatter([0, 1], [1, 1], c=[0, 1], cmap=cmap, norm=norm)
  297. plt.colorbar()
  298. def test_auto_papersize_removal():
  299. fig = plt.figure()
  300. with pytest.raises(ValueError, match="'auto' is not a valid value"):
  301. fig.savefig(io.BytesIO(), format='eps', papertype='auto')
  302. with pytest.raises(ValueError, match="'auto' is not a valid value"):
  303. mpl.rcParams['ps.papersize'] = 'auto'