1105 lines
40 KiB
Python
1105 lines
40 KiB
Python
|
import atexit
|
||
|
import codecs
|
||
|
import datetime
|
||
|
import functools
|
||
|
from io import BytesIO
|
||
|
import logging
|
||
|
import math
|
||
|
import os
|
||
|
import pathlib
|
||
|
import re
|
||
|
import shutil
|
||
|
import subprocess
|
||
|
from tempfile import TemporaryDirectory
|
||
|
import weakref
|
||
|
|
||
|
from PIL import Image
|
||
|
|
||
|
import matplotlib as mpl
|
||
|
from matplotlib import _api, cbook, font_manager as fm
|
||
|
from matplotlib.backend_bases import (
|
||
|
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
|
||
|
GraphicsContextBase, RendererBase, _no_output_draw
|
||
|
)
|
||
|
from matplotlib.backends.backend_mixed import MixedModeRenderer
|
||
|
from matplotlib.backends.backend_pdf import (
|
||
|
_create_pdf_info_dict, _datetime_to_pdf)
|
||
|
from matplotlib.path import Path
|
||
|
from matplotlib.figure import Figure
|
||
|
from matplotlib._pylab_helpers import Gcf
|
||
|
|
||
|
_log = logging.getLogger(__name__)
|
||
|
|
||
|
|
||
|
# Note: When formatting floating point values, it is important to use the
|
||
|
# %f/{:f} format rather than %s/{} to avoid triggering scientific notation,
|
||
|
# which is not recognized by TeX.
|
||
|
|
||
|
|
||
|
def get_fontspec():
|
||
|
"""Build fontspec preamble from rc."""
|
||
|
latex_fontspec = []
|
||
|
texcommand = mpl.rcParams["pgf.texsystem"]
|
||
|
|
||
|
if texcommand != "pdflatex":
|
||
|
latex_fontspec.append("\\usepackage{fontspec}")
|
||
|
|
||
|
if texcommand != "pdflatex" and mpl.rcParams["pgf.rcfonts"]:
|
||
|
families = ["serif", "sans\\-serif", "monospace"]
|
||
|
commands = ["setmainfont", "setsansfont", "setmonofont"]
|
||
|
for family, command in zip(families, commands):
|
||
|
# 1) Forward slashes also work on Windows, so don't mess with
|
||
|
# backslashes. 2) The dirname needs to include a separator.
|
||
|
path = pathlib.Path(fm.findfont(family))
|
||
|
latex_fontspec.append(r"\%s{%s}[Path=\detokenize{%s}]" % (
|
||
|
command, path.name, path.parent.as_posix() + "/"))
|
||
|
|
||
|
return "\n".join(latex_fontspec)
|
||
|
|
||
|
|
||
|
def get_preamble():
|
||
|
"""Get LaTeX preamble from rc."""
|
||
|
return mpl.rcParams["pgf.preamble"]
|
||
|
|
||
|
###############################################################################
|
||
|
|
||
|
# This almost made me cry!!!
|
||
|
# In the end, it's better to use only one unit for all coordinates, since the
|
||
|
# arithmetic in latex seems to produce inaccurate conversions.
|
||
|
latex_pt_to_in = 1. / 72.27
|
||
|
latex_in_to_pt = 1. / latex_pt_to_in
|
||
|
mpl_pt_to_in = 1. / 72.
|
||
|
mpl_in_to_pt = 1. / mpl_pt_to_in
|
||
|
|
||
|
###############################################################################
|
||
|
# helper functions
|
||
|
|
||
|
NO_ESCAPE = r"(?<!\\)(?:\\\\)*"
|
||
|
re_mathsep = re.compile(NO_ESCAPE + r"\$")
|
||
|
|
||
|
|
||
|
_replace_escapetext = functools.partial(
|
||
|
# When the next character is _, ^, $, or % (not preceded by an escape),
|
||
|
# insert a backslash.
|
||
|
re.compile(NO_ESCAPE + "(?=[_^$%])").sub, "\\\\")
|
||
|
_replace_mathdefault = functools.partial(
|
||
|
# Replace \mathdefault (when not preceded by an escape) by empty string.
|
||
|
re.compile(NO_ESCAPE + r"(\\mathdefault)").sub, "")
|
||
|
|
||
|
|
||
|
def common_texification(text):
|
||
|
r"""
|
||
|
Do some necessary and/or useful substitutions for texts to be included in
|
||
|
LaTeX documents.
|
||
|
|
||
|
This distinguishes text-mode and math-mode by replacing the math separator
|
||
|
``$`` with ``\(\displaystyle %s\)``. Escaped math separators (``\$``)
|
||
|
are ignored.
|
||
|
|
||
|
The following characters are escaped in text segments: ``_^$%``
|
||
|
"""
|
||
|
# Sometimes, matplotlib adds the unknown command \mathdefault.
|
||
|
# Not using \mathnormal instead since this looks odd for the latex cm font.
|
||
|
text = _replace_mathdefault(text)
|
||
|
text = text.replace("\N{MINUS SIGN}", r"\ensuremath{-}")
|
||
|
# split text into normaltext and inline math parts
|
||
|
parts = re_mathsep.split(text)
|
||
|
for i, s in enumerate(parts):
|
||
|
if not i % 2:
|
||
|
# textmode replacements
|
||
|
s = _replace_escapetext(s)
|
||
|
else:
|
||
|
# mathmode replacements
|
||
|
s = r"\(\displaystyle %s\)" % s
|
||
|
parts[i] = s
|
||
|
return "".join(parts)
|
||
|
|
||
|
|
||
|
def writeln(fh, line):
|
||
|
# every line of a file included with \\input must be terminated with %
|
||
|
# if not, latex will create additional vertical spaces for some reason
|
||
|
fh.write(line)
|
||
|
fh.write("%\n")
|
||
|
|
||
|
|
||
|
def _font_properties_str(prop):
|
||
|
# translate font properties to latex commands, return as string
|
||
|
commands = []
|
||
|
|
||
|
families = {"serif": r"\rmfamily", "sans": r"\sffamily",
|
||
|
"sans-serif": r"\sffamily", "monospace": r"\ttfamily"}
|
||
|
family = prop.get_family()[0]
|
||
|
if family in families:
|
||
|
commands.append(families[family])
|
||
|
elif (any(font.name == family for font in fm.fontManager.ttflist)
|
||
|
and mpl.rcParams["pgf.texsystem"] != "pdflatex"):
|
||
|
commands.append(r"\setmainfont{%s}\rmfamily" % family)
|
||
|
else:
|
||
|
_log.warning("Ignoring unknown font: %s", family)
|
||
|
|
||
|
size = prop.get_size_in_points()
|
||
|
commands.append(r"\fontsize{%f}{%f}" % (size, size * 1.2))
|
||
|
|
||
|
styles = {"normal": r"", "italic": r"\itshape", "oblique": r"\slshape"}
|
||
|
commands.append(styles[prop.get_style()])
|
||
|
|
||
|
boldstyles = ["semibold", "demibold", "demi", "bold", "heavy",
|
||
|
"extra bold", "black"]
|
||
|
if prop.get_weight() in boldstyles:
|
||
|
commands.append(r"\bfseries")
|
||
|
|
||
|
commands.append(r"\selectfont")
|
||
|
return "".join(commands)
|
||
|
|
||
|
|
||
|
def _metadata_to_str(key, value):
|
||
|
"""Convert metadata key/value to a form that hyperref accepts."""
|
||
|
if isinstance(value, datetime.datetime):
|
||
|
value = _datetime_to_pdf(value)
|
||
|
elif key == 'Trapped':
|
||
|
value = value.name.decode('ascii')
|
||
|
else:
|
||
|
value = str(value)
|
||
|
return f'{key}={{{value}}}'
|
||
|
|
||
|
|
||
|
def make_pdf_to_png_converter():
|
||
|
"""Return a function that converts a pdf file to a png file."""
|
||
|
if shutil.which("pdftocairo"):
|
||
|
def cairo_convert(pdffile, pngfile, dpi):
|
||
|
cmd = ["pdftocairo", "-singlefile", "-png", "-r", "%d" % dpi,
|
||
|
pdffile, os.path.splitext(pngfile)[0]]
|
||
|
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
||
|
return cairo_convert
|
||
|
try:
|
||
|
gs_info = mpl._get_executable_info("gs")
|
||
|
except mpl.ExecutableNotFoundError:
|
||
|
pass
|
||
|
else:
|
||
|
def gs_convert(pdffile, pngfile, dpi):
|
||
|
cmd = [gs_info.executable,
|
||
|
'-dQUIET', '-dSAFER', '-dBATCH', '-dNOPAUSE', '-dNOPROMPT',
|
||
|
'-dUseCIEColor', '-dTextAlphaBits=4',
|
||
|
'-dGraphicsAlphaBits=4', '-dDOINTERPOLATE',
|
||
|
'-sDEVICE=png16m', '-sOutputFile=%s' % pngfile,
|
||
|
'-r%d' % dpi, pdffile]
|
||
|
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
||
|
return gs_convert
|
||
|
raise RuntimeError("No suitable pdf to png renderer found.")
|
||
|
|
||
|
|
||
|
class LatexError(Exception):
|
||
|
def __init__(self, message, latex_output=""):
|
||
|
super().__init__(message)
|
||
|
self.latex_output = latex_output
|
||
|
|
||
|
def __str__(self):
|
||
|
s, = self.args
|
||
|
if self.latex_output:
|
||
|
s += "\n" + self.latex_output
|
||
|
return s
|
||
|
|
||
|
|
||
|
class LatexManager:
|
||
|
"""
|
||
|
The LatexManager opens an instance of the LaTeX application for
|
||
|
determining the metrics of text elements. The LaTeX environment can be
|
||
|
modified by setting fonts and/or a custom preamble in `.rcParams`.
|
||
|
"""
|
||
|
|
||
|
@staticmethod
|
||
|
def _build_latex_header():
|
||
|
latex_preamble = get_preamble()
|
||
|
latex_fontspec = get_fontspec()
|
||
|
# Create LaTeX header with some content, else LaTeX will load some math
|
||
|
# fonts later when we don't expect the additional output on stdout.
|
||
|
# TODO: is this sufficient?
|
||
|
latex_header = [
|
||
|
r"\documentclass{minimal}",
|
||
|
# Include TeX program name as a comment for cache invalidation.
|
||
|
# TeX does not allow this to be the first line.
|
||
|
rf"% !TeX program = {mpl.rcParams['pgf.texsystem']}",
|
||
|
# Test whether \includegraphics supports interpolate option.
|
||
|
r"\usepackage{graphicx}",
|
||
|
latex_preamble,
|
||
|
latex_fontspec,
|
||
|
r"\begin{document}",
|
||
|
r"text $math \mu$", # force latex to load fonts now
|
||
|
r"\typeout{pgf_backend_query_start}",
|
||
|
]
|
||
|
return "\n".join(latex_header)
|
||
|
|
||
|
@classmethod
|
||
|
def _get_cached_or_new(cls):
|
||
|
"""
|
||
|
Return the previous LatexManager if the header and tex system did not
|
||
|
change, or a new instance otherwise.
|
||
|
"""
|
||
|
return cls._get_cached_or_new_impl(cls._build_latex_header())
|
||
|
|
||
|
@classmethod
|
||
|
@functools.lru_cache(1)
|
||
|
def _get_cached_or_new_impl(cls, header): # Helper for _get_cached_or_new.
|
||
|
return cls()
|
||
|
|
||
|
def _stdin_writeln(self, s):
|
||
|
if self.latex is None:
|
||
|
self._setup_latex_process()
|
||
|
self.latex.stdin.write(s)
|
||
|
self.latex.stdin.write("\n")
|
||
|
self.latex.stdin.flush()
|
||
|
|
||
|
def _expect(self, s):
|
||
|
s = list(s)
|
||
|
chars = []
|
||
|
while True:
|
||
|
c = self.latex.stdout.read(1)
|
||
|
chars.append(c)
|
||
|
if chars[-len(s):] == s:
|
||
|
break
|
||
|
if not c:
|
||
|
self.latex.kill()
|
||
|
self.latex = None
|
||
|
raise LatexError("LaTeX process halted", "".join(chars))
|
||
|
return "".join(chars)
|
||
|
|
||
|
def _expect_prompt(self):
|
||
|
return self._expect("\n*")
|
||
|
|
||
|
def __init__(self):
|
||
|
# create a tmp directory for running latex, register it for deletion
|
||
|
self._tmpdir = TemporaryDirectory()
|
||
|
self.tmpdir = self._tmpdir.name
|
||
|
self._finalize_tmpdir = weakref.finalize(self, self._tmpdir.cleanup)
|
||
|
|
||
|
# test the LaTeX setup to ensure a clean startup of the subprocess
|
||
|
self.texcommand = mpl.rcParams["pgf.texsystem"]
|
||
|
self.latex_header = LatexManager._build_latex_header()
|
||
|
latex_end = "\n\\makeatletter\n\\@@end\n"
|
||
|
try:
|
||
|
latex = subprocess.Popen(
|
||
|
[self.texcommand, "-halt-on-error"],
|
||
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||
|
encoding="utf-8", cwd=self.tmpdir)
|
||
|
except FileNotFoundError as err:
|
||
|
raise RuntimeError(
|
||
|
f"{self.texcommand} not found. Install it or change "
|
||
|
f"rcParams['pgf.texsystem'] to an available TeX "
|
||
|
f"implementation.") from err
|
||
|
except OSError as err:
|
||
|
raise RuntimeError("Error starting process %r" %
|
||
|
self.texcommand) from err
|
||
|
test_input = self.latex_header + latex_end
|
||
|
stdout, stderr = latex.communicate(test_input)
|
||
|
if latex.returncode != 0:
|
||
|
raise LatexError("LaTeX returned an error, probably missing font "
|
||
|
"or error in preamble.", stdout)
|
||
|
|
||
|
self.latex = None # Will be set up on first use.
|
||
|
self.str_cache = {} # cache for strings already processed
|
||
|
|
||
|
def _setup_latex_process(self):
|
||
|
# Open LaTeX process for real work; register it for deletion. On
|
||
|
# Windows, we must ensure that the subprocess has quit before being
|
||
|
# able to delete the tmpdir in which it runs; in order to do so, we
|
||
|
# must first `kill()` it, and then `communicate()` with it.
|
||
|
self.latex = subprocess.Popen(
|
||
|
[self.texcommand, "-halt-on-error"],
|
||
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||
|
encoding="utf-8", cwd=self.tmpdir)
|
||
|
|
||
|
def finalize_latex(latex):
|
||
|
latex.kill()
|
||
|
latex.communicate()
|
||
|
|
||
|
self._finalize_latex = weakref.finalize(
|
||
|
self, finalize_latex, self.latex)
|
||
|
# write header with 'pgf_backend_query_start' token
|
||
|
self._stdin_writeln(self._build_latex_header())
|
||
|
# read all lines until our 'pgf_backend_query_start' token appears
|
||
|
self._expect("*pgf_backend_query_start")
|
||
|
self._expect_prompt()
|
||
|
|
||
|
@_api.deprecated("3.3")
|
||
|
def latex_stdin_utf8(self):
|
||
|
return self.latex.stdin
|
||
|
|
||
|
def get_width_height_descent(self, text, prop):
|
||
|
"""
|
||
|
Get the width, total height and descent for a text typeset by the
|
||
|
current LaTeX environment.
|
||
|
"""
|
||
|
|
||
|
# apply font properties and define textbox
|
||
|
prop_cmds = _font_properties_str(prop)
|
||
|
textbox = "\\sbox0{%s %s}" % (prop_cmds, text)
|
||
|
|
||
|
# check cache
|
||
|
if textbox in self.str_cache:
|
||
|
return self.str_cache[textbox]
|
||
|
|
||
|
# send textbox to LaTeX and wait for prompt
|
||
|
self._stdin_writeln(textbox)
|
||
|
try:
|
||
|
self._expect_prompt()
|
||
|
except LatexError as e:
|
||
|
raise ValueError("Error processing '{}'\nLaTeX Output:\n{}"
|
||
|
.format(text, e.latex_output)) from e
|
||
|
|
||
|
# typeout width, height and text offset of the last textbox
|
||
|
self._stdin_writeln(r"\typeout{\the\wd0,\the\ht0,\the\dp0}")
|
||
|
# read answer from latex and advance to the next prompt
|
||
|
try:
|
||
|
answer = self._expect_prompt()
|
||
|
except LatexError as e:
|
||
|
raise ValueError("Error processing '{}'\nLaTeX Output:\n{}"
|
||
|
.format(text, e.latex_output)) from e
|
||
|
|
||
|
# parse metrics from the answer string
|
||
|
try:
|
||
|
width, height, offset = answer.splitlines()[0].split(",")
|
||
|
except Exception as err:
|
||
|
raise ValueError("Error processing '{}'\nLaTeX Output:\n{}"
|
||
|
.format(text, answer)) from err
|
||
|
w, h, o = float(width[:-2]), float(height[:-2]), float(offset[:-2])
|
||
|
|
||
|
# the height returned from LaTeX goes from base to top.
|
||
|
# the height matplotlib expects goes from bottom to top.
|
||
|
self.str_cache[textbox] = (w, h + o, o)
|
||
|
return w, h + o, o
|
||
|
|
||
|
|
||
|
@functools.lru_cache(1)
|
||
|
def _get_image_inclusion_command():
|
||
|
man = LatexManager._get_cached_or_new()
|
||
|
man._stdin_writeln(
|
||
|
r"\includegraphics[interpolate=true]{%s}"
|
||
|
# Don't mess with backslashes on Windows.
|
||
|
% cbook._get_data_path("images/matplotlib.png").as_posix())
|
||
|
try:
|
||
|
prompt = man._expect_prompt()
|
||
|
return r"\includegraphics"
|
||
|
except LatexError:
|
||
|
# Discard the broken manager.
|
||
|
LatexManager._get_cached_or_new_impl.cache_clear()
|
||
|
return r"\pgfimage"
|
||
|
|
||
|
|
||
|
class RendererPgf(RendererBase):
|
||
|
|
||
|
@_api.delete_parameter("3.3", "dummy")
|
||
|
def __init__(self, figure, fh, dummy=False):
|
||
|
"""
|
||
|
Create a new PGF renderer that translates any drawing instruction
|
||
|
into text commands to be interpreted in a latex pgfpicture environment.
|
||
|
|
||
|
Attributes
|
||
|
----------
|
||
|
figure : `matplotlib.figure.Figure`
|
||
|
Matplotlib figure to initialize height, width and dpi from.
|
||
|
fh : file-like
|
||
|
File handle for the output of the drawing commands.
|
||
|
"""
|
||
|
|
||
|
super().__init__()
|
||
|
self.dpi = figure.dpi
|
||
|
self.fh = fh
|
||
|
self.figure = figure
|
||
|
self.image_counter = 0
|
||
|
|
||
|
if dummy:
|
||
|
# dummy==True deactivate all methods
|
||
|
for m in RendererPgf.__dict__:
|
||
|
if m.startswith("draw_"):
|
||
|
self.__dict__[m] = lambda *args, **kwargs: None
|
||
|
|
||
|
def draw_markers(self, gc, marker_path, marker_trans, path, trans,
|
||
|
rgbFace=None):
|
||
|
# docstring inherited
|
||
|
|
||
|
writeln(self.fh, r"\begin{pgfscope}")
|
||
|
|
||
|
# convert from display units to in
|
||
|
f = 1. / self.dpi
|
||
|
|
||
|
# set style and clip
|
||
|
self._print_pgf_clip(gc)
|
||
|
self._print_pgf_path_styles(gc, rgbFace)
|
||
|
|
||
|
# build marker definition
|
||
|
bl, tr = marker_path.get_extents(marker_trans).get_points()
|
||
|
coords = bl[0] * f, bl[1] * f, tr[0] * f, tr[1] * f
|
||
|
writeln(self.fh,
|
||
|
r"\pgfsys@defobject{currentmarker}"
|
||
|
r"{\pgfqpoint{%fin}{%fin}}{\pgfqpoint{%fin}{%fin}}{" % coords)
|
||
|
self._print_pgf_path(None, marker_path, marker_trans)
|
||
|
self._pgf_path_draw(stroke=gc.get_linewidth() != 0.0,
|
||
|
fill=rgbFace is not None)
|
||
|
writeln(self.fh, r"}")
|
||
|
|
||
|
# draw marker for each vertex
|
||
|
for point, code in path.iter_segments(trans, simplify=False):
|
||
|
x, y = point[0] * f, point[1] * f
|
||
|
writeln(self.fh, r"\begin{pgfscope}")
|
||
|
writeln(self.fh, r"\pgfsys@transformshift{%fin}{%fin}" % (x, y))
|
||
|
writeln(self.fh, r"\pgfsys@useobject{currentmarker}{}")
|
||
|
writeln(self.fh, r"\end{pgfscope}")
|
||
|
|
||
|
writeln(self.fh, r"\end{pgfscope}")
|
||
|
|
||
|
def draw_path(self, gc, path, transform, rgbFace=None):
|
||
|
# docstring inherited
|
||
|
writeln(self.fh, r"\begin{pgfscope}")
|
||
|
# draw the path
|
||
|
self._print_pgf_clip(gc)
|
||
|
self._print_pgf_path_styles(gc, rgbFace)
|
||
|
self._print_pgf_path(gc, path, transform, rgbFace)
|
||
|
self._pgf_path_draw(stroke=gc.get_linewidth() != 0.0,
|
||
|
fill=rgbFace is not None)
|
||
|
writeln(self.fh, r"\end{pgfscope}")
|
||
|
|
||
|
# if present, draw pattern on top
|
||
|
if gc.get_hatch():
|
||
|
writeln(self.fh, r"\begin{pgfscope}")
|
||
|
self._print_pgf_path_styles(gc, rgbFace)
|
||
|
|
||
|
# combine clip and path for clipping
|
||
|
self._print_pgf_clip(gc)
|
||
|
self._print_pgf_path(gc, path, transform, rgbFace)
|
||
|
writeln(self.fh, r"\pgfusepath{clip}")
|
||
|
|
||
|
# build pattern definition
|
||
|
writeln(self.fh,
|
||
|
r"\pgfsys@defobject{currentpattern}"
|
||
|
r"{\pgfqpoint{0in}{0in}}{\pgfqpoint{1in}{1in}}{")
|
||
|
writeln(self.fh, r"\begin{pgfscope}")
|
||
|
writeln(self.fh,
|
||
|
r"\pgfpathrectangle"
|
||
|
r"{\pgfqpoint{0in}{0in}}{\pgfqpoint{1in}{1in}}")
|
||
|
writeln(self.fh, r"\pgfusepath{clip}")
|
||
|
scale = mpl.transforms.Affine2D().scale(self.dpi)
|
||
|
self._print_pgf_path(None, gc.get_hatch_path(), scale)
|
||
|
self._pgf_path_draw(stroke=True)
|
||
|
writeln(self.fh, r"\end{pgfscope}")
|
||
|
writeln(self.fh, r"}")
|
||
|
# repeat pattern, filling the bounding rect of the path
|
||
|
f = 1. / self.dpi
|
||
|
(xmin, ymin), (xmax, ymax) = \
|
||
|
path.get_extents(transform).get_points()
|
||
|
xmin, xmax = f * xmin, f * xmax
|
||
|
ymin, ymax = f * ymin, f * ymax
|
||
|
repx, repy = math.ceil(xmax - xmin), math.ceil(ymax - ymin)
|
||
|
writeln(self.fh,
|
||
|
r"\pgfsys@transformshift{%fin}{%fin}" % (xmin, ymin))
|
||
|
for iy in range(repy):
|
||
|
for ix in range(repx):
|
||
|
writeln(self.fh, r"\pgfsys@useobject{currentpattern}{}")
|
||
|
writeln(self.fh, r"\pgfsys@transformshift{1in}{0in}")
|
||
|
writeln(self.fh, r"\pgfsys@transformshift{-%din}{0in}" % repx)
|
||
|
writeln(self.fh, r"\pgfsys@transformshift{0in}{1in}")
|
||
|
|
||
|
writeln(self.fh, r"\end{pgfscope}")
|
||
|
|
||
|
def _print_pgf_clip(self, gc):
|
||
|
f = 1. / self.dpi
|
||
|
# check for clip box
|
||
|
bbox = gc.get_clip_rectangle()
|
||
|
if bbox:
|
||
|
p1, p2 = bbox.get_points()
|
||
|
w, h = p2 - p1
|
||
|
coords = p1[0] * f, p1[1] * f, w * f, h * f
|
||
|
writeln(self.fh,
|
||
|
r"\pgfpathrectangle"
|
||
|
r"{\pgfqpoint{%fin}{%fin}}{\pgfqpoint{%fin}{%fin}}"
|
||
|
% coords)
|
||
|
writeln(self.fh, r"\pgfusepath{clip}")
|
||
|
|
||
|
# check for clip path
|
||
|
clippath, clippath_trans = gc.get_clip_path()
|
||
|
if clippath is not None:
|
||
|
self._print_pgf_path(gc, clippath, clippath_trans)
|
||
|
writeln(self.fh, r"\pgfusepath{clip}")
|
||
|
|
||
|
def _print_pgf_path_styles(self, gc, rgbFace):
|
||
|
# cap style
|
||
|
capstyles = {"butt": r"\pgfsetbuttcap",
|
||
|
"round": r"\pgfsetroundcap",
|
||
|
"projecting": r"\pgfsetrectcap"}
|
||
|
writeln(self.fh, capstyles[gc.get_capstyle()])
|
||
|
|
||
|
# join style
|
||
|
joinstyles = {"miter": r"\pgfsetmiterjoin",
|
||
|
"round": r"\pgfsetroundjoin",
|
||
|
"bevel": r"\pgfsetbeveljoin"}
|
||
|
writeln(self.fh, joinstyles[gc.get_joinstyle()])
|
||
|
|
||
|
# filling
|
||
|
has_fill = rgbFace is not None
|
||
|
|
||
|
if gc.get_forced_alpha():
|
||
|
fillopacity = strokeopacity = gc.get_alpha()
|
||
|
else:
|
||
|
strokeopacity = gc.get_rgb()[3]
|
||
|
fillopacity = rgbFace[3] if has_fill and len(rgbFace) > 3 else 1.0
|
||
|
|
||
|
if has_fill:
|
||
|
writeln(self.fh,
|
||
|
r"\definecolor{currentfill}{rgb}{%f,%f,%f}"
|
||
|
% tuple(rgbFace[:3]))
|
||
|
writeln(self.fh, r"\pgfsetfillcolor{currentfill}")
|
||
|
if has_fill and fillopacity != 1.0:
|
||
|
writeln(self.fh, r"\pgfsetfillopacity{%f}" % fillopacity)
|
||
|
|
||
|
# linewidth and color
|
||
|
lw = gc.get_linewidth() * mpl_pt_to_in * latex_in_to_pt
|
||
|
stroke_rgba = gc.get_rgb()
|
||
|
writeln(self.fh, r"\pgfsetlinewidth{%fpt}" % lw)
|
||
|
writeln(self.fh,
|
||
|
r"\definecolor{currentstroke}{rgb}{%f,%f,%f}"
|
||
|
% stroke_rgba[:3])
|
||
|
writeln(self.fh, r"\pgfsetstrokecolor{currentstroke}")
|
||
|
if strokeopacity != 1.0:
|
||
|
writeln(self.fh, r"\pgfsetstrokeopacity{%f}" % strokeopacity)
|
||
|
|
||
|
# line style
|
||
|
dash_offset, dash_list = gc.get_dashes()
|
||
|
if dash_list is None:
|
||
|
writeln(self.fh, r"\pgfsetdash{}{0pt}")
|
||
|
else:
|
||
|
writeln(self.fh,
|
||
|
r"\pgfsetdash{%s}{%fpt}"
|
||
|
% ("".join(r"{%fpt}" % dash for dash in dash_list),
|
||
|
dash_offset))
|
||
|
|
||
|
def _print_pgf_path(self, gc, path, transform, rgbFace=None):
|
||
|
f = 1. / self.dpi
|
||
|
# check for clip box / ignore clip for filled paths
|
||
|
bbox = gc.get_clip_rectangle() if gc else None
|
||
|
if bbox and (rgbFace is None):
|
||
|
p1, p2 = bbox.get_points()
|
||
|
clip = (p1[0], p1[1], p2[0], p2[1])
|
||
|
else:
|
||
|
clip = None
|
||
|
# build path
|
||
|
for points, code in path.iter_segments(transform, clip=clip):
|
||
|
if code == Path.MOVETO:
|
||
|
x, y = tuple(points)
|
||
|
writeln(self.fh,
|
||
|
r"\pgfpathmoveto{\pgfqpoint{%fin}{%fin}}" %
|
||
|
(f * x, f * y))
|
||
|
elif code == Path.CLOSEPOLY:
|
||
|
writeln(self.fh, r"\pgfpathclose")
|
||
|
elif code == Path.LINETO:
|
||
|
x, y = tuple(points)
|
||
|
writeln(self.fh,
|
||
|
r"\pgfpathlineto{\pgfqpoint{%fin}{%fin}}" %
|
||
|
(f * x, f * y))
|
||
|
elif code == Path.CURVE3:
|
||
|
cx, cy, px, py = tuple(points)
|
||
|
coords = cx * f, cy * f, px * f, py * f
|
||
|
writeln(self.fh,
|
||
|
r"\pgfpathquadraticcurveto"
|
||
|
r"{\pgfqpoint{%fin}{%fin}}{\pgfqpoint{%fin}{%fin}}"
|
||
|
% coords)
|
||
|
elif code == Path.CURVE4:
|
||
|
c1x, c1y, c2x, c2y, px, py = tuple(points)
|
||
|
coords = c1x * f, c1y * f, c2x * f, c2y * f, px * f, py * f
|
||
|
writeln(self.fh,
|
||
|
r"\pgfpathcurveto"
|
||
|
r"{\pgfqpoint{%fin}{%fin}}"
|
||
|
r"{\pgfqpoint{%fin}{%fin}}"
|
||
|
r"{\pgfqpoint{%fin}{%fin}}"
|
||
|
% coords)
|
||
|
|
||
|
def _pgf_path_draw(self, stroke=True, fill=False):
|
||
|
actions = []
|
||
|
if stroke:
|
||
|
actions.append("stroke")
|
||
|
if fill:
|
||
|
actions.append("fill")
|
||
|
writeln(self.fh, r"\pgfusepath{%s}" % ",".join(actions))
|
||
|
|
||
|
def option_scale_image(self):
|
||
|
# docstring inherited
|
||
|
return True
|
||
|
|
||
|
def option_image_nocomposite(self):
|
||
|
# docstring inherited
|
||
|
return not mpl.rcParams['image.composite_image']
|
||
|
|
||
|
def draw_image(self, gc, x, y, im, transform=None):
|
||
|
# docstring inherited
|
||
|
|
||
|
h, w = im.shape[:2]
|
||
|
if w == 0 or h == 0:
|
||
|
return
|
||
|
|
||
|
if not os.path.exists(getattr(self.fh, "name", "")):
|
||
|
_api.warn_external(
|
||
|
"streamed pgf-code does not support raster graphics, consider "
|
||
|
"using the pgf-to-pdf option.")
|
||
|
|
||
|
# save the images to png files
|
||
|
path = pathlib.Path(self.fh.name)
|
||
|
fname_img = "%s-img%d.png" % (path.stem, self.image_counter)
|
||
|
Image.fromarray(im[::-1]).save(path.parent / fname_img)
|
||
|
self.image_counter += 1
|
||
|
|
||
|
# reference the image in the pgf picture
|
||
|
writeln(self.fh, r"\begin{pgfscope}")
|
||
|
self._print_pgf_clip(gc)
|
||
|
f = 1. / self.dpi # from display coords to inch
|
||
|
if transform is None:
|
||
|
writeln(self.fh,
|
||
|
r"\pgfsys@transformshift{%fin}{%fin}" % (x * f, y * f))
|
||
|
w, h = w * f, h * f
|
||
|
else:
|
||
|
tr1, tr2, tr3, tr4, tr5, tr6 = transform.frozen().to_values()
|
||
|
writeln(self.fh,
|
||
|
r"\pgfsys@transformcm{%f}{%f}{%f}{%f}{%fin}{%fin}" %
|
||
|
(tr1 * f, tr2 * f, tr3 * f, tr4 * f,
|
||
|
(tr5 + x) * f, (tr6 + y) * f))
|
||
|
w = h = 1 # scale is already included in the transform
|
||
|
interp = str(transform is None).lower() # interpolation in PDF reader
|
||
|
writeln(self.fh,
|
||
|
r"\pgftext[left,bottom]"
|
||
|
r"{%s[interpolate=%s,width=%fin,height=%fin]{%s}}" %
|
||
|
(_get_image_inclusion_command(),
|
||
|
interp, w, h, fname_img))
|
||
|
writeln(self.fh, r"\end{pgfscope}")
|
||
|
|
||
|
def draw_tex(self, gc, x, y, s, prop, angle, ismath="TeX!", mtext=None):
|
||
|
# docstring inherited
|
||
|
self.draw_text(gc, x, y, s, prop, angle, ismath, mtext)
|
||
|
|
||
|
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
|
||
|
# docstring inherited
|
||
|
|
||
|
# prepare string for tex
|
||
|
s = common_texification(s)
|
||
|
prop_cmds = _font_properties_str(prop)
|
||
|
s = r"%s %s" % (prop_cmds, s)
|
||
|
|
||
|
writeln(self.fh, r"\begin{pgfscope}")
|
||
|
|
||
|
alpha = gc.get_alpha()
|
||
|
if alpha != 1.0:
|
||
|
writeln(self.fh, r"\pgfsetfillopacity{%f}" % alpha)
|
||
|
writeln(self.fh, r"\pgfsetstrokeopacity{%f}" % alpha)
|
||
|
rgb = tuple(gc.get_rgb())[:3]
|
||
|
writeln(self.fh, r"\definecolor{textcolor}{rgb}{%f,%f,%f}" % rgb)
|
||
|
writeln(self.fh, r"\pgfsetstrokecolor{textcolor}")
|
||
|
writeln(self.fh, r"\pgfsetfillcolor{textcolor}")
|
||
|
s = r"\color{textcolor}" + s
|
||
|
|
||
|
dpi = self.figure.dpi
|
||
|
text_args = []
|
||
|
if mtext and (
|
||
|
(angle == 0 or
|
||
|
mtext.get_rotation_mode() == "anchor") and
|
||
|
mtext.get_verticalalignment() != "center_baseline"):
|
||
|
# if text anchoring can be supported, get the original coordinates
|
||
|
# and add alignment information
|
||
|
pos = mtext.get_unitless_position()
|
||
|
x, y = mtext.get_transform().transform(pos)
|
||
|
halign = {"left": "left", "right": "right", "center": ""}
|
||
|
valign = {"top": "top", "bottom": "bottom",
|
||
|
"baseline": "base", "center": ""}
|
||
|
text_args.extend([
|
||
|
f"x={x/dpi:f}in",
|
||
|
f"y={y/dpi:f}in",
|
||
|
halign[mtext.get_horizontalalignment()],
|
||
|
valign[mtext.get_verticalalignment()],
|
||
|
])
|
||
|
else:
|
||
|
# if not, use the text layout provided by Matplotlib.
|
||
|
text_args.append(f"x={x/dpi:f}in, y={y/dpi:f}in, left, base")
|
||
|
|
||
|
if angle != 0:
|
||
|
text_args.append("rotate=%f" % angle)
|
||
|
|
||
|
writeln(self.fh, r"\pgftext[%s]{%s}" % (",".join(text_args), s))
|
||
|
writeln(self.fh, r"\end{pgfscope}")
|
||
|
|
||
|
def get_text_width_height_descent(self, s, prop, ismath):
|
||
|
# docstring inherited
|
||
|
|
||
|
# check if the math is supposed to be displaystyled
|
||
|
s = common_texification(s)
|
||
|
|
||
|
# get text metrics in units of latex pt, convert to display units
|
||
|
w, h, d = (LatexManager._get_cached_or_new()
|
||
|
.get_width_height_descent(s, prop))
|
||
|
# TODO: this should be latex_pt_to_in instead of mpl_pt_to_in
|
||
|
# but having a little bit more space around the text looks better,
|
||
|
# plus the bounding box reported by LaTeX is VERY narrow
|
||
|
f = mpl_pt_to_in * self.dpi
|
||
|
return w * f, h * f, d * f
|
||
|
|
||
|
def flipy(self):
|
||
|
# docstring inherited
|
||
|
return False
|
||
|
|
||
|
def get_canvas_width_height(self):
|
||
|
# docstring inherited
|
||
|
return (self.figure.get_figwidth() * self.dpi,
|
||
|
self.figure.get_figheight() * self.dpi)
|
||
|
|
||
|
def points_to_pixels(self, points):
|
||
|
# docstring inherited
|
||
|
return points * mpl_pt_to_in * self.dpi
|
||
|
|
||
|
|
||
|
@_api.deprecated("3.3", alternative="GraphicsContextBase")
|
||
|
class GraphicsContextPgf(GraphicsContextBase):
|
||
|
pass
|
||
|
|
||
|
|
||
|
@_api.deprecated("3.4")
|
||
|
class TmpDirCleaner:
|
||
|
_remaining_tmpdirs = set()
|
||
|
|
||
|
@_api.classproperty
|
||
|
@_api.deprecated("3.4")
|
||
|
def remaining_tmpdirs(cls):
|
||
|
return cls._remaining_tmpdirs
|
||
|
|
||
|
@staticmethod
|
||
|
@_api.deprecated("3.4")
|
||
|
def add(tmpdir):
|
||
|
TmpDirCleaner._remaining_tmpdirs.add(tmpdir)
|
||
|
|
||
|
@staticmethod
|
||
|
@_api.deprecated("3.4")
|
||
|
@atexit.register
|
||
|
def cleanup_remaining_tmpdirs():
|
||
|
for tmpdir in TmpDirCleaner._remaining_tmpdirs:
|
||
|
error_message = "error deleting tmp directory {}".format(tmpdir)
|
||
|
shutil.rmtree(
|
||
|
tmpdir,
|
||
|
onerror=lambda *args: _log.error(error_message))
|
||
|
|
||
|
|
||
|
class FigureCanvasPgf(FigureCanvasBase):
|
||
|
filetypes = {"pgf": "LaTeX PGF picture",
|
||
|
"pdf": "LaTeX compiled PGF picture",
|
||
|
"png": "Portable Network Graphics", }
|
||
|
|
||
|
def get_default_filetype(self):
|
||
|
return 'pdf'
|
||
|
|
||
|
@_check_savefig_extra_args
|
||
|
def _print_pgf_to_fh(self, fh, *, bbox_inches_restore=None):
|
||
|
|
||
|
header_text = """%% Creator: Matplotlib, PGF backend
|
||
|
%%
|
||
|
%% To include the figure in your LaTeX document, write
|
||
|
%% \\input{<filename>.pgf}
|
||
|
%%
|
||
|
%% Make sure the required packages are loaded in your preamble
|
||
|
%% \\usepackage{pgf}
|
||
|
%%
|
||
|
%% Figures using additional raster images can only be included by \\input if
|
||
|
%% they are in the same directory as the main LaTeX file. For loading figures
|
||
|
%% from other directories you can use the `import` package
|
||
|
%% \\usepackage{import}
|
||
|
%%
|
||
|
%% and then include the figures with
|
||
|
%% \\import{<path to file>}{<filename>.pgf}
|
||
|
%%
|
||
|
"""
|
||
|
|
||
|
# append the preamble used by the backend as a comment for debugging
|
||
|
header_info_preamble = ["%% Matplotlib used the following preamble"]
|
||
|
for line in get_preamble().splitlines():
|
||
|
header_info_preamble.append("%% " + line)
|
||
|
for line in get_fontspec().splitlines():
|
||
|
header_info_preamble.append("%% " + line)
|
||
|
header_info_preamble.append("%%")
|
||
|
header_info_preamble = "\n".join(header_info_preamble)
|
||
|
|
||
|
# get figure size in inch
|
||
|
w, h = self.figure.get_figwidth(), self.figure.get_figheight()
|
||
|
dpi = self.figure.get_dpi()
|
||
|
|
||
|
# create pgfpicture environment and write the pgf code
|
||
|
fh.write(header_text)
|
||
|
fh.write(header_info_preamble)
|
||
|
fh.write("\n")
|
||
|
writeln(fh, r"\begingroup")
|
||
|
writeln(fh, r"\makeatletter")
|
||
|
writeln(fh, r"\begin{pgfpicture}")
|
||
|
writeln(fh,
|
||
|
r"\pgfpathrectangle{\pgfpointorigin}{\pgfqpoint{%fin}{%fin}}"
|
||
|
% (w, h))
|
||
|
writeln(fh, r"\pgfusepath{use as bounding box, clip}")
|
||
|
renderer = MixedModeRenderer(self.figure, w, h, dpi,
|
||
|
RendererPgf(self.figure, fh),
|
||
|
bbox_inches_restore=bbox_inches_restore)
|
||
|
self.figure.draw(renderer)
|
||
|
|
||
|
# end the pgfpicture environment
|
||
|
writeln(fh, r"\end{pgfpicture}")
|
||
|
writeln(fh, r"\makeatother")
|
||
|
writeln(fh, r"\endgroup")
|
||
|
|
||
|
def print_pgf(self, fname_or_fh, *args, **kwargs):
|
||
|
"""
|
||
|
Output pgf macros for drawing the figure so it can be included and
|
||
|
rendered in latex documents.
|
||
|
"""
|
||
|
with cbook.open_file_cm(fname_or_fh, "w", encoding="utf-8") as file:
|
||
|
if not cbook.file_requires_unicode(file):
|
||
|
file = codecs.getwriter("utf-8")(file)
|
||
|
self._print_pgf_to_fh(file, *args, **kwargs)
|
||
|
|
||
|
def print_pdf(self, fname_or_fh, *args, metadata=None, **kwargs):
|
||
|
"""Use LaTeX to compile a pgf generated figure to pdf."""
|
||
|
w, h = self.figure.get_figwidth(), self.figure.get_figheight()
|
||
|
|
||
|
info_dict = _create_pdf_info_dict('pgf', metadata or {})
|
||
|
hyperref_options = ','.join(
|
||
|
_metadata_to_str(k, v) for k, v in info_dict.items())
|
||
|
|
||
|
with TemporaryDirectory() as tmpdir:
|
||
|
tmppath = pathlib.Path(tmpdir)
|
||
|
|
||
|
# print figure to pgf and compile it with latex
|
||
|
self.print_pgf(tmppath / "figure.pgf", *args, **kwargs)
|
||
|
|
||
|
latexcode = """
|
||
|
\\PassOptionsToPackage{pdfinfo={%s}}{hyperref}
|
||
|
\\RequirePackage{hyperref}
|
||
|
\\documentclass[12pt]{minimal}
|
||
|
\\usepackage[paperwidth=%fin, paperheight=%fin, margin=0in]{geometry}
|
||
|
%s
|
||
|
%s
|
||
|
\\usepackage{pgf}
|
||
|
|
||
|
\\begin{document}
|
||
|
\\centering
|
||
|
\\input{figure.pgf}
|
||
|
\\end{document}""" % (hyperref_options, w, h, get_preamble(), get_fontspec())
|
||
|
(tmppath / "figure.tex").write_text(latexcode, encoding="utf-8")
|
||
|
|
||
|
texcommand = mpl.rcParams["pgf.texsystem"]
|
||
|
cbook._check_and_log_subprocess(
|
||
|
[texcommand, "-interaction=nonstopmode", "-halt-on-error",
|
||
|
"figure.tex"], _log, cwd=tmpdir)
|
||
|
|
||
|
with (tmppath / "figure.pdf").open("rb") as orig, \
|
||
|
cbook.open_file_cm(fname_or_fh, "wb") as dest:
|
||
|
shutil.copyfileobj(orig, dest) # copy file contents to target
|
||
|
|
||
|
def print_png(self, fname_or_fh, *args, **kwargs):
|
||
|
"""Use LaTeX to compile a pgf figure to pdf and convert it to png."""
|
||
|
converter = make_pdf_to_png_converter()
|
||
|
with TemporaryDirectory() as tmpdir:
|
||
|
tmppath = pathlib.Path(tmpdir)
|
||
|
pdf_path = tmppath / "figure.pdf"
|
||
|
png_path = tmppath / "figure.png"
|
||
|
self.print_pdf(pdf_path, *args, **kwargs)
|
||
|
converter(pdf_path, png_path, dpi=self.figure.dpi)
|
||
|
with png_path.open("rb") as orig, \
|
||
|
cbook.open_file_cm(fname_or_fh, "wb") as dest:
|
||
|
shutil.copyfileobj(orig, dest) # copy file contents to target
|
||
|
|
||
|
def get_renderer(self):
|
||
|
return RendererPgf(self.figure, None)
|
||
|
|
||
|
def draw(self):
|
||
|
_no_output_draw(self.figure)
|
||
|
return super().draw()
|
||
|
|
||
|
|
||
|
FigureManagerPgf = FigureManagerBase
|
||
|
|
||
|
|
||
|
@_Backend.export
|
||
|
class _BackendPgf(_Backend):
|
||
|
FigureCanvas = FigureCanvasPgf
|
||
|
|
||
|
|
||
|
class PdfPages:
|
||
|
"""
|
||
|
A multi-page PDF file using the pgf backend
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
>>> import matplotlib.pyplot as plt
|
||
|
>>> # Initialize:
|
||
|
>>> with PdfPages('foo.pdf') as pdf:
|
||
|
... # As many times as you like, create a figure fig and save it:
|
||
|
... fig = plt.figure()
|
||
|
... pdf.savefig(fig)
|
||
|
... # When no figure is specified the current figure is saved
|
||
|
... pdf.savefig()
|
||
|
"""
|
||
|
__slots__ = (
|
||
|
'_output_name',
|
||
|
'keep_empty',
|
||
|
'_n_figures',
|
||
|
'_file',
|
||
|
'_info_dict',
|
||
|
'_metadata',
|
||
|
)
|
||
|
metadata = _api.deprecated('3.3')(property(lambda self: self._metadata))
|
||
|
|
||
|
def __init__(self, filename, *, keep_empty=True, metadata=None):
|
||
|
"""
|
||
|
Create a new PdfPages object.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
filename : str or path-like
|
||
|
Plots using `PdfPages.savefig` will be written to a file at this
|
||
|
location. Any older file with the same name is overwritten.
|
||
|
|
||
|
keep_empty : bool, default: True
|
||
|
If set to False, then empty pdf files will be deleted automatically
|
||
|
when closed.
|
||
|
|
||
|
metadata : dict, optional
|
||
|
Information dictionary object (see PDF reference section 10.2.1
|
||
|
'Document Information Dictionary'), e.g.:
|
||
|
``{'Creator': 'My software', 'Author': 'Me', 'Title': 'Awesome'}``.
|
||
|
|
||
|
The standard keys are 'Title', 'Author', 'Subject', 'Keywords',
|
||
|
'Creator', 'Producer', 'CreationDate', 'ModDate', and
|
||
|
'Trapped'. Values have been predefined for 'Creator', 'Producer'
|
||
|
and 'CreationDate'. They can be removed by setting them to `None`.
|
||
|
"""
|
||
|
self._output_name = filename
|
||
|
self._n_figures = 0
|
||
|
self.keep_empty = keep_empty
|
||
|
self._metadata = (metadata or {}).copy()
|
||
|
if metadata:
|
||
|
for key in metadata:
|
||
|
canonical = {
|
||
|
'creationdate': 'CreationDate',
|
||
|
'moddate': 'ModDate',
|
||
|
}.get(key.lower(), key.lower().title())
|
||
|
if canonical != key:
|
||
|
_api.warn_deprecated(
|
||
|
'3.3', message='Support for setting PDF metadata keys '
|
||
|
'case-insensitively is deprecated since %(since)s and '
|
||
|
'will be removed %(removal)s; '
|
||
|
f'set {canonical} instead of {key}.')
|
||
|
self._metadata[canonical] = self._metadata.pop(key)
|
||
|
self._info_dict = _create_pdf_info_dict('pgf', self._metadata)
|
||
|
self._file = BytesIO()
|
||
|
|
||
|
def _write_header(self, width_inches, height_inches):
|
||
|
hyperref_options = ','.join(
|
||
|
_metadata_to_str(k, v) for k, v in self._info_dict.items())
|
||
|
|
||
|
latex_preamble = get_preamble()
|
||
|
latex_fontspec = get_fontspec()
|
||
|
latex_header = r"""\PassOptionsToPackage{{
|
||
|
pdfinfo={{
|
||
|
{metadata}
|
||
|
}}
|
||
|
}}{{hyperref}}
|
||
|
\RequirePackage{{hyperref}}
|
||
|
\documentclass[12pt]{{minimal}}
|
||
|
\usepackage[
|
||
|
paperwidth={width}in,
|
||
|
paperheight={height}in,
|
||
|
margin=0in
|
||
|
]{{geometry}}
|
||
|
{preamble}
|
||
|
{fontspec}
|
||
|
\usepackage{{pgf}}
|
||
|
\setlength{{\parindent}}{{0pt}}
|
||
|
|
||
|
\begin{{document}}%%
|
||
|
""".format(
|
||
|
width=width_inches,
|
||
|
height=height_inches,
|
||
|
preamble=latex_preamble,
|
||
|
fontspec=latex_fontspec,
|
||
|
metadata=hyperref_options,
|
||
|
)
|
||
|
self._file.write(latex_header.encode('utf-8'))
|
||
|
|
||
|
def __enter__(self):
|
||
|
return self
|
||
|
|
||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||
|
self.close()
|
||
|
|
||
|
def close(self):
|
||
|
"""
|
||
|
Finalize this object, running LaTeX in a temporary directory
|
||
|
and moving the final pdf file to *filename*.
|
||
|
"""
|
||
|
self._file.write(rb'\end{document}\n')
|
||
|
if self._n_figures > 0:
|
||
|
self._run_latex()
|
||
|
elif self.keep_empty:
|
||
|
open(self._output_name, 'wb').close()
|
||
|
self._file.close()
|
||
|
|
||
|
def _run_latex(self):
|
||
|
texcommand = mpl.rcParams["pgf.texsystem"]
|
||
|
with TemporaryDirectory() as tmpdir:
|
||
|
tex_source = pathlib.Path(tmpdir, "pdf_pages.tex")
|
||
|
tex_source.write_bytes(self._file.getvalue())
|
||
|
cbook._check_and_log_subprocess(
|
||
|
[texcommand, "-interaction=nonstopmode", "-halt-on-error",
|
||
|
tex_source],
|
||
|
_log, cwd=tmpdir)
|
||
|
shutil.move(tex_source.with_suffix(".pdf"), self._output_name)
|
||
|
|
||
|
def savefig(self, figure=None, **kwargs):
|
||
|
"""
|
||
|
Save a `.Figure` to this file as a new page.
|
||
|
|
||
|
Any other keyword arguments are passed to `~.Figure.savefig`.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
figure : `.Figure` or int, optional
|
||
|
Specifies what figure is saved to file. If not specified, the
|
||
|
active figure is saved. If a `.Figure` instance is provided, this
|
||
|
figure is saved. If an int is specified, the figure instance to
|
||
|
save is looked up by number.
|
||
|
"""
|
||
|
if not isinstance(figure, Figure):
|
||
|
if figure is None:
|
||
|
manager = Gcf.get_active()
|
||
|
else:
|
||
|
manager = Gcf.get_fig_manager(figure)
|
||
|
if manager is None:
|
||
|
raise ValueError("No figure {}".format(figure))
|
||
|
figure = manager.canvas.figure
|
||
|
|
||
|
try:
|
||
|
orig_canvas = figure.canvas
|
||
|
figure.canvas = FigureCanvasPgf(figure)
|
||
|
|
||
|
width, height = figure.get_size_inches()
|
||
|
if self._n_figures == 0:
|
||
|
self._write_header(width, height)
|
||
|
else:
|
||
|
# \pdfpagewidth and \pdfpageheight exist on pdftex, xetex, and
|
||
|
# luatex<0.85; they were renamed to \pagewidth and \pageheight
|
||
|
# on luatex>=0.85.
|
||
|
self._file.write(
|
||
|
br'\newpage'
|
||
|
br'\ifdefined\pdfpagewidth\pdfpagewidth'
|
||
|
br'\else\pagewidth\fi=%ain'
|
||
|
br'\ifdefined\pdfpageheight\pdfpageheight'
|
||
|
br'\else\pageheight\fi=%ain'
|
||
|
b'%%\n' % (width, height)
|
||
|
)
|
||
|
|
||
|
figure.savefig(self._file, format="pgf", **kwargs)
|
||
|
self._n_figures += 1
|
||
|
finally:
|
||
|
figure.canvas = orig_canvas
|
||
|
|
||
|
def get_pagecount(self):
|
||
|
"""Return the current number of pages in the multipage pdf file."""
|
||
|
return self._n_figures
|