FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Faster path drawing for the cairo backend (cairocffi only) by anntzer · Pull Request #8787 · matplotlib/matplotlib · GitHub

Repository navigation

197 changes: 158 additions & 39 deletions lib/matplotlib/backends/backend_cairo.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
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
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Not sure why this is different than before?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Could be += to make one line?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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
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
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Also +=?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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

Back | FazBrowse Home | New Git URL