Skip to content

Commit 0f7dc4b

Browse files
committed
Fixed a bug in mixed mode renderer that images produced by
an rasterizing backend are placed with incorrect size. svn path=/trunk/matplotlib/; revision=7044
1 parent 00e4dc2 commit 0f7dc4b

File tree

6 files changed

+219
-113
lines changed

6 files changed

+219
-113
lines changed

examples/misc/tight_bbox_test.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import matplotlib.pyplot as plt
2+
import numpy as np
3+
4+
ax = plt.axes([0.1, 0.3, 0.5, 0.5])
5+
6+
ax.pcolormesh(np.array([[1,2],[3,4]]))
7+
plt.yticks([0.5, 1.5], ["long long tick label",
8+
"tick label"])
9+
plt.ylabel("My y-label")
10+
plt.title("Check saved figures for their bboxes")
11+
for ext in ["png", "pdf", "svg", "svgz", "eps"]:
12+
print "saving tight_bbox_test.%s" % (ext,)
13+
plt.savefig("tight_bbox_test.%s" % (ext,), bbox_inches="tight")
14+
plt.show()

lib/matplotlib/backend_bases.py

Lines changed: 19 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
from matplotlib.transforms import Bbox, TransformedBbox, Affine2D
3737
import cStringIO
3838

39+
import matplotlib.tight_bbox as tight_bbox
40+
3941
class RendererBase:
4042
"""An abstract base class to handle drawing/rendering operations.
4143
@@ -271,7 +273,6 @@ def _iter_collection(self, path_ids, cliprect, clippath, clippath_trans,
271273
gc.set_alpha(rgbFace[-1])
272274
rgbFace = rgbFace[:3]
273275
gc.set_antialiased(antialiaseds[i % Naa])
274-
275276
if Nurls:
276277
gc.set_url(urls[i % Nurls])
277278

@@ -1426,7 +1427,16 @@ def print_figure(self, filename, dpi=None, facecolor='w', edgecolor='w',
14261427
if bbox_inches:
14271428
# call adjust_bbox to save only the given area
14281429
if bbox_inches == "tight":
1429-
# save the figure to estimate the bounding box
1430+
# when bbox_inches == "tight", it saves the figure
1431+
# twice. The first save command is just to estimate
1432+
# the bounding box of the figure. A stringIO object is
1433+
# used as a temporary file object, but it causes a
1434+
# problem for some backends (ps backend with
1435+
# usetex=True) if they expect a filename, not a
1436+
# file-like object. As I think it is best to change
1437+
# the backend to support file-like object, i'm going
1438+
# to leave it as it is. However, a better solution
1439+
# than stringIO seems to be needed. -JJL
14301440
result = getattr(self, method_name)(
14311441
cStringIO.StringIO(),
14321442
dpi=dpi,
@@ -1439,9 +1449,12 @@ def print_figure(self, filename, dpi=None, facecolor='w', edgecolor='w',
14391449
pad = kwargs.pop("pad_inches", 0.1)
14401450
bbox_inches = bbox_inches.padded(pad)
14411451

1442-
restore_bbox = self._adjust_bbox(self.figure, format,
1443-
bbox_inches)
1444-
1452+
restore_bbox = tight_bbox.adjust_bbox(self.figure, format,
1453+
bbox_inches)
1454+
1455+
_bbox_inches_restore = (bbox_inches, restore_bbox)
1456+
else:
1457+
_bbox_inches_restore = None
14451458

14461459
try:
14471460
result = getattr(self, method_name)(
@@ -1450,6 +1463,7 @@ def print_figure(self, filename, dpi=None, facecolor='w', edgecolor='w',
14501463
facecolor=facecolor,
14511464
edgecolor=edgecolor,
14521465
orientation=orientation,
1466+
bbox_inches_restore=_bbox_inches_restore,
14531467
**kwargs)
14541468
finally:
14551469
if bbox_inches and restore_bbox:
@@ -1463,106 +1477,6 @@ def print_figure(self, filename, dpi=None, facecolor='w', edgecolor='w',
14631477
return result
14641478

14651479

1466-
def _adjust_bbox(self, fig, format, bbox_inches):
1467-
"""
1468-
Temporarily adjust the figure so that only the specified area
1469-
(bbox_inches) is saved.
1470-
1471-
It modifies fig.bbox, fig.bbox_inches,
1472-
fig.transFigure._boxout, and fig.patch. While the figure size
1473-
changes, the scale of the original figure is conserved. A
1474-
function whitch restores the original values are returned.
1475-
"""
1476-
1477-
origBbox = fig.bbox
1478-
origBboxInches = fig.bbox_inches
1479-
_boxout = fig.transFigure._boxout
1480-
1481-
asp_list = []
1482-
locator_list = []
1483-
for ax in fig.axes:
1484-
pos = ax.get_position(original=False).frozen()
1485-
locator_list.append(ax.get_axes_locator())
1486-
asp_list.append(ax.get_aspect())
1487-
1488-
def _l(a, r, pos=pos): return pos
1489-
ax.set_axes_locator(_l)
1490-
ax.set_aspect("auto")
1491-
1492-
1493-
1494-
def restore_bbox():
1495-
1496-
for ax, asp, loc in zip(fig.axes, asp_list, locator_list):
1497-
ax.set_aspect(asp)
1498-
ax.set_axes_locator(loc)
1499-
1500-
fig.bbox = origBbox
1501-
fig.bbox_inches = origBboxInches
1502-
fig.transFigure._boxout = _boxout
1503-
fig.transFigure.invalidate()
1504-
fig.patch.set_bounds(0, 0, 1, 1)
1505-
1506-
if format in ["png", "raw", "rgba"]:
1507-
self._adjust_bbox_png(fig, bbox_inches)
1508-
return restore_bbox
1509-
elif format in ["pdf", "eps"]:
1510-
self._adjust_bbox_pdf(fig, bbox_inches)
1511-
return restore_bbox
1512-
else:
1513-
warnings.warn("bbox_inches option for %s backend is not implemented yet." % (format))
1514-
return None
1515-
1516-
1517-
def _adjust_bbox_png(self, fig, bbox_inches):
1518-
"""
1519-
_adjust_bbox for png (Agg) format
1520-
"""
1521-
1522-
tr = fig.dpi_scale_trans
1523-
1524-
_bbox = TransformedBbox(bbox_inches,
1525-
tr)
1526-
x0, y0 = _bbox.x0, _bbox.y0
1527-
fig.bbox_inches = Bbox.from_bounds(0, 0,
1528-
bbox_inches.width,
1529-
bbox_inches.height)
1530-
1531-
x0, y0 = _bbox.x0, _bbox.y0
1532-
w1, h1 = fig.bbox.width, fig.bbox.height
1533-
self.figure.transFigure._boxout = Bbox.from_bounds(-x0, -y0,
1534-
w1, h1)
1535-
self.figure.transFigure.invalidate()
1536-
1537-
fig.bbox = TransformedBbox(fig.bbox_inches, tr)
1538-
1539-
fig.patch.set_bounds(x0/w1, y0/h1,
1540-
fig.bbox.width/w1, fig.bbox.height/h1)
1541-
1542-
1543-
def _adjust_bbox_pdf(self, fig, bbox_inches):
1544-
"""
1545-
_adjust_bbox for pdf & eps format
1546-
"""
1547-
1548-
tr = Affine2D().scale(72)
1549-
1550-
_bbox = TransformedBbox(bbox_inches, tr)
1551-
1552-
fig.bbox_inches = Bbox.from_bounds(0, 0,
1553-
bbox_inches.width,
1554-
bbox_inches.height)
1555-
x0, y0 = _bbox.x0, _bbox.y0
1556-
f = 72. / fig.dpi
1557-
w1, h1 = fig.bbox.width*f, fig.bbox.height*f
1558-
self.figure.transFigure._boxout = Bbox.from_bounds(-x0, -y0,
1559-
w1, h1)
1560-
self.figure.transFigure.invalidate()
1561-
1562-
fig.bbox = TransformedBbox(fig.bbox_inches, tr)
1563-
1564-
fig.patch.set_bounds(x0/w1, y0/h1,
1565-
fig.bbox.width/w1, fig.bbox.height/h1)
15661480

15671481

15681482
def get_default_filetype(self):

lib/matplotlib/backends/backend_mixed.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from matplotlib._image import frombuffer
22
from matplotlib.backends.backend_agg import RendererAgg
3+
from matplotlib.tight_bbox import process_figure_for_rasterizing
34

45
class MixedModeRenderer(object):
56
"""
@@ -9,8 +10,12 @@ class MixedModeRenderer(object):
910
complex objects, such as quad meshes, are rasterised and then
1011
output as images.
1112
"""
12-
def __init__(self, width, height, dpi, vector_renderer, raster_renderer_class=None):
13+
def __init__(self, figure, width, height, dpi, vector_renderer,
14+
raster_renderer_class=None,
15+
bbox_inches_restore=None):
1316
"""
17+
figure: The figure instance.
18+
1419
width: The width of the canvas in logical units
1520
1621
height: The height of the canvas in logical units
@@ -38,6 +43,13 @@ def __init__(self, width, height, dpi, vector_renderer, raster_renderer_class=No
3843
self._raster_renderer = None
3944
self._rasterizing = 0
4045

46+
# A renference to the figure is needed as we need to change
47+
# the figure dpi before and after the rasterization. Although
48+
# this looks ugly, I couldn't find a better solution. -JJL
49+
self.figure=figure
50+
51+
self._bbox_inches_restore = bbox_inches_restore
52+
4153
self._set_current_renderer(vector_renderer)
4254

4355
_methods = """
@@ -56,6 +68,7 @@ def _set_current_renderer(self, renderer):
5668
renderer.start_rasterizing = self.start_rasterizing
5769
renderer.stop_rasterizing = self.stop_rasterizing
5870

71+
5972
def start_rasterizing(self):
6073
"""
6174
Enter "raster" mode. All subsequent drawing commands (until
@@ -65,12 +78,25 @@ def start_rasterizing(self):
6578
If start_rasterizing is called multiple times before
6679
stop_rasterizing is called, this method has no effect.
6780
"""
81+
82+
# change the dpi of the figure temporarily.
83+
self.figure.set_dpi(self.dpi)
84+
85+
if self._bbox_inches_restore: # when tight bbox is used
86+
r = process_figure_for_rasterizing(self.figure,
87+
self._bbox_inches_restore,
88+
mode="png")
89+
90+
self._bbox_inches_restore = r
91+
92+
6893
if self._rasterizing == 0:
6994
self._raster_renderer = self._raster_renderer_class(
7095
self._width*self.dpi, self._height*self.dpi, self.dpi)
7196
self._set_current_renderer(self._raster_renderer)
7297
self._rasterizing += 1
7398

99+
74100
def stop_rasterizing(self):
75101
"""
76102
Exit "raster" mode. All of the drawing that was done since
@@ -91,6 +117,17 @@ def stop_rasterizing(self):
91117
image = frombuffer(buffer, w, h, True)
92118
image.is_grayscale = False
93119
image.flipud_out()
94-
self._renderer.draw_image(l, height - b - h, image, None)
120+
self._renderer.draw_image(int(float(l)/self.dpi*72.),
121+
int((float(height) - b - h)/self.dpi*72.),
122+
image, None)
95123
self._raster_renderer = None
96124
self._rasterizing = False
125+
126+
# restore the figure dpi.
127+
self.figure.set_dpi(72)
128+
129+
if self._bbox_inches_restore: # when tight bbox is used
130+
r = process_figure_for_rasterizing(self.figure,
131+
self._bbox_inches_restore,
132+
mode="pdf")
133+
self._bbox_inches_restore = r

lib/matplotlib/backends/backend_pdf.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1990,8 +1990,10 @@ def print_pdf(self, filename, **kwargs):
19901990
else:
19911991
file = PdfFile(filename)
19921992
file.newPage(width, height)
1993-
renderer = MixedModeRenderer(
1994-
width, height, 72, RendererPdf(file, image_dpi))
1993+
_bbox_inches_restore = kwargs.pop("bbox_inches_restore", None)
1994+
renderer = MixedModeRenderer(self.figure,
1995+
width, height, image_dpi, RendererPdf(file, image_dpi),
1996+
bbox_inches_restore=_bbox_inches_restore)
19951997
self.figure.draw(renderer)
19961998
renderer.finalize()
19971999
if isinstance(filename, PdfPages): # finish off this page

lib/matplotlib/backends/backend_svg.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,7 @@ def print_svg(self, filename, *args, **kwargs):
612612
fh_to_close = None
613613
else:
614614
raise ValueError("filename must be a path or a file-like object")
615-
return self._print_svg(filename, svgwriter, fh_to_close)
615+
return self._print_svg(filename, svgwriter, fh_to_close, **kwargs)
616616

617617
def print_svgz(self, filename, *args, **kwargs):
618618
if is_string_like(filename):
@@ -625,16 +625,28 @@ def print_svgz(self, filename, *args, **kwargs):
625625
raise ValueError("filename must be a path or a file-like object")
626626
return self._print_svg(filename, svgwriter, fh_to_close)
627627

628-
def _print_svg(self, filename, svgwriter, fh_to_close=None):
628+
def _print_svg(self, filename, svgwriter, fh_to_close=None, **kwargs):
629629
self.figure.set_dpi(72.0)
630630
width, height = self.figure.get_size_inches()
631631
w, h = width*72, height*72
632632

633633
if rcParams['svg.image_noscale']:
634634
renderer = RendererSVG(w, h, svgwriter, filename)
635635
else:
636-
renderer = MixedModeRenderer(
637-
width, height, 72.0, RendererSVG(w, h, svgwriter, filename))
636+
# setting mixed renderer dpi other than 72 results in
637+
# incorrect size of the rasterized image. It seems that the
638+
# svg internally uses fixed dpi of 72 and seems to cause
639+
# the problem. I hope someone who knows the svg backends
640+
# take a look at this problem. Meanwhile, the dpi
641+
# parameter is ignored and image_dpi is fixed at 72. - JJL
642+
643+
#image_dpi = kwargs.pop("dpi", 72)
644+
image_dpi = 72
645+
_bbox_inches_restore = kwargs.pop("bbox_inches_restore", None)
646+
renderer = MixedModeRenderer(self.figure,
647+
width, height, image_dpi, RendererSVG(w, h, svgwriter, filename),
648+
bbox_inches_restore=_bbox_inches_restore)
649+
638650
self.figure.draw(renderer)
639651
renderer.finalize()
640652
if fh_to_close is not None:

0 commit comments

Comments
 (0)