backend_cairo.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. """
  2. A Cairo backend for matplotlib
  3. ==============================
  4. :Author: Steve Chaplin and others
  5. This backend depends on cairocffi or pycairo.
  6. """
  7. import gzip
  8. import numpy as np
  9. try:
  10. import cairo
  11. if cairo.version_info < (1, 11, 0):
  12. # Introduced create_for_data for Py3.
  13. raise ImportError
  14. except ImportError:
  15. try:
  16. import cairocffi as cairo
  17. except ImportError:
  18. raise ImportError(
  19. "cairo backend requires that pycairo>=1.11.0 or cairocffi"
  20. "is installed")
  21. from .. import cbook
  22. from matplotlib.backend_bases import (
  23. _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase,
  24. RendererBase)
  25. from matplotlib.font_manager import ttfFontProperty
  26. from matplotlib.mathtext import MathTextParser
  27. from matplotlib.path import Path
  28. from matplotlib.transforms import Affine2D
  29. backend_version = cairo.version
  30. if cairo.__name__ == "cairocffi":
  31. # Convert a pycairo context to a cairocffi one.
  32. def _to_context(ctx):
  33. if not isinstance(ctx, cairo.Context):
  34. ctx = cairo.Context._from_pointer(
  35. cairo.ffi.cast(
  36. 'cairo_t **',
  37. id(ctx) + object.__basicsize__)[0],
  38. incref=True)
  39. return ctx
  40. else:
  41. # Pass-through a pycairo context.
  42. def _to_context(ctx):
  43. return ctx
  44. def _append_path(ctx, path, transform, clip=None):
  45. for points, code in path.iter_segments(
  46. transform, remove_nans=True, clip=clip):
  47. if code == Path.MOVETO:
  48. ctx.move_to(*points)
  49. elif code == Path.CLOSEPOLY:
  50. ctx.close_path()
  51. elif code == Path.LINETO:
  52. ctx.line_to(*points)
  53. elif code == Path.CURVE3:
  54. cur = np.asarray(ctx.get_current_point())
  55. a = points[:2]
  56. b = points[-2:]
  57. ctx.curve_to(*(cur / 3 + a * 2 / 3), *(a * 2 / 3 + b / 3), *b)
  58. elif code == Path.CURVE4:
  59. ctx.curve_to(*points)
  60. class RendererCairo(RendererBase):
  61. fontweights = {
  62. 100: cairo.FONT_WEIGHT_NORMAL,
  63. 200: cairo.FONT_WEIGHT_NORMAL,
  64. 300: cairo.FONT_WEIGHT_NORMAL,
  65. 400: cairo.FONT_WEIGHT_NORMAL,
  66. 500: cairo.FONT_WEIGHT_NORMAL,
  67. 600: cairo.FONT_WEIGHT_BOLD,
  68. 700: cairo.FONT_WEIGHT_BOLD,
  69. 800: cairo.FONT_WEIGHT_BOLD,
  70. 900: cairo.FONT_WEIGHT_BOLD,
  71. 'ultralight': cairo.FONT_WEIGHT_NORMAL,
  72. 'light': cairo.FONT_WEIGHT_NORMAL,
  73. 'normal': cairo.FONT_WEIGHT_NORMAL,
  74. 'medium': cairo.FONT_WEIGHT_NORMAL,
  75. 'regular': cairo.FONT_WEIGHT_NORMAL,
  76. 'semibold': cairo.FONT_WEIGHT_BOLD,
  77. 'bold': cairo.FONT_WEIGHT_BOLD,
  78. 'heavy': cairo.FONT_WEIGHT_BOLD,
  79. 'ultrabold': cairo.FONT_WEIGHT_BOLD,
  80. 'black': cairo.FONT_WEIGHT_BOLD,
  81. }
  82. fontangles = {
  83. 'italic': cairo.FONT_SLANT_ITALIC,
  84. 'normal': cairo.FONT_SLANT_NORMAL,
  85. 'oblique': cairo.FONT_SLANT_OBLIQUE,
  86. }
  87. def __init__(self, dpi):
  88. self.dpi = dpi
  89. self.gc = GraphicsContextCairo(renderer=self)
  90. self.text_ctx = cairo.Context(
  91. cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1))
  92. self.mathtext_parser = MathTextParser('Cairo')
  93. RendererBase.__init__(self)
  94. def set_ctx_from_surface(self, surface):
  95. self.gc.ctx = cairo.Context(surface)
  96. # Although it may appear natural to automatically call
  97. # `self.set_width_height(surface.get_width(), surface.get_height())`
  98. # here (instead of having the caller do so separately), this would fail
  99. # for PDF/PS/SVG surfaces, which have no way to report their extents.
  100. def set_width_height(self, width, height):
  101. self.width = width
  102. self.height = height
  103. def _fill_and_stroke(self, ctx, fill_c, alpha, alpha_overrides):
  104. if fill_c is not None:
  105. ctx.save()
  106. if len(fill_c) == 3 or alpha_overrides:
  107. ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], alpha)
  108. else:
  109. ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], fill_c[3])
  110. ctx.fill_preserve()
  111. ctx.restore()
  112. ctx.stroke()
  113. def draw_path(self, gc, path, transform, rgbFace=None):
  114. # docstring inherited
  115. ctx = gc.ctx
  116. # Clip the path to the actual rendering extents if it isn't filled.
  117. clip = (ctx.clip_extents()
  118. if rgbFace is None and gc.get_hatch() is None
  119. else None)
  120. transform = (transform
  121. + Affine2D().scale(1, -1).translate(0, self.height))
  122. ctx.new_path()
  123. _append_path(ctx, path, transform, clip)
  124. self._fill_and_stroke(
  125. ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())
  126. def draw_markers(self, gc, marker_path, marker_trans, path, transform,
  127. rgbFace=None):
  128. # docstring inherited
  129. ctx = gc.ctx
  130. ctx.new_path()
  131. # Create the path for the marker; it needs to be flipped here already!
  132. _append_path(ctx, marker_path, marker_trans + Affine2D().scale(1, -1))
  133. marker_path = ctx.copy_path_flat()
  134. # Figure out whether the path has a fill
  135. x1, y1, x2, y2 = ctx.fill_extents()
  136. if x1 == 0 and y1 == 0 and x2 == 0 and y2 == 0:
  137. filled = False
  138. # No fill, just unset this (so we don't try to fill it later on)
  139. rgbFace = None
  140. else:
  141. filled = True
  142. transform = (transform
  143. + Affine2D().scale(1, -1).translate(0, self.height))
  144. ctx.new_path()
  145. for i, (vertices, codes) in enumerate(
  146. path.iter_segments(transform, simplify=False)):
  147. if len(vertices):
  148. x, y = vertices[-2:]
  149. ctx.save()
  150. # Translate and apply path
  151. ctx.translate(x, y)
  152. ctx.append_path(marker_path)
  153. ctx.restore()
  154. # Slower code path if there is a fill; we need to draw
  155. # the fill and stroke for each marker at the same time.
  156. # Also flush out the drawing every once in a while to
  157. # prevent the paths from getting way too long.
  158. if filled or i % 1000 == 0:
  159. self._fill_and_stroke(
  160. ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())
  161. # Fast path, if there is no fill, draw everything in one step
  162. if not filled:
  163. self._fill_and_stroke(
  164. ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())
  165. def draw_image(self, gc, x, y, im):
  166. im = cbook._unmultiplied_rgba8888_to_premultiplied_argb32(im[::-1])
  167. surface = cairo.ImageSurface.create_for_data(
  168. im.ravel().data, cairo.FORMAT_ARGB32,
  169. im.shape[1], im.shape[0], im.shape[1] * 4)
  170. ctx = gc.ctx
  171. y = self.height - y - im.shape[0]
  172. ctx.save()
  173. ctx.set_source_surface(surface, float(x), float(y))
  174. ctx.paint()
  175. ctx.restore()
  176. def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
  177. # docstring inherited
  178. # Note: (x, y) are device/display coords, not user-coords, unlike other
  179. # draw_* methods
  180. if ismath:
  181. self._draw_mathtext(gc, x, y, s, prop, angle)
  182. else:
  183. ctx = gc.ctx
  184. ctx.new_path()
  185. ctx.move_to(x, y)
  186. ctx.select_font_face(prop.get_name(),
  187. self.fontangles[prop.get_style()],
  188. self.fontweights[prop.get_weight()])
  189. size = prop.get_size_in_points() * self.dpi / 72.0
  190. ctx.save()
  191. if angle:
  192. ctx.rotate(np.deg2rad(-angle))
  193. ctx.set_font_size(size)
  194. ctx.show_text(s)
  195. ctx.restore()
  196. def _draw_mathtext(self, gc, x, y, s, prop, angle):
  197. ctx = gc.ctx
  198. width, height, descent, glyphs, rects = self.mathtext_parser.parse(
  199. s, self.dpi, prop)
  200. ctx.save()
  201. ctx.translate(x, y)
  202. if angle:
  203. ctx.rotate(np.deg2rad(-angle))
  204. for font, fontsize, s, ox, oy in glyphs:
  205. ctx.new_path()
  206. ctx.move_to(ox, oy)
  207. fontProp = ttfFontProperty(font)
  208. ctx.select_font_face(fontProp.name,
  209. self.fontangles[fontProp.style],
  210. self.fontweights[fontProp.weight])
  211. size = fontsize * self.dpi / 72.0
  212. ctx.set_font_size(size)
  213. ctx.show_text(s)
  214. for ox, oy, w, h in rects:
  215. ctx.new_path()
  216. ctx.rectangle(ox, oy, w, h)
  217. ctx.set_source_rgb(0, 0, 0)
  218. ctx.fill_preserve()
  219. ctx.restore()
  220. def get_canvas_width_height(self):
  221. # docstring inherited
  222. return self.width, self.height
  223. def get_text_width_height_descent(self, s, prop, ismath):
  224. # docstring inherited
  225. if ismath:
  226. width, height, descent, fonts, used_characters = \
  227. self.mathtext_parser.parse(s, self.dpi, prop)
  228. return width, height, descent
  229. ctx = self.text_ctx
  230. ctx.save()
  231. ctx.select_font_face(prop.get_name(),
  232. self.fontangles[prop.get_style()],
  233. self.fontweights[prop.get_weight()])
  234. # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c
  235. # but if /96.0 is used the font is too small
  236. size = prop.get_size_in_points() * self.dpi / 72
  237. # problem - scale remembers last setting and font can become
  238. # enormous causing program to crash
  239. # save/restore prevents the problem
  240. ctx.set_font_size(size)
  241. y_bearing, w, h = ctx.text_extents(s)[1:4]
  242. ctx.restore()
  243. return w, h, h + y_bearing
  244. def new_gc(self):
  245. # docstring inherited
  246. self.gc.ctx.save()
  247. self.gc._alpha = 1
  248. self.gc._forced_alpha = False # if True, _alpha overrides A from RGBA
  249. return self.gc
  250. def points_to_pixels(self, points):
  251. # docstring inherited
  252. return points / 72 * self.dpi
  253. class GraphicsContextCairo(GraphicsContextBase):
  254. _joind = {
  255. 'bevel': cairo.LINE_JOIN_BEVEL,
  256. 'miter': cairo.LINE_JOIN_MITER,
  257. 'round': cairo.LINE_JOIN_ROUND,
  258. }
  259. _capd = {
  260. 'butt': cairo.LINE_CAP_BUTT,
  261. 'projecting': cairo.LINE_CAP_SQUARE,
  262. 'round': cairo.LINE_CAP_ROUND,
  263. }
  264. def __init__(self, renderer):
  265. GraphicsContextBase.__init__(self)
  266. self.renderer = renderer
  267. def restore(self):
  268. self.ctx.restore()
  269. def set_alpha(self, alpha):
  270. GraphicsContextBase.set_alpha(self, alpha)
  271. _alpha = self.get_alpha()
  272. rgb = self._rgb
  273. if self.get_forced_alpha():
  274. self.ctx.set_source_rgba(rgb[0], rgb[1], rgb[2], _alpha)
  275. else:
  276. self.ctx.set_source_rgba(rgb[0], rgb[1], rgb[2], rgb[3])
  277. # def set_antialiased(self, b):
  278. # cairo has many antialiasing modes, we need to pick one for True and
  279. # one for False.
  280. def set_capstyle(self, cs):
  281. self.ctx.set_line_cap(cbook._check_getitem(self._capd, capstyle=cs))
  282. self._capstyle = cs
  283. def set_clip_rectangle(self, rectangle):
  284. if not rectangle:
  285. return
  286. x, y, w, h = np.round(rectangle.bounds)
  287. ctx = self.ctx
  288. ctx.new_path()
  289. ctx.rectangle(x, self.renderer.height - h - y, w, h)
  290. ctx.clip()
  291. def set_clip_path(self, path):
  292. if not path:
  293. return
  294. tpath, affine = path.get_transformed_path_and_affine()
  295. ctx = self.ctx
  296. ctx.new_path()
  297. affine = (affine
  298. + Affine2D().scale(1, -1).translate(0, self.renderer.height))
  299. _append_path(ctx, tpath, affine)
  300. ctx.clip()
  301. def set_dashes(self, offset, dashes):
  302. self._dashes = offset, dashes
  303. if dashes is None:
  304. self.ctx.set_dash([], 0) # switch dashes off
  305. else:
  306. self.ctx.set_dash(
  307. list(self.renderer.points_to_pixels(np.asarray(dashes))),
  308. offset)
  309. def set_foreground(self, fg, isRGBA=None):
  310. GraphicsContextBase.set_foreground(self, fg, isRGBA)
  311. if len(self._rgb) == 3:
  312. self.ctx.set_source_rgb(*self._rgb)
  313. else:
  314. self.ctx.set_source_rgba(*self._rgb)
  315. def get_rgb(self):
  316. return self.ctx.get_source().get_rgba()[:3]
  317. def set_joinstyle(self, js):
  318. self.ctx.set_line_join(cbook._check_getitem(self._joind, joinstyle=js))
  319. self._joinstyle = js
  320. def set_linewidth(self, w):
  321. self._linewidth = float(w)
  322. self.ctx.set_line_width(self.renderer.points_to_pixels(w))
  323. class FigureCanvasCairo(FigureCanvasBase):
  324. def print_png(self, fobj, *args, **kwargs):
  325. self._get_printed_image_surface().write_to_png(fobj)
  326. def print_rgba(self, fobj, *args, **kwargs):
  327. width, height = self.get_width_height()
  328. buf = self._get_printed_image_surface().get_data()
  329. fobj.write(cbook._premultiplied_argb32_to_unmultiplied_rgba8888(
  330. np.asarray(buf).reshape((width, height, 4))))
  331. print_raw = print_rgba
  332. def _get_printed_image_surface(self):
  333. width, height = self.get_width_height()
  334. renderer = RendererCairo(self.figure.dpi)
  335. renderer.set_width_height(width, height)
  336. surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
  337. renderer.set_ctx_from_surface(surface)
  338. self.figure.draw(renderer)
  339. return surface
  340. def print_pdf(self, fobj, *args, **kwargs):
  341. return self._save(fobj, 'pdf', *args, **kwargs)
  342. def print_ps(self, fobj, *args, **kwargs):
  343. return self._save(fobj, 'ps', *args, **kwargs)
  344. def print_svg(self, fobj, *args, **kwargs):
  345. return self._save(fobj, 'svg', *args, **kwargs)
  346. def print_svgz(self, fobj, *args, **kwargs):
  347. return self._save(fobj, 'svgz', *args, **kwargs)
  348. def _save(self, fo, fmt, **kwargs):
  349. # save PDF/PS/SVG
  350. orientation = kwargs.get('orientation', 'portrait')
  351. dpi = 72
  352. self.figure.dpi = dpi
  353. w_in, h_in = self.figure.get_size_inches()
  354. width_in_points, height_in_points = w_in * dpi, h_in * dpi
  355. if orientation == 'landscape':
  356. width_in_points, height_in_points = (
  357. height_in_points, width_in_points)
  358. if fmt == 'ps':
  359. if not hasattr(cairo, 'PSSurface'):
  360. raise RuntimeError('cairo has not been compiled with PS '
  361. 'support enabled')
  362. surface = cairo.PSSurface(fo, width_in_points, height_in_points)
  363. elif fmt == 'pdf':
  364. if not hasattr(cairo, 'PDFSurface'):
  365. raise RuntimeError('cairo has not been compiled with PDF '
  366. 'support enabled')
  367. surface = cairo.PDFSurface(fo, width_in_points, height_in_points)
  368. elif fmt in ('svg', 'svgz'):
  369. if not hasattr(cairo, 'SVGSurface'):
  370. raise RuntimeError('cairo has not been compiled with SVG '
  371. 'support enabled')
  372. if fmt == 'svgz':
  373. if isinstance(fo, str):
  374. fo = gzip.GzipFile(fo, 'wb')
  375. else:
  376. fo = gzip.GzipFile(None, 'wb', fileobj=fo)
  377. surface = cairo.SVGSurface(fo, width_in_points, height_in_points)
  378. else:
  379. raise ValueError("Unknown format: {!r}".format(fmt))
  380. # surface.set_dpi() can be used
  381. renderer = RendererCairo(self.figure.dpi)
  382. renderer.set_width_height(width_in_points, height_in_points)
  383. renderer.set_ctx_from_surface(surface)
  384. ctx = renderer.gc.ctx
  385. if orientation == 'landscape':
  386. ctx.rotate(np.pi / 2)
  387. ctx.translate(0, -height_in_points)
  388. # Perhaps add an '%%Orientation: Landscape' comment?
  389. self.figure.draw(renderer)
  390. ctx.show_page()
  391. surface.finish()
  392. if fmt == 'svgz':
  393. fo.close()
  394. @_Backend.export
  395. class _BackendCairo(_Backend):
  396. FigureCanvas = FigureCanvasCairo
  397. FigureManager = FigureManagerBase