Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 9743319

Browse filesBrowse files
committed
BF: MixedModeRenderer scale rasters to "true" bbox
1 parent 52e04f5 commit 9743319
Copy full SHA for 9743319

File tree

5 files changed

+145
-16
lines changed
Filter options

5 files changed

+145
-16
lines changed

‎lib/matplotlib/backends/backend_mixed.py

Copy file name to clipboardExpand all lines: lib/matplotlib/backends/backend_mixed.py
+91-4Lines changed: 91 additions & 4 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,78 @@ 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(self, gc, path, transform, rgbFace=None):
96+
if self._rasterizing > 0:
97+
bbox = Bbox.null()
98+
bbox.update_from_path(path, ignore=True)
99+
self._update_true_bbox(bbox, transform)
100+
return self._renderer.draw_path(gc, path, transform, rgbFace)
101+
102+
def draw_markers(self, gc, marker_path, marker_trans, path,
103+
trans, rgbFace=None):
104+
if self._rasterizing > 0:
105+
raise NotImplementedError("TODO")
106+
return self._renderer.draw_markers(
107+
gc, marker_path, marker_trans, path, trans, rgbFace)
108+
109+
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
110+
offsets, offsetTrans, facecolors, edgecolors,
111+
linewidths, linestyles, antialiaseds, urls,
112+
offset_position):
113+
if self._rasterizing > 0:
114+
bbox = Bbox.null()
115+
#TODO probably faster to merge all coordinates from path using
116+
# numpy for large lists of paths, such as the one produced by the
117+
# test case tests/test_backed_pgf.py:test_mixed_mode
118+
for path in paths:
119+
bbox.update_from_path(path, ignore=False)
120+
self._update_true_bbox(bbox, master_transform)
121+
return self._renderer.draw_path_collection(
122+
gc, master_transform, paths, all_transforms, offsets,
123+
offsetTrans, facecolors, edgecolors, linewidths, linestyles,
124+
antialiaseds, urls, offset_position)
125+
126+
def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight,
127+
coordinates, offsets, offsetTrans, facecolors,
128+
antialiased, edgecolors):
129+
if self._rasterizing > 0:
130+
#TODO should check if this is always Bbox.unit for efficiency
131+
bbox = Bbox.null()
132+
cshape = coordinates.shape
133+
flat_coords = coordinates.reshape((cshape[0]*cshape[1], cshape[2]))
134+
bbox.update_from_data_xy(flat_coords, ignore=True)
135+
self._update_true_bbox(bbox, master_transform)
136+
137+
return self._renderer.draw_quad_mesh(
138+
gc, master_transform, meshWidth, meshHeight, coordinates,
139+
offsets, offsetTrans, facecolors, antialiased, edgecolors)
140+
141+
def draw_gouraud_triangle(self, gc, points, colors, transform):
142+
if self._rasterizing > 0:
143+
bbox = Bbox.null()
144+
bbox.update_from_data_xy(points, ignore=True)
145+
self._update_true_bbox(bbox, transform)
146+
return self._renderer.draw_gouraud_triangle(
147+
gc, points, colors, transform)
148+
76149
def start_rasterizing(self):
77150
"""
78151
Enter "raster" mode. All subsequent drawing commands (until
@@ -88,6 +161,7 @@ def start_rasterizing(self):
88161
self._raster_renderer = self._raster_renderer_class(
89162
self._width*self.dpi, self._height*self.dpi, self.dpi)
90163
self._renderer = self._raster_renderer
164+
self._true_bbox = None
91165
self._rasterizing += 1
92166

93167
def stop_rasterizing(self):
@@ -105,21 +179,34 @@ def stop_rasterizing(self):
105179
self._renderer = self._vector_renderer
106180

107181
height = self._height * self.dpi
182+
# these bounds are in pixels, relative to the figure when pixelated
183+
# at the requested DPI. However, the vectorized backends draw at a
184+
# fixed DPI of 72, and typically aren't snapped to the
185+
# requested-DPI pixel grid, so we have to grab the actual bounds to
186+
# put the image into some other way
108187
buffer, bounds = self._raster_renderer.tostring_rgba_minimized()
109188
l, b, w, h = bounds
110189
if w > 0 and h > 0:
190+
if self._true_bbox is None:
191+
raise NotImplementedError(
192+
"Something was drawn using a method not wrapped by "
193+
"MixedModeRenderer.")
111194
image = np.frombuffer(buffer, dtype=np.uint8)
112195
image = image.reshape((h, w, 4))
113196
image = image[::-1]
197+
114198
gc = self._renderer.new_gc()
115199
# TODO: If the mixedmode resolution differs from the figure's
116200
# dpi, the image must be scaled (dpi->_figdpi). Not all
117201
# backends support this.
202+
# because rasterizing will have rounded size to nearest
203+
# pixel, we need to rescale our drawing to fit the original
204+
# intended Bbox. This results in a slightly different DPI than
205+
# requested, but that's better than the drawing not fitting
206+
# into the space requested, see Issue #6827
118207
self._renderer.draw_image(
119-
gc,
120-
l * self._figdpi / self.dpi,
121-
(height-b-h) * self._figdpi / self.dpi,
122-
image)
208+
gc, self._true_bbox.x0, self._true_bbox.y0, image,
209+
true_size=(self._true_bbox.width, self._true_bbox.height))
123210
self._raster_renderer = None
124211
self._rasterizing = False
125212

‎lib/matplotlib/backends/backend_pdf.py

Copy file name to clipboardExpand all lines: lib/matplotlib/backends/backend_pdf.py
+10-3Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1735,21 +1735,28 @@ 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 option_true_bbox_image(self):
1739+
return True
1740+
1741+
def draw_image(self, gc, x, y, im, transform=None, true_size=None):
17391742
# docstring inherited
17401743

17411744
h, w = im.shape[:2]
17421745
if w == 0 or h == 0:
17431746
return
17441747

1748+
if true_size is not None:
1749+
w, h = true_size
1750+
17451751
if transform is None:
17461752
# If there's no transform, alpha has already been applied
17471753
gc.set_alpha(1.0)
17481754

17491755
self.check_gc(gc)
17501756

1751-
w = 72.0 * w / self.image_dpi
1752-
h = 72.0 * h / self.image_dpi
1757+
if true_size is None:
1758+
w = 72.0 * w / self.image_dpi
1759+
h = 72.0 * h / self.image_dpi
17531760

17541761
imob = self.file.imageObject(im)
17551762

‎lib/matplotlib/backends/backend_pgf.py

Copy file name to clipboardExpand all lines: lib/matplotlib/backends/backend_pgf.py
+7-1Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -639,13 +639,19 @@ 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 option_true_bbox_image(self):
643+
return True
644+
645+
def draw_image(self, gc, x, y, im, transform=None, true_size=None):
643646
# docstring inherited
644647

645648
h, w = im.shape[:2]
646649
if w == 0 or h == 0:
647650
return
648651

652+
if true_size is not None:
653+
w, h = true_size
654+
649655
if not os.path.exists(getattr(self.fh, "name", "")):
650656
cbook._warn_external(
651657
"streamed pgf-code does not support raster graphics, consider "

‎lib/matplotlib/backends/backend_ps.py

Copy file name to clipboardExpand all lines: lib/matplotlib/backends/backend_ps.py
+15-5Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -280,10 +280,18 @@ 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 option_true_bbox_image(self):
284+
return True
285+
286+
def draw_image(self, gc, x, y, im, transform=None, true_size=None):
284287
# docstring inherited
285288

286289
h, w = im.shape[:2]
290+
if h == 0 or w == 0:
291+
return
292+
if true_size is not None:
293+
w, h = true_size
294+
287295
imagecmd = "false 3 colorimage"
288296
data = im[::-1, :, :3] # Vertically flipped rgb values.
289297
# data.tobytes().hex() has no spaces, so can be linewrapped by relying
@@ -292,12 +300,14 @@ def draw_image(self, gc, x, y, im, transform=None):
292300

293301
if transform is None:
294302
matrix = "1 0 0 1 0 0"
295-
xscale = w / self.image_magnification
296-
yscale = h / self.image_magnification
303+
if true_size is None:
304+
xscale = w / self.image_magnification
305+
yscale = h / self.image_magnification
306+
else:
307+
xscale = 1.0
308+
yscale = 1.0
297309
else:
298310
matrix = " ".join(map(str, transform.frozen().to_values()))
299-
xscale = 1.0
300-
yscale = 1.0
301311

302312
bbox = gc.get_clip_rectangle()
303313
clippath, clippath_trans = gc.get_clip_path()

‎lib/matplotlib/backends/backend_svg.py

Copy file name to clipboardExpand all lines: lib/matplotlib/backends/backend_svg.py
+22-3Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -804,10 +804,13 @@ def option_scale_image(self):
804804
# docstring inherited
805805
return True
806806

807+
def option_true_bbox_image(self):
808+
return True
809+
807810
def get_image_magnification(self):
808811
return self.image_dpi / 72.0
809812

810-
def draw_image(self, gc, x, y, im, transform=None):
813+
def draw_image(self, gc, x, y, im, transform=None, true_size=None):
811814
# docstring inherited
812815

813816
h, w = im.shape[:2]
@@ -851,12 +854,28 @@ def draw_image(self, gc, x, y, im, transform=None):
851854
w = 72.0 * w / self.image_dpi
852855
h = 72.0 * h / self.image_dpi
853856

857+
if true_size is not None:
858+
width, height = true_size
859+
# because rasterization happens only for integer pixels, the
860+
# round-trip width w = # int(width/72*image_dpi)*72/image_dpi
861+
# need not match the "real" width
862+
scale_x = width/w
863+
scale_y = height/h
864+
real_h = height
865+
else:
866+
scale_x = 1
867+
scale_y = 1
868+
real_h = h
869+
854870
self.writer.element(
855871
'image',
856872
transform=generate_transform([
857-
('scale', (1, -1)), ('translate', (0, -h))]),
873+
('translate', (x*(1 - scale_x),
874+
y*(1 - scale_y) + real_h)),
875+
('scale', (scale_x, -scale_y))
876+
]),
858877
x=short_float_fmt(x),
859-
y=short_float_fmt(-(self.height - y - h)),
878+
y=short_float_fmt(-(self.height - y - real_h)),
860879
width=short_float_fmt(w), height=short_float_fmt(h),
861880
attrib=attrib)
862881
else:

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.