1from __future__ import (absolute_import, division, print_function,
2                        unicode_literals)
3
4import six
5
6import atexit
7import codecs
8import errno
9import math
10import os
11import re
12import shutil
13import sys
14import tempfile
15import warnings
16import weakref
17
18import matplotlib as mpl
19from matplotlib import _png, rcParams
20from matplotlib.backend_bases import (
21    _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase,
22    RendererBase)
23from matplotlib.backends.backend_mixed import MixedModeRenderer
24from matplotlib.cbook import is_writable_file_like
25from matplotlib.compat import subprocess
26from matplotlib.compat.subprocess import check_output
27from matplotlib.path import Path
28
29
30###############################################################################
31
32# create a list of system fonts, all of these should work with xe/lua-latex
33system_fonts = []
34if sys.platform.startswith('win'):
35    from matplotlib import font_manager
36    for f in font_manager.win32InstalledFonts():
37        try:
38            system_fonts.append(font_manager.get_font(str(f)).family_name)
39        except:
40            pass # unknown error, skip this font
41else:
42    # assuming fontconfig is installed and the command 'fc-list' exists
43    try:
44        # list scalable (non-bitmap) fonts
45        fc_list = check_output([str('fc-list'), ':outline,scalable', 'family'])
46        fc_list = fc_list.decode('utf8')
47        system_fonts = [f.split(',')[0] for f in fc_list.splitlines()]
48        system_fonts = list(set(system_fonts))
49    except:
50        warnings.warn('error getting fonts from fc-list', UserWarning)
51
52def get_texcommand():
53    """Get chosen TeX system from rc."""
54    texsystem_options = ["xelatex", "lualatex", "pdflatex"]
55    texsystem = rcParams["pgf.texsystem"]
56    return texsystem if texsystem in texsystem_options else "xelatex"
57
58
59def get_fontspec():
60    """Build fontspec preamble from rc."""
61    latex_fontspec = []
62    texcommand = get_texcommand()
63
64    if texcommand != "pdflatex":
65        latex_fontspec.append("\\usepackage{fontspec}")
66
67    if texcommand != "pdflatex" and rcParams["pgf.rcfonts"]:
68        # try to find fonts from rc parameters
69        families = ["serif", "sans-serif", "monospace"]
70        fontspecs = [r"\setmainfont{%s}", r"\setsansfont{%s}",
71                     r"\setmonofont{%s}"]
72        for family, fontspec in zip(families, fontspecs):
73            matches = [f for f in rcParams["font." + family]
74                       if f in system_fonts]
75            if matches:
76                latex_fontspec.append(fontspec % matches[0])
77            else:
78                pass  # no fonts found, fallback to LaTeX defaule
79
80    return "\n".join(latex_fontspec)
81
82
83def get_preamble():
84    """Get LaTeX preamble from rc."""
85    return "\n".join(rcParams["pgf.preamble"])
86
87###############################################################################
88
89# This almost made me cry!!!
90# In the end, it's better to use only one unit for all coordinates, since the
91# arithmetic in latex seems to produce inaccurate conversions.
92latex_pt_to_in = 1. / 72.27
93latex_in_to_pt = 1. / latex_pt_to_in
94mpl_pt_to_in = 1. / 72.
95mpl_in_to_pt = 1. / mpl_pt_to_in
96
97###############################################################################
98# helper functions
99
100NO_ESCAPE = r"(?<!\\)(?:\\\\)*"
101re_mathsep = re.compile(NO_ESCAPE + r"\$")
102re_escapetext = re.compile(NO_ESCAPE + "([_^$%])")
103repl_escapetext = lambda m: "\\" + m.group(1)
104re_mathdefault = re.compile(NO_ESCAPE + r"(\\mathdefault)")
105repl_mathdefault = lambda m: m.group(0)[:-len(m.group(1))]
106
107
108def common_texification(text):
109    """
110    Do some necessary and/or useful substitutions for texts to be included in
111    LaTeX documents.
112    """
113
114    # Sometimes, matplotlib adds the unknown command \mathdefault.
115    # Not using \mathnormal instead since this looks odd for the latex cm font.
116    text = re_mathdefault.sub(repl_mathdefault, text)
117
118    # split text into normaltext and inline math parts
119    parts = re_mathsep.split(text)
120    for i, s in enumerate(parts):
121        if not i % 2:
122            # textmode replacements
123            s = re_escapetext.sub(repl_escapetext, s)
124        else:
125            # mathmode replacements
126            s = r"\(\displaystyle %s\)" % s
127        parts[i] = s
128
129    return "".join(parts)
130
131
132def writeln(fh, line):
133    # every line of a file included with \\input must be terminated with %
134    # if not, latex will create additional vertical spaces for some reason
135    fh.write(line)
136    fh.write("%\n")
137
138
139def _font_properties_str(prop):
140    # translate font properties to latex commands, return as string
141    commands = []
142
143    families = {"serif": r"\rmfamily", "sans": r"\sffamily",
144                "sans-serif": r"\sffamily", "monospace": r"\ttfamily"}
145    family = prop.get_family()[0]
146    if family in families:
147        commands.append(families[family])
148    elif family in system_fonts and get_texcommand() != "pdflatex":
149        commands.append(r"\setmainfont{%s}\rmfamily" % family)
150    else:
151        pass  # print warning?
152
153    size = prop.get_size_in_points()
154    commands.append(r"\fontsize{%f}{%f}" % (size, size * 1.2))
155
156    styles = {"normal": r"", "italic": r"\itshape", "oblique": r"\slshape"}
157    commands.append(styles[prop.get_style()])
158
159    boldstyles = ["semibold", "demibold", "demi", "bold", "heavy",
160                  "extra bold", "black"]
161    if prop.get_weight() in boldstyles:
162        commands.append(r"\bfseries")
163
164    commands.append(r"\selectfont")
165    return "".join(commands)
166
167
168def make_pdf_to_png_converter():
169    """
170    Returns a function that converts a pdf file to a png file.
171    """
172
173    tools_available = []
174    # check for pdftocairo
175    try:
176        check_output([str("pdftocairo"), "-v"], stderr=subprocess.STDOUT)
177        tools_available.append("pdftocairo")
178    except:
179        pass
180    # check for ghostscript
181    gs, ver = mpl.checkdep_ghostscript()
182    if gs:
183        tools_available.append("gs")
184
185    # pick converter
186    if "pdftocairo" in tools_available:
187        def cairo_convert(pdffile, pngfile, dpi):
188            cmd = [str("pdftocairo"), "-singlefile", "-png", "-r", "%d" % dpi,
189                   pdffile, os.path.splitext(pngfile)[0]]
190            check_output(cmd, stderr=subprocess.STDOUT)
191        return cairo_convert
192    elif "gs" in tools_available:
193        def gs_convert(pdffile, pngfile, dpi):
194            cmd = [str(gs),
195                   '-dQUIET', '-dSAFER', '-dBATCH', '-dNOPAUSE', '-dNOPROMPT',
196                   '-dUseCIEColor', '-dTextAlphaBits=4',
197                   '-dGraphicsAlphaBits=4', '-dDOINTERPOLATE',
198                   '-sDEVICE=png16m', '-sOutputFile=%s' % pngfile,
199                   '-r%d' % dpi, pdffile]
200            check_output(cmd, stderr=subprocess.STDOUT)
201        return gs_convert
202    else:
203        raise RuntimeError("No suitable pdf to png renderer found.")
204
205
206class LatexError(Exception):
207    def __init__(self, message, latex_output=""):
208        Exception.__init__(self, message)
209        self.latex_output = latex_output
210
211
212class LatexManagerFactory(object):
213    previous_instance = None
214
215    @staticmethod
216    def get_latex_manager():
217        texcommand = get_texcommand()
218        latex_header = LatexManager._build_latex_header()
219        prev = LatexManagerFactory.previous_instance
220
221        # Check if the previous instance of LatexManager can be reused.
222        if (prev and prev.latex_header == latex_header
223                and prev.texcommand == texcommand):
224            if rcParams["pgf.debug"]:
225                print("reusing LatexManager")
226            return prev
227        else:
228            if rcParams["pgf.debug"]:
229                print("creating LatexManager")
230            new_inst = LatexManager()
231            LatexManagerFactory.previous_instance = new_inst
232            return new_inst
233
234
235class LatexManager(object):
236    """
237    The LatexManager opens an instance of the LaTeX application for
238    determining the metrics of text elements. The LaTeX environment can be
239    modified by setting fonts and/or a custem preamble in the rc parameters.
240    """
241    _unclean_instances = weakref.WeakSet()
242
243    @staticmethod
244    def _build_latex_header():
245        latex_preamble = get_preamble()
246        latex_fontspec = get_fontspec()
247        # Create LaTeX header with some content, else LaTeX will load some math
248        # fonts later when we don't expect the additional output on stdout.
249        # TODO: is this sufficient?
250        latex_header = [r"\documentclass{minimal}",
251                        latex_preamble,
252                        latex_fontspec,
253                        r"\begin{document}",
254                        r"text $math \mu$",  # force latex to load fonts now
255                        r"\typeout{pgf_backend_query_start}"]
256        return "\n".join(latex_header)
257
258    @staticmethod
259    def _cleanup_remaining_instances():
260        unclean_instances = list(LatexManager._unclean_instances)
261        for latex_manager in unclean_instances:
262            latex_manager._cleanup()
263
264    def _stdin_writeln(self, s):
265        self.latex_stdin_utf8.write(s)
266        self.latex_stdin_utf8.write("\n")
267        self.latex_stdin_utf8.flush()
268
269    def _expect(self, s):
270        exp = s.encode("utf8")
271        buf = bytearray()
272        while True:
273            b = self.latex.stdout.read(1)
274            buf += b
275            if buf[-len(exp):] == exp:
276                break
277            if not len(b):
278                raise LatexError("LaTeX process halted", buf.decode("utf8"))
279        return buf.decode("utf8")
280
281    def _expect_prompt(self):
282        return self._expect("\n*")
283
284    def __init__(self):
285        # store references for __del__
286        self._os_path = os.path
287        self._shutil = shutil
288        self._debug = rcParams["pgf.debug"]
289
290        # create a tmp directory for running latex, remember to cleanup
291        self.tmpdir = tempfile.mkdtemp(prefix="mpl_pgf_lm_")
292        LatexManager._unclean_instances.add(self)
293
294        # test the LaTeX setup to ensure a clean startup of the subprocess
295        self.texcommand = get_texcommand()
296        self.latex_header = LatexManager._build_latex_header()
297        latex_end = "\n\\makeatletter\n\\@@end\n"
298        try:
299            latex = subprocess.Popen([str(self.texcommand), "-halt-on-error"],
300                                     stdin=subprocess.PIPE,
301                                     stdout=subprocess.PIPE,
302                                     cwd=self.tmpdir)
303        except OSError as e:
304            if e.errno == errno.ENOENT:
305                raise RuntimeError(
306                    "Latex command not found. Install %r or change "
307                    "pgf.texsystem to the desired command." % self.texcommand)
308            else:
309                raise RuntimeError(
310                    "Error starting process %r" % self.texcommand)
311        test_input = self.latex_header + latex_end
312        stdout, stderr = latex.communicate(test_input.encode("utf-8"))
313        if latex.returncode != 0:
314            raise LatexError("LaTeX returned an error, probably missing font "
315                             "or error in preamble:\n%s" % stdout)
316
317        # open LaTeX process for real work
318        latex = subprocess.Popen([str(self.texcommand), "-halt-on-error"],
319                                 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
320                                 cwd=self.tmpdir)
321        self.latex = latex
322        self.latex_stdin_utf8 = codecs.getwriter("utf8")(self.latex.stdin)
323        # write header with 'pgf_backend_query_start' token
324        self._stdin_writeln(self._build_latex_header())
325        # read all lines until our 'pgf_backend_query_start' token appears
326        self._expect("*pgf_backend_query_start")
327        self._expect_prompt()
328
329        # cache for strings already processed
330        self.str_cache = {}
331
332    def _cleanup(self):
333        if not self._os_path.isdir(self.tmpdir):
334            return
335        try:
336            self.latex.communicate()
337            self.latex_stdin_utf8.close()
338            self.latex.stdout.close()
339        except:
340            pass
341        try:
342            self._shutil.rmtree(self.tmpdir)
343            LatexManager._unclean_instances.discard(self)
344        except:
345            sys.stderr.write("error deleting tmp directory %s\n" % self.tmpdir)
346
347    def __del__(self):
348        if self._debug:
349            print("deleting LatexManager")
350        self._cleanup()
351
352    def get_width_height_descent(self, text, prop):
353        """
354        Get the width, total height and descent for a text typesetted by the
355        current LaTeX environment.
356        """
357
358        # apply font properties and define textbox
359        prop_cmds = _font_properties_str(prop)
360        textbox = "\\sbox0{%s %s}" % (prop_cmds, text)
361
362        # check cache
363        if textbox in self.str_cache:
364            return self.str_cache[textbox]
365
366        # send textbox to LaTeX and wait for prompt
367        self._stdin_writeln(textbox)
368        try:
369            self._expect_prompt()
370        except LatexError as e:
371            raise ValueError("Error processing '{}'\nLaTeX Output:\n{}"
372                             .format(text, e.latex_output))
373
374        # typeout width, height and text offset of the last textbox
375        self._stdin_writeln(r"\typeout{\the\wd0,\the\ht0,\the\dp0}")
376        # read answer from latex and advance to the next prompt
377        try:
378            answer = self._expect_prompt()
379        except LatexError as e:
380            raise ValueError("Error processing '{}'\nLaTeX Output:\n{}"
381                             .format(text, e.latex_output))
382
383        # parse metrics from the answer string
384        try:
385            width, height, offset = answer.splitlines()[0].split(",")
386        except:
387            raise ValueError("Error processing '{}'\nLaTeX Output:\n{}"
388                             .format(text, answer))
389        w, h, o = float(width[:-2]), float(height[:-2]), float(offset[:-2])
390
391        # the height returned from LaTeX goes from base to top.
392        # the height matplotlib expects goes from bottom to top.
393        self.str_cache[textbox] = (w, h + o, o)
394        return w, h + o, o
395
396
397class RendererPgf(RendererBase):
398
399    def __init__(self, figure, fh, dummy=False):
400        """
401        Creates a new PGF renderer that translates any drawing instruction
402        into text commands to be interpreted in a latex pgfpicture environment.
403
404        Attributes
405        ----------
406        figure : `matplotlib.figure.Figure`
407            Matplotlib figure to initialize height, width and dpi from.
408        fh : file-like
409            File handle for the output of the drawing commands.
410
411        """
412        RendererBase.__init__(self)
413        self.dpi = figure.dpi
414        self.fh = fh
415        self.figure = figure
416        self.image_counter = 0
417
418        # get LatexManager instance
419        self.latexManager = LatexManagerFactory.get_latex_manager()
420
421        if dummy:
422            # dummy==True deactivate all methods
423            nop = lambda *args, **kwargs: None
424            for m in RendererPgf.__dict__:
425                if m.startswith("draw_"):
426                    self.__dict__[m] = nop
427        else:
428            # if fh does not belong to a filename, deactivate draw_image
429            if not hasattr(fh, 'name') or not os.path.exists(fh.name):
430                warnings.warn("streamed pgf-code does not support raster "
431                              "graphics, consider using the pgf-to-pdf option",
432                              UserWarning)
433                self.__dict__["draw_image"] = lambda *args, **kwargs: None
434
435    def draw_markers(self, gc, marker_path, marker_trans, path, trans,
436                     rgbFace=None):
437        writeln(self.fh, r"\begin{pgfscope}")
438
439        # convert from display units to in
440        f = 1. / self.dpi
441
442        # set style and clip
443        self._print_pgf_clip(gc)
444        self._print_pgf_path_styles(gc, rgbFace)
445
446        # build marker definition
447        bl, tr = marker_path.get_extents(marker_trans).get_points()
448        coords = bl[0] * f, bl[1] * f, tr[0] * f, tr[1] * f
449        writeln(self.fh,
450                r"\pgfsys@defobject{currentmarker}"
451                r"{\pgfqpoint{%fin}{%fin}}{\pgfqpoint{%fin}{%fin}}{" % coords)
452        self._print_pgf_path(None, marker_path, marker_trans)
453        self._pgf_path_draw(stroke=gc.get_linewidth() != 0.0,
454                            fill=rgbFace is not None)
455        writeln(self.fh, r"}")
456
457        # draw marker for each vertex
458        for point, code in path.iter_segments(trans, simplify=False):
459            x, y = point[0] * f, point[1] * f
460            writeln(self.fh, r"\begin{pgfscope}")
461            writeln(self.fh, r"\pgfsys@transformshift{%fin}{%fin}" % (x, y))
462            writeln(self.fh, r"\pgfsys@useobject{currentmarker}{}")
463            writeln(self.fh, r"\end{pgfscope}")
464
465        writeln(self.fh, r"\end{pgfscope}")
466
467    def draw_path(self, gc, path, transform, rgbFace=None):
468        writeln(self.fh, r"\begin{pgfscope}")
469        # draw the path
470        self._print_pgf_clip(gc)
471        self._print_pgf_path_styles(gc, rgbFace)
472        self._print_pgf_path(gc, path, transform, rgbFace)
473        self._pgf_path_draw(stroke=gc.get_linewidth() != 0.0,
474                            fill=rgbFace is not None)
475        writeln(self.fh, r"\end{pgfscope}")
476
477        # if present, draw pattern on top
478        if gc.get_hatch():
479            writeln(self.fh, r"\begin{pgfscope}")
480            self._print_pgf_path_styles(gc, rgbFace)
481
482            # combine clip and path for clipping
483            self._print_pgf_clip(gc)
484            self._print_pgf_path(gc, path, transform, rgbFace)
485            writeln(self.fh, r"\pgfusepath{clip}")
486
487            # build pattern definition
488            writeln(self.fh,
489                    r"\pgfsys@defobject{currentpattern}"
490                    r"{\pgfqpoint{0in}{0in}}{\pgfqpoint{1in}{1in}}{")
491            writeln(self.fh, r"\begin{pgfscope}")
492            writeln(self.fh,
493                    r"\pgfpathrectangle"
494                    r"{\pgfqpoint{0in}{0in}}{\pgfqpoint{1in}{1in}}")
495            writeln(self.fh, r"\pgfusepath{clip}")
496            scale = mpl.transforms.Affine2D().scale(self.dpi)
497            self._print_pgf_path(None, gc.get_hatch_path(), scale)
498            self._pgf_path_draw(stroke=True)
499            writeln(self.fh, r"\end{pgfscope}")
500            writeln(self.fh, r"}")
501            # repeat pattern, filling the bounding rect of the path
502            f = 1. / self.dpi
503            (xmin, ymin), (xmax, ymax) = \
504                path.get_extents(transform).get_points()
505            xmin, xmax = f * xmin, f * xmax
506            ymin, ymax = f * ymin, f * ymax
507            repx, repy = int(math.ceil(xmax-xmin)), int(math.ceil(ymax-ymin))
508            writeln(self.fh,
509                    r"\pgfsys@transformshift{%fin}{%fin}" % (xmin, ymin))
510            for iy in range(repy):
511                for ix in range(repx):
512                    writeln(self.fh, r"\pgfsys@useobject{currentpattern}{}")
513                    writeln(self.fh, r"\pgfsys@transformshift{1in}{0in}")
514                writeln(self.fh, r"\pgfsys@transformshift{-%din}{0in}" % repx)
515                writeln(self.fh, r"\pgfsys@transformshift{0in}{1in}")
516
517            writeln(self.fh, r"\end{pgfscope}")
518
519    def _print_pgf_clip(self, gc):
520        f = 1. / self.dpi
521        # check for clip box
522        bbox = gc.get_clip_rectangle()
523        if bbox:
524            p1, p2 = bbox.get_points()
525            w, h = p2 - p1
526            coords = p1[0] * f, p1[1] * f, w * f, h * f
527            writeln(self.fh,
528                    r"\pgfpathrectangle"
529                    r"{\pgfqpoint{%fin}{%fin}}{\pgfqpoint{%fin}{%fin}}"
530                    % coords)
531            writeln(self.fh, r"\pgfusepath{clip}")
532
533        # check for clip path
534        clippath, clippath_trans = gc.get_clip_path()
535        if clippath is not None:
536            self._print_pgf_path(gc, clippath, clippath_trans)
537            writeln(self.fh, r"\pgfusepath{clip}")
538
539    def _print_pgf_path_styles(self, gc, rgbFace):
540        # cap style
541        capstyles = {"butt": r"\pgfsetbuttcap",
542                     "round": r"\pgfsetroundcap",
543                     "projecting": r"\pgfsetrectcap"}
544        writeln(self.fh, capstyles[gc.get_capstyle()])
545
546        # join style
547        joinstyles = {"miter": r"\pgfsetmiterjoin",
548                      "round": r"\pgfsetroundjoin",
549                      "bevel": r"\pgfsetbeveljoin"}
550        writeln(self.fh, joinstyles[gc.get_joinstyle()])
551
552        # filling
553        has_fill = rgbFace is not None
554
555        if gc.get_forced_alpha():
556            fillopacity = strokeopacity = gc.get_alpha()
557        else:
558            strokeopacity = gc.get_rgb()[3]
559            fillopacity = rgbFace[3] if has_fill and len(rgbFace) > 3 else 1.0
560
561        if has_fill:
562            writeln(self.fh,
563                    r"\definecolor{currentfill}{rgb}{%f,%f,%f}"
564                    % tuple(rgbFace[:3]))
565            writeln(self.fh, r"\pgfsetfillcolor{currentfill}")
566        if has_fill and fillopacity != 1.0:
567            writeln(self.fh, r"\pgfsetfillopacity{%f}" % fillopacity)
568
569        # linewidth and color
570        lw = gc.get_linewidth() * mpl_pt_to_in * latex_in_to_pt
571        stroke_rgba = gc.get_rgb()
572        writeln(self.fh, r"\pgfsetlinewidth{%fpt}" % lw)
573        writeln(self.fh,
574                r"\definecolor{currentstroke}{rgb}{%f,%f,%f}"
575                % stroke_rgba[:3])
576        writeln(self.fh, r"\pgfsetstrokecolor{currentstroke}")
577        if strokeopacity != 1.0:
578            writeln(self.fh, r"\pgfsetstrokeopacity{%f}" % strokeopacity)
579
580        # line style
581        dash_offset, dash_list = gc.get_dashes()
582        if dash_list is None:
583            writeln(self.fh, r"\pgfsetdash{}{0pt}")
584        else:
585            writeln(self.fh,
586                    r"\pgfsetdash{%s}{%fpt}"
587                    % ("".join(r"{%fpt}" % dash for dash in dash_list),
588                       dash_offset))
589
590    def _print_pgf_path(self, gc, path, transform, rgbFace=None):
591        f = 1. / self.dpi
592        # check for clip box / ignore clip for filled paths
593        bbox = gc.get_clip_rectangle() if gc else None
594        if bbox and (rgbFace is None):
595            p1, p2 = bbox.get_points()
596            clip = (p1[0], p1[1], p2[0], p2[1])
597        else:
598            clip = None
599        # build path
600        for points, code in path.iter_segments(transform, clip=clip):
601            if code == Path.MOVETO:
602                x, y = tuple(points)
603                writeln(self.fh,
604                        r"\pgfpathmoveto{\pgfqpoint{%fin}{%fin}}" %
605                        (f * x, f * y))
606            elif code == Path.CLOSEPOLY:
607                writeln(self.fh, r"\pgfpathclose")
608            elif code == Path.LINETO:
609                x, y = tuple(points)
610                writeln(self.fh,
611                        r"\pgfpathlineto{\pgfqpoint{%fin}{%fin}}" %
612                        (f * x, f * y))
613            elif code == Path.CURVE3:
614                cx, cy, px, py = tuple(points)
615                coords = cx * f, cy * f, px * f, py * f
616                writeln(self.fh,
617                        r"\pgfpathquadraticcurveto"
618                        r"{\pgfqpoint{%fin}{%fin}}{\pgfqpoint{%fin}{%fin}}"
619                        % coords)
620            elif code == Path.CURVE4:
621                c1x, c1y, c2x, c2y, px, py = tuple(points)
622                coords = c1x * f, c1y * f, c2x * f, c2y * f, px * f, py * f
623                writeln(self.fh,
624                        r"\pgfpathcurveto"
625                        r"{\pgfqpoint{%fin}{%fin}}"
626                        r"{\pgfqpoint{%fin}{%fin}}"
627                        r"{\pgfqpoint{%fin}{%fin}}"
628                        % coords)
629
630    def _pgf_path_draw(self, stroke=True, fill=False):
631        actions = []
632        if stroke:
633            actions.append("stroke")
634        if fill:
635            actions.append("fill")
636        writeln(self.fh, r"\pgfusepath{%s}" % ",".join(actions))
637
638    def option_scale_image(self):
639        """
640        pgf backend supports affine transform of image.
641        """
642        return True
643
644    def option_image_nocomposite(self):
645        """
646        return whether to generate a composite image from multiple images on
647        a set of axes
648        """
649        return not rcParams['image.composite_image']
650
651    def draw_image(self, gc, x, y, im, transform=None):
652        h, w = im.shape[:2]
653        if w == 0 or h == 0:
654            return
655
656        # save the images to png files
657        path = os.path.dirname(self.fh.name)
658        fname = os.path.splitext(os.path.basename(self.fh.name))[0]
659        fname_img = "%s-img%d.png" % (fname, self.image_counter)
660        self.image_counter += 1
661        _png.write_png(im[::-1], os.path.join(path, fname_img))
662
663        # reference the image in the pgf picture
664        writeln(self.fh, r"\begin{pgfscope}")
665        self._print_pgf_clip(gc)
666        f = 1. / self.dpi  # from display coords to inch
667        if transform is None:
668            writeln(self.fh,
669                    r"\pgfsys@transformshift{%fin}{%fin}" % (x * f, y * f))
670            w, h = w * f, h * f
671        else:
672            tr1, tr2, tr3, tr4, tr5, tr6 = transform.frozen().to_values()
673            writeln(self.fh,
674                    r"\pgfsys@transformcm{%f}{%f}{%f}{%f}{%fin}{%fin}" %
675                    (tr1 * f, tr2 * f, tr3 * f, tr4 * f,
676                     (tr5 + x) * f, (tr6 + y) * f))
677            w = h = 1  # scale is already included in the transform
678        interp = str(transform is None).lower()  # interpolation in PDF reader
679        writeln(self.fh,
680                r"\pgftext[left,bottom]"
681                r"{\pgfimage[interpolate=%s,width=%fin,height=%fin]{%s}}" %
682                (interp, w, h, fname_img))
683        writeln(self.fh, r"\end{pgfscope}")
684
685    def draw_tex(self, gc, x, y, s, prop, angle, ismath="TeX!", mtext=None):
686        self.draw_text(gc, x, y, s, prop, angle, ismath, mtext)
687
688    def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
689        # prepare string for tex
690        s = common_texification(s)
691        prop_cmds = _font_properties_str(prop)
692        s = r"%s %s" % (prop_cmds, s)
693
694
695        writeln(self.fh, r"\begin{pgfscope}")
696
697        alpha = gc.get_alpha()
698        if alpha != 1.0:
699            writeln(self.fh, r"\pgfsetfillopacity{%f}" % alpha)
700            writeln(self.fh, r"\pgfsetstrokeopacity{%f}" % alpha)
701        rgb = tuple(gc.get_rgb())[:3]
702        if rgb != (0, 0, 0):
703            writeln(self.fh, r"\definecolor{textcolor}{rgb}{%f,%f,%f}" % rgb)
704            writeln(self.fh, r"\pgfsetstrokecolor{textcolor}")
705            writeln(self.fh, r"\pgfsetfillcolor{textcolor}")
706            s = r"\color{textcolor}" + s
707
708        f = 1.0 / self.figure.dpi
709        text_args = []
710        if mtext and (
711                (angle == 0 or
712                 mtext.get_rotation_mode() == "anchor") and
713                mtext.get_va() != "center_baseline"):
714            # if text anchoring can be supported, get the original coordinates
715            # and add alignment information
716            x, y = mtext.get_transform().transform_point(mtext.get_position())
717            text_args.append("x=%fin" % (x * f))
718            text_args.append("y=%fin" % (y * f))
719
720            halign = {"left": "left", "right": "right", "center": ""}
721            valign = {"top": "top", "bottom": "bottom",
722                      "baseline": "base", "center": ""}
723            text_args.append(halign[mtext.get_ha()])
724            text_args.append(valign[mtext.get_va()])
725        else:
726            # if not, use the text layout provided by matplotlib
727            text_args.append("x=%fin" % (x * f))
728            text_args.append("y=%fin" % (y * f))
729            text_args.append("left")
730            text_args.append("base")
731
732        if angle != 0:
733            text_args.append("rotate=%f" % angle)
734
735        writeln(self.fh, r"\pgftext[%s]{%s}" % (",".join(text_args), s))
736        writeln(self.fh, r"\end{pgfscope}")
737
738    def get_text_width_height_descent(self, s, prop, ismath):
739        # check if the math is supposed to be displaystyled
740        s = common_texification(s)
741
742        # get text metrics in units of latex pt, convert to display units
743        w, h, d = self.latexManager.get_width_height_descent(s, prop)
744        # TODO: this should be latex_pt_to_in instead of mpl_pt_to_in
745        # but having a little bit more space around the text looks better,
746        # plus the bounding box reported by LaTeX is VERY narrow
747        f = mpl_pt_to_in * self.dpi
748        return w * f, h * f, d * f
749
750    def flipy(self):
751        return False
752
753    def get_canvas_width_height(self):
754        return self.figure.get_figwidth(), self.figure.get_figheight()
755
756    def points_to_pixels(self, points):
757        return points * mpl_pt_to_in * self.dpi
758
759    def new_gc(self):
760        return GraphicsContextPgf()
761
762
763class GraphicsContextPgf(GraphicsContextBase):
764    pass
765
766########################################################################
767
768
769class TmpDirCleaner(object):
770    remaining_tmpdirs = set()
771
772    @staticmethod
773    def add(tmpdir):
774        TmpDirCleaner.remaining_tmpdirs.add(tmpdir)
775
776    @staticmethod
777    def cleanup_remaining_tmpdirs():
778        for tmpdir in TmpDirCleaner.remaining_tmpdirs:
779            try:
780                shutil.rmtree(tmpdir)
781            except:
782                sys.stderr.write("error deleting tmp directory %s\n" % tmpdir)
783
784
785class FigureCanvasPgf(FigureCanvasBase):
786    filetypes = {"pgf": "LaTeX PGF picture",
787                 "pdf": "LaTeX compiled PGF picture",
788                 "png": "Portable Network Graphics", }
789
790    def get_default_filetype(self):
791        return 'pdf'
792
793    def _print_pgf_to_fh(self, fh, *args, **kwargs):
794        if kwargs.get("dryrun", False):
795            renderer = RendererPgf(self.figure, None, dummy=True)
796            self.figure.draw(renderer)
797            return
798
799        header_text = """%% Creator: Matplotlib, PGF backend
800%%
801%% To include the figure in your LaTeX document, write
802%%   \\input{<filename>.pgf}
803%%
804%% Make sure the required packages are loaded in your preamble
805%%   \\usepackage{pgf}
806%%
807%% Figures using additional raster images can only be included by \\input if
808%% they are in the same directory as the main LaTeX file. For loading figures
809%% from other directories you can use the `import` package
810%%   \\usepackage{import}
811%% and then include the figures with
812%%   \\import{<path to file>}{<filename>.pgf}
813%%
814"""
815
816        # append the preamble used by the backend as a comment for debugging
817        header_info_preamble = ["%% Matplotlib used the following preamble"]
818        for line in get_preamble().splitlines():
819            header_info_preamble.append("%%   " + line)
820        for line in get_fontspec().splitlines():
821            header_info_preamble.append("%%   " + line)
822        header_info_preamble.append("%%")
823        header_info_preamble = "\n".join(header_info_preamble)
824
825        # get figure size in inch
826        w, h = self.figure.get_figwidth(), self.figure.get_figheight()
827        dpi = self.figure.get_dpi()
828
829        # create pgfpicture environment and write the pgf code
830        fh.write(header_text)
831        fh.write(header_info_preamble)
832        fh.write("\n")
833        writeln(fh, r"\begingroup")
834        writeln(fh, r"\makeatletter")
835        writeln(fh, r"\begin{pgfpicture}")
836        writeln(fh,
837                r"\pgfpathrectangle{\pgfpointorigin}{\pgfqpoint{%fin}{%fin}}"
838                % (w, h))
839        writeln(fh, r"\pgfusepath{use as bounding box, clip}")
840        _bbox_inches_restore = kwargs.pop("bbox_inches_restore", None)
841        renderer = MixedModeRenderer(self.figure, w, h, dpi,
842                                     RendererPgf(self.figure, fh),
843                                     bbox_inches_restore=_bbox_inches_restore)
844        self.figure.draw(renderer)
845
846        # end the pgfpicture environment
847        writeln(fh, r"\end{pgfpicture}")
848        writeln(fh, r"\makeatother")
849        writeln(fh, r"\endgroup")
850
851    def print_pgf(self, fname_or_fh, *args, **kwargs):
852        """
853        Output pgf commands for drawing the figure so it can be included and
854        rendered in latex documents.
855        """
856        if kwargs.get("dryrun", False):
857            self._print_pgf_to_fh(None, *args, **kwargs)
858            return
859
860        # figure out where the pgf is to be written to
861        if isinstance(fname_or_fh, six.string_types):
862            with codecs.open(fname_or_fh, "w", encoding="utf-8") as fh:
863                self._print_pgf_to_fh(fh, *args, **kwargs)
864        elif is_writable_file_like(fname_or_fh):
865            fh = codecs.getwriter("utf-8")(fname_or_fh)
866            self._print_pgf_to_fh(fh, *args, **kwargs)
867        else:
868            raise ValueError("filename must be a path")
869
870    def _print_pdf_to_fh(self, fh, *args, **kwargs):
871        w, h = self.figure.get_figwidth(), self.figure.get_figheight()
872
873        try:
874            # create temporary directory for compiling the figure
875            tmpdir = tempfile.mkdtemp(prefix="mpl_pgf_")
876            fname_pgf = os.path.join(tmpdir, "figure.pgf")
877            fname_tex = os.path.join(tmpdir, "figure.tex")
878            fname_pdf = os.path.join(tmpdir, "figure.pdf")
879
880            # print figure to pgf and compile it with latex
881            self.print_pgf(fname_pgf, *args, **kwargs)
882
883            latex_preamble = get_preamble()
884            latex_fontspec = get_fontspec()
885            latexcode = """
886\\documentclass[12pt]{minimal}
887\\usepackage[paperwidth=%fin, paperheight=%fin, margin=0in]{geometry}
888%s
889%s
890\\usepackage{pgf}
891
892\\begin{document}
893\\centering
894\\input{figure.pgf}
895\\end{document}""" % (w, h, latex_preamble, latex_fontspec)
896            with codecs.open(fname_tex, "w", "utf-8") as fh_tex:
897                fh_tex.write(latexcode)
898
899            texcommand = get_texcommand()
900            cmdargs = [str(texcommand), "-interaction=nonstopmode",
901                       "-halt-on-error", "figure.tex"]
902            try:
903                check_output(cmdargs, stderr=subprocess.STDOUT, cwd=tmpdir)
904            except subprocess.CalledProcessError as e:
905                raise RuntimeError(
906                    "%s was not able to process your file.\n\nFull log:\n%s"
907                    % (texcommand, e.output))
908
909            # copy file contents to target
910            with open(fname_pdf, "rb") as fh_src:
911                shutil.copyfileobj(fh_src, fh)
912        finally:
913            try:
914                shutil.rmtree(tmpdir)
915            except:
916                TmpDirCleaner.add(tmpdir)
917
918    def print_pdf(self, fname_or_fh, *args, **kwargs):
919        """
920        Use LaTeX to compile a Pgf generated figure to PDF.
921        """
922        if kwargs.get("dryrun", False):
923            self._print_pgf_to_fh(None, *args, **kwargs)
924            return
925
926        # figure out where the pdf is to be written to
927        if isinstance(fname_or_fh, six.string_types):
928            with open(fname_or_fh, "wb") as fh:
929                self._print_pdf_to_fh(fh, *args, **kwargs)
930        elif is_writable_file_like(fname_or_fh):
931            self._print_pdf_to_fh(fname_or_fh, *args, **kwargs)
932        else:
933            raise ValueError("filename must be a path or a file-like object")
934
935    def _print_png_to_fh(self, fh, *args, **kwargs):
936        converter = make_pdf_to_png_converter()
937
938        try:
939            # create temporary directory for pdf creation and png conversion
940            tmpdir = tempfile.mkdtemp(prefix="mpl_pgf_")
941            fname_pdf = os.path.join(tmpdir, "figure.pdf")
942            fname_png = os.path.join(tmpdir, "figure.png")
943            # create pdf and try to convert it to png
944            self.print_pdf(fname_pdf, *args, **kwargs)
945            converter(fname_pdf, fname_png, dpi=self.figure.dpi)
946            # copy file contents to target
947            with open(fname_png, "rb") as fh_src:
948                shutil.copyfileobj(fh_src, fh)
949        finally:
950            try:
951                shutil.rmtree(tmpdir)
952            except:
953                TmpDirCleaner.add(tmpdir)
954
955    def print_png(self, fname_or_fh, *args, **kwargs):
956        """
957        Use LaTeX to compile a pgf figure to pdf and convert it to png.
958        """
959        if kwargs.get("dryrun", False):
960            self._print_pgf_to_fh(None, *args, **kwargs)
961            return
962
963        if isinstance(fname_or_fh, six.string_types):
964            with open(fname_or_fh, "wb") as fh:
965                self._print_png_to_fh(fh, *args, **kwargs)
966        elif is_writable_file_like(fname_or_fh):
967            self._print_png_to_fh(fname_or_fh, *args, **kwargs)
968        else:
969            raise ValueError("filename must be a path or a file-like object")
970
971    def get_renderer(self):
972        return RendererPgf(self.figure, None, dummy=True)
973
974
975class FigureManagerPgf(FigureManagerBase):
976    def __init__(self, *args):
977        FigureManagerBase.__init__(self, *args)
978
979
980@_Backend.export
981class _BackendPgf(_Backend):
982    FigureCanvas = FigureCanvasPgf
983    FigureManager = FigureManagerPgf
984
985
986def _cleanup_all():
987    LatexManager._cleanup_remaining_instances()
988    TmpDirCleaner.cleanup_remaining_tmpdirs()
989
990atexit.register(_cleanup_all)
991