8000 BUGFIX: use true bbox for rasters in backend_mixed · matplotlib/matplotlib@b8840cf · GitHub
[go: up one dir, main page]

Skip to content

Commit b8840cf

Browse files
committed
BUGFIX: use true bbox for rasters in backend_mixed
1 parent 52e04f5 commit b8840cf

File tree

8 files changed

+123
-18
lines changed

8 files changed

+123
-18
lines changed

lib/matplotlib/backends/backend_cairo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ def draw_markers(self, gc, marker_path, marker_trans, path, transform,
217217
self._fill_and_stroke(
218218
ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())
219219

220-
def draw_image(self, gc, x, y, im):
220+
def draw_image(self, gc, x, y, im, bbox=None):
221221
im = cbook._unmultiplied_rgba8888_to_premultiplied_argb32(im[::-1])
222222
surface = cairo.ImageSurface.create_for_data(
223223
im.ravel().data, cairo.FORMAT_ARGB32,

lib/matplotlib/backends/backend_mixed.py

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from matplotlib.backends.backend_agg import RendererAgg
44
from matplotlib.tight_bbox import process_figure_for_rasterizing
5+
from matplotlib.transforms import Bbox, Affine2D, IdentityTransform
56

67

78
class MixedModeRenderer:
@@ -73,6 +74,56 @@ def __getattr__(self, attr):
7374
# to the underlying C implementation).
7475
return getattr(self._renderer, attr)
7576

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+
76127
def start_rasterizing(self):
77128
"""
78129
Enter "raster" mode. All subsequent drawing commands (until
@@ -88,6 +139,7 @@ def start_rasterizing(self):
88139
self._raster_renderer = self._raster_renderer_class(
89140
self._width*self.dpi, self._height*self.dpi, self.dpi)
90141
self._renderer = self._raster_renderer
142+
self._true_bbox = None
91143
self._rasterizing += 1
92144

93145
def stop_rasterizing(self):
@@ -105,21 +157,36 @@ def stop_rasterizing(self):
105157
self._renderer = self._vector_renderer
106158

107159
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
108165
buffer, bounds = self._raster_renderer.tostring_rgba_minimized()
109166
l, b, w, h = bounds
110167
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.")
111172
image = np.frombuffer(buffer, dtype=np.uint8)
112173
image = image.reshape((h, w, 4))
113174
image = image[::-1]
175+
114176
gc = self._renderer.new_gc()
115177
# TODO: If the mixedmode resolution differs from the figure's
116178
# dpi, the image must be scaled (dpi->_figdpi). Not all
117179
# 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
118185
self._renderer.draw_image(
119186
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)
123190
self._raster_renderer = None
124191
self._rasterizing = False
125192

lib/matplotlib/backends/backend_pdf.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1735,21 +1735,26 @@ def merge_used_characters(self, *args, **kwargs):
17351735
def get_image_magnification(self):
17361736
return self.image_dpi/72.0
17371737

1738-
def draw_image(self, gc, x, y, im, transform=None):
1738+
def draw_image(self, gc, x, y, im, transform=None, bbox=None):
17391739
# docstring inherited
17401740

17411741
h, w = im.shape[:2]
17421742
if w == 0 or h == 0:
17431743
return
17441744

1745+
if bbox is not None:
1746+
h = bbox.height
1747+
w = bbox.width
1748+
17451749
if transform is None:
17461750
# If there's no transform, alpha has already been applied
17471751
gc.set_alpha(1.0)
17481752

17491753
self.check_gc(gc)
17501754

1751-
w = 72.0 * w / self.image_dpi
1752-
h = 72.0 * h / self.image_dpi
1755+
if bbox is None:
1756+
w = 72.0 * w / self.image_dpi
1757+
h = 72.0 * h / self.image_dpi
17531758

17541759
imob = self.file.imageObject(im)
17551760

lib/matplotlib/backends/backend_pgf.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -639,13 +639,17 @@ def option_image_nocomposite(self):
639639
# docstring inherited
640640
return not mpl.rcParams['image.composite_image']
641641

642-
def draw_image(self, gc, x, y, im, transform=None):
642+
def draw_image(self, gc, x, y, im, transform=None, bbox=None):
643643
# docstring inherited
644644

645645
h, w = im.shape[:2]
646646
if w == 0 or h == 0:
647647
return
648648

649+
if bbox is not None:
650+
h = bbox.height
651+
w = bbox.width
652+
649653
if not os.path.exists(getattr(self.fh, "name", "")):
650654
cbook._warn_external(
651655
"streamed pgf-code does not support raster graphics, consider "

lib/matplotlib/backends/backend_ps.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -280,10 +280,15 @@ def get_image_magnification(self):
280280
"""
281281
return self.image_magnification
282282

283-
def draw_image(self, gc, x, y, im, transform=None):
283+
def draw_image(self, gc, x, y, im, transform=None, bbox=None):
284284
# docstring inherited
285285

286286
h, w = im.shape[:2]
287+
if h == 0 or w == 0:
288+
return
289+
if bbox is not None:
290+
h = bbox.height
291+
w = bbox.width
287292
imagecmd = "false 3 colorimage"
288293
data = im[::-1, :, :3] # Vertically flipped rgb values.
289294
# data.tobytes().hex() has no spaces, so can be linewrapped by relying
@@ -292,8 +297,9 @@ def draw_image(self, gc, x, y, im, transform=None):
292297

293298
if transform is None:
294299
matrix = "1 0 0 1 0 0"
295-
xscale = w / self.image_magnification
296-
yscale = h / self.image_magnification
300+
if bbox is None:
301+
xscale = w / self.image_magnification
302+
yscale = h / self.image_magnification
297303
else:
298304
matrix = " ".join(map(str, transform.frozen().to_values()))
299305
xscale = 1.0

lib/matplotlib/backends/backend_svg.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -807,7 +807,7 @@ def option_scale_image(self):
807807
def get_image_magnification(self):
808808
return self.image_dpi / 72.0
809809

810-
def draw_image(self, gc, x, y, im, transform=None):
810+
def draw_image(self, gc, x, y, im, transform=None, bbox=None):
811811
# docstring inherited
812812

813813
h, w = im.shape[:2]
@@ -851,12 +851,27 @@ def draw_image(self, gc, x, y, im, transform=None):
851851
w = 72.0 * w / self.image_dpi
852852
h = 72.0 * h / self.image_dpi
853853

854+
if bbox is not None:
855+
# because rasterization happens only for integer pixels, the
856+
# round-trip width w = # int(width/72*image_dpi)*72/image_dpi
857+
# need not match the "real" width
858+
scale_x = bbox.width/w
859+
scale_y = bbox.height/h
860+
real_h = bbox.height
861+
else:
862+
scale_x = 1
863+
scale_y = 1
864+
real_h = h
865+
854866
self.writer.element(
855867
'image',
856868
transform=generate_transform([
857-
('scale', (1, -1)), ('translate', (0, -h))]),
869+
('translate', (x*(1 - scale_x),
870+
y*(1 - scale_y) + real_h)),
871+
('scale', (scale_x, -scale_y))
872+
]),
858873
x=short_float_fmt(x),
859-
y=short_float_fmt(-(self.height - y - h)),
874+
y=short_float_fmt(-(self.height - y - real_h)),
860875
width=short_float_fmt(w), height=short_float_fmt(h),
861876
attrib=attrib)
862877
else:

lib/matplotlib/backends/backend_wx.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,9 @@ def draw_path(self, gc, path, transform, rgbFace=None):
227227
gfx_ctx.StrokePath(wxpath)
228228
gc.unselect()
229229

230-
def draw_image(self, gc, x, y, im):
231-
bbox = gc.get_clip_rectangle()
230+
def draw_image(self, gc, x, y, im, bbox=None):
231+
if bbox is None:
232+
bbox = gc.get_clip_rectangle()
232233
if bbox is not None:
233234
l, b, w, h = bbox.bounds
234235
else:

lib/matplotlib/image.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,11 @@ def flush_images():
144144
gc = renderer.new_gc()
145145
gc.set_clip_rectangle(parent.bbox)
146146
gc.set_clip_path(parent.get_clip_path())
147-
renderer.draw_image(gc, round(l), round(b), data)
147+
if type(renderer) == mpl.backends.backend_agg.RendererAgg:
148+
renderer.draw_image(gc, l, b, im)
149+
else:
150+
renderer.draw_image(gc, round(l), round(b), data,
151+
bbox=parent.bbox)
148152
gc.restore()
149153
del image_group[:]
150154

@@ -620,7 +624,10 @@ def draw(self, renderer, *args, **kwargs):
620624
im, l, b, trans = self.make_image(
621625
renderer, renderer.get_image_magnification())
622626
if im is not None:
623-
renderer.draw_image(gc, l, b, im)
627+
if type(renderer) == mpl.backends.backend_agg.RendererAgg:
628+
renderer.draw_image(gc, l, b, im)
629+
else:
630+
renderer.draw_image(gc, l, b, im, bbox=self.get_clip_box())
624631
gc.restore()
625632
self.stale = False
626633

0 commit comments

Comments
 (0)
0