Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6b2cecc

Browse files
authored
Merge pull request #17159 from samtygier/fix-17149
Merge consecutive rasterizations
2 parents be2e5d7 + 8e9c89a commit 6b2cecc

File tree

7 files changed

+190
-38
lines changed

7 files changed

+190
-38
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Consecutive rasterized draws now merged
2+
---------------------------------------
3+
4+
Tracking of depth of raster draws has moved from
5+
`.backend_mixed.MixedModeRenderer.start_rasterizing` and
6+
`.backend_mixed.MixedModeRenderer.stop_rasterizing` into
7+
`.artist.allow_rasterization`. This means the start and stop functions are
8+
only called when the rasterization actually needs to be started and stopped.
9+
10+
The output of vector backends will change in the case that rasterized
11+
elements are merged. This should not change the appearance of outputs.
12+
13+
The renders in 3rd party backends are now expected to have
14+
``self._raster_depth`` and ``self._rasterizing`` initialized to ``0`` and
15+
``False`` respectively.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Consecutive rasterized draws now merged
2+
---------------------------------------
3+
4+
Elements of a vector output can be individually set to rasterized, using
5+
the ``rasterized`` keyword, or `~.artist.Artist.set_rasterized()`. This can
6+
be useful to reduce file sizes. For figures with multiple raster elements
7+
they are now automatically merged into a smaller number of bitmaps where
8+
this will not effect the visual output. For cases with many elements this
9+
can result in significantly smaller file sizes.
10+
11+
To ensure this happens do not place vector elements between raster ones.
12+
13+
To inhibit this merging set ``Figure.suppressComposite`` to True.

lib/matplotlib/artist.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,17 @@ def allow_rasterization(draw):
3434
def draw_wrapper(artist, renderer, *args, **kwargs):
3535
try:
3636
if artist.get_rasterized():
37-
renderer.start_rasterizing()
37+
if renderer._raster_depth == 0 and not renderer._rasterizing:
38+
renderer.start_rasterizing()
39+
renderer._rasterizing = True
40+
renderer._raster_depth += 1
41+
else:
42+
if renderer._raster_depth == 0 and renderer._rasterizing:
43+
# Only stop when we are not in a rasterized parent
44+
# and something has be rasterized since last stop
45+
renderer.stop_rasterizing()
46+
renderer._rasterizing = False
47+
3848
if artist.get_agg_filter() is not None:
3949
renderer.start_filter()
4050

@@ -43,12 +53,32 @@ def draw_wrapper(artist, renderer, *args, **kwargs):
4353
if artist.get_agg_filter() is not None:
4454
renderer.stop_filter(artist.get_agg_filter())
4555
if artist.get_rasterized():
56+
renderer._raster_depth -= 1
57+
if (renderer._rasterizing and artist.figure and
58+
artist.figure.suppressComposite):
59+
# restart rasterizing to prevent merging
4660
renderer.stop_rasterizing()
61+
renderer.start_rasterizing()
4762

4863
draw_wrapper._supports_rasterization = True
4964
return draw_wrapper
5065

5166

67+
def _finalize_rasterization(draw):
68+
"""
69+
Decorator for Artist.draw method. Needed on the outermost artist, i.e.
70+
Figure, to finish up if the render is still in rasterized mode.
71+
"""
72+
@wraps(draw)
73+
def draw_wrapper(artist, renderer, *args, **kwargs):
74+
result = draw(artist, renderer, *args, **kwargs)
75+
if renderer._rasterizing:
76+
renderer.stop_rasterizing()
77+
renderer._rasterizing = False
78+
return result
79+
return draw_wrapper
80+
81+
5282
def _stale_axes_callback(self, val):
5383
if self.axes:
5484
self.axes.stale = val

lib/matplotlib/backend_bases.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ def __init__(self):
143143
super().__init__()
144144
self._texmanager = None
145145
self._text2path = textpath.TextToPath()
146+
self._raster_depth = 0
147+
self._rasterizing = False
146148

147149
def open_group(self, s, gid=None):
148150
"""

lib/matplotlib/backends/backend_mixed.py

Lines changed: 27 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ def __init__(self, figure, width, height, dpi, vector_renderer,
5252
self._vector_renderer = vector_renderer
5353

5454
self._raster_renderer = None
55-
self._rasterizing = 0
5655

5756
# A reference to the figure is needed as we need to change
5857
# the figure dpi before and after the rasterization. Although
@@ -84,47 +83,40 @@ def start_rasterizing(self):
8483
r = process_figure_for_rasterizing(self.figure,
8584
self._bbox_inches_restore)
8685
self._bbox_inches_restore = r
87-
if self._rasterizing == 0:
88-
self._raster_renderer = self._raster_renderer_class(
89-
self._width*self.dpi, self._height*self.dpi, self.dpi)
90-
self._renderer = self._raster_renderer
91-
self._rasterizing += 1
86+
87+
self._raster_renderer = self._raster_renderer_class(
88+
self._width*self.dpi, self._height*self.dpi, self.dpi)
89+
self._renderer = self._raster_renderer
9290

9391
def stop_rasterizing(self):
9492
"""
9593
Exit "raster" mode. All of the drawing that was done since
9694
the last `start_rasterizing` call will be copied to the
9795
vector backend by calling draw_image.
98-
99-
If `start_rasterizing` has been called multiple times,
100-
`stop_rasterizing` must be called the same number of times before
101-
"raster" mode is exited.
10296
"""
103-
self._rasterizing -= 1
104-
if self._rasterizing == 0:
105-
self._renderer = self._vector_renderer
106-
107-
height = self._height * self.dpi
108-
buffer, bounds = self._raster_renderer.tostring_rgba_minimized()
109-
l, b, w, h = bounds
110-
if w > 0 and h > 0:
111-
image = np.frombuffer(buffer, dtype=np.uint8)
112-
image = image.reshape((h, w, 4))
113-
image = image[::-1]
114-
gc = self._renderer.new_gc()
115-
# TODO: If the mixedmode resolution differs from the figure's
116-
# dpi, the image must be scaled (dpi->_figdpi). Not all
117-
# backends support this.
118-
self._renderer.draw_image(
119-
gc,
120-
l * self._figdpi / self.dpi,
121-
(height-b-h) * self._figdpi / self.dpi,
122-
image)
123-
self._raster_renderer = None
124-
self._rasterizing = False
125-
126-
# restore the figure dpi.
127-
self.figure.set_dpi(self._figdpi)
97+
98+
self._renderer = self._vector_renderer
99+
100+
height = self._height * self.dpi
101+
buffer, bounds = self._raster_renderer.tostring_rgba_minimized()
102+
l, b, w, h = bounds
103+
if w > 0 and h > 0:
104+
image = np.frombuffer(buffer, dtype=np.uint8)
105+
image = image.reshape((h, w, 4))
106+
image = image[::-1]
107+
gc = self._renderer.new_gc()
108+
# TODO: If the mixedmode resolution differs from the figure's
109+
# dpi, the image must be scaled (dpi->_figdpi). Not all
110+
# backends support this.
111+
self._renderer.draw_image(
112+
gc,
113+
l * self._figdpi / self.dpi,
114+
(height-b-h) * self._figdpi / self.dpi,
115+
image)
116+
self._raster_renderer = None
117+
118+
# restore the figure dpi.
119+
self.figure.set_dpi(self._figdpi)
128120

129121
if self._bbox_inches_restore: # when tight bbox is used
130122
r = process_figure_for_rasterizing(self.figure,

lib/matplotlib/figure.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
from matplotlib import __version__ as _mpl_version
2020

2121
import matplotlib.artist as martist
22-
from matplotlib.artist import Artist, allow_rasterization
22+
from matplotlib.artist import (
23+
Artist, allow_rasterization, _finalize_rasterization)
2324
from matplotlib.backend_bases import (
2425
FigureCanvasBase, NonGuiException, MouseButton)
2526
import matplotlib.cbook as cbook
@@ -1814,6 +1815,7 @@ def clear(self, keep_observers=False):
18141815
"""Clear the figure -- synonym for `clf`."""
18151816
self.clf(keep_observers=keep_observers)
18161817

1818+
@_finalize_rasterization
18171819
@allow_rasterization
18181820
def draw(self, renderer):
18191821
# docstring inherited

lib/matplotlib/tests/test_backend_svg.py

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from matplotlib import dviread
1313
from matplotlib.figure import Figure
1414
import matplotlib.pyplot as plt
15-
from matplotlib.testing.decorators import image_comparison
15+
from matplotlib.testing.decorators import image_comparison, check_figures_equal
1616

1717

1818
needs_usetex = pytest.mark.skipif(
@@ -91,6 +91,104 @@ def test_bold_font_output_with_none_fonttype():
9191
ax.set_title('bold-title', fontweight='bold')
9292

9393

94+
@check_figures_equal(tol=20)
95+
def test_rasterized(fig_test, fig_ref):
96+
t = np.arange(0, 100) * (2.3)
97+
x = np.cos(t)
98+
y = np.sin(t)
99+
100+
ax_ref = fig_ref.subplots()
101+
ax_ref.plot(x, y, "-", c="r", lw=10)
102+
ax_ref.plot(x+1, y, "-", c="b", lw=10)
103+
104+
ax_test = fig_test.subplots()
105+
ax_test.plot(x, y, "-", c="r", lw=10, rasterized=True)
106+
ax_test.plot(x+1, y, "-", c="b", lw=10, rasterized=True)
107+
108+
109+
@check_figures_equal()
110+
def test_rasterized_ordering(fig_test, fig_ref):
111+
t = np.arange(0, 100) * (2.3)
112+
x = np.cos(t)
113+
y = np.sin(t)
114+
115+
ax_ref = fig_ref.subplots()
116+
ax_ref.set_xlim(0, 3)
117+
ax_ref.set_ylim(-1.1, 1.1)
118+
ax_ref.plot(x, y, "-", c="r", lw=10, rasterized=True)
119+
ax_ref.plot(x+1, y, "-", c="b", lw=10, rasterized=False)
120+
ax_ref.plot(x+2, y, "-", c="g", lw=10, rasterized=True)
121+
ax_ref.plot(x+3, y, "-", c="m", lw=10, rasterized=True)
122+
123+
ax_test = fig_test.subplots()
124+
ax_test.set_xlim(0, 3)
125+
ax_test.set_ylim(-1.1, 1.1)
126+
ax_test.plot(x, y, "-", c="r", lw=10, rasterized=True, zorder=1.1)
127+
ax_test.plot(x+2, y, "-", c="g", lw=10, rasterized=True, zorder=1.3)
128+
ax_test.plot(x+3, y, "-", c="m", lw=10, rasterized=True, zorder=1.4)
129+
ax_test.plot(x+1, y, "-", c="b", lw=10, rasterized=False, zorder=1.2)
130+
131+
132+
def test_count_bitmaps():
133+
def count_tag(fig, tag):
134+
fd = BytesIO()
135+
fig.savefig(fd, format='svg')
136+
fd.seek(0)
137+
buf = fd.read().decode()
138+
fd.close()
139+
open("test.svg", "w").write(buf)
140+
return buf.count("<%s" % tag)
141+
142+
# No rasterized elements
143+
fig1 = plt.figure()
144+
ax1 = fig1.add_subplot(1, 1, 1)
145+
ax1.set_axis_off()
146+
for n in range(5):
147+
ax1.plot([0, 20], [0, n], "b-", rasterized=False)
148+
assert count_tag(fig1, "image") == 0
149+
assert count_tag(fig1, "path") == 6 # axis patch plus lines
150+
151+
# rasterized can be merged
152+
fig2 = plt.figure()
153+
ax2 = fig2.add_subplot(1, 1, 1)
154+
ax2.set_axis_off()
155+
for n in range(5):
156+
ax2.plot([0, 20], [0, n], "b-", rasterized=True)
157+
assert count_tag(fig2, "image") == 1
158+
assert count_tag(fig2, "path") == 1 # axis patch
159+
160+
# rasterized can't be merged without effecting draw order
161+
fig3 = plt.figure()
162+
ax3 = fig3.add_subplot(1, 1, 1)
163+
ax3.set_axis_off()
164+
for n in range(5):
165+
ax3.plot([0, 20], [n, 0], "b-", rasterized=False)
166+
ax3.plot([0, 20], [0, n], "b-", rasterized=True)
167+
assert count_tag(fig3, "image") == 5
168+
assert count_tag(fig3, "path") == 6
169+
170+
# rasterized whole axes
171+
fig4 = plt.figure()
172+
ax4 = fig4.add_subplot(1, 1, 1)
173+
ax4.set_axis_off()
174+
ax4.set_rasterized(True)
175+
for n in range(5):
176+
ax4.plot([0, 20], [n, 0], "b-", rasterized=False)
177+
ax4.plot([0, 20], [0, n], "b-", rasterized=True)
178+
assert count_tag(fig4, "image") == 1
179+
assert count_tag(fig4, "path") == 1
180+
181+
# rasterized can be merged, but inhibited by suppressComposite
182+
fig5 = plt.figure()
183+
fig5.suppressComposite = True
184+
ax5 = fig5.add_subplot(1, 1, 1)
185+
ax5.set_axis_off()
186+
for n in range(5):
187+
ax5.plot([0, 20], [0, n], "b-", rasterized=True)
188+
assert count_tag(fig5, "image") == 5
189+
assert count_tag(fig5, "path") == 1 # axis patch
190+
191+
94192
@needs_usetex
95193
def test_missing_psfont(monkeypatch):
96194
"""An error is raised if a TeX font lacks a Type-1 equivalent"""

0 commit comments

Comments
 (0)