-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
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
Changes from all commits
3edc982
b549d12
8a07eef
52ed746
5312461
1a2848d
aca804f
7a727a1
abbcb3e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could be There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()) | ||
|
||
|
@@ -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
|
@@ -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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as above |
||
|
||
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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to worry about draw order and such here? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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': | ||
|
@@ -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): | ||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)..