backend_cairo.py 18 KB

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