2
2
3
3
from matplotlib .backends .backend_agg import RendererAgg
4
4
from matplotlib .tight_bbox import process_figure_for_rasterizing
5
+ from matplotlib .transforms import Bbox , Affine2D , IdentityTransform
5
6
6
7
7
8
class MixedModeRenderer :
@@ -73,6 +74,56 @@ def __getattr__(self, attr):
73
74
# to the underlying C implementation).
74
75
return getattr (self ._renderer , attr )
75
76
77
+ # need to wrap each drawing function that might be called on the rasterized
78
+ # version of the renderer to save what the "true" bbox is for scaling the
79
+ # output correctly
80
+ # the functions we might want to overwrite are:
81
+ # `draw_path`, `draw_image`, `draw_gouraud_triangle`, `draw_text`,
82
+ # `draw_markers`, `draw_path_collection`, `draw_quad_mesh`
83
+
84
+ def _update_true_bbox (self , bbox , transform = None ):
85
+ """Convert to real units and update"""
86
+ if transform is None :
87
+ transform = IdentityTransform ()
88
+ bbox = bbox .transformed (transform + Affine2D ().scale (
89
+ self ._figdpi / self .dpi ))
90
+ if self ._true_bbox is None :
91
+ self ._true_bbox = bbox
92
+ else :
93
+ self ._true_bbox = Bbox .union ([self ._true_bbox , bbox ])
94
+
95
+ def draw_path_collection (self , gc , master_transform , paths , all_transforms ,
96
+ offsets , offsetTrans , facecolors , edgecolors ,
97
+ linewidths , linestyles , antialiaseds , urls ,
98
+ offset_position ):
99
+ if self ._rasterizing > 0 :
100
+ bbox = Bbox .null ()
101
+ #TODO probably faster to merge all coordinates from path using
102
+ # numpy for large lists of paths, such as the one produced by the
103
+ # test case tests/test_backed_pgf.py:test_mixed_mode
104
+ for path in paths :
105
+ bbox .update_from_path (path , ignore = False )
106
+ self ._update_true_bbox (bbox , master_transform )
107
+ return self ._renderer .draw_path_collection (
108
+ gc , master_transform , paths , all_transforms , offsets ,
109
+ offsetTrans , facecolors , edgecolors , linewidths , linestyles ,
110
+ antialiaseds , urls , offset_position )
111
+
112
+ def draw_quad_mesh (self , gc , master_transform , meshWidth , meshHeight ,
113
+ coordinates , offsets , offsetTrans , facecolors ,
114
+ antialiased , edgecolors ):
115
+ if self ._rasterizing > 0 :
116
+ #TODO should check if this is always Bbox.unit for efficiency
117
+ bbox = Bbox .null ()
118
+ cshape = coordinates .shape
119
+ flat_coords = coordinates .reshape ((cshape [0 ]* cshape [1 ], cshape [2 ]))
120
+ bbox .update_from_data_xy (flat_coords , ignore = True )
121
+ self ._update_true_bbox (bbox , master_transform )
122
+
123
+ return self ._renderer .draw_quad_mesh (
124
+ gc , master_transform , meshWidth , meshHeight , coordinates ,
125
+ offsets , offsetTrans , facecolors , antialiased , edgecolors )
126
+
76
127
def start_rasterizing (self ):
77
128
"""
78
129
Enter "raster" mode. All subsequent drawing commands (until
@@ -88,6 +139,7 @@ def start_rasterizing(self):
88
139
self ._raster_renderer = self ._raster_renderer_class (
89
140
self ._width * self .dpi , self ._height * self .dpi , self .dpi )
90
141
self ._renderer = self ._raster_renderer
142
+ self ._true_bbox = None
91
143
self ._rasterizing += 1
92
144
93
145
def stop_rasterizing (self ):
@@ -105,21 +157,36 @@ def stop_rasterizing(self):
105
157
self ._renderer = self ._vector_renderer
106
158
107
159
height = self ._height * self .dpi
160
+ # these bounds are in pixels, relative to the figure when pixelated
161
+ # at the requested DPI. However, the vectorized backends draw at a
162
+ # fixed DPI of 72, and typically aren't snapped to the
163
+ # requested-DPI pixel grid, so we have to grab the actual bounds to
164
+ # put the image into some other way
108
165
buffer , bounds = self ._raster_renderer .tostring_rgba_minimized ()
109
166
l , b , w , h = bounds
110
167
if w > 0 and h > 0 :
168
+ if self ._true_bbox is None :
169
+ raise NotImplementedError (
170
+ "Something was drawn using a method not wrapped by "
171
+ "MixedModeRenderer." )
111
172
image = np .frombuffer (buffer , dtype = np .uint8 )
112
173
image = image .reshape ((h , w , 4 ))
113
174
image = image [::- 1 ]
175
+
114
176
gc = self ._renderer .new_gc ()
115
177
# TODO: If the mixedmode resolution differs from the figure's
116
178
# dpi, the image must be scaled (dpi->_figdpi). Not all
117
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
118
185
self ._renderer .draw_image (
119
186
gc ,
120
- l * self ._figdpi / self . dpi ,
121
- ( height - b - h ) * self ._figdpi / self . dpi ,
122
- image )
187
+ self ._true_bbox . x0 ,
188
+ self ._true_bbox . y0 ,
189
+ image , bbox = self . _true_bbox )
123
190
self ._raster_renderer = None
124
191
self ._rasterizing = False
125
192
0 commit comments