8000 Faster path drawing for the cairo backend (cairocffi only) by anntzer · Pull Request #8787 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content

Faster path drawing for the cairo backend (cairocffi only) #8787

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 6, 2018
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 158 additions & 39 deletions lib/matplotlib/backends/backend_cairo.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
==============================
:Author: Steve Chaplin and others

This backend depends on `cairo <http://cairographics.org>`_, 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
Expand All @@ -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):
Expand Down Expand Up @@ -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]))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this is different than before?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

previous version was actually incorrect (see basically https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Degree_elevation)..

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,
Expand Down Expand Up @@ -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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be += to make one line?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

transforms are mutable but don't implement iadd (so += actually doesn't modify the transform in place), but I was too lazy to check and want to make clear that we're indeed not mutating the transform.

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())

Expand All @@ -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
EDBE Expand All @@ -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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also +=?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as above


ctx.new_path()
for i, (vertices, codes) in enumerate(
Expand Down Expand Up @@ -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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to worry about draw order and such here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The draw order is maintained, it's just that I batch the draws that use the same gc together.

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':
Expand All @@ -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)
Expand All @@ -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()
Expand Down Expand Up @@ -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])
Expand All @@ -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()
Expand Down Expand Up @@ -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):
Expand Down
0