diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index 3836281ad1c8..189f29671aee 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -3,12 +3,12 @@ ============================== :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 +import copy import gzip import sys import warnings @@ -35,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): @@ -79,6 +80,93 @@ 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 _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: + ctx.move_to(*points) + elif code == Path.CLOSEPOLY: + ctx.close_path() + elif code == Path.LINETO: + ctx.line_to(*points) + elif code == Path.CURVE3: + 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) + + +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)--------- + # ----------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 + + # 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]) + + # 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) + 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_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() + + # 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) + + +_append_paths = _append_paths_fast if HAS_CAIRO_CFFI else _append_paths_slow + + +def _append_path(ctx, path, transform, clip=None): + return _append_paths(ctx, [path], [transform], clip) + + class RendererCairo(RendererBase): fontweights = { 100 : cairo.FONT_WEIGHT_NORMAL, @@ -139,37 +227,20 @@ def _fill_and_stroke(self, ctx, fill_c, alpha, alpha_overrides): ctx.stroke() @staticmethod + @cbook.deprecated("3.0") 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) + _append_path(ctx, path, transform, clip) 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) - + _append_path(ctx, path, transform, clip) self._fill_and_stroke( ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) @@ -179,8 +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! - self.convert_path( - ctx, marker_path, marker_trans + Affine2D().scale(1.0, -1.0)) + _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 @@ -193,7 +263,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( @@ -221,6 +291,57 @@ 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) + # 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: + pass + gc.ctx.new_path() + paths, transforms = zip(*grouped_draw) + grouped_draw.clear() + _append_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 - 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: + 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': @@ -233,12 +354,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) @@ -247,7 +368,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() @@ -299,7 +420,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 +429,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() @@ -415,7 +534,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) + _append_path(ctx, tpath, affine) ctx.clip() def set_dashes(self, offset, dashes):