123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188 |
- import atexit
- import codecs
- import datetime
- import functools
- import logging
- import math
- import os
- import pathlib
- import re
- import shutil
- import subprocess
- import sys
- import tempfile
- import weakref
- from PIL import Image
- import matplotlib as mpl
- from matplotlib import cbook, font_manager as fm
- from matplotlib.backend_bases import (
- _Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
- GraphicsContextBase, RendererBase)
- 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=%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"\$")
- @cbook.deprecated("3.2")
- def repl_escapetext(m):
- return "\\" + m.group(1)
- @cbook.deprecated("3.2")
- def repl_mathdefault(m):
- return m.group(0)[:-len(m.group(1))]
- _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)
- # 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:
- pass # print warning?
- 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
- 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`.
- """
- _unclean_instances = weakref.WeakSet()
- @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()
- @staticmethod
- def _cleanup_remaining_instances():
- unclean_instances = list(LatexManager._unclean_instances)
- for latex_manager in unclean_instances:
- latex_manager._cleanup()
- 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):
- # store references for __del__
- self._os_path = os.path
- self._shutil = shutil
- # create a tmp directory for running latex, remember to cleanup
- self.tmpdir = tempfile.mkdtemp(prefix="mpl_pgf_lm_")
- LatexManager._unclean_instances.add(self)
- # 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:\n%s" % 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
- self.latex = subprocess.Popen(
- [self.texcommand, "-halt-on-error"],
- stdin=subprocess.PIPE, stdout=subprocess.PIPE,
- encoding="utf-8", cwd=self.tmpdir)
- # 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()
- @cbook.deprecated("3.3")
- def latex_stdin_utf8(self):
- return self.latex.stdin
- def _cleanup(self):
- if not self._os_path.isdir(self.tmpdir):
- return
- try:
- self.latex.communicate()
- except Exception:
- pass
- try:
- self._shutil.rmtree(self.tmpdir)
- LatexManager._unclean_instances.discard(self)
- except Exception:
- sys.stderr.write("error deleting tmp directory %s\n" % self.tmpdir)
- def __del__(self):
- _log.debug("deleting LatexManager")
- self._cleanup()
- 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):
- @cbook._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.
- """
- RendererBase.__init__(self)
- self.dpi = figure.dpi
- self.fh = fh
- self.figure = figure
- self.image_counter = 0
- self._latexManager = LatexManager._get_cached_or_new() # deprecated
- if dummy:
- # dummy==True deactivate all methods
- for m in RendererPgf.__dict__:
- if m.startswith("draw_"):
- self.__dict__[m] = lambda *args, **kwargs: None
- latexManager = cbook._deprecate_privatize_attribute("3.2")
- 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", "")):
- cbook._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
- @cbook.deprecated("3.3", alternative="GraphicsContextBase")
- class GraphicsContextPgf(GraphicsContextBase):
- pass
- class TmpDirCleaner:
- remaining_tmpdirs = set()
- @staticmethod
- def add(tmpdir):
- TmpDirCleaner.remaining_tmpdirs.add(tmpdir)
- @staticmethod
- 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
- @cbook._delete_parameter("3.2", "dryrun")
- def _print_pgf_to_fh(self, fh, *,
- dryrun=False, bbox_inches_restore=None):
- if dryrun:
- renderer = RendererPgf(self.figure, None, dummy=True)
- self.figure.draw(renderer)
- return
- 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}
- %%
- %% and, on pdftex
- %% \\usepackage[utf8]{inputenc}\\DeclareUnicodeCharacter{2212}{-}
- %%
- %% or, on luatex and xetex
- %% \\usepackage{unicode-math}
- %%
- %% 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.
- """
- if kwargs.get("dryrun", False):
- self._print_pgf_to_fh(None, *args, **kwargs)
- return
- 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_to_fh(self, fh, *args, metadata=None, **kwargs):
- 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())
- try:
- # create temporary directory for compiling the figure
- tmpdir = tempfile.mkdtemp(prefix="mpl_pgf_")
- fname_pgf = os.path.join(tmpdir, "figure.pgf")
- fname_tex = os.path.join(tmpdir, "figure.tex")
- fname_pdf = os.path.join(tmpdir, "figure.pdf")
- # print figure to pgf and compile it with latex
- self.print_pgf(fname_pgf, *args, **kwargs)
- latex_preamble = get_preamble()
- latex_fontspec = get_fontspec()
- 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, latex_preamble, latex_fontspec)
- pathlib.Path(fname_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)
- # copy file contents to target
- with open(fname_pdf, "rb") as fh_src:
- shutil.copyfileobj(fh_src, fh)
- finally:
- try:
- shutil.rmtree(tmpdir)
- except:
- TmpDirCleaner.add(tmpdir)
- def print_pdf(self, fname_or_fh, *args, **kwargs):
- """Use LaTeX to compile a Pgf generated figure to PDF."""
- if kwargs.get("dryrun", False):
- self._print_pgf_to_fh(None, *args, **kwargs)
- return
- with cbook.open_file_cm(fname_or_fh, "wb") as file:
- self._print_pdf_to_fh(file, *args, **kwargs)
- def _print_png_to_fh(self, fh, *args, **kwargs):
- converter = make_pdf_to_png_converter()
- try:
- # create temporary directory for pdf creation and png conversion
- tmpdir = tempfile.mkdtemp(prefix="mpl_pgf_")
- fname_pdf = os.path.join(tmpdir, "figure.pdf")
- fname_png = os.path.join(tmpdir, "figure.png")
- # create pdf and try to convert it to png
- self.print_pdf(fname_pdf, *args, **kwargs)
- converter(fname_pdf, fname_png, dpi=self.figure.dpi)
- # copy file contents to target
- with open(fname_png, "rb") as fh_src:
- shutil.copyfileobj(fh_src, fh)
- finally:
- try:
- shutil.rmtree(tmpdir)
- except:
- TmpDirCleaner.add(tmpdir)
- def print_png(self, fname_or_fh, *args, **kwargs):
- """Use LaTeX to compile a pgf figure to pdf and convert it to png."""
- if kwargs.get("dryrun", False):
- self._print_pgf_to_fh(None, *args, **kwargs)
- return
- with cbook.open_file_cm(fname_or_fh, "wb") as file:
- self._print_png_to_fh(file, *args, **kwargs)
- def get_renderer(self):
- return RendererPgf(self.figure, None)
- FigureManagerPgf = FigureManagerBase
- @_Backend.export
- class _BackendPgf(_Backend):
- FigureCanvas = FigureCanvasPgf
- def _cleanup_all():
- LatexManager._cleanup_remaining_instances()
- TmpDirCleaner.cleanup_remaining_tmpdirs()
- atexit.register(_cleanup_all)
- 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__ = (
- '_outputfile',
- 'keep_empty',
- '_tmpdir',
- '_basename',
- '_fname_tex',
- '_fname_pdf',
- '_n_figures',
- '_file',
- '_info_dict',
- '_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._outputfile = 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:
- cbook.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)
- # create temporary directory for compiling the figure
- self._tmpdir = tempfile.mkdtemp(prefix="mpl_pgf_pdfpages_")
- self._basename = 'pdf_pages'
- self._fname_tex = os.path.join(self._tmpdir, self._basename + ".tex")
- self._fname_pdf = os.path.join(self._tmpdir, self._basename + ".pdf")
- self._file = open(self._fname_tex, 'wb')
- @cbook.deprecated('3.3')
- @property
- def metadata(self):
- return self._metadata
- 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')
- self._file.close()
- if self._n_figures > 0:
- try:
- self._run_latex()
- finally:
- try:
- shutil.rmtree(self._tmpdir)
- except:
- TmpDirCleaner.add(self._tmpdir)
- elif self.keep_empty:
- open(self._outputfile, 'wb').close()
- def _run_latex(self):
- texcommand = mpl.rcParams["pgf.texsystem"]
- cbook._check_and_log_subprocess(
- [texcommand, "-interaction=nonstopmode", "-halt-on-error",
- os.path.basename(self._fname_tex)],
- _log, cwd=self._tmpdir)
- # copy file contents to target
- shutil.copyfile(self._fname_pdf, self._outputfile)
- 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
|