|
17 | 17 | import weakref
|
18 | 18 |
|
19 | 19 | import matplotlib as mpl
|
20 |
| -from matplotlib import _png, rcParams |
| 20 | +from matplotlib import _png, rcParams, __version__ |
21 | 21 | from matplotlib.backend_bases import (
|
22 | 22 | _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase,
|
23 | 23 | RendererBase)
|
24 | 24 | from matplotlib.backends.backend_mixed import MixedModeRenderer
|
25 | 25 | from matplotlib.cbook import is_writable_file_like
|
26 | 26 | from matplotlib.path import Path
|
| 27 | +from matplotlib.figure import Figure |
| 28 | +from matplotlib._pylab_helpers import Gcf |
27 | 29 |
|
28 | 30 |
|
29 | 31 | ###############################################################################
|
|
50 | 52 | warnings.warn('error getting fonts from fc-list', UserWarning)
|
51 | 53 |
|
52 | 54 |
|
| 55 | +_luatex_version_re = re.compile( |
| 56 | + 'This is LuaTeX, Version (?:beta-)?([0-9]+)\.([0-9]+)\.([0-9]+)' |
| 57 | +) |
| 58 | + |
| 59 | + |
53 | 60 | def get_texcommand():
|
54 | 61 | """Get chosen TeX system from rc."""
|
55 | 62 | texsystem_options = ["xelatex", "lualatex", "pdflatex"]
|
56 | 63 | texsystem = rcParams["pgf.texsystem"]
|
57 | 64 | return texsystem if texsystem in texsystem_options else "xelatex"
|
58 | 65 |
|
59 | 66 |
|
| 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 | + |
60 | 79 | def get_fontspec():
|
61 | 80 | """Build fontspec preamble from rc."""
|
62 | 81 | latex_fontspec = []
|
@@ -990,4 +1009,221 @@ def _cleanup_all():
|
990 | 1009 | LatexManager._cleanup_remaining_instances()
|
991 | 1010 | TmpDirCleaner.cleanup_remaining_tmpdirs()
|
992 | 1011 |
|
| 1012 | + |
993 | 1013 | 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