Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0d71eba

Browse 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

File tree

1 file changed

+158
-39
lines changed

1 file changed

+158
-39
lines changed

lib/matplotlib/backends/backend_cairo.py

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