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

Skip to content

Commit b16411d

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 2e00dc0 commit b16411d

File tree

4 files changed

+57
-25
lines changed

4 files changed

+57
-25
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2274,11 +2274,7 @@ def print_figure(
22742274
self.figure.set_facecolor(facecolor)
22752275
self.figure.set_edgecolor(edgecolor)
22762276

2277-
if bbox_inches is None:
2278-
bbox_inches = rcParams['savefig.bbox']
2279-
2280-
if (self.figure.get_constrained_layout() or
2281-
bbox_inches == "tight"):
2277+
if self.figure.get_constrained_layout():
22822278
# we need to trigger a draw before printing to make sure
22832279
# CL works. "tight" also needs a draw to get the right
22842280
# locations:
@@ -2293,22 +2289,6 @@ def print_figure(
22932289
with ctx:
22942290
self.figure.draw(renderer)
22952291

2296-
if bbox_inches:
2297-
if bbox_inches == "tight":
2298-
bbox_inches = self.figure.get_tightbbox(
2299-
renderer, bbox_extra_artists=bbox_extra_artists)
2300-
if pad_inches is None:
2301-
pad_inches = rcParams['savefig.pad_inches']
2302-
bbox_inches = bbox_inches.padded(pad_inches)
2303-
2304-
# call adjust_bbox to save only the given area
2305-
restore_bbox = tight_bbox.adjust_bbox(self.figure, bbox_inches,
2306-
canvas.fixed_dpi)
2307-
2308-
_bbox_inches_restore = (bbox_inches, restore_bbox)
2309-
else:
2310-
_bbox_inches_restore = None
2311-
23122292
# we have already done CL above, so turn it off:
23132293
cl_state = self.figure.get_constrained_layout()
23142294
self.figure.set_constrained_layout(False)
@@ -2321,12 +2301,19 @@ def print_figure(
23212301
facecolor=facecolor,
23222302
edgecolor=edgecolor,
23232303
orientation=orientation,
2324-
bbox_inches_restore=_bbox_inches_restore,
23252304
**kwargs)
2305+
if bbox_inches is None:
2306+
bbox_inches = rcParams["savefig.bbox"]
2307+
if bbox_inches == "tight":
2308+
bbox_inches = self.figure.get_tightbbox(
2309+
self.figure._cachedRenderer,
2310+
bbox_extra_artists=bbox_extra_artists)
2311+
if pad_inches is None:
2312+
pad_inches = rcParams["savefig.pad_inches"]
2313+
bbox_inches = bbox_inches.padded(pad_inches)
2314+
if bbox_inches:
2315+
canvas.adjust_bbox(filename, bbox_inches)
23262316
finally:
2327-
if bbox_inches and restore_bbox:
2328-
restore_bbox()
2329-
23302317
self.figure.set_facecolor(origfacecolor)
23312318
self.figure.set_edgecolor(origedgecolor)
23322319
self.figure.set_canvas(self)

lib/matplotlib/backends/backend_agg.py

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

567567
print_tiff = print_tif
568568

569+
def adjust_bbox(self, filename, bbox_inches):
570+
bbox = self.figure.dpi_scale_trans.transform_bbox(bbox_inches)
571+
h = self.figure.bbox.height
572+
img = Image.open(filename)
573+
img = img.crop((bbox.x0, h - bbox.y1, bbox.x1, h - bbox.y0))
574+
img.save(filename, format=img.format) # TODO: also copy metadata
575+
569576

570577
@_Backend.export
571578
class _BackendAgg(_Backend):

lib/matplotlib/backends/backend_pdf.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2737,6 +2737,32 @@ def draw(self):
27372737
_no_output_draw(self.figure)
27382738
return super().draw()
27392739

2740+
def adjust_bbox(self, filename, bbox_inches):
2741+
bbox = self.figure.dpi_scale_trans.transform_bbox(bbox_inches)
2742+
# What about PdfPages?
2743+
with open(filename, "a+b") as file:
2744+
file.seek(0)
2745+
buf = file.read()
2746+
page_pos = file.tell()
2747+
pageid = self.figure._cachedRenderer.file.pageList[-1].id
2748+
page_match = re.search(
2749+
rb"(?s)%d 0 obj\n.*?/MediaBox.*?\nendobj\n" % pageid, buf)
2750+
file.write(re.sub(br"/MediaBox \[[^]]*\]", b"/MediaBox %s"
2751+
% pdfRepr(bbox.extents.tolist()), page_match[0]))
2752+
startxref_pos = file.tell()
2753+
file.write(b"xref\n")
2754+
file.write(b"0 1\n0000000000 65535 f \n")
2755+
file.write(b"%d 1\n%010d 00000 n \n" % (pageid, page_pos))
2756+
file.write(b"trailer\n")
2757+
trailer_match = re.search(
2758+
rb"(?s)trailer\n<< (.*) >>\nstartxref\n(\d+)\n%%EOF",
2759+
buf[buf.rfind(b"trailer\n"):])
2760+
file.write(
2761+
b"<< %s /Prev %s >>\n" % (trailer_match[1], trailer_match[2]))
2762+
file.write(b"startxref\n")
2763+
file.write(b"%d\n" % startxref_pos)
2764+
file.write(b"%%EOF\n")
2765+
27402766

27412767
FigureManagerPdf = FigureManagerBase
27422768

lib/matplotlib/backends/backend_svg.py

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

@@ -1347,6 +1348,17 @@ def draw(self):
13471348
_no_output_draw(self.figure)
13481349
return super().draw()
13491350

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

13511363
FigureManagerSVG = FigureManagerBase
13521364

0 commit comments

Comments
 (0)