3
3
==============================
4
4
:Author: Steve Chaplin and others
5
5
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.
8
7
"""
9
8
10
9
import six
11
10
11
+ import copy
12
12
import gzip
13
13
import sys
14
14
import warnings
35
35
"cairo>=1.4.0 is required" .format (cairo .version ))
36
36
backend_version = cairo .version
37
37
38
+ from matplotlib import cbook
38
39
from matplotlib .backend_bases import (
39
40
_Backend , FigureCanvasBase , FigureManagerBase , GraphicsContextBase ,
40
41
RendererBase )
42
+ from matplotlib .font_manager import ttfFontProperty
41
43
from matplotlib .mathtext import MathTextParser
42
44
from matplotlib .path import Path
43
45
from matplotlib .transforms import Affine2D
44
- from matplotlib .font_manager import ttfFontProperty
45
46
46
47
47
48
def _premultiplied_argb32_to_unmultiplied_rgba8888 (buf ):
@@ -79,6 +80,93 @@ def buffer_info(self):
79
80
return (self .__data , self .__size )
80
81
81
82
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
+
82
170
class RendererCairo (RendererBase ):
83
171
fontweights = {
84
172
100 : cairo .FONT_WEIGHT_NORMAL ,
@@ -139,37 +227,20 @@ def _fill_and_stroke(self, ctx, fill_c, alpha, alpha_overrides):
139
227
ctx .stroke ()
140
228
141
229
@staticmethod
230
+ @cbook .deprecated ("3.0" )
142
231
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 )
156
233
157
234
def draw_path (self , gc , path , transform , rgbFace = None ):
158
235
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 )
167
240
transform = (transform
168
- + Affine2D ().scale (1.0 , - 1.0 ).translate (0 , self .height ))
169
-
241
+ + Affine2D ().scale (1 , - 1 ).translate (0 , self .height ))
170
242
ctx .new_path ()
171
- self .convert_path (ctx , path , transform , clip )
172
-
243
+ _append_path (ctx , path , transform , clip )
173
244
self ._fill_and_stroke (
174
245
ctx , rgbFace , gc .get_alpha (), gc .get_forced_alpha ())
175
246
@@ -179,8 +250,7 @@ def draw_markers(self, gc, marker_path, marker_trans, path, transform,
179
250
180
251
ctx .new_path ()
181
252
# 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 ))
184
254
marker_path = ctx .copy_path_flat ()
185
255
186
256
# Figure out whether the path has a fill
@@ -193,7 +263,7 @@ def draw_markers(self, gc, marker_path, marker_trans, path, transform,
193
263
filled = True
194
264
195
265
transform = (transform
196
- + Affine2D ().scale (1.0 , - 1.0 ).translate (0 , self .height ))
266
+ + Affine2D ().scale (1 , - 1 ).translate (0 , self .height ))
197
267
198
268
ctx .new_path ()
199
269
for i , (vertices , codes ) in enumerate (
@@ -221,6 +291,57 @@ def draw_markers(self, gc, marker_path, marker_trans, path, transform,
221
291
self ._fill_and_stroke (
222
292
ctx , rgbFace , gc .get_alpha (), gc .get_forced_alpha ())
223
293
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
+
224
345
def draw_image (self , gc , x , y , im ):
225
346
# bbox - not currently used
226
347
if sys .byteorder == 'little' :
@@ -233,12 +354,12 @@ def draw_image(self, gc, x, y, im):
233
354
# on ctypes to get a pointer to the numpy array. This works
234
355
# correctly on a numpy array in python3 but not 2.7. We replicate
235
356
# the array.array functionality here to get cross version support.
236
- imbuffer = ArrayWrapper (im .flatten ())
357
+ imbuffer = ArrayWrapper (im .ravel ())
237
358
else :
238
- # pycairo uses PyObject_AsWriteBuffer to get a pointer to the
359
+ # py2cairo uses PyObject_AsWriteBuffer to get a pointer to the
239
360
# 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 ()
242
363
surface = cairo .ImageSurface .create_for_data (
243
364
imbuffer , cairo .FORMAT_ARGB32 ,
244
365
im .shape [1 ], im .shape [0 ], im .shape [1 ]* 4 )
@@ -247,7 +368,7 @@ def draw_image(self, gc, x, y, im):
247
368
248
369
ctx .save ()
249
370
ctx .set_source_surface (surface , float (x ), float (y ))
250
- if gc .get_alpha () != 1.0 :
371
+ if gc .get_alpha () != 1 :
251
372
ctx .paint_with_alpha (gc .get_alpha ())
252
373
else :
253
374
ctx .paint ()
@@ -299,7 +420,6 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle):
299
420
ctx .move_to (ox , oy )
300
421
301
422
fontProp = ttfFontProperty (font )
302
- ctx .save ()
303
423
ctx .select_font_face (fontProp .name ,
304
424
self .fontangles [fontProp .style ],
305
425
self .fontweights [fontProp .weight ])
@@ -309,7 +429,6 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle):
309
429
if not six .PY3 and isinstance (s , six .text_type ):
310
430
s = s .encode ("utf-8" )
311
431
ctx .show_text (s )
312
- ctx .restore ()
313
432
314
433
for ox , oy , w , h in rects :
315
434
ctx .new_path ()
@@ -415,7 +534,7 @@ def set_clip_path(self, path):
415
534
ctx .new_path ()
416
535
affine = (affine
417
536
+ Affine2D ().scale (1 , - 1 ).translate (0 , self .renderer .height ))
418
- RendererCairo . convert_path (ctx , tpath , affine )
537
+ _append_path (ctx , tpath , affine )
419
538
ctx .clip ()
420
539
421
540
def set_dashes (self , offset , dashes ):
0 commit comments