Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 0d71eba

Browse filesBrowse files
authored
Merge pull request #8787 from anntzer/cairo-fast-path
Faster path drawing for the cairo backend (cairocffi only)
2 parents d6ae8a5 + abbcb3e commit 0d71eba
Copy full SHA for 0d71eba

File tree

Expand file treeCollapse file tree

1 file changed

+158
-39
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

1 file changed

+158
-39
lines changed
Open diff view settings
Collapse file

‎lib/matplotlib/backends/backend_cairo.py‎

Copy file name to clipboardExpand all lines: lib/matplotlib/backends/backend_cairo.py
+158-39Lines changed: 158 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
==============================
44
:Author: Steve Chaplin and others
55
6-
This backend depends on `cairo <http://cairographics.org>`_, and either on
7-
cairocffi, or (Python 2 only) on pycairo.
6+
This backend depends on cairocffi or pycairo.
87
"""
98

109
import six
1110

11+
import copy
1212
import gzip
1313
import sys
1414
import warnings
@@ -35,13 +35,14 @@
3535
"cairo>=1.4.0 is required".format(cairo.version))
3636
backend_version = cairo.version
3737

38+
from matplotlib import cbook
3839
from matplotlib.backend_bases import (
3940
_Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase,
4041
RendererBase)
42+
from matplotlib.font_manager import ttfFontProperty
4143
from matplotlib.mathtext import MathTextParser
4244
from matplotlib.path import Path
4345
from matplotlib.transforms import Affine2D
44-
from matplotlib.font_manager import ttfFontProperty
4546

4647

4748
def _premultiplied_argb32_to_unmultiplied_rgba8888(buf):
@@ -79,6 +80,93 @@ def buffer_info(self):
7980
return (self.__data, self.__size)
8081

8182

83+
# Mapping from Matplotlib Path codes to cairo path codes.
84+
_MPL_TO_CAIRO_PATH_TYPE = np.zeros(80, dtype=int) # CLOSEPOLY = 79.
85+
_MPL_TO_CAIRO_PATH_TYPE[Path.MOVETO] = cairo.PATH_MOVE_TO
86+
_MPL_TO_CAIRO_PATH_TYPE[Path.LINETO] = cairo.PATH_LINE_TO
87+
_MPL_TO_CAIRO_PATH_TYPE[Path.CURVE4] = cairo.PATH_CURVE_TO
88+
_MPL_TO_CAIRO_PATH_TYPE[Path.CLOSEPOLY] = cairo.PATH_CLOSE_PATH
89+
# Sizes in cairo_path_data_t of each cairo path element.
90+
_CAIRO_PATH_TYPE_SIZES = np.zeros(4, dtype=int)
91+
_CAIRO_PATH_TYPE_SIZES[cairo.PATH_MOVE_TO] = 2
92+
_CAIRO_PATH_TYPE_SIZES[cairo.PATH_LINE_TO] = 2
93+
_CAIRO_PATH_TYPE_SIZES[cairo.PATH_CURVE_TO] = 4
94+
_CAIRO_PATH_TYPE_SIZES[cairo.PATH_CLOSE_PATH] = 1
95+
96+
97+
def _append_paths_slow(ctx, paths, transforms, clip=None):
98+
for path, transform in zip(paths, transforms):
99+
for points, code in path.iter_segments(transform, clip=clip):
100+
if code == Path.MOVETO:
101+
ctx.move_to(*points)
102+
elif code == Path.CLOSEPOLY:
103+
ctx.close_path()
104+
elif code == Path.LINETO:
105+
ctx.line_to(*points)
106+
elif code == Path.CURVE3:
107+
cur = ctx.get_current_point()
108+
ctx.curve_to(
109+
*np.concatenate([cur / 3 + points[:2] * 2 / 3,
110+
points[:2] * 2 / 3 + points[-2:] / 3]))
111+
elif code == Path.CURVE4:
112+
ctx.curve_to(*points)
113+
114+
115+
def _append_paths_fast(ctx, paths, transforms, clip=None):
116+
# We directly convert to the internal representation used by cairo, for
117+
# which ABI compatibility is guaranteed. The layout for each item is
118+
# --CODE(4)-- -LENGTH(4)- ---------PAD(8)---------
119+
# ----------X(8)---------- ----------Y(8)----------
120+
# with the size in bytes in parentheses, and (X, Y) repeated as many times
121+
# as there are points for the current code.
122+
ffi = cairo.ffi
123+
124+
# Convert curves to segment, so that 1. we don't have to handle
125+
# variable-sized CURVE-n codes, and 2. we don't have to implement degree
126+
# elevation for quadratic Beziers.
127+
cleaneds = [path.cleaned(transform=transform, clip=clip, curves=False)
128+
for path, transform in zip(paths, transforms)]
129+
vertices = np.concatenate([cleaned.vertices for cleaned in cleaneds])
130+
codes = np.concatenate([cleaned.codes for cleaned in cleaneds])
131+
132+
# Remove unused vertices and convert to cairo codes. Note that unlike
133+
# cairo_close_path, we do not explicitly insert an extraneous MOVE_TO after
134+
# CLOSE_PATH, so our resulting buffer may be smaller.
135+
vertices = vertices[(codes != Path.STOP) & (codes != Path.CLOSEPOLY)]
136+
codes = codes[codes != Path.STOP]
137+
codes = _MPL_TO_CAIRO_PATH_TYPE[codes]
138+
139+
# Where are the headers of each cairo portions?
140+
cairo_type_sizes = _CAIRO_PATH_TYPE_SIZES[codes]
141+
cairo_type_positions = np.insert(np.cumsum(cairo_type_sizes), 0, 0)
142+
cairo_num_data = cairo_type_positions[-1]
143+
cairo_type_positions = cairo_type_positions[:-1]
144+
145+
# Fill the buffer.
146+
buf = np.empty(cairo_num_data * 16, np.uint8)
147+
as_int = np.frombuffer(buf.data, np.int32)
148+
as_int[::4][cairo_type_positions] = codes
149+
as_int[1::4][cairo_type_positions] = cairo_type_sizes
150+
as_float = np.frombuffer(buf.data, np.float64)
151+
mask = np.ones_like(as_float, bool)
152+
mask[::2][cairo_type_positions] = mask[1::2][cairo_type_positions] = False
153+
as_float[mask] = vertices.ravel()
154+
155+
# Construct the cairo_path_t, and pass it to the context.
156+
ptr = ffi.new("cairo_path_t *")
157+
ptr.status = cairo.STATUS_SUCCESS
158+
ptr.data = ffi.cast("cairo_path_data_t *", ffi.from_buffer(buf))
159+
ptr.num_data = cairo_num_data
160+
cairo.cairo.cairo_append_path(ctx._pointer, ptr)
161+
162+
163+
_append_paths = _append_paths_fast if HAS_CAIRO_CFFI else _append_paths_slow
164+
165+
166+
def _append_path(ctx, path, transform, clip=None):
167+
return _append_paths(ctx, [path], [transform], clip)
168+
169+
82170
class RendererCairo(RendererBase):
83171
fontweights = {
84172
100 : cairo.FONT_WEIGHT_NORMAL,
@@ -139,37 +227,20 @@ def _fill_and_stroke(self, ctx, fill_c, alpha, alpha_overrides):
139227
ctx.stroke()
140228

141229
@staticmethod
230+
@cbook.deprecated("3.0")
142231
def convert_path(ctx, path, transform, clip=None):
143-
for points, code in path.iter_segments(transform, clip=clip):
144-
if code == Path.MOVETO:
145-
ctx.move_to(*points)
146-
elif code == Path.CLOSEPOLY:
147-
ctx.close_path()
148-
elif code == Path.LINETO:
149-
ctx.line_to(*points)
150-
elif code == Path.CURVE3:
151-
ctx.curve_to(points[0], points[1],
152-
points[0], points[1],
153-
points[2], points[3])
154-
elif code == Path.CURVE4:
155-
ctx.curve_to(*points)
232+
_append_path(ctx, path, transform, clip)
156233

157234
def draw_path(self, gc, path, transform, rgbFace=None):
158235
ctx = gc.ctx
159-
160-
# We'll clip the path to the actual rendering extents
161-
# if the path isn't filled.
162-
if rgbFace is None and gc.get_hatch() is None:
163-
clip = ctx.clip_extents()
164-
else:
165-
clip = None
166-
236+
# Clip the path to the actual rendering extents if it isn't filled.
237+
clip = (ctx.clip_extents()
238+
if rgbFace is None and gc.get_hatch() is None
239+
else None)
167240
transform = (transform
168-
+ Affine2D().scale(1.0, -1.0).translate(0, self.height))
169-
241+
+ Affine2D().scale(1, -1).translate(0, self.height))
170242
ctx.new_path()
171-
self.convert_path(ctx, path, transform, clip)
172-
243+
_append_path(ctx, path, transform, clip)
173244
self._fill_and_stroke(
174245
ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())
175246

@@ -179,8 +250,7 @@ def draw_markers(self, gc, marker_path, marker_trans, path, transform,
179250

180251
ctx.new_path()
181252
# Create the path for the marker; it needs to be flipped here already!
182-
self.convert_path(
183-
ctx, marker_path, marker_trans + Affine2D().scale(1.0, -1.0))
253+
_append_path(ctx, marker_path, marker_trans + Affine2D().scale(1, -1))
184254
marker_path = ctx.copy_path_flat()
185255

186256
# Figure out whether the path has a fill
@@ -193,7 +263,7 @@ def draw_markers(self, gc, marker_path, marker_trans, path, transform,
193263
filled = True
194264

195265
transform = (transform
196-
+ Affine2D().scale(1.0, -1.0).translate(0, self.height))
266+
+ Affine2D().scale(1, -1).translate(0, self.height))
197267

198268
ctx.new_path()
199269
for i, (vertices, codes) in enumerate(
@@ -221,6 +291,57 @@ def draw_markers(self, gc, marker_path, marker_trans, path, transform,
221291
self._fill_and_stroke(
222292
ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())
223293

294+
def draw_path_collection(
295+
self, gc, master_transform, paths, all_transforms, offsets,
296+
offsetTrans, facecolors, edgecolors, linewidths, linestyles,
297+
antialiaseds, urls, offset_position):
298+
299+
path_ids = []
300+
for path, transform in self._iter_collection_raw_paths(
301+
master_transform, paths, all_transforms):
302+
path_ids.append((path, Affine2D(transform)))
303+
304+
reuse_key = None
305+
grouped_draw = []
306+
307+
def _draw_paths():
308+
if not grouped_draw:
309+
return
310+
gc_vars, rgb_fc = reuse_key
311+
gc = copy.copy(gc0)
312+
# We actually need to call the setters to reset the internal state.
313+
vars(gc).update(gc_vars)
314+
for k, v in gc_vars.items():
315+
if k == "_linestyle": # Deprecated, no effect.
316+
continue
317+
try:
318+
getattr(gc, "set" + k)(v)
319+
except (AttributeError, TypeError) as e:
320+
pass
321+
gc.ctx.new_path()
322+
paths, transforms = zip(*grouped_draw)
323+
grouped_draw.clear()
324+
_append_paths(gc.ctx, paths, transforms)
325+
self._fill_and_stroke(
326+
gc.ctx, rgb_fc, gc.get_alpha(), gc.get_forced_alpha())
327+
328+
for xo, yo, path_id, gc0, rgb_fc in self._iter_collection(
329+
gc, master_transform, all_transforms, path_ids, offsets,
330+
offsetTrans, facecolors, edgecolors, linewidths, linestyles,
331+
antialiaseds, urls, offset_position):
332+
path, transform = path_id
333+
transform = (Affine2D(transform.get_matrix())
334+
.translate(xo, yo - self.height).scale(1, -1))
335+
# rgb_fc could be a ndarray, for which equality is elementwise.
336+
new_key = vars(gc0), tuple(rgb_fc) if rgb_fc is not None else None
337+
if new_key == reuse_key:
338+
grouped_draw.append((path, transform))
339+
else:
340+
_draw_paths()
341+
grouped_draw.append((path, transform))
342+
reuse_key = new_key
343+
_draw_paths()
344+
224345
def draw_image(self, gc, x, y, im):
225346
# bbox - not currently used
226347
if sys.byteorder == 'little':
@@ -233,12 +354,12 @@ def draw_image(self, gc, x, y, im):
233354
# on ctypes to get a pointer to the numpy array. This works
234355
# correctly on a numpy array in python3 but not 2.7. We replicate
235356
# the array.array functionality here to get cross version support.
236-
imbuffer = ArrayWrapper(im.flatten())
357+
imbuffer = ArrayWrapper(im.ravel())
237358
else:
238-
# pycairo uses PyObject_AsWriteBuffer to get a pointer to the
359+
# py2cairo uses PyObject_AsWriteBuffer to get a pointer to the
239360
# numpy array; this works correctly on a regular numpy array but
240-
# not on a py2 memoryview.
241-
imbuffer = im.flatten()
361+
# not on a memory view.
362+
imbuffer = im.ravel()
242363
surface = cairo.ImageSurface.create_for_data(
243364
imbuffer, cairo.FORMAT_ARGB32,
244365
im.shape[1], im.shape[0], im.shape[1]*4)
@@ -247,7 +368,7 @@ def draw_image(self, gc, x, y, im):
247368

248369
ctx.save()
249370
ctx.set_source_surface(surface, float(x), float(y))
250-
if gc.get_alpha() != 1.0:
371+
if gc.get_alpha() != 1:
251372
ctx.paint_with_alpha(gc.get_alpha())
252373
else:
253374
ctx.paint()
@@ -299,7 +420,6 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle):
299420
ctx.move_to(ox, oy)
300421

301422
fontProp = ttfFontProperty(font)
302-
ctx.save()
303423
ctx.select_font_face(fontProp.name,
304424
self.fontangles[fontProp.style],
305425
self.fontweights[fontProp.weight])
@@ -309,7 +429,6 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle):
309429
if not six.PY3 and isinstance(s, six.text_type):
310430
s = s.encode("utf-8")
311431
ctx.show_text(s)
312-
ctx.restore()
313432

314433
for ox, oy, w, h in rects:
315434
ctx.new_path()
@@ -415,7 +534,7 @@ def set_clip_path(self, path):
415534
ctx.new_path()
416535
affine = (affine
417536
+ Affine2D().scale(1, -1).translate(0, self.renderer.height))
418-
RendererCairo.convert_path(ctx, tpath, affine)
537+
_append_path(ctx, tpath, affine)
419538
ctx.clip()
420539

421540
def set_dashes(self, offset, dashes):

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.