11import numpy as np
22
33from matplotlib import cbook
4- from .backend_agg import RendererAgg
54from matplotlib ._tight_bbox import process_figure_for_rasterizing
5+ from matplotlib .backends .backend_agg import RendererAgg
6+ from matplotlib .transforms import Bbox , Affine2D , IdentityTransform
67
78
89class MixedModeRenderer :
@@ -68,6 +69,71 @@ def __getattr__(self, attr):
6869 # to the underlying C implementation).
6970 return getattr (self ._renderer , attr )
7071
72+ # need to wrap each drawing function that might be called on the rasterized
73+ # version of the renderer to save what the "true" bbox is for scaling the
74+ # output correctly
75+ # the functions we might want to overwrite are:
76+ # `draw_path`, `draw_image`, `draw_gouraud_triangle`, `draw_text`,
77+ # `draw_markers`, `draw_path_collection`, `draw_quad_mesh`
78+
79+ def _update_true_bbox (self , bbox , transform = None ):
80+ """Convert to real units and update"""
81+ if transform is None :
82+ transform = IdentityTransform ()
83+ bbox = bbox .transformed (transform + Affine2D ().scale (
84+ self ._figdpi / self .dpi ))
85+ if self ._true_bbox is None :
86+ self ._true_bbox = bbox
87+ else :
88+ self ._true_bbox = Bbox .union ([self ._true_bbox , bbox ])
89+
90+ def draw_path (self , gc , path , transform , rgbFace = None ):
91+ if self ._rasterizing > 0 :
92+ bbox = Bbox .null ()
93+ bbox .update_from_path (path , ignore = True )
94+ self ._update_true_bbox (bbox , transform )
95+ return self ._renderer .draw_path (gc , path , transform , rgbFace )
96+
97+ def draw_path_collection (self , gc , master_transform , paths , all_transforms ,
98+ offsets , offsetTrans , facecolors , edgecolors ,
99+ linewidths , linestyles , antialiaseds , urls ,
100+ offset_position ):
101+ if self ._rasterizing > 0 :
102+ bbox = Bbox .null ()
103+ # TODO probably faster to merge all coordinates from path using
104+ # numpy for large lists of paths, such as the one produced by the
105+ # test case tests/test_backed_pgf.py:test_mixed_mode
106+ for path in paths :
107+ bbox .update_from_path (path , ignore = False )
108+ self ._update_true_bbox (bbox , master_transform )
109+ return self ._renderer .draw_path_collection (
110+ gc , master_transform , paths , all_transforms , offsets ,
111+ offsetTrans , facecolors , edgecolors , linewidths , linestyles ,
112+ antialiaseds , urls , offset_position )
113+
114+ def draw_quad_mesh (self , gc , master_transform , meshWidth , meshHeight ,
115+ coordinates , offsets , offsetTrans , facecolors ,
116+ antialiased , edgecolors ):
117+ if self ._rasterizing > 0 :
118+ # TODO should check if this is always Bbox.unit for efficiency
119+ bbox = Bbox .null ()
120+ cshape = coordinates .shape
121+ flat_coords = coordinates .reshape ((cshape [0 ]* cshape [1 ], cshape [2 ]))
122+ bbox .update_from_data_xy (flat_coords , ignore = True )
123+ self ._update_true_bbox (bbox , master_transform )
124+
125+ return self ._renderer .draw_quad_mesh (
126+ gc , master_transform , meshWidth , meshHeight , coordinates ,
127+ offsets , offsetTrans , facecolors , antialiased , edgecolors )
128+
129+ def draw_gouraud_triangle (self , gc , points , colors , transform ):
130+ if self ._rasterizing > 0 :
131+ bbox = Bbox .null ()
132+ bbox .update_from_data_xy (points , ignore = True )
133+ self ._update_true_bbox (bbox , transform )
134+ return self ._renderer .draw_gouraud_triangle (
135+ gc , points , colors , transform )
136+
71137 def start_rasterizing (self ):
72138 """
73139 Enter "raster" mode. All subsequent drawing commands (until
@@ -83,6 +149,7 @@ def start_rasterizing(self):
83149 self ._raster_renderer = self ._raster_renderer_class (
84150 self ._width * self .dpi , self ._height * self .dpi , self .dpi )
85151 self ._renderer = self ._raster_renderer
152+ self ._true_bbox = None
86153
87154 def stop_rasterizing (self ):
88155 """
@@ -92,21 +159,35 @@ def stop_rasterizing(self):
92159 """
93160
94161 self ._renderer = self ._vector_renderer
95-
96162 height = self ._height * self .dpi
97- img = np .asarray (self ._raster_renderer .buffer_rgba ())
98- slice_y , slice_x = cbook ._get_nonzero_slices (img [..., 3 ])
99- cropped_img = img [slice_y , slice_x ]
100- if cropped_img .size :
101- gc = self ._renderer .new_gc ()
102- # TODO: If the mixedmode resolution differs from the figure's
103- # dpi, the image must be scaled (dpi->_figdpi). Not all
104- # backends support this.
105- self ._renderer .draw_image (
106- gc ,
107- slice_x .start * self ._figdpi / self .dpi ,
108- (height - slice_y .stop ) * self ._figdpi / self .dpi ,
109- cropped_img [::- 1 ])
163+ # these bounds are in pixels, relative to the figure when pixelated
164+ # at the requested DPI. However, the vectorized backends draw at a
165+ # fixed DPI of 72, and typically aren't snapped to the
166+ # requested-DPI pixel grid, so we have to grab the actual bounds to
167+ # put the image into some other way
168+ if self ._true_bbox is not None :
169+ # raise NotImplementedError(
170+ # "Something was drawn using a method not wrapped by "
171+ # "MixedModeRenderer.")
172+ img = np .asarray (self ._raster_renderer .buffer_rgba ())
173+ slice_y , slice_x = cbook ._get_nonzero_slices (img [..., 3 ])
174+ cropped_img = img [slice_y , slice_x ]
175+ if cropped_img .size :
176+ gc = self ._renderer .new_gc ()
177+ # TODO: If the mixedmode resolution differs from the figure's
178+ # dpi, the image must be scaled (dpi->_figdpi). Not all
179+ # backends support this.
180+ # because rasterizing will have rounded size to nearest
181+ # pixel, we need to rescale our drawing to fit the original
182+ # intended Bbox. This results in a slightly different DPI than
183+ # requested, but that's better than the drawing not fitting
184+ # into the space requested, see Issue #6827
185+
186+ self ._renderer .draw_image (
187+ gc , self ._true_bbox .x0 , self ._true_bbox .y0 , cropped_img [::- 1 ],
188+ true_size = (self ._true_bbox .width , self ._true_bbox .height )
189+ )
190+
110191 self ._raster_renderer = None
111192
112193 # restore the figure dpi.
0 commit comments