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
109import six
1110
11+ import copy
1212import gzip
1313import sys
1414import warnings
3535 "cairo>=1.4.0 is required" .format (cairo .version ))
3636backend_version = cairo .version
3737
38+ from matplotlib import cbook
3839from matplotlib .backend_bases import (
3940 _Backend , FigureCanvasBase , FigureManagerBase , GraphicsContextBase ,
4041 RendererBase )
42+ from matplotlib .font_manager import ttfFontProperty
4143from matplotlib .mathtext import MathTextParser
4244from matplotlib .path import Path
4345from matplotlib .transforms import Affine2D
44- from matplotlib .font_manager import ttfFontProperty
4546
4647
4748def _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+
82170class 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