Skip to content

Commit

Permalink
Merge pull request #26479 from QuLogic/ps-papersize-figure
Browse files Browse the repository at this point in the history
ps: Add option to use figure size as paper size
  • Loading branch information
ksunden committed Aug 9, 2023
2 parents fb3a97a + 96b26fe commit 3ab6e1f
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 30 deletions.
6 changes: 6 additions & 0 deletions doc/api/next_api_changes/behavior/26479-ES.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
PostScript paper type adds option to use figure size
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The :rc:`ps.papertype` rcParam can now be set to ``'figure'``, which will use
a paper size that corresponds exactly with the size of the figure that is being
saved.
49 changes: 28 additions & 21 deletions lib/matplotlib/backends/backend_ps.py
Original file line number Diff line number Diff line change
Expand Up @@ -841,7 +841,7 @@ def _print_ps(
if papertype is None:
papertype = mpl.rcParams['ps.papersize']
papertype = papertype.lower()
_api.check_in_list(['auto', *papersize], papertype=papertype)
_api.check_in_list(['figure', 'auto', *papersize], papertype=papertype)

orientation = _api.check_getitem(
_Orientation, orientation=orientation.lower())
Expand Down Expand Up @@ -873,24 +873,16 @@ def _print_figure(
width, height = self.figure.get_size_inches()
if papertype == 'auto':
_api.warn_deprecated("3.8", name="papertype='auto'",
addendum="Pass an explicit paper type, or omit the "
"*papertype* argument entirely.")
addendum="Pass an explicit paper type, 'figure', or "
"omit the *papertype* argument entirely.")
papertype = _get_papertype(*orientation.swap_if_landscape((width, height)))

if is_eps:
if is_eps or papertype == 'figure':
paper_width, paper_height = width, height
else:
paper_width, paper_height = orientation.swap_if_landscape(
papersize[papertype])

if mpl.rcParams['ps.usedistiller']:
# distillers improperly clip eps files if pagesize is too small
if width > paper_width or height > paper_height:
papertype = _get_papertype(
*orientation.swap_if_landscape((width, height)))
paper_width, paper_height = orientation.swap_if_landscape(
papersize[papertype])

# center the figure on the paper
xo = 72 * 0.5 * (paper_width - width)
yo = 72 * 0.5 * (paper_height - height)
Expand Down Expand Up @@ -921,10 +913,10 @@ def print_figure_impl(fh):
if is_eps:
print("%!PS-Adobe-3.0 EPSF-3.0", file=fh)
else:
print(f"%!PS-Adobe-3.0\n"
f"%%DocumentPaperSizes: {papertype}\n"
f"%%Pages: 1\n",
end="", file=fh)
print("%!PS-Adobe-3.0", file=fh)
if papertype != 'figure':
print(f"%%DocumentPaperSizes: {papertype}", file=fh)
print("%%Pages: 1", file=fh)
print(f"%%LanguageLevel: 3\n"
f"{dsc_comments}\n"
f"%%Orientation: {orientation.name}\n"
Expand Down Expand Up @@ -1061,7 +1053,7 @@ def _print_figure_tex(
# set the paper size to the figure size if is_eps. The
# resulting ps file has the given size with correct bounding
# box so that there is no need to call 'pstoeps'
if is_eps:
if is_eps or papertype == 'figure':
paper_width, paper_height = orientation.swap_if_landscape(
self.figure.get_size_inches())
else:
Expand Down Expand Up @@ -1160,17 +1152,22 @@ def gs_distill(tmpfile, eps=False, ptype='letter', bbox=None, rotated=False):
"""

if eps:
paper_option = "-dEPSCrop"
paper_option = ["-dEPSCrop"]
elif ptype == "figure":
# The bbox will have its lower-left corner at (0, 0), so upper-right
# corner corresponds with paper size.
paper_option = [f"-dDEVICEWIDTHPOINTS={bbox[2]}",
f"-dDEVICEHEIGHTPOINTS={bbox[3]}"]
else:
paper_option = "-sPAPERSIZE=%s" % ptype
paper_option = [f"-sPAPERSIZE={ptype}"]

psfile = tmpfile + '.ps'
dpi = mpl.rcParams['ps.distiller.res']

cbook._check_and_log_subprocess(
[mpl._get_executable_info("gs").executable,
"-dBATCH", "-dNOPAUSE", "-r%d" % dpi, "-sDEVICE=ps2write",
paper_option, "-sOutputFile=%s" % psfile, tmpfile],
*paper_option, f"-sOutputFile={psfile}", tmpfile],
_log)

os.remove(tmpfile)
Expand All @@ -1196,6 +1193,16 @@ def xpdf_distill(tmpfile, eps=False, ptype='letter', bbox=None, rotated=False):
mpl._get_executable_info("gs") # Effectively checks for ps2pdf.
mpl._get_executable_info("pdftops")

if eps:
paper_option = ["-dEPSCrop"]
elif ptype == "figure":
# The bbox will have its lower-left corner at (0, 0), so upper-right
# corner corresponds with paper size.
paper_option = [f"-dDEVICEWIDTHPOINTS#{bbox[2]}",
f"-dDEVICEHEIGHTPOINTS#{bbox[3]}"]
else:
paper_option = [f"-sPAPERSIZE#{ptype}"]

with TemporaryDirectory() as tmpdir:
tmppdf = pathlib.Path(tmpdir, "tmp.pdf")
tmpps = pathlib.Path(tmpdir, "tmp.ps")
Expand All @@ -1208,7 +1215,7 @@ def xpdf_distill(tmpfile, eps=False, ptype='letter', bbox=None, rotated=False):
"-sAutoRotatePages#None",
"-sGrayImageFilter#FlateEncode",
"-sColorImageFilter#FlateEncode",
"-dEPSCrop" if eps else "-sPAPERSIZE#%s" % ptype,
*paper_option,
tmpfile, tmppdf], _log)
cbook._check_and_log_subprocess(
["pdftops", "-paper", "match", "-level3", tmppdf, tmpps], _log)
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/mpl-data/matplotlibrc
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@
#tk.window_focus: False # Maintain shell focus for TkAgg

### ps backend params
#ps.papersize: letter # {letter, legal, ledger, A0-A10, B0-B10}
#ps.papersize: letter # {figure, letter, legal, ledger, A0-A10, B0-B10}
#ps.useafm: False # use AFM fonts, results in small files
#ps.usedistiller: False # {ghostscript, xpdf, None}
# Experimental: may produce smaller files.
Expand Down
6 changes: 3 additions & 3 deletions lib/matplotlib/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,13 +441,13 @@ def validate_ps_distiller(s):
def _validate_papersize(s):
# Re-inline this validator when the 'auto' deprecation expires.
s = ValidateInStrings("ps.papersize",
["auto", "letter", "legal", "ledger",
["figure", "auto", "letter", "legal", "ledger",
*[f"{ab}{i}" for ab in "ab" for i in range(11)]],
ignorecase=True)(s)
if s == "auto":
_api.warn_deprecated("3.8", name="ps.papersize='auto'",
addendum="Pass an explicit paper type, or omit the "
"*ps.papersize* rcParam entirely.")
addendum="Pass an explicit paper type, figure, or omit "
"the *ps.papersize* rcParam entirely.")
return s


Expand Down
44 changes: 39 additions & 5 deletions lib/matplotlib/tests/test_backend_ps.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

# This tests tends to hit a TeX cache lock on AppVeyor.
@pytest.mark.flaky(reruns=3)
@pytest.mark.parametrize('papersize', ['letter', 'figure'])
@pytest.mark.parametrize('orientation', ['portrait', 'landscape'])
@pytest.mark.parametrize('format, use_log, rcParams', [
('ps', False, {}),
Expand All @@ -38,7 +39,19 @@
'eps afm',
'eps with usetex'
])
def test_savefig_to_stringio(format, use_log, rcParams, orientation):
def test_savefig_to_stringio(format, use_log, rcParams, orientation, papersize):
if rcParams.get("ps.usedistiller") == "ghostscript":
try:
mpl._get_executable_info("gs")
except mpl.ExecutableNotFoundError as exc:
pytest.skip(str(exc))
elif rcParams.get("ps.userdistiller") == "xpdf":
try:
mpl._get_executable_info("gs") # Effectively checks for ps2pdf.
mpl._get_executable_info("pdftops")
except mpl.ExecutableNotFoundError as exc:
pytest.skip(str(exc))

mpl.rcParams.update(rcParams)

fig, ax = plt.subplots()
Expand All @@ -54,15 +67,15 @@ def test_savefig_to_stringio(format, use_log, rcParams, orientation):
title += " \N{MINUS SIGN}\N{EURO SIGN}"
ax.set_title(title)
allowable_exceptions = []
if rcParams.get("ps.usedistiller"):
allowable_exceptions.append(mpl.ExecutableNotFoundError)
if rcParams.get("text.usetex"):
allowable_exceptions.append(RuntimeError)
if rcParams.get("ps.useafm"):
allowable_exceptions.append(mpl.MatplotlibDeprecationWarning)
try:
fig.savefig(s_buf, format=format, orientation=orientation)
fig.savefig(b_buf, format=format, orientation=orientation)
fig.savefig(s_buf, format=format, orientation=orientation,
papertype=papersize)
fig.savefig(b_buf, format=format, orientation=orientation,
papertype=papersize)
except tuple(allowable_exceptions) as exc:
pytest.skip(str(exc))

Expand All @@ -71,6 +84,27 @@ def test_savefig_to_stringio(format, use_log, rcParams, orientation):
s_val = s_buf.getvalue().encode('ascii')
b_val = b_buf.getvalue()

if format == 'ps':
# Default figsize = (8, 6) inches = (576, 432) points = (203.2, 152.4) mm.
# Landscape orientation will swap dimensions.
if rcParams.get("ps.usedistiller") == "xpdf":
# Some versions specifically show letter/203x152, but not all,
# so we can only use this simpler test.
if papersize == 'figure':
assert b'letter' not in s_val.lower()
else:
assert b'letter' in s_val.lower()
elif rcParams.get("ps.usedistiller") or rcParams.get("text.usetex"):
width = b'432.0' if orientation == 'landscape' else b'576.0'
wanted = (b'-dDEVICEWIDTHPOINTS=' + width if papersize == 'figure'
else b'-sPAPERSIZE')
assert wanted in s_val
else:
if papersize == 'figure':
assert b'%%DocumentPaperSizes' not in s_val
else:
assert b'%%DocumentPaperSizes' in s_val

# Strip out CreationDate: ghostscript and cairo don't obey
# SOURCE_DATE_EPOCH, and that environment variable is already tested in
# test_determinism.
Expand Down

0 comments on commit 3ab6e1f

Please sign in to comment.