Skip to content

Commit a2db882

Browse files
authored
Merge pull request #10548 from MaxNoe/pgf_pdf_pages
Implement PdfPages for backend pgf
2 parents bd8791d + 05e7f77 commit a2db882

File tree

9 files changed

+413
-6
lines changed

9 files changed

+413
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ result_images
8080

8181
# Nose/Pytest generated files #
8282
###############################
83+
.pytest_cache/
8384
.cache/
8485
.coverage
8586
.coverage.*

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ addons:
3737
- texlive-latex-extra
3838
- texlive-latex-recommended
3939
- texlive-xetex
40+
- texlive-luatex
4041

4142
env:
4243
global:

doc/faq/howto_faq.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ Finally, the multipage pdf object has to be closed::
136136

137137
pp.close()
138138

139+
The same can be done using the pgf backend::
140+
141+
from matplotlib.backends.backend_pgf import PdfPages
142+
139143

140144
.. _howto-subplots-adjust:
141145

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Multipage PDF support for pgf backend
2+
-------------------------------------
3+
4+
The pgf backend now also supports multipage PDF files.
5+
6+
.. code-block:: python
7+
8+
from matplotlib.backends.backend_pgf import PdfPages
9+
import matplotlib.pyplot as plt
10+
11+
with PdfPages('multipage.pdf') as pdf:
12+
# page 1
13+
plt.plot([2, 1, 3])
14+
pdf.savefig()
15+
16+
# page 2
17+
plt.cla()
18+
plt.plot([3, 1, 2])
19+
pdf.savefig()

doc/users/whats_new.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ revision, see the :ref:`github-stats`.
1414
..
1515
For a release, add a new section after this, then comment out the include
1616
and toctree below by indenting them. Uncomment them after the release.
17-
.. include:: next_whats_new/README.rst
18-
.. toctree::
19-
:glob:
20-
:maxdepth: 1
17+
.. include:: next_whats_new/README.rst
18+
.. toctree::
19+
:glob:
20+
:maxdepth: 1
2121

22-
next_whats_new/*
22+
next_whats_new/*
2323

2424

2525
New in Matplotlib 2.2

examples/misc/multipage_pdf.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
66
This is a demo of creating a pdf file with several pages,
77
as well as adding metadata and annotations to pdf files.
8+
9+
If you want to use a multipage pdf file using LaTeX, you need
10+
to use `from matplotlib.backends.backend_pgf import PdfPages`.
11+
This version however does not support `attach_note`.
812
"""
913

1014
import datetime

lib/matplotlib/backends/backend_pgf.py

Lines changed: 237 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
import weakref
1818

1919
import matplotlib as mpl
20-
from matplotlib import _png, rcParams
20+
from matplotlib import _png, rcParams, __version__
2121
from matplotlib.backend_bases import (
2222
_Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase,
2323
RendererBase)
2424
from matplotlib.backends.backend_mixed import MixedModeRenderer
2525
from matplotlib.cbook import is_writable_file_like
2626
from matplotlib.path import Path
27+
from matplotlib.figure import Figure
28+
from matplotlib._pylab_helpers import Gcf
2729

2830

2931
###############################################################################
@@ -50,13 +52,30 @@
5052
warnings.warn('error getting fonts from fc-list', UserWarning)
5153

5254

55+
_luatex_version_re = re.compile(
56+
'This is LuaTeX, Version (?:beta-)?([0-9]+)\.([0-9]+)\.([0-9]+)'
57+
)
58+
59+
5360
def get_texcommand():
5461
"""Get chosen TeX system from rc."""
5562
texsystem_options = ["xelatex", "lualatex", "pdflatex"]
5663
texsystem = rcParams["pgf.texsystem"]
5764
return texsystem if texsystem in texsystem_options else "xelatex"
5865

5966

67+
def _get_lualatex_version():
68+
"""Get version of luatex"""
69+
output = subprocess.check_output(['lualatex', '--version'])
70+
return _parse_lualatex_version(output.decode())
71+
72+
73+
def _parse_lualatex_version(output):
74+
'''parse the lualatex version from the output of `lualatex --version`'''
75+
match = _luatex_version_re.match(output)
76+
return tuple(map(int, match.groups()))
77+
78+
6079
def get_fontspec():
6180
"""Build fontspec preamble from rc."""
6281
latex_fontspec = []
@@ -990,4 +1009,221 @@ def _cleanup_all():
9901009
LatexManager._cleanup_remaining_instances()
9911010
TmpDirCleaner.cleanup_remaining_tmpdirs()
9921011

1012+
9931013
atexit.register(_cleanup_all)
1014+
1015+
1016+
class PdfPages:
1017+
"""
1018+
A multi-page PDF file using the pgf backend
1019+
1020+
Examples
1021+
--------
1022+
1023+
>>> import matplotlib.pyplot as plt
1024+
>>> # Initialize:
1025+
>>> with PdfPages('foo.pdf') as pdf:
1026+
... # As many times as you like, create a figure fig and save it:
1027+
... fig = plt.figure()
1028+
... pdf.savefig(fig)
1029+
... # When no figure is specified the current figure is saved
1030+
... pdf.savefig()
1031+
"""
1032+
__slots__ = (
1033+
'_outputfile',
1034+
'keep_empty',
1035+
'_tmpdir',
1036+
'_basename',
1037+
'_fname_tex',
1038+
'_fname_pdf',
1039+
'_n_figures',
1040+
'_file',
1041+
'metadata',
1042+
)
1043+
1044+
def __init__(self, filename, *, keep_empty=True, metadata=None):
1045+
"""
1046+
Create a new PdfPages object.
1047+
1048+
Parameters
1049+
----------
1050+
1051+
filename : str
1052+
Plots using :meth:`PdfPages.savefig` will be written to a file at
1053+
this location. Any older file with the same name is overwritten.
1054+
keep_empty : bool, optional
1055+
If set to False, then empty pdf files will be deleted automatically
1056+
when closed.
1057+
metadata : dictionary, optional
1058+
Information dictionary object (see PDF reference section 10.2.1
1059+
'Document Information Dictionary'), e.g.:
1060+
`{'Creator': 'My software', 'Author': 'Me',
1061+
'Title': 'Awesome fig'}`
1062+
1063+
The standard keys are `'Title'`, `'Author'`, `'Subject'`,
1064+
`'Keywords'`, `'Producer'`, `'Creator'` and `'Trapped'`.
1065+
Values have been predefined for `'Creator'` and `'Producer'`.
1066+
They can be removed by setting them to the empty string.
1067+
"""
1068+
self._outputfile = filename
1069+
self._n_figures = 0
1070+
self.keep_empty = keep_empty
1071+
self.metadata = metadata or {}
1072+
1073+
# create temporary directory for compiling the figure
1074+
self._tmpdir = tempfile.mkdtemp(prefix="mpl_pgf_pdfpages_")
1075+
self._basename = 'pdf_pages'
1076+
self._fname_tex = os.path.join(self._tmpdir, self._basename + ".tex")
1077+
self._fname_pdf = os.path.join(self._tmpdir, self._basename + ".pdf")
1078+
self._file = open(self._fname_tex, 'wb')
1079+
1080+
def _write_header(self, width_inches, height_inches):
1081+
supported_keys = {
1082+
'title', 'author', 'subject', 'keywords', 'creator',
1083+
'producer', 'trapped'
1084+
}
1085+
infoDict = {
1086+
'creator': 'matplotlib %s, https://matplotlib.org' % __version__,
1087+
'producer': 'matplotlib pgf backend %s' % __version__,
1088+
}
1089+
metadata = {k.lower(): v for k, v in self.metadata.items()}
1090+
infoDict.update(metadata)
1091+
hyperref_options = ''
1092+
for k, v in infoDict.items():
1093+
if k not in supported_keys:
1094+
raise ValueError(
1095+
'Not a supported pdf metadata field: "{}"'.format(k)
1096+
)
1097+
hyperref_options += 'pdf' + k + '={' + str(v) + '},'
1098+
1099+
latex_preamble = get_preamble()
1100+
latex_fontspec = get_fontspec()
1101+
latex_header = r"""\PassOptionsToPackage{{
1102+
{metadata}
1103+
}}{{hyperref}}
1104+
\RequirePackage{{hyperref}}
1105+
\documentclass[12pt]{{minimal}}
1106+
\usepackage[
1107+
paperwidth={width}in,
1108+
paperheight={height}in,
1109+
margin=0in
1110+
]{{geometry}}
1111+
{preamble}
1112+
{fontspec}
1113+
\usepackage{{pgf}}
1114+
\setlength{{\parindent}}{{0pt}}
1115+
1116+
\begin{{document}}%%
1117+
""".format(
1118+
width=width_inches,
1119+
height=height_inches,
1120+
preamble=latex_preamble,
1121+
fontspec=latex_fontspec,
1122+
metadata=hyperref_options,
1123+
)
1124+
self._file.write(latex_header.encode('utf-8'))
1125+
1126+
def __enter__(self):
1127+
return self
1128+
1129+
def __exit__(self, exc_type, exc_val, exc_tb):
1130+
self.close()
1131+
1132+
def close(self):
1133+
"""
1134+
Finalize this object, running LaTeX in a temporary directory
1135+
and moving the final pdf file to `filename`.
1136+
"""
1137+
self._file.write(rb'\end{document}\n')
1138+
self._file.close()
1139+
1140+
if self._n_figures > 0:
1141+
try:
1142+
self._run_latex()
1143+
finally:
1144+
try:
1145+
shutil.rmtree(self._tmpdir)
1146+
except:
1147+
TmpDirCleaner.add(self._tmpdir)
1148+
elif self.keep_empty:
1149+
open(self._outputfile, 'wb').close()
1150+
1151+
def _run_latex(self):
1152+
texcommand = get_texcommand()
1153+
cmdargs = [
1154+
str(texcommand),
1155+
"-interaction=nonstopmode",
1156+
"-halt-on-error",
1157+
os.path.basename(self._fname_tex),
1158+
]
1159+
try:
1160+
subprocess.check_output(
1161+
cmdargs, stderr=subprocess.STDOUT, cwd=self._tmpdir
1162+
)
1163+
except subprocess.CalledProcessError as e:
1164+
raise RuntimeError(
1165+
"%s was not able to process your file.\n\nFull log:\n%s"
1166+
% (texcommand, e.output.decode('utf-8')))
1167+
1168+
# copy file contents to target
1169+
shutil.copyfile(self._fname_pdf, self._outputfile)
1170+
1171+
def savefig(self, figure=None, **kwargs):
1172+
"""
1173+
Saves a :class:`~matplotlib.figure.Figure` to this file as a new page.
1174+
1175+
Any other keyword arguments are passed to
1176+
:meth:`~matplotlib.figure.Figure.savefig`.
1177+
1178+
Parameters
1179+
----------
1180+
1181+
figure : :class:`~matplotlib.figure.Figure` or int, optional
1182+
Specifies what figure is saved to file. If not specified, the
1183+
active figure is saved. If a :class:`~matplotlib.figure.Figure`
1184+
instance is provided, this figure is saved. If an int is specified,
1185+
the figure instance to save is looked up by number.
1186+
"""
1187+
if not isinstance(figure, Figure):
1188+
if figure is None:
1189+
manager = Gcf.get_active()
1190+
else:
1191+
manager = Gcf.get_fig_manager(figure)
1192+
if manager is None:
1193+
raise ValueError("No figure {}".format(figure))
1194+
figure = manager.canvas.figure
1195+
1196+
try:
1197+
orig_canvas = figure.canvas
1198+
figure.canvas = FigureCanvasPgf(figure)
1199+
1200+
width, height = figure.get_size_inches()
1201+
if self._n_figures == 0:
1202+
self._write_header(width, height)
1203+
else:
1204+
self._file.write(self._build_newpage_command(width, height))
1205+
1206+
figure.savefig(self._file, format="pgf", **kwargs)
1207+
self._n_figures += 1
1208+
finally:
1209+
figure.canvas = orig_canvas
1210+
1211+
def _build_newpage_command(self, width, height):
1212+
'''LuaLaTeX from version 0.85 removed the `\pdf*` primitives,
1213+
so we need to check the lualatex version and use `\pagewidth` if
1214+
the version is 0.85 or newer
1215+
'''
1216+
texcommand = get_texcommand()
1217+
if texcommand == 'lualatex' and _get_lualatex_version() >= (0, 85, 0):
1218+
cmd = r'\page'
1219+
else:
1220+
cmd = r'\pdfpage'
1221+
1222+
newpage = r'\newpage{cmd}width={w}in,{cmd}height={h}in%' + '\n'
1223+
return newpage.format(cmd=cmd, w=width, h=height).encode('utf-8')
1224+
1225+
def get_pagecount(self):
1226+
"""
1227+
Returns the current number of pages in the multipage pdf file.
1228+
"""
1229+
return self._n_figures

0 commit comments

Comments
 (0)