backend_agg.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. """
  2. An agg_ backend.
  3. .. _agg: http://antigrain.com/
  4. Features that are implemented:
  5. * capstyles and join styles
  6. * dashes
  7. * linewidth
  8. * lines, rectangles, ellipses
  9. * clipping to a rectangle
  10. * output to RGBA and Pillow-supported image formats
  11. * alpha blending
  12. * DPI scaling properly - everything scales properly (dashes, linewidths, etc)
  13. * draw polygon
  14. * freetype2 w/ ft2font
  15. TODO:
  16. * integrate screen dpi w/ ppi and text
  17. """
  18. try:
  19. import threading
  20. except ImportError:
  21. import dummy_threading as threading
  22. try:
  23. from contextlib import nullcontext
  24. except ImportError:
  25. from contextlib import ExitStack as nullcontext # Py 3.6.
  26. from math import radians, cos, sin
  27. import numpy as np
  28. from PIL import Image
  29. import matplotlib as mpl
  30. from matplotlib import cbook
  31. from matplotlib import colors as mcolors
  32. from matplotlib.backend_bases import (
  33. _Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
  34. RendererBase)
  35. from matplotlib.font_manager import findfont, get_font
  36. from matplotlib.ft2font import (LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING,
  37. LOAD_DEFAULT, LOAD_NO_AUTOHINT)
  38. from matplotlib.mathtext import MathTextParser
  39. from matplotlib.path import Path
  40. from matplotlib.transforms import Bbox, BboxBase
  41. from matplotlib.backends._backend_agg import RendererAgg as _RendererAgg
  42. backend_version = 'v2.2'
  43. def get_hinting_flag():
  44. mapping = {
  45. 'default': LOAD_DEFAULT,
  46. 'no_autohint': LOAD_NO_AUTOHINT,
  47. 'force_autohint': LOAD_FORCE_AUTOHINT,
  48. 'no_hinting': LOAD_NO_HINTING,
  49. True: LOAD_FORCE_AUTOHINT,
  50. False: LOAD_NO_HINTING,
  51. 'either': LOAD_DEFAULT,
  52. 'native': LOAD_NO_AUTOHINT,
  53. 'auto': LOAD_FORCE_AUTOHINT,
  54. 'none': LOAD_NO_HINTING,
  55. }
  56. return mapping[mpl.rcParams['text.hinting']]
  57. class RendererAgg(RendererBase):
  58. """
  59. The renderer handles all the drawing primitives using a graphics
  60. context instance that controls the colors/styles
  61. """
  62. # we want to cache the fonts at the class level so that when
  63. # multiple figures are created we can reuse them. This helps with
  64. # a bug on windows where the creation of too many figures leads to
  65. # too many open file handles. However, storing them at the class
  66. # level is not thread safe. The solution here is to let the
  67. # FigureCanvas acquire a lock on the fontd at the start of the
  68. # draw, and release it when it is done. This allows multiple
  69. # renderers to share the cached fonts, but only one figure can
  70. # draw at time and so the font cache is used by only one
  71. # renderer at a time.
  72. lock = threading.RLock()
  73. def __init__(self, width, height, dpi):
  74. RendererBase.__init__(self)
  75. self.dpi = dpi
  76. self.width = width
  77. self.height = height
  78. self._renderer = _RendererAgg(int(width), int(height), dpi)
  79. self._filter_renderers = []
  80. self._update_methods()
  81. self.mathtext_parser = MathTextParser('Agg')
  82. self.bbox = Bbox.from_bounds(0, 0, self.width, self.height)
  83. def __getstate__(self):
  84. # We only want to preserve the init keywords of the Renderer.
  85. # Anything else can be re-created.
  86. return {'width': self.width, 'height': self.height, 'dpi': self.dpi}
  87. def __setstate__(self, state):
  88. self.__init__(state['width'], state['height'], state['dpi'])
  89. def _update_methods(self):
  90. self.draw_gouraud_triangle = self._renderer.draw_gouraud_triangle
  91. self.draw_gouraud_triangles = self._renderer.draw_gouraud_triangles
  92. self.draw_image = self._renderer.draw_image
  93. self.draw_markers = self._renderer.draw_markers
  94. # This is its own method for the duration of the deprecation of
  95. # offset_position = "data".
  96. # self.draw_path_collection = self._renderer.draw_path_collection
  97. self.draw_quad_mesh = self._renderer.draw_quad_mesh
  98. self.copy_from_bbox = self._renderer.copy_from_bbox
  99. self.get_content_extents = self._renderer.get_content_extents
  100. def tostring_rgba_minimized(self):
  101. extents = self.get_content_extents()
  102. bbox = [[extents[0], self.height - (extents[1] + extents[3])],
  103. [extents[0] + extents[2], self.height - extents[1]]]
  104. region = self.copy_from_bbox(bbox)
  105. return np.array(region), extents
  106. def draw_path(self, gc, path, transform, rgbFace=None):
  107. # docstring inherited
  108. nmax = mpl.rcParams['agg.path.chunksize'] # here at least for testing
  109. npts = path.vertices.shape[0]
  110. if (npts > nmax > 100 and path.should_simplify and
  111. rgbFace is None and gc.get_hatch() is None):
  112. nch = np.ceil(npts / nmax)
  113. chsize = int(np.ceil(npts / nch))
  114. i0 = np.arange(0, npts, chsize)
  115. i1 = np.zeros_like(i0)
  116. i1[:-1] = i0[1:] - 1
  117. i1[-1] = npts
  118. for ii0, ii1 in zip(i0, i1):
  119. v = path.vertices[ii0:ii1, :]
  120. c = path.codes
  121. if c is not None:
  122. c = c[ii0:ii1]
  123. c[0] = Path.MOVETO # move to end of last chunk
  124. p = Path(v, c)
  125. try:
  126. self._renderer.draw_path(gc, p, transform, rgbFace)
  127. except OverflowError as err:
  128. raise OverflowError(
  129. "Exceeded cell block limit (set 'agg.path.chunksize' "
  130. "rcparam)") from err
  131. else:
  132. try:
  133. self._renderer.draw_path(gc, path, transform, rgbFace)
  134. except OverflowError as err:
  135. raise OverflowError("Exceeded cell block limit (set "
  136. "'agg.path.chunksize' rcparam)") from err
  137. def draw_path_collection(self, gc, master_transform, paths, all_transforms,
  138. offsets, offsetTrans, facecolors, edgecolors,
  139. linewidths, linestyles, antialiaseds, urls,
  140. offset_position):
  141. if offset_position == "data":
  142. cbook.warn_deprecated(
  143. "3.3", message="Support for offset_position='data' is "
  144. "deprecated since %(since)s and will be removed %(removal)s.")
  145. return self._renderer.draw_path_collection(
  146. gc, master_transform, paths, all_transforms, offsets, offsetTrans,
  147. facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls,
  148. offset_position)
  149. def draw_mathtext(self, gc, x, y, s, prop, angle):
  150. """Draw mathtext using :mod:`matplotlib.mathtext`."""
  151. ox, oy, width, height, descent, font_image, used_characters = \
  152. self.mathtext_parser.parse(s, self.dpi, prop)
  153. xd = descent * sin(radians(angle))
  154. yd = descent * cos(radians(angle))
  155. x = round(x + ox + xd)
  156. y = round(y - oy + yd)
  157. self._renderer.draw_text_image(font_image, x, y + 1, angle, gc)
  158. def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
  159. # docstring inherited
  160. if ismath:
  161. return self.draw_mathtext(gc, x, y, s, prop, angle)
  162. flags = get_hinting_flag()
  163. font = self._get_agg_font(prop)
  164. if font is None:
  165. return None
  166. # We pass '0' for angle here, since it will be rotated (in raster
  167. # space) in the following call to draw_text_image).
  168. font.set_text(s, 0, flags=flags)
  169. font.draw_glyphs_to_bitmap(
  170. antialiased=mpl.rcParams['text.antialiased'])
  171. d = font.get_descent() / 64.0
  172. # The descent needs to be adjusted for the angle.
  173. xo, yo = font.get_bitmap_offset()
  174. xo /= 64.0
  175. yo /= 64.0
  176. xd = d * sin(radians(angle))
  177. yd = d * cos(radians(angle))
  178. x = round(x + xo + xd)
  179. y = round(y + yo + yd)
  180. self._renderer.draw_text_image(font, x, y + 1, angle, gc)
  181. def get_text_width_height_descent(self, s, prop, ismath):
  182. # docstring inherited
  183. if ismath in ["TeX", "TeX!"]:
  184. if ismath == "TeX!":
  185. cbook._warn_deprecated(
  186. "3.3", message="Support for ismath='TeX!' is deprecated "
  187. "since %(since)s and will be removed %(removal)s; use "
  188. "ismath='TeX' instead.")
  189. # todo: handle props
  190. texmanager = self.get_texmanager()
  191. fontsize = prop.get_size_in_points()
  192. w, h, d = texmanager.get_text_width_height_descent(
  193. s, fontsize, renderer=self)
  194. return w, h, d
  195. if ismath:
  196. ox, oy, width, height, descent, fonts, used_characters = \
  197. self.mathtext_parser.parse(s, self.dpi, prop)
  198. return width, height, descent
  199. flags = get_hinting_flag()
  200. font = self._get_agg_font(prop)
  201. font.set_text(s, 0.0, flags=flags)
  202. w, h = font.get_width_height() # width and height of unrotated string
  203. d = font.get_descent()
  204. w /= 64.0 # convert from subpixels
  205. h /= 64.0
  206. d /= 64.0
  207. return w, h, d
  208. @cbook._delete_parameter("3.2", "ismath")
  209. def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
  210. # docstring inherited
  211. # todo, handle props, angle, origins
  212. size = prop.get_size_in_points()
  213. texmanager = self.get_texmanager()
  214. Z = texmanager.get_grey(s, size, self.dpi)
  215. Z = np.array(Z * 255.0, np.uint8)
  216. w, h, d = self.get_text_width_height_descent(s, prop, ismath="TeX")
  217. xd = d * sin(radians(angle))
  218. yd = d * cos(radians(angle))
  219. x = round(x + xd)
  220. y = round(y + yd)
  221. self._renderer.draw_text_image(Z, x, y, angle, gc)
  222. def get_canvas_width_height(self):
  223. # docstring inherited
  224. return self.width, self.height
  225. def _get_agg_font(self, prop):
  226. """
  227. Get the font for text instance t, caching for efficiency
  228. """
  229. fname = findfont(prop)
  230. font = get_font(fname)
  231. font.clear()
  232. size = prop.get_size_in_points()
  233. font.set_size(size, self.dpi)
  234. return font
  235. def points_to_pixels(self, points):
  236. # docstring inherited
  237. return points * self.dpi / 72
  238. def buffer_rgba(self):
  239. return memoryview(self._renderer)
  240. def tostring_argb(self):
  241. return np.asarray(self._renderer).take([3, 0, 1, 2], axis=2).tobytes()
  242. def tostring_rgb(self):
  243. return np.asarray(self._renderer).take([0, 1, 2], axis=2).tobytes()
  244. def clear(self):
  245. self._renderer.clear()
  246. def option_image_nocomposite(self):
  247. # docstring inherited
  248. # It is generally faster to composite each image directly to
  249. # the Figure, and there's no file size benefit to compositing
  250. # with the Agg backend
  251. return True
  252. def option_scale_image(self):
  253. # docstring inherited
  254. return False
  255. def restore_region(self, region, bbox=None, xy=None):
  256. """
  257. Restore the saved region. If bbox (instance of BboxBase, or
  258. its extents) is given, only the region specified by the bbox
  259. will be restored. *xy* (a pair of floats) optionally
  260. specifies the new position (the LLC of the original region,
  261. not the LLC of the bbox) where the region will be restored.
  262. >>> region = renderer.copy_from_bbox()
  263. >>> x1, y1, x2, y2 = region.get_extents()
  264. >>> renderer.restore_region(region, bbox=(x1+dx, y1, x2, y2),
  265. ... xy=(x1-dx, y1))
  266. """
  267. if bbox is not None or xy is not None:
  268. if bbox is None:
  269. x1, y1, x2, y2 = region.get_extents()
  270. elif isinstance(bbox, BboxBase):
  271. x1, y1, x2, y2 = bbox.extents
  272. else:
  273. x1, y1, x2, y2 = bbox
  274. if xy is None:
  275. ox, oy = x1, y1
  276. else:
  277. ox, oy = xy
  278. # The incoming data is float, but the _renderer type-checking wants
  279. # to see integers.
  280. self._renderer.restore_region(region, int(x1), int(y1),
  281. int(x2), int(y2), int(ox), int(oy))
  282. else:
  283. self._renderer.restore_region(region)
  284. def start_filter(self):
  285. """
  286. Start filtering. It simply create a new canvas (the old one is saved).
  287. """
  288. self._filter_renderers.append(self._renderer)
  289. self._renderer = _RendererAgg(int(self.width), int(self.height),
  290. self.dpi)
  291. self._update_methods()
  292. def stop_filter(self, post_processing):
  293. """
  294. Save the plot in the current canvas as a image and apply
  295. the *post_processing* function.
  296. def post_processing(image, dpi):
  297. # ny, nx, depth = image.shape
  298. # image (numpy array) has RGBA channels and has a depth of 4.
  299. ...
  300. # create a new_image (numpy array of 4 channels, size can be
  301. # different). The resulting image may have offsets from
  302. # lower-left corner of the original image
  303. return new_image, offset_x, offset_y
  304. The saved renderer is restored and the returned image from
  305. post_processing is plotted (using draw_image) on it.
  306. """
  307. width, height = int(self.width), int(self.height)
  308. buffer, (l, b, w, h) = self.tostring_rgba_minimized()
  309. self._renderer = self._filter_renderers.pop()
  310. self._update_methods()
  311. if w > 0 and h > 0:
  312. img = np.frombuffer(buffer, np.uint8)
  313. img, ox, oy = post_processing(img.reshape((h, w, 4)) / 255.,
  314. self.dpi)
  315. gc = self.new_gc()
  316. if img.dtype.kind == 'f':
  317. img = np.asarray(img * 255., np.uint8)
  318. img = img[::-1]
  319. self._renderer.draw_image(gc, l + ox, height - b - h + oy, img)
  320. class FigureCanvasAgg(FigureCanvasBase):
  321. # docstring inherited
  322. def copy_from_bbox(self, bbox):
  323. renderer = self.get_renderer()
  324. return renderer.copy_from_bbox(bbox)
  325. def restore_region(self, region, bbox=None, xy=None):
  326. renderer = self.get_renderer()
  327. return renderer.restore_region(region, bbox, xy)
  328. def draw(self):
  329. # docstring inherited
  330. self.renderer = self.get_renderer(cleared=True)
  331. # Acquire a lock on the shared font cache.
  332. with RendererAgg.lock, \
  333. (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
  334. else nullcontext()):
  335. self.figure.draw(self.renderer)
  336. # A GUI class may be need to update a window using this draw, so
  337. # don't forget to call the superclass.
  338. super().draw()
  339. def get_renderer(self, cleared=False):
  340. w, h = self.figure.bbox.size
  341. key = w, h, self.figure.dpi
  342. reuse_renderer = (hasattr(self, "renderer")
  343. and getattr(self, "_lastKey", None) == key)
  344. if not reuse_renderer:
  345. self.renderer = RendererAgg(w, h, self.figure.dpi)
  346. self._lastKey = key
  347. elif cleared:
  348. self.renderer.clear()
  349. return self.renderer
  350. def tostring_rgb(self):
  351. """
  352. Get the image as RGB `bytes`.
  353. `draw` must be called at least once before this function will work and
  354. to update the renderer for any subsequent changes to the Figure.
  355. """
  356. return self.renderer.tostring_rgb()
  357. def tostring_argb(self):
  358. """
  359. Get the image as ARGB `bytes`.
  360. `draw` must be called at least once before this function will work and
  361. to update the renderer for any subsequent changes to the Figure.
  362. """
  363. return self.renderer.tostring_argb()
  364. def buffer_rgba(self):
  365. """
  366. Get the image as a `memoryview` to the renderer's buffer.
  367. `draw` must be called at least once before this function will work and
  368. to update the renderer for any subsequent changes to the Figure.
  369. """
  370. return self.renderer.buffer_rgba()
  371. @_check_savefig_extra_args
  372. def print_raw(self, filename_or_obj, *args):
  373. FigureCanvasAgg.draw(self)
  374. renderer = self.get_renderer()
  375. with cbook.open_file_cm(filename_or_obj, "wb") as fh:
  376. fh.write(renderer.buffer_rgba())
  377. print_rgba = print_raw
  378. @_check_savefig_extra_args
  379. def print_png(self, filename_or_obj, *args,
  380. metadata=None, pil_kwargs=None):
  381. """
  382. Write the figure to a PNG file.
  383. Parameters
  384. ----------
  385. filename_or_obj : str or path-like or file-like
  386. The file to write to.
  387. metadata : dict, optional
  388. Metadata in the PNG file as key-value pairs of bytes or latin-1
  389. encodable strings.
  390. According to the PNG specification, keys must be shorter than 79
  391. chars.
  392. The `PNG specification`_ defines some common keywords that may be
  393. used as appropriate:
  394. - Title: Short (one line) title or caption for image.
  395. - Author: Name of image's creator.
  396. - Description: Description of image (possibly long).
  397. - Copyright: Copyright notice.
  398. - Creation Time: Time of original image creation
  399. (usually RFC 1123 format).
  400. - Software: Software used to create the image.
  401. - Disclaimer: Legal disclaimer.
  402. - Warning: Warning of nature of content.
  403. - Source: Device used to create the image.
  404. - Comment: Miscellaneous comment;
  405. conversion from other image format.
  406. Other keywords may be invented for other purposes.
  407. If 'Software' is not given, an autogenerated value for Matplotlib
  408. will be used. This can be removed by setting it to *None*.
  409. For more details see the `PNG specification`_.
  410. .. _PNG specification: \
  411. https://www.w3.org/TR/2003/REC-PNG-20031110/#11keywords
  412. pil_kwargs : dict, optional
  413. Keyword arguments passed to `PIL.Image.Image.save`.
  414. If the 'pnginfo' key is present, it completely overrides
  415. *metadata*, including the default 'Software' key.
  416. """
  417. FigureCanvasAgg.draw(self)
  418. mpl.image.imsave(
  419. filename_or_obj, self.buffer_rgba(), format="png", origin="upper",
  420. dpi=self.figure.dpi, metadata=metadata, pil_kwargs=pil_kwargs)
  421. def print_to_buffer(self):
  422. FigureCanvasAgg.draw(self)
  423. renderer = self.get_renderer()
  424. return (bytes(renderer.buffer_rgba()),
  425. (int(renderer.width), int(renderer.height)))
  426. # Note that these methods should typically be called via savefig() and
  427. # print_figure(), and the latter ensures that `self.figure.dpi` already
  428. # matches the dpi kwarg (if any).
  429. @_check_savefig_extra_args(
  430. extra_kwargs=["quality", "optimize", "progressive"])
  431. @cbook._delete_parameter("3.2", "dryrun")
  432. @cbook._delete_parameter("3.3", "quality",
  433. alternative="pil_kwargs={'quality': ...}")
  434. @cbook._delete_parameter("3.3", "optimize",
  435. alternative="pil_kwargs={'optimize': ...}")
  436. @cbook._delete_parameter("3.3", "progressive",
  437. alternative="pil_kwargs={'progressive': ...}")
  438. def print_jpg(self, filename_or_obj, *args, dryrun=False, pil_kwargs=None,
  439. **kwargs):
  440. """
  441. Write the figure to a JPEG file.
  442. Parameters
  443. ----------
  444. filename_or_obj : str or path-like or file-like
  445. The file to write to.
  446. Other Parameters
  447. ----------------
  448. quality : int, default: :rc:`savefig.jpeg_quality`
  449. The image quality, on a scale from 1 (worst) to 95 (best).
  450. Values above 95 should be avoided; 100 disables portions of
  451. the JPEG compression algorithm, and results in large files
  452. with hardly any gain in image quality. This parameter is
  453. deprecated.
  454. optimize : bool, default: False
  455. Whether the encoder should make an extra pass over the image
  456. in order to select optimal encoder settings. This parameter is
  457. deprecated.
  458. progressive : bool, default: False
  459. Whether the image should be stored as a progressive JPEG file.
  460. This parameter is deprecated.
  461. pil_kwargs : dict, optional
  462. Additional keyword arguments that are passed to
  463. `PIL.Image.Image.save` when saving the figure. These take
  464. precedence over *quality*, *optimize* and *progressive*.
  465. """
  466. # Remove transparency by alpha-blending on an assumed white background.
  467. r, g, b, a = mcolors.to_rgba(self.figure.get_facecolor())
  468. try:
  469. self.figure.set_facecolor(a * np.array([r, g, b]) + 1 - a)
  470. FigureCanvasAgg.draw(self)
  471. finally:
  472. self.figure.set_facecolor((r, g, b, a))
  473. if dryrun:
  474. return
  475. if pil_kwargs is None:
  476. pil_kwargs = {}
  477. for k in ["quality", "optimize", "progressive"]:
  478. if k in kwargs:
  479. pil_kwargs.setdefault(k, kwargs.pop(k))
  480. if "quality" not in pil_kwargs:
  481. quality = pil_kwargs["quality"] = \
  482. dict.__getitem__(mpl.rcParams, "savefig.jpeg_quality")
  483. if quality not in [0, 75, 95]: # default qualities.
  484. cbook.warn_deprecated(
  485. "3.3", name="savefig.jpeg_quality", obj_type="rcParam",
  486. addendum="Set the quality using "
  487. "`pil_kwargs={'quality': ...}`; the future default "
  488. "quality will be 75, matching the default of Pillow and "
  489. "libjpeg.")
  490. pil_kwargs.setdefault("dpi", (self.figure.dpi, self.figure.dpi))
  491. # Drop alpha channel now.
  492. return (Image.fromarray(np.asarray(self.buffer_rgba())[..., :3])
  493. .save(filename_or_obj, format='jpeg', **pil_kwargs))
  494. print_jpeg = print_jpg
  495. @_check_savefig_extra_args
  496. @cbook._delete_parameter("3.2", "dryrun")
  497. def print_tif(self, filename_or_obj, *, dryrun=False, pil_kwargs=None):
  498. FigureCanvasAgg.draw(self)
  499. if dryrun:
  500. return
  501. if pil_kwargs is None:
  502. pil_kwargs = {}
  503. pil_kwargs.setdefault("dpi", (self.figure.dpi, self.figure.dpi))
  504. return (Image.fromarray(np.asarray(self.buffer_rgba()))
  505. .save(filename_or_obj, format='tiff', **pil_kwargs))
  506. print_tiff = print_tif
  507. @_Backend.export
  508. class _BackendAgg(_Backend):
  509. FigureCanvas = FigureCanvasAgg
  510. FigureManager = FigureManagerBase