test_agg.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import io
  2. import numpy as np
  3. from numpy.testing import assert_array_almost_equal
  4. from PIL import Image, TiffTags
  5. import pytest
  6. from matplotlib import (
  7. collections, patheffects, pyplot as plt, transforms as mtransforms,
  8. rcParams, rc_context)
  9. from matplotlib.backends.backend_agg import RendererAgg
  10. from matplotlib.figure import Figure
  11. from matplotlib.image import imread
  12. from matplotlib.path import Path
  13. from matplotlib.testing.decorators import image_comparison
  14. from matplotlib.transforms import IdentityTransform
  15. def test_repeated_save_with_alpha():
  16. # We want an image which has a background color of bluish green, with an
  17. # alpha of 0.25.
  18. fig = Figure([1, 0.4])
  19. fig.set_facecolor((0, 1, 0.4))
  20. fig.patch.set_alpha(0.25)
  21. # The target color is fig.patch.get_facecolor()
  22. buf = io.BytesIO()
  23. fig.savefig(buf,
  24. facecolor=fig.get_facecolor(),
  25. edgecolor='none')
  26. # Save the figure again to check that the
  27. # colors don't bleed from the previous renderer.
  28. buf.seek(0)
  29. fig.savefig(buf,
  30. facecolor=fig.get_facecolor(),
  31. edgecolor='none')
  32. # Check the first pixel has the desired color & alpha
  33. # (approx: 0, 1.0, 0.4, 0.25)
  34. buf.seek(0)
  35. assert_array_almost_equal(tuple(imread(buf)[0, 0]),
  36. (0.0, 1.0, 0.4, 0.250),
  37. decimal=3)
  38. def test_large_single_path_collection():
  39. buff = io.BytesIO()
  40. # Generates a too-large single path in a path collection that
  41. # would cause a segfault if the draw_markers optimization is
  42. # applied.
  43. f, ax = plt.subplots()
  44. collection = collections.PathCollection(
  45. [Path([[-10, 5], [10, 5], [10, -5], [-10, -5], [-10, 5]])])
  46. ax.add_artist(collection)
  47. ax.set_xlim(10**-3, 1)
  48. plt.savefig(buff)
  49. def test_marker_with_nan():
  50. # This creates a marker with nans in it, which was segfaulting the
  51. # Agg backend (see #3722)
  52. fig, ax = plt.subplots(1)
  53. steps = 1000
  54. data = np.arange(steps)
  55. ax.semilogx(data)
  56. ax.fill_between(data, data*0.8, data*1.2)
  57. buf = io.BytesIO()
  58. fig.savefig(buf, format='png')
  59. def test_long_path():
  60. buff = io.BytesIO()
  61. fig = Figure()
  62. ax = fig.subplots()
  63. points = np.ones(100_000)
  64. points[::2] *= -1
  65. ax.plot(points)
  66. fig.savefig(buff, format='png')
  67. @image_comparison(['agg_filter.png'], remove_text=True)
  68. def test_agg_filter():
  69. def smooth1d(x, window_len):
  70. # copied from https://scipy-cookbook.readthedocs.io/
  71. s = np.r_[
  72. 2*x[0] - x[window_len:1:-1], x, 2*x[-1] - x[-1:-window_len:-1]]
  73. w = np.hanning(window_len)
  74. y = np.convolve(w/w.sum(), s, mode='same')
  75. return y[window_len-1:-window_len+1]
  76. def smooth2d(A, sigma=3):
  77. window_len = max(int(sigma), 3) * 2 + 1
  78. A = np.apply_along_axis(smooth1d, 0, A, window_len)
  79. A = np.apply_along_axis(smooth1d, 1, A, window_len)
  80. return A
  81. class BaseFilter:
  82. def get_pad(self, dpi):
  83. return 0
  84. def process_image(self, padded_src, dpi):
  85. raise NotImplementedError("Should be overridden by subclasses")
  86. def __call__(self, im, dpi):
  87. pad = self.get_pad(dpi)
  88. padded_src = np.pad(im, [(pad, pad), (pad, pad), (0, 0)],
  89. "constant")
  90. tgt_image = self.process_image(padded_src, dpi)
  91. return tgt_image, -pad, -pad
  92. class OffsetFilter(BaseFilter):
  93. def __init__(self, offsets=(0, 0)):
  94. self.offsets = offsets
  95. def get_pad(self, dpi):
  96. return int(max(self.offsets) / 72 * dpi)
  97. def process_image(self, padded_src, dpi):
  98. ox, oy = self.offsets
  99. a1 = np.roll(padded_src, int(ox / 72 * dpi), axis=1)
  100. a2 = np.roll(a1, -int(oy / 72 * dpi), axis=0)
  101. return a2
  102. class GaussianFilter(BaseFilter):
  103. """Simple Gaussian filter."""
  104. def __init__(self, sigma, alpha=0.5, color=(0, 0, 0)):
  105. self.sigma = sigma
  106. self.alpha = alpha
  107. self.color = color
  108. def get_pad(self, dpi):
  109. return int(self.sigma*3 / 72 * dpi)
  110. def process_image(self, padded_src, dpi):
  111. tgt_image = np.empty_like(padded_src)
  112. tgt_image[:, :, :3] = self.color
  113. tgt_image[:, :, 3] = smooth2d(padded_src[:, :, 3] * self.alpha,
  114. self.sigma / 72 * dpi)
  115. return tgt_image
  116. class DropShadowFilter(BaseFilter):
  117. def __init__(self, sigma, alpha=0.3, color=(0, 0, 0), offsets=(0, 0)):
  118. self.gauss_filter = GaussianFilter(sigma, alpha, color)
  119. self.offset_filter = OffsetFilter(offsets)
  120. def get_pad(self, dpi):
  121. return max(self.gauss_filter.get_pad(dpi),
  122. self.offset_filter.get_pad(dpi))
  123. def process_image(self, padded_src, dpi):
  124. t1 = self.gauss_filter.process_image(padded_src, dpi)
  125. t2 = self.offset_filter.process_image(t1, dpi)
  126. return t2
  127. fig, ax = plt.subplots()
  128. # draw lines
  129. line1, = ax.plot([0.1, 0.5, 0.9], [0.1, 0.9, 0.5], "bo-",
  130. mec="b", mfc="w", lw=5, mew=3, ms=10, label="Line 1")
  131. line2, = ax.plot([0.1, 0.5, 0.9], [0.5, 0.2, 0.7], "ro-",
  132. mec="r", mfc="w", lw=5, mew=3, ms=10, label="Line 1")
  133. gauss = DropShadowFilter(4)
  134. for line in [line1, line2]:
  135. # draw shadows with same lines with slight offset.
  136. xx = line.get_xdata()
  137. yy = line.get_ydata()
  138. shadow, = ax.plot(xx, yy)
  139. shadow.update_from(line)
  140. # offset transform
  141. transform = mtransforms.offset_copy(line.get_transform(), ax.figure,
  142. x=4.0, y=-6.0, units='points')
  143. shadow.set_transform(transform)
  144. # adjust zorder of the shadow lines so that it is drawn below the
  145. # original lines
  146. shadow.set_zorder(line.get_zorder() - 0.5)
  147. shadow.set_agg_filter(gauss)
  148. shadow.set_rasterized(True) # to support mixed-mode renderers
  149. ax.set_xlim(0., 1.)
  150. ax.set_ylim(0., 1.)
  151. ax.xaxis.set_visible(False)
  152. ax.yaxis.set_visible(False)
  153. def test_too_large_image():
  154. fig = plt.figure(figsize=(300, 1000))
  155. buff = io.BytesIO()
  156. with pytest.raises(ValueError):
  157. fig.savefig(buff)
  158. def test_chunksize():
  159. x = range(200)
  160. # Test without chunksize
  161. fig, ax = plt.subplots()
  162. ax.plot(x, np.sin(x))
  163. fig.canvas.draw()
  164. # Test with chunksize
  165. fig, ax = plt.subplots()
  166. rcParams['agg.path.chunksize'] = 105
  167. ax.plot(x, np.sin(x))
  168. fig.canvas.draw()
  169. @pytest.mark.backend('Agg')
  170. def test_jpeg_dpi():
  171. # Check that dpi is set correctly in jpg files.
  172. plt.plot([0, 1, 2], [0, 1, 0])
  173. buf = io.BytesIO()
  174. plt.savefig(buf, format="jpg", dpi=200)
  175. im = Image.open(buf)
  176. assert im.info['dpi'] == (200, 200)
  177. def test_pil_kwargs_png():
  178. from PIL.PngImagePlugin import PngInfo
  179. buf = io.BytesIO()
  180. pnginfo = PngInfo()
  181. pnginfo.add_text("Software", "test")
  182. plt.figure().savefig(buf, format="png", pil_kwargs={"pnginfo": pnginfo})
  183. im = Image.open(buf)
  184. assert im.info["Software"] == "test"
  185. def test_pil_kwargs_tiff():
  186. buf = io.BytesIO()
  187. pil_kwargs = {"description": "test image"}
  188. plt.figure().savefig(buf, format="tiff", pil_kwargs=pil_kwargs)
  189. im = Image.open(buf)
  190. tags = {TiffTags.TAGS_V2[k].name: v for k, v in im.tag_v2.items()}
  191. assert tags["ImageDescription"] == "test image"
  192. def test_pil_kwargs_webp():
  193. plt.plot([0, 1, 2], [0, 1, 0])
  194. buf_small = io.BytesIO()
  195. pil_kwargs_low = {"quality": 1}
  196. plt.savefig(buf_small, format="webp", pil_kwargs=pil_kwargs_low)
  197. assert len(pil_kwargs_low) == 1
  198. buf_large = io.BytesIO()
  199. pil_kwargs_high = {"quality": 100}
  200. plt.savefig(buf_large, format="webp", pil_kwargs=pil_kwargs_high)
  201. assert len(pil_kwargs_high) == 1
  202. assert buf_large.getbuffer().nbytes > buf_small.getbuffer().nbytes
  203. def test_webp_alpha():
  204. plt.plot([0, 1, 2], [0, 1, 0])
  205. buf = io.BytesIO()
  206. plt.savefig(buf, format="webp", transparent=True)
  207. im = Image.open(buf)
  208. assert im.mode == "RGBA"
  209. def test_draw_path_collection_error_handling():
  210. fig, ax = plt.subplots()
  211. ax.scatter([1], [1]).set_paths(Path([(0, 1), (2, 3)]))
  212. with pytest.raises(TypeError):
  213. fig.canvas.draw()
  214. def test_chunksize_fails():
  215. # NOTE: This test covers multiple independent test scenarios in a single
  216. # function, because each scenario uses ~2GB of memory and we don't
  217. # want parallel test executors to accidentally run multiple of these
  218. # at the same time.
  219. N = 100_000
  220. dpi = 500
  221. w = 5*dpi
  222. h = 6*dpi
  223. # make a Path that spans the whole w-h rectangle
  224. x = np.linspace(0, w, N)
  225. y = np.ones(N) * h
  226. y[::2] = 0
  227. path = Path(np.vstack((x, y)).T)
  228. # effectively disable path simplification (but leaving it "on")
  229. path.simplify_threshold = 0
  230. # setup the minimal GraphicsContext to draw a Path
  231. ra = RendererAgg(w, h, dpi)
  232. gc = ra.new_gc()
  233. gc.set_linewidth(1)
  234. gc.set_foreground('r')
  235. gc.set_hatch('/')
  236. with pytest.raises(OverflowError, match='cannot split hatched path'):
  237. ra.draw_path(gc, path, IdentityTransform())
  238. gc.set_hatch(None)
  239. with pytest.raises(OverflowError, match='cannot split filled path'):
  240. ra.draw_path(gc, path, IdentityTransform(), (1, 0, 0))
  241. # Set to zero to disable, currently defaults to 0, but let's be sure.
  242. with rc_context({'agg.path.chunksize': 0}):
  243. with pytest.raises(OverflowError, match='Please set'):
  244. ra.draw_path(gc, path, IdentityTransform())
  245. # Set big enough that we do not try to chunk.
  246. with rc_context({'agg.path.chunksize': 1_000_000}):
  247. with pytest.raises(OverflowError, match='Please reduce'):
  248. ra.draw_path(gc, path, IdentityTransform())
  249. # Small enough we will try to chunk, but big enough we will fail to render.
  250. with rc_context({'agg.path.chunksize': 90_000}):
  251. with pytest.raises(OverflowError, match='Please reduce'):
  252. ra.draw_path(gc, path, IdentityTransform())
  253. path.should_simplify = False
  254. with pytest.raises(OverflowError, match="should_simplify is False"):
  255. ra.draw_path(gc, path, IdentityTransform())
  256. def test_non_tuple_rgbaface():
  257. # This passes rgbaFace as a ndarray to draw_path.
  258. fig = plt.figure()
  259. fig.add_subplot(projection="3d").scatter(
  260. [0, 1, 2], [0, 1, 2], path_effects=[patheffects.Stroke(linewidth=4)])
  261. fig.canvas.draw()