8000 BF: MixedModeRenderer scale rasters to "true" bbox · matplotlib/matplotlib@bd3ff10 · GitHub
[go: up one dir, main page]

Skip to content

Commit bd3ff10

Browse files
brunobeltranjklymaktacaswell
committed
BF: MixedModeRenderer scale rasters to "true" bbox
Co-authored-by: Jody Klymak <jklymak@gmail.com> Co-authored-by: Thomas A Caswell <tcaswell@gmail.com>
1 parent e631edb commit bd3ff10

19 files changed

+2499
-1172
lines changed

lib/matplotlib/backends/backend_mixed.py

Lines changed: 96 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import numpy as np
22

33
from matplotlib import cbook
4-
from .backend_agg import RendererAgg
54
from 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

89
class 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+
# " F987 ;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.

lib/matplotlib/backends/backend_pdf.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1980,21 +1980,28 @@ def check_gc(self, gc, fillcolor=None):
19801980
def get_image_magnification(self):
19811981
return self.image_dpi/72.0
19821982

1983-
def draw_image(self, gc, x, y, im, transform=None):
1983+
def option_true_bbox_image(self):
1984+
return True
1985+
1986+
def draw_image(self, gc, x, y, im, transform=None, true_size=None):
19841987
# docstring inherited
19851988

19861989
h, w = im.shape[:2]
19871990
if w == 0 or h == 0:
19881991
return
19891992

1993+
if true_size is not None:
1994+
w, h = true_size
1995+
19901996
if transform is None:
19911997
# If there's no transform, alpha has already been applied
19921998
gc.set_alpha(1.0)
19931999

19942000
self.check_gc(gc)
19952001

1996-
w = 72.0 * w / self.image_dpi
1997-
h = 72.0 * h / self.image_dpi
2002+
if true_size is None:
2003+
w = 72.0 * w / self.image_dpi
2004+
h = 72.0 * h / self.image_dpi
19982005

19992006
imob = self.file.imageObject(im)
20002007

lib/matplotlib/backends/backend_pgf.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -605,13 +605,19 @@ def option_image_nocomposite(self):
605605
# docstring inherited
606606
return not mpl.rcParams['image.composite_image']
607607

608-
def draw_image(self, gc, x, y, im, transform=None):
608+
def option_true_bbox_image(self):
609+
return True
610+
611+
def draw_image(self, gc, x, y, im, transform=None, true_size=None):
609612
# docstring inherited
610613

611614
h, w = im.shape[:2]
612615
if w == 0 or h == 0:
613616
return
614617

618+
if true_size is not None:
619+
w, h = true_size
620+
615621
if not os.path.exists(getattr(self.fh, "name", "")):
616622
raise ValueError(
617623
"streamed pgf-code does not support raster graphics, consider "

lib/matplotlib/backends/backend_ps.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -424,30 +424,41 @@ def _get_clip_cmd(self, gc):
424424
clip.append(f"{custom_clip_cmd}\n")
425425
return "".join(clip)
426426

427+
def option_true_bbox_image(self):
428+
return True
429+
427430
@_log_if_debug_on
428-
def draw_image(self, gc, x, y, im, transform=None):
431+
def draw_image(self, gc, x, y, im, transform=None, true_size=None):
429432
# docstring inherited
430433

431434
h, w = im.shape[:2]
435+
436+
if h == 0 or w == 0:
437+
return
438+
432439
imagecmd = "false 3 colorimage"
433440
data = im[::-1, :, :3] # Vertically flipped rgb values.
434441
hexdata = data.tobytes().hex("\n", -64) # Linewrap to 128 chars.
435442

436443
if transform is None:
437444
matrix = "1 0 0 1 0 0"
438-
xscale = w / self.image_magnification
439-
yscale = h / self.image_magnification
445+
if true_size is None:
446+
xscale = w / self.image_magnification
447+
yscale = h / self.image_magnification
448+
else:
449+
xscale = true_size[0]
450+
yscale = true_size[1]
440451
else:
441-
matrix = " ".join(map(str, transform.frozen().to_values()))
442452
xscale = 1.0
443453
yscale = 1.0
454+
matrix = " ".join(map(str, transform.frozen().to_values()))
444455

445456
self._pswriter.write(f"""\
446457
gsave
447458
{self._get_clip_cmd(gc)}
448-
{x:g} {y:g} translate
459+
{x:.2f} {y:.2f} translate
449460
[{matrix}] concat
450-
{xscale:g} {yscale:g} scale
461+
{xscale:.2f} {yscale:.2f} scale
451462
/DataString {w:d} string def
452463
{w:d} {h:d} 8 [ {w:d} 0 0 -{h:d} 0 {h:d} ]
453464
{{

lib/matplotlib/backends/backend_svg.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -915,10 +915,13 @@ def option_scale_image(self):
915915
# docstring inherited
916916
return True
917917

918+
def option_true_bbox_image(self):
919+
return True
920+
918921
def get_image_magnification(self):
919922
return self.image_dpi / 72.0
920923

921-
def draw_image(self, gc, x, y, im, transform=None):
924+
def draw_image(self, gc, x, y, im, transform=None, true_size=None):
922925
# docstring inherited
923926

924927
h, w = im.shape[:2]
@@ -960,12 +963,28 @@ def draw_image(self, gc, x, y, im, transform=None):
960963
w = 72.0 * w / self.image_dpi
961964
h = 72.0 * h / self.image_dpi
962965

966+
if true_size is not None:
967+
width, height = true_size
968+
# because rasterization happens only for integer pixels, the
969+
# round-trip width w = # int(width/72*image_dpi)*72/image_dpi
970+
# need not match the "real" width
971+
scale_x = width/w
972+
scale_y = height/h
973+
real_h = height
974+
else:
975+
scale_x = 1
976+
scale_y = 1
977+
real_h = h
978+
963979
self.writer.element(
964980
'image',
965981
transform=_generate_transform([
966-
('scale', (1, -1)), ('translate', (0, -h))]),
982+
('translate',
983+
(x*(1 - scale_x), y*(1 - scale_y) + real_h)),
984+
('scale', (scale_x, -scale_y))
985+
]),
967986
x=_short_float_fmt(x),
968-
y=_short_float_fmt(-(self.height - y - h)),
987+
y=_short_float_fmt(-(self.height - y - real_h)),
969988
width=_short_float_fmt(w), height=_short_float_fmt(h),
970989
attrib=attrib)
971990
else:
Binary file not shown.

0 commit comments

Comments
 (0)
0