From 3edc9827f641d08cac67ce24f0bc94dbb4ca5025 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 21 Jun 2017 21:01:55 -0700 Subject: [PATCH 1/9] cairo: Don't copy the ravelled image. flatten() always makes a copy, whereas ravel() does not. --- lib/matplotlib/backends/backend_cairo.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index 3836281ad1c8..c40c1a681df5 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -233,12 +233,12 @@ def draw_image(self, gc, x, y, im): # on ctypes to get a pointer to the numpy array. This works # correctly on a numpy array in python3 but not 2.7. We replicate # the array.array functionality here to get cross version support. - imbuffer = ArrayWrapper(im.flatten()) + imbuffer = ArrayWrapper(im.ravel()) else: - # pycairo uses PyObject_AsWriteBuffer to get a pointer to the + # py2cairo uses PyObject_AsWriteBuffer to get a pointer to the # numpy array; this works correctly on a regular numpy array but - # not on a py2 memoryview. - imbuffer = im.flatten() + # not on a memory view. + imbuffer = im.ravel() surface = cairo.ImageSurface.create_for_data( imbuffer, cairo.FORMAT_ARGB32, im.shape[1], im.shape[0], im.shape[1]*4) From b549d12b7f54b9849a2bc043a450741fe68b9be0 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 21 Jun 2017 22:22:07 -0700 Subject: [PATCH 2/9] cairo: save a pair of ctx.save/ctx.restore. The removed pair of ctx.save and ctx.restore was clearly unnecessary (the outer one is still there, and the font is reset at each loop iteration). --- lib/matplotlib/backends/backend_cairo.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index c40c1a681df5..6b60bc344eff 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -299,7 +299,6 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle): ctx.move_to(ox, oy) fontProp = ttfFontProperty(font) - ctx.save() ctx.select_font_face(fontProp.name, self.fontangles[fontProp.style], self.fontweights[fontProp.weight]) @@ -309,7 +308,6 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle): if not six.PY3 and isinstance(s, six.text_type): s = s.encode("utf-8") ctx.show_text(s) - ctx.restore() for ox, oy, w, h in rects: ctx.new_path() From 8a07eefa7dcd0360a873f87d37819376f4b4c35b Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 21 Jun 2017 14:32:42 -0700 Subject: [PATCH 3/9] Faster path drawing with cairocffi. Improves the performance of mplot3d/wire3d_animation on the gtk3cairo backend from ~8.3fps to ~10.5fps (as a comparison, gtk3agg is at ~16.2fps). --- lib/matplotlib/backends/backend_cairo.py | 125 +++++++++++++++++------ 1 file changed, 92 insertions(+), 33 deletions(-) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index 6b60bc344eff..328c61eaffa8 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -79,6 +79,87 @@ def buffer_info(self): return (self.__data, self.__size) +# Mapping from Matplotlib Path codes to cairo path codes. +_MPL_TO_CAIRO_PATH_TYPE = np.zeros(80, dtype=int) # CLOSEPOLY = 79. +_MPL_TO_CAIRO_PATH_TYPE[Path.MOVETO] = cairo.PATH_MOVE_TO +_MPL_TO_CAIRO_PATH_TYPE[Path.LINETO] = cairo.PATH_LINE_TO +_MPL_TO_CAIRO_PATH_TYPE[Path.CURVE4] = cairo.PATH_CURVE_TO +_MPL_TO_CAIRO_PATH_TYPE[Path.CLOSEPOLY] = cairo.PATH_CLOSE_PATH +# Sizes in cairo_path_data_t of each cairo path element. +_CAIRO_PATH_TYPE_SIZES = np.zeros(4, dtype=int) +_CAIRO_PATH_TYPE_SIZES[cairo.PATH_MOVE_TO] = 2 +_CAIRO_PATH_TYPE_SIZES[cairo.PATH_LINE_TO] = 2 +_CAIRO_PATH_TYPE_SIZES[cairo.PATH_CURVE_TO] = 4 +_CAIRO_PATH_TYPE_SIZES[cairo.PATH_CLOSE_PATH] = 1 + + +def _convert_path(ctx, path, transform, clip=None): + if HAS_CAIRO_CFFI: + try: + return _convert_path_fast(ctx, path, transform, clip) + except NotImplementedError: + pass + return _convert_path_slow(ctx, path, transform, clip) + + +def _convert_path_slow(ctx, path, transform, clip=None): + for points, code in path.iter_segments(transform, clip=clip): + if code == Path.MOVETO: + ctx.move_to(*points) + elif code == Path.CLOSEPOLY: + ctx.close_path() + elif code == Path.LINETO: + ctx.line_to(*points) + elif code == Path.CURVE3: + ctx.curve_to(points[0], points[1], + points[0], points[1], + points[2], points[3]) + elif code == Path.CURVE4: + ctx.curve_to(*points) + + +def _convert_path_fast(ctx, path, transform, clip=None): + ffi = cairo.ffi + cleaned = path.cleaned(transform=transform, clip=clip) + vertices = cleaned.vertices + codes = cleaned.codes + + # TODO: Implement Bezier degree elevation formula. Note that the "slow" + # implementation is, in fact, also incorrect... + if np.any(codes == Path.CURVE3): + raise NotImplementedError("Quadratic Bezier curves are not supported") + # Remove unused vertices and convert to cairo codes. Note that unlike + # cairo_close_path, we do not explicitly insert an extraneous MOVE_TO after + # CLOSE_PATH, so our resulting buffer may be smaller. + if codes[-1] == Path.STOP: + codes = codes[:-1] + vertices = vertices[:-1] + vertices = vertices[codes != Path.CLOSEPOLY] + codes = _MPL_TO_CAIRO_PATH_TYPE[codes] + # Where are the headers of each cairo portions? + cairo_type_sizes = _CAIRO_PATH_TYPE_SIZES[codes] + cairo_type_positions = np.insert(np.cumsum(cairo_type_sizes), 0, 0) + cairo_num_data = cairo_type_positions[-1] + cairo_type_positions = cairo_type_positions[:-1] + + # Fill the buffer. + buf = np.empty(cairo_num_data * 16, np.uint8) + as_int = np.frombuffer(buf.data, np.int32) + as_float = np.frombuffer(buf.data, np.float64) + mask = np.ones_like(as_float, bool) + as_int[::4][cairo_type_positions] = codes + as_int[1::4][cairo_type_positions] = cairo_type_sizes + mask[::2][cairo_type_positions] = mask[1::2][cairo_type_positions] = False + as_float[mask] = vertices.ravel() + + # Construct the cairo_path_t, and pass it to the context. + ptr = ffi.new("cairo_path_t *") + ptr.status = cairo.STATUS_SUCCESS + ptr.data = ffi.cast("cairo_path_data_t *", ffi.from_buffer(buf)) + ptr.num_data = cairo_num_data + cairo.cairo.cairo_append_path(ctx._pointer, ptr) + + class RendererCairo(RendererBase): fontweights = { 100 : cairo.FONT_WEIGHT_NORMAL, @@ -138,38 +219,16 @@ def _fill_and_stroke(self, ctx, fill_c, alpha, alpha_overrides): ctx.restore() ctx.stroke() - @staticmethod - def convert_path(ctx, path, transform, clip=None): - for points, code in path.iter_segments(transform, clip=clip): - if code == Path.MOVETO: - ctx.move_to(*points) - elif code == Path.CLOSEPOLY: - ctx.close_path() - elif code == Path.LINETO: - ctx.line_to(*points) - elif code == Path.CURVE3: - ctx.curve_to(points[0], points[1], - points[0], points[1], - points[2], points[3]) - elif code == Path.CURVE4: - ctx.curve_to(*points) - def draw_path(self, gc, path, transform, rgbFace=None): ctx = gc.ctx - - # We'll clip the path to the actual rendering extents - # if the path isn't filled. - if rgbFace is None and gc.get_hatch() is None: - clip = ctx.clip_extents() - else: - clip = None - + # Clip the path to the actual rendering extents if it isn't filled. + clip = (ctx.clip_extents() + if rgbFace is None and gc.get_hatch() is None + else None) transform = (transform - + Affine2D().scale(1.0, -1.0).translate(0, self.height)) - + + Affine2D().scale(1, -1).translate(0, self.height)) ctx.new_path() - self.convert_path(ctx, path, transform, clip) - + _convert_path(ctx, path, transform, clip) self._fill_and_stroke( ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) @@ -179,8 +238,8 @@ def draw_markers(self, gc, marker_path, marker_trans, path, transform, ctx.new_path() # Create the path for the marker; it needs to be flipped here already! - self.convert_path( - ctx, marker_path, marker_trans + Affine2D().scale(1.0, -1.0)) + _convert_path( + ctx, marker_path, marker_trans + Affine2D().scale(1, -1)) marker_path = ctx.copy_path_flat() # Figure out whether the path has a fill @@ -193,7 +252,7 @@ def draw_markers(self, gc, marker_path, marker_trans, path, transform, filled = True transform = (transform - + Affine2D().scale(1.0, -1.0).translate(0, self.height)) + + Affine2D().scale(1, -1).translate(0, self.height)) ctx.new_path() for i, (vertices, codes) in enumerate( @@ -247,7 +306,7 @@ def draw_image(self, gc, x, y, im): ctx.save() ctx.set_source_surface(surface, float(x), float(y)) - if gc.get_alpha() != 1.0: + if gc.get_alpha() != 1: ctx.paint_with_alpha(gc.get_alpha()) else: ctx.paint() @@ -413,7 +472,7 @@ def set_clip_path(self, path): ctx.new_path() affine = (affine + Affine2D().scale(1, -1).translate(0, self.renderer.height)) - RendererCairo.convert_path(ctx, tpath, affine) + _convert_path(ctx, tpath, affine) ctx.clip() def set_dashes(self, offset, dashes): From 52ed746afacac0a9b8c5a4ff138476599689f86c Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 21 Jun 2017 15:48:16 -0700 Subject: [PATCH 4/9] Implement draw_path_collection for cairo. Further increase the performance of mplot3d/wire3d_animation on the gtk3cairo backend from ~10.5fps to ~11.6fps (as a comparison, gtk3agg is at ~16.2fps). --- lib/matplotlib/backends/backend_cairo.py | 109 +++++++++++++++++------ 1 file changed, 81 insertions(+), 28 deletions(-) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index 328c61eaffa8..c6296bdd1312 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -9,6 +9,7 @@ import six +import copy import gzip import sys import warnings @@ -94,35 +95,41 @@ def buffer_info(self): def _convert_path(ctx, path, transform, clip=None): + return _convert_paths(ctx, [path], [transform], clip) + + +def _convert_paths(ctx, paths, transforms, clip=None): if HAS_CAIRO_CFFI: try: - return _convert_path_fast(ctx, path, transform, clip) + return _convert_paths_fast(ctx, paths, transforms, clip) except NotImplementedError: pass - return _convert_path_slow(ctx, path, transform, clip) - - -def _convert_path_slow(ctx, path, transform, clip=None): - for points, code in path.iter_segments(transform, clip=clip): - if code == Path.MOVETO: - ctx.move_to(*points) - elif code == Path.CLOSEPOLY: - ctx.close_path() - elif code == Path.LINETO: - ctx.line_to(*points) - elif code == Path.CURVE3: - ctx.curve_to(points[0], points[1], - points[0], points[1], - points[2], points[3]) - elif code == Path.CURVE4: - ctx.curve_to(*points) - - -def _convert_path_fast(ctx, path, transform, clip=None): + return _convert_paths_slow(ctx, paths, transforms, clip) + + +def _convert_paths_slow(ctx, paths, transforms, clip=None): + for path, transform in zip(paths, transforms): + for points, code in path.iter_segments(transform, clip=clip): + if code == Path.MOVETO: + ctx.move_to(*points) + elif code == Path.CLOSEPOLY: + ctx.close_path() + elif code == Path.LINETO: + ctx.line_to(*points) + elif code == Path.CURVE3: + ctx.curve_to(points[0], points[1], + points[0], points[1], + points[2], points[3]) + elif code == Path.CURVE4: + ctx.curve_to(*points) + + +def _convert_paths_fast(ctx, paths, transforms, clip=None): ffi = cairo.ffi - cleaned = path.cleaned(transform=transform, clip=clip) - vertices = cleaned.vertices - codes = cleaned.codes + cleaneds = [path.cleaned(transform=transform, clip=clip) + for path, transform in zip(paths, transforms)] + vertices = np.concatenate([cleaned.vertices for cleaned in cleaneds]) + codes = np.concatenate([cleaned.codes for cleaned in cleaneds]) # TODO: Implement Bezier degree elevation formula. Note that the "slow" # implementation is, in fact, also incorrect... @@ -131,10 +138,8 @@ def _convert_path_fast(ctx, path, transform, clip=None): # Remove unused vertices and convert to cairo codes. Note that unlike # cairo_close_path, we do not explicitly insert an extraneous MOVE_TO after # CLOSE_PATH, so our resulting buffer may be smaller. - if codes[-1] == Path.STOP: - codes = codes[:-1] - vertices = vertices[:-1] - vertices = vertices[codes != Path.CLOSEPOLY] + vertices = vertices[(codes != Path.STOP) & (codes != Path.CLOSEPOLY)] + codes = codes[codes != Path.STOP] codes = _MPL_TO_CAIRO_PATH_TYPE[codes] # Where are the headers of each cairo portions? cairo_type_sizes = _CAIRO_PATH_TYPE_SIZES[codes] @@ -280,6 +285,54 @@ def draw_markers(self, gc, marker_path, marker_trans, path, transform, self._fill_and_stroke( ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) + def draw_path_collection( + self, gc, master_transform, paths, all_transforms, offsets, + offsetTrans, facecolors, edgecolors, linewidths, linestyles, + antialiaseds, urls, offset_position): + + path_ids = [] + for path, transform in self._iter_collection_raw_paths( + master_transform, paths, all_transforms): + path_ids.append((path, Affine2D(transform))) + + reuse_key = None + grouped_draw = [] + + def _draw_paths(): + if not grouped_draw: + return + gc_vars, rgb_fc = reuse_key + gc = copy.copy(gc0) + vars(gc).update(gc_vars) + for k, v in gc_vars.items(): + try: + getattr(gc, "set" + k)(v) + except (AttributeError, TypeError): + pass + gc.ctx.new_path() + paths, transforms = zip(*grouped_draw) + grouped_draw.clear() + _convert_paths(gc.ctx, paths, transforms) + self._fill_and_stroke( + gc.ctx, rgb_fc, gc.get_alpha(), gc.get_forced_alpha()) + + for xo, yo, path_id, gc0, rgb_fc in self._iter_collection( + gc, master_transform, all_transforms, path_ids, offsets, + offsetTrans, facecolors, edgecolors, linewidths, linestyles, + antialiaseds, urls, offset_position): + path, transform = path_id + transform = (Affine2D(transform.get_matrix()).translate(xo, yo) + + Affine2D().scale(1, -1).translate(0, self.height)) + # rgb_fc could be a ndarray, for which equality is elementwise. + new_key = vars(gc0), tuple(rgb_fc) if rgb_fc is not None else None + if new_key == reuse_key: + grouped_draw.append((path, transform)) + else: + _draw_paths() + grouped_draw.append((path, transform)) + reuse_key = new_key + _draw_paths() + def draw_image(self, gc, x, y, im): # bbox - not currently used if sys.byteorder == 'little': From 5312461680bd6826629a7b2670f08c87ac8aec80 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 21 Jun 2017 23:39:55 -0700 Subject: [PATCH 5/9] Document a bit the cairo fast path. --- lib/matplotlib/backends/backend_cairo.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index c6296bdd1312..b5adc55822e3 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -125,22 +125,31 @@ def _convert_paths_slow(ctx, paths, transforms, clip=None): def _convert_paths_fast(ctx, paths, transforms, clip=None): + # We directly convert to the internal representation used by cairo, for + # which ABI compatibility is guaranteed. The layout is for each item is + # --CODE(4)-- -LENGTH(4)- ---------PAD(8)--------- + # ----------X(8)---------- ----------Y(8)---------- + # with the size in bytes in parentheses, and (X, Y) repeated as many times + # as there are points for the current code. ffi = cairo.ffi cleaneds = [path.cleaned(transform=transform, clip=clip) for path, transform in zip(paths, transforms)] vertices = np.concatenate([cleaned.vertices for cleaned in cleaneds]) codes = np.concatenate([cleaned.codes for cleaned in cleaneds]) - # TODO: Implement Bezier degree elevation formula. Note that the "slow" - # implementation is, in fact, also incorrect... + # TODO: Implement Bezier degree elevation formula. For now, fall back to + # the "slow" implementation, though note that that implementation is, in + # fact, also incorrect... if np.any(codes == Path.CURVE3): raise NotImplementedError("Quadratic Bezier curves are not supported") + # Remove unused vertices and convert to cairo codes. Note that unlike # cairo_close_path, we do not explicitly insert an extraneous MOVE_TO after # CLOSE_PATH, so our resulting buffer may be smaller. vertices = vertices[(codes != Path.STOP) & (codes != Path.CLOSEPOLY)] codes = codes[codes != Path.STOP] codes = _MPL_TO_CAIRO_PATH_TYPE[codes] + # Where are the headers of each cairo portions? cairo_type_sizes = _CAIRO_PATH_TYPE_SIZES[codes] cairo_type_positions = np.insert(np.cumsum(cairo_type_sizes), 0, 0) @@ -303,6 +312,7 @@ def _draw_paths(): return gc_vars, rgb_fc = reuse_key gc = copy.copy(gc0) + # We actually need to call the setters to reset the internal state. vars(gc).update(gc_vars) for k, v in gc_vars.items(): try: From 1a2848dc6617d8b71050ec16cb99d840b6173c6e Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 23 Jun 2017 14:06:45 -0700 Subject: [PATCH 6/9] Actually support quadratic Beziers. For the slow code path, implement the degree elevation formula. For the fast code path, the path cleaner was already handling this for us, converting everything to lines. --- lib/matplotlib/backends/backend_cairo.py | 27 ++++++++++-------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index b5adc55822e3..824ad5f8144e 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -99,12 +99,8 @@ def _convert_path(ctx, path, transform, clip=None): def _convert_paths(ctx, paths, transforms, clip=None): - if HAS_CAIRO_CFFI: - try: - return _convert_paths_fast(ctx, paths, transforms, clip) - except NotImplementedError: - pass - return _convert_paths_slow(ctx, paths, transforms, clip) + return (_convert_paths_fast if HAS_CAIRO_CFFI else _convert_paths_slow)( + ctx, paths, transforms, clip) def _convert_paths_slow(ctx, paths, transforms, clip=None): @@ -117,9 +113,10 @@ def _convert_paths_slow(ctx, paths, transforms, clip=None): elif code == Path.LINETO: ctx.line_to(*points) elif code == Path.CURVE3: - ctx.curve_to(points[0], points[1], - points[0], points[1], - points[2], points[3]) + cur = ctx.get_current_point() + ctx.curve_to( + *np.concatenate([cur / 3 + points[:2] * 2 / 3, + points[:2] * 2 / 3 + points[-2:] / 3])) elif code == Path.CURVE4: ctx.curve_to(*points) @@ -132,17 +129,15 @@ def _convert_paths_fast(ctx, paths, transforms, clip=None): # with the size in bytes in parentheses, and (X, Y) repeated as many times # as there are points for the current code. ffi = cairo.ffi - cleaneds = [path.cleaned(transform=transform, clip=clip) + + # Convert curves to segment, so that 1. we don't have to handle + # variable-sized CURVE-n codes, and 2. we don't have to implement degree + # elevation for quadratic Beziers. + cleaneds = [path.cleaned(transform=transform, clip=clip, curves=False) for path, transform in zip(paths, transforms)] vertices = np.concatenate([cleaned.vertices for cleaned in cleaneds]) codes = np.concatenate([cleaned.codes for cleaned in cleaneds]) - # TODO: Implement Bezier degree elevation formula. For now, fall back to - # the "slow" implementation, though note that that implementation is, in - # fact, also incorrect... - if np.any(codes == Path.CURVE3): - raise NotImplementedError("Quadratic Bezier curves are not supported") - # Remove unused vertices and convert to cairo codes. Note that unlike # cairo_close_path, we do not explicitly insert an extraneous MOVE_TO after # CLOSE_PATH, so our resulting buffer may be smaller. From aca804f865fb08df66df0c8d97787d113013d75f Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 3 Feb 2018 05:15:59 -0800 Subject: [PATCH 7/9] Address review comments. --- doc/api/backend_cairo_api.rst | 13 +++++++--- lib/matplotlib/backends/backend_cairo.py | 31 +++++++++++------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/doc/api/backend_cairo_api.rst b/doc/api/backend_cairo_api.rst index 2623270c6781..208b5c41e0d4 100644 --- a/doc/api/backend_cairo_api.rst +++ b/doc/api/backend_cairo_api.rst @@ -2,7 +2,12 @@ :mod:`matplotlib.backends.backend_cairo` ======================================== -.. automodule:: matplotlib.backends.backend_cairo - :members: - :undoc-members: - :show-inheritance: +.. Building the docs requires either adding pycairo/cairocffi as docs build + dependency, or bumping the minimal numpy version to one that supports + MagicMocks (which does define `__index__`) as indices (recent numpys do, but + 1.7.1 doesn't). + +.. .. automodule:: matplotlib.backends.backend_cairo +.. :members: +.. :undoc-members: +.. :show-inheritance: diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index 824ad5f8144e..1a11dc8e1bdd 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -94,15 +94,6 @@ def buffer_info(self): _CAIRO_PATH_TYPE_SIZES[cairo.PATH_CLOSE_PATH] = 1 -def _convert_path(ctx, path, transform, clip=None): - return _convert_paths(ctx, [path], [transform], clip) - - -def _convert_paths(ctx, paths, transforms, clip=None): - return (_convert_paths_fast if HAS_CAIRO_CFFI else _convert_paths_slow)( - ctx, paths, transforms, clip) - - def _convert_paths_slow(ctx, paths, transforms, clip=None): for path, transform in zip(paths, transforms): for points, code in path.iter_segments(transform, clip=clip): @@ -123,7 +114,7 @@ def _convert_paths_slow(ctx, paths, transforms, clip=None): def _convert_paths_fast(ctx, paths, transforms, clip=None): # We directly convert to the internal representation used by cairo, for - # which ABI compatibility is guaranteed. The layout is for each item is + # which ABI compatibility is guaranteed. The layout for each item is # --CODE(4)-- -LENGTH(4)- ---------PAD(8)--------- # ----------X(8)---------- ----------Y(8)---------- # with the size in bytes in parentheses, and (X, Y) repeated as many times @@ -154,10 +145,10 @@ def _convert_paths_fast(ctx, paths, transforms, clip=None): # Fill the buffer. buf = np.empty(cairo_num_data * 16, np.uint8) as_int = np.frombuffer(buf.data, np.int32) - as_float = np.frombuffer(buf.data, np.float64) - mask = np.ones_like(as_float, bool) as_int[::4][cairo_type_positions] = codes as_int[1::4][cairo_type_positions] = cairo_type_sizes + as_float = np.frombuffer(buf.data, np.float64) + mask = np.ones_like(as_float, bool) mask[::2][cairo_type_positions] = mask[1::2][cairo_type_positions] = False as_float[mask] = vertices.ravel() @@ -169,6 +160,13 @@ def _convert_paths_fast(ctx, paths, transforms, clip=None): cairo.cairo.cairo_append_path(ctx._pointer, ptr) +_convert_paths = _convert_paths_fast if HAS_CAIRO_CFFI else _convert_paths_slow + + +def _convert_path(ctx, path, transform, clip=None): + return _convert_paths(ctx, [path], [transform], clip) + + class RendererCairo(RendererBase): fontweights = { 100 : cairo.FONT_WEIGHT_NORMAL, @@ -247,8 +245,7 @@ def draw_markers(self, gc, marker_path, marker_trans, path, transform, ctx.new_path() # Create the path for the marker; it needs to be flipped here already! - _convert_path( - ctx, marker_path, marker_trans + Affine2D().scale(1, -1)) + _convert_path(ctx, marker_path, marker_trans + Affine2D().scale(1, -1)) marker_path = ctx.copy_path_flat() # Figure out whether the path has a fill @@ -312,7 +309,7 @@ def _draw_paths(): for k, v in gc_vars.items(): try: getattr(gc, "set" + k)(v) - except (AttributeError, TypeError): + except (AttributeError, TypeError) as e: pass gc.ctx.new_path() paths, transforms = zip(*grouped_draw) @@ -326,8 +323,8 @@ def _draw_paths(): offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): path, transform = path_id - transform = (Affine2D(transform.get_matrix()).translate(xo, yo) - + Affine2D().scale(1, -1).translate(0, self.height)) + transform = (Affine2D(transform.get_matrix()) + .translate(xo, yo - self.height).scale(1, -1)) # rgb_fc could be a ndarray, for which equality is elementwise. new_key = vars(gc0), tuple(rgb_fc) if rgb_fc is not None else None if new_key == reuse_key: From 7a727a1eda7076daa01dcdcbed77ed0c519d986b Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 6 Feb 2018 09:27:01 +0100 Subject: [PATCH 8/9] Avoid triggering a DeprecationWarning. --- lib/matplotlib/backends/backend_cairo.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index 1a11dc8e1bdd..62b4ce97ea4e 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -307,6 +307,8 @@ def _draw_paths(): # We actually need to call the setters to reset the internal state. vars(gc).update(gc_vars) for k, v in gc_vars.items(): + if k == "_linestyle": # Deprecated, no effect. + continue try: getattr(gc, "set" + k)(v) except (AttributeError, TypeError) as e: From abbcb3e358ce0e0c399c198e90d58193ec894f31 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 6 May 2018 14:16:15 -0700 Subject: [PATCH 9/9] Rebase for mpl3. --- doc/api/backend_cairo_api.rst | 13 ++++------- lib/matplotlib/backends/backend_cairo.py | 29 ++++++++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/doc/api/backend_cairo_api.rst b/doc/api/backend_cairo_api.rst index 208b5c41e0d4..2623270c6781 100644 --- a/doc/api/backend_cairo_api.rst +++ b/doc/api/backend_cairo_api.rst @@ -2,12 +2,7 @@ :mod:`matplotlib.backends.backend_cairo` ======================================== -.. Building the docs requires either adding pycairo/cairocffi as docs build - dependency, or bumping the minimal numpy version to one that supports - MagicMocks (which does define `__index__`) as indices (recent numpys do, but - 1.7.1 doesn't). - -.. .. automodule:: matplotlib.backends.backend_cairo -.. :members: -.. :undoc-members: -.. :show-inheritance: +.. automodule:: matplotlib.backends.backend_cairo + :members: + :undoc-members: + :show-inheritance: diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index 62b4ce97ea4e..189f29671aee 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -3,8 +3,7 @@ ============================== :Author: Steve Chaplin and others -This backend depends on `cairo `_, and either on -cairocffi, or (Python 2 only) on pycairo. +This backend depends on cairocffi or pycairo. """ import six @@ -36,13 +35,14 @@ "cairo>=1.4.0 is required".format(cairo.version)) backend_version = cairo.version +from matplotlib import cbook from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase, RendererBase) +from matplotlib.font_manager import ttfFontProperty from matplotlib.mathtext import MathTextParser from matplotlib.path import Path from matplotlib.transforms import Affine2D -from matplotlib.font_manager import ttfFontProperty def _premultiplied_argb32_to_unmultiplied_rgba8888(buf): @@ -94,7 +94,7 @@ def buffer_info(self): _CAIRO_PATH_TYPE_SIZES[cairo.PATH_CLOSE_PATH] = 1 -def _convert_paths_slow(ctx, paths, transforms, clip=None): +def _append_paths_slow(ctx, paths, transforms, clip=None): for path, transform in zip(paths, transforms): for points, code in path.iter_segments(transform, clip=clip): if code == Path.MOVETO: @@ -112,7 +112,7 @@ def _convert_paths_slow(ctx, paths, transforms, clip=None): ctx.curve_to(*points) -def _convert_paths_fast(ctx, paths, transforms, clip=None): +def _append_paths_fast(ctx, paths, transforms, clip=None): # We directly convert to the internal representation used by cairo, for # which ABI compatibility is guaranteed. The layout for each item is # --CODE(4)-- -LENGTH(4)- ---------PAD(8)--------- @@ -160,11 +160,11 @@ def _convert_paths_fast(ctx, paths, transforms, clip=None): cairo.cairo.cairo_append_path(ctx._pointer, ptr) -_convert_paths = _convert_paths_fast if HAS_CAIRO_CFFI else _convert_paths_slow +_append_paths = _append_paths_fast if HAS_CAIRO_CFFI else _append_paths_slow -def _convert_path(ctx, path, transform, clip=None): - return _convert_paths(ctx, [path], [transform], clip) +def _append_path(ctx, path, transform, clip=None): + return _append_paths(ctx, [path], [transform], clip) class RendererCairo(RendererBase): @@ -226,6 +226,11 @@ def _fill_and_stroke(self, ctx, fill_c, alpha, alpha_overrides): ctx.restore() ctx.stroke() + @staticmethod + @cbook.deprecated("3.0") + def convert_path(ctx, path, transform, clip=None): + _append_path(ctx, path, transform, clip) + def draw_path(self, gc, path, transform, rgbFace=None): ctx = gc.ctx # Clip the path to the actual rendering extents if it isn't filled. @@ -235,7 +240,7 @@ def draw_path(self, gc, path, transform, rgbFace=None): transform = (transform + Affine2D().scale(1, -1).translate(0, self.height)) ctx.new_path() - _convert_path(ctx, path, transform, clip) + _append_path(ctx, path, transform, clip) self._fill_and_stroke( ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) @@ -245,7 +250,7 @@ def draw_markers(self, gc, marker_path, marker_trans, path, transform, ctx.new_path() # Create the path for the marker; it needs to be flipped here already! - _convert_path(ctx, marker_path, marker_trans + Affine2D().scale(1, -1)) + _append_path(ctx, marker_path, marker_trans + Affine2D().scale(1, -1)) marker_path = ctx.copy_path_flat() # Figure out whether the path has a fill @@ -316,7 +321,7 @@ def _draw_paths(): gc.ctx.new_path() paths, transforms = zip(*grouped_draw) grouped_draw.clear() - _convert_paths(gc.ctx, paths, transforms) + _append_paths(gc.ctx, paths, transforms) self._fill_and_stroke( gc.ctx, rgb_fc, gc.get_alpha(), gc.get_forced_alpha()) @@ -529,7 +534,7 @@ def set_clip_path(self, path): ctx.new_path() affine = (affine + Affine2D().scale(1, -1).translate(0, self.renderer.height)) - _convert_path(ctx, tpath, affine) + _append_path(ctx, tpath, affine) ctx.clip() def set_dashes(self, offset, dashes):