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

Skip to content

Commit 663f2b3

Browse files
committed
Implement bbox_inches="tight" as a post-processing step.
The current implementation of bbox_inches="tight" is to no-output-draw the figure, compute a tight bbox, and then shift all artists to their "new" position in tight-bbox-coordinates, draw, and shift back -- which causes all kinds of grief. Instead, we can perform the real draw the first time, and then crop the resulting image (either actually crop it for raster backends, or just adjust the viewport for vector ones). This PR is a proof-of-concept for such an approach (tests fail, that's expected). Currently it writes the first file to the filesystem and then edits it; likely the first file should be just kept in memory instead (after all the final output could also just be an im-memory buffer). This would also avoid having to dupe the metadata handling (for agg). Perhaps the method should be on the renderer instead (the exact API is needs to be discussed). Extra points if the API is designed in such a way that mplcairo can directly call into it. Currently not implemented for postscript because bbox_inches="tight" doesn't work there anyways (although we should fix it for postscript). Not sure about pgf output, but that's probably workable... Final version should keep some backcompat layer, not present here to improve readability.
1 parent de2ea8b commit 663f2b3

File tree

4 files changed

+58
-26
lines changed

4 files changed

+58
-26
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
import matplotlib as mpl
4646
from matplotlib import (
4747
_api, backend_tools as tools, cbook, colors, docstring, textpath,
48-
tight_bbox, transforms, widgets, get_backend, is_interactive, rcParams)
48+
transforms, widgets, get_backend, is_interactive, rcParams)
4949
from matplotlib._pylab_helpers import Gcf
5050
from matplotlib.backend_managers import ToolManager
5151
from matplotlib.cbook import _setattr_cm
@@ -2273,11 +2273,7 @@ def print_figure(
22732273
if not cbook._str_equal(color, "auto"):
22742274
stack.enter_context(self.figure._cm_set(**{prop: color}))
22752275

2276-
if bbox_inches is None:
2277-
bbox_inches = rcParams['savefig.bbox']
2278-
2279-
if (self.figure.get_constrained_layout() or
2280-
bbox_inches == "tight"):
2276+
if self.figure.get_constrained_layout():
22812277
# we need to trigger a draw before printing to make sure
22822278
# CL works. "tight" also needs a draw to get the right
22832279
# locations:
@@ -2289,22 +2285,6 @@ def print_figure(
22892285
with getattr(renderer, "_draw_disabled", nullcontext)():
22902286
self.figure.draw(renderer)
22912287

2292-
if bbox_inches:
2293-
if bbox_inches == "tight":
2294-
bbox_inches = self.figure.get_tightbbox(
2295-
renderer, bbox_extra_artists=bbox_extra_artists)
2296-
if pad_inches is None:
2297-
pad_inches = rcParams['savefig.pad_inches']
2298-
bbox_inches = bbox_inches.padded(pad_inches)
2299-
2300-
# call adjust_bbox to save only the given area
2301-
restore_bbox = tight_bbox.adjust_bbox(self.figure, bbox_inches,
2302-
canvas.fixed_dpi)
2303-
2304-
_bbox_inches_restore = (bbox_inches, restore_bbox)
2305-
else:
2306-
_bbox_inches_restore = None
2307-
23082288
# we have already done CL above, so turn it off:
23092289
stack.enter_context(self.figure._cm_set(constrained_layout=False))
23102290
try:
@@ -2316,12 +2296,19 @@ def print_figure(
23162296
facecolor=facecolor,
23172297
edgecolor=edgecolor,
23182298
orientation=orientation,
2319-
bbox_inches_restore=_bbox_inches_restore,
23202299
**kwargs)
2300+
if bbox_inches is None:
2301+
bbox_inches = rcParams["savefig.bbox"]
2302+
if bbox_inches == "tight":
2303+
bbox_inches = self.figure.get_tightbbox(
2304+
self.figure._cachedRenderer,
2305+
bbox_extra_artists=bbox_extra_artists)
2306+
if pad_inches is None:
2307+
pad_inches = rcParams["savefig.pad_inches"]
2308+
bbox_inches = bbox_inches.padded(pad_inches)
2309+
if bbox_inches:
2310+
canvas.adjust_bbox(filename, bbox_inches)
23212311
finally:
2322-
if bbox_inches and restore_bbox:
2323-
restore_bbox()
2324-
23252312
self.figure.set_canvas(self)
23262313
return result
23272314

lib/matplotlib/backends/backend_agg.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,13 @@ def print_tif(self, filename_or_obj, *, pil_kwargs=None):
562562

563563
print_tiff = print_tif
564564

565+
def adjust_bbox(self, filename, bbox_inches):
566+
bbox = self.figure.dpi_scale_trans.transform_bbox(bbox_inches)
567+
h = self.figure.bbox.height
568+
img = Image.open(filename)
569+
img = img.crop((bbox.x0, h - bbox.y1, bbox.x1, h - bbox.y0))
570+
img.save(filename, format=img.format) # TODO: also copy metadata
571+
565572

566573
@_Backend.export
567574
class _BackendAgg(_Backend):

lib/matplotlib/backends/backend_pdf.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2719,6 +2719,32 @@ def draw(self):
27192719
self.figure.draw_no_output()
27202720
return super().draw()
27212721

2722+
def adjust_bbox(self, filename, bbox_inches):
2723+
bbox = self.figure.dpi_scale_trans.transform_bbox(bbox_inches)
2724+
# What about PdfPages?
2725+
with open(filename, "a+b") as file:
2726+
file.seek(0)
2727+
buf = file.read()
2728+
page_pos = file.tell()
2729+
pageid = self.figure._cachedRenderer.file.pageList[-1].id
2730+
page_match = re.search(
2731+
rb"(?s)%d 0 obj\n.*?/MediaBox.*?\nendobj\n" % pageid, buf)
2732+
file.write(re.sub(br"/MediaBox \[[^]]*\]", b"/MediaBox %s"
2733+
% pdfRepr(bbox.extents.tolist()), page_match[0]))
2734+
startxref_pos = file.tell()
2735+
file.write(b"xref\n")
2736+
file.write(b"0 1\n0000000000 65535 f \n")
2737+
file.write(b"%d 1\n%010d 00000 n \n" % (pageid, page_pos))
2738+
file.write(b"trailer\n")
2739+
trailer_match = re.search(
2740+
rb"(?s)trailer\n<< (.*) >>\nstartxref\n(\d+)\n%%EOF",
2741+
buf[buf.rfind(b"trailer\n"):])
2742+
file.write(
2743+
b"<< %s /Prev %s >>\n" % (trailer_match[1], trailer_match[2]))
2744+
file.write(b"startxref\n")
2745+
file.write(b"%d\n" % startxref_pos)
2746+
file.write(b"%%EOF\n")
2747+
27222748

27232749
FigureManagerPdf = FigureManagerBase
27242750

lib/matplotlib/backends/backend_svg.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import itertools
77
import logging
88
import os
9+
import pathlib
910
import re
1011
import uuid
1112

@@ -1345,6 +1346,17 @@ def draw(self):
13451346
self.figure.draw_no_output()
13461347
return super().draw()
13471348

1349+
def adjust_bbox(self, filename, bbox_inches):
1350+
bbox = self.figure.dpi_scale_trans.transform_bbox(bbox_inches)
1351+
buf = pathlib.Path(filename).read_text()
1352+
w, h, x, y = map(short_float_fmt, [
1353+
bbox.width, bbox.height,
1354+
bbox.x0, self.figure.bbox.height - bbox.y1])
1355+
buf = re.sub('width="[^"]*"', f'width="{w}pt"', buf, 1)
1356+
buf = re.sub('height="[^"]*"', f'height="{h}pt"', buf, 1)
1357+
buf = re.sub('viewBox="[^"]*"', f'viewBox="{x} {y} {w} {h}"', buf, 1)
1358+
pathlib.Path(filename).write_text(buf)
1359+
13481360

13491361
FigureManagerSVG = FigureManagerBase
13501362

0 commit comments

Comments
 (0)