diff --git a/doc/_static/pgf_fonts.pdf b/doc/_static/pgf_fonts.pdf new file mode 100644 index 000000000000..7c5784f6200a Binary files /dev/null and b/doc/_static/pgf_fonts.pdf differ diff --git a/doc/_static/pgf_fonts.png b/doc/_static/pgf_fonts.png new file mode 100644 index 000000000000..6aa4e360b566 Binary files /dev/null and b/doc/_static/pgf_fonts.png differ diff --git a/doc/_static/pgf_preamble.pdf b/doc/_static/pgf_preamble.pdf new file mode 100644 index 000000000000..e8e085043219 Binary files /dev/null and b/doc/_static/pgf_preamble.pdf differ diff --git a/doc/_static/pgf_preamble.png b/doc/_static/pgf_preamble.png new file mode 100644 index 000000000000..6841e3b9bdd4 Binary files /dev/null and b/doc/_static/pgf_preamble.png differ diff --git a/doc/_static/pgf_texsystem.pdf b/doc/_static/pgf_texsystem.pdf new file mode 100644 index 000000000000..fbae0ea766ff Binary files /dev/null and b/doc/_static/pgf_texsystem.pdf differ diff --git a/doc/_static/pgf_texsystem.png b/doc/_static/pgf_texsystem.png new file mode 100644 index 000000000000..6075e7b764dd Binary files /dev/null and b/doc/_static/pgf_texsystem.png differ diff --git a/doc/users/index_text.rst b/doc/users/index_text.rst index bc4459803f43..bba1f330299f 100644 --- a/doc/users/index_text.rst +++ b/doc/users/index_text.rst @@ -8,6 +8,7 @@ Working with text text_intro.rst text_props.rst mathtext.rst + pgf.rst usetex.rst annotations_intro.rst diff --git a/doc/users/pgf.rst b/doc/users/pgf.rst new file mode 100644 index 000000000000..6411a9f9c6e3 --- /dev/null +++ b/doc/users/pgf.rst @@ -0,0 +1,128 @@ +.. _pgf-tutorial: + +********************************* +Typesetting With XeLaTeX/LuaLaTeX +********************************* + +Using the ``pgf`` backend, matplotlib can export figures as pgf drawing commands +that can be processed with pdflatex, xelatex or lualatex. XeLaTeX and LuaLaTeX +have full unicode support and can use any fonts installed in the operating +system, making use of advanced typographic features of OpenType, AAT and +Graphite. Pgf pictures created by ``plt.savefig('figure.pgf')`` can be +embedded as raw commands in LaTeX documents. Figures can also be directly +compiled and saved to PDF with ``plt.savefig('figure.pdf')``. + +Matplotlib's pgf support requires a working LaTeX_ installation (such as +TeXLive_), preferably including XeLaTeX or LuaLaTeX. If pdftocairo or +ghostscript is installed, figures can optionally be saved to PNG images. +The executables for all applications must be located on your :envvar:`PATH`. + +Rc parameters that control the behavior of the pgf backend: + + ================= ===================================================== + Parameter Documentation + ================= ===================================================== + pgf.preamble Lines to be included in the LaTeX preamble + pgf.rcfonts Setup fonts from rc params using the fontspec package + pgf.texsystem Either "xelatex", "lualatex" or "pdflatex" + ================= ===================================================== + +.. note:: + + TeX defines a set of secial characters, such as:: + + # $ % & ~ _ ^ \ { } + + Generally, these characters must be escaped correctly. For convenience, + some characters (_,^,%) are automatically escaped outside of math + environments. + +.. _pgf-rcfonts: + +Font specification +================== + +The fonts used for obtaining the size of text elements or when compiling +figures to PDF are usually defined in the matplotlib rc parameters. You can +also use the LaTeX default Computer Modern fonts by clearing the lists for +``font.serif``, ``font.sans-serif`` or ``font.monospace``. Please note that +the glyph coverage of these fonts is very limited. For extended unicode support +the `Computer Modern Unicode `_ +fonts "CMU Serif", "CMU Sans Serif" are recommended. + +.. literalinclude:: plotting/examples/pgf_fonts.py + :end-before: plt.savefig + +.. image:: /_static/pgf_fonts.* + + +.. _pgf-preamble: + +Custom preamble +=============== + +Full customization is possible by adding your own commands to the preamble. +Use the ``pgf.preamble`` parameter if you want to configure the math fonts or +for loading additional packages. Also, if you want to do the font configuration +yourself instead of using the fonts specified in the rc parameters, make sure +to disable ``pgf.rcfonts``. + +.. htmlonly:: + + .. literalinclude:: plotting/examples/pgf_preamble.py + :end-before: plt.savefig + +.. latexonly:: + + .. literalinclude:: plotting/examples/pgf_preamble.py + :end-before: import matplotlib.pyplot as plt + +.. image:: /_static/pgf_preamble.* + + +.. _pgf-texsystem: + +Choosing the TeX system +======================= + +The TeX system to be used by matplotlib is chosen by the ``pgf.texsystem`` +parameter. Possible values are ``'xelatex'`` (default), ``'lualatex'`` and +``'pdflatex'``. Please note that when selecting pdflatex the fonts and +unicode handling must be configured in the preamble. + +.. literalinclude:: plotting/examples/pgf_texsystem.py + :end-before: plt.savefig + +.. image:: /_static/pgf_texsystem.* + +.. _pgf-hangups: + +Possible hangups +================ + +* On Windows, the :envvar:`PATH` environment variable may need to be modified + to include the directories containing the latex, dvipng and ghostscript + executables. See :ref:`environment-variables` and + :ref:`setting-windows-environment-variables` for details. + +* Sometimes the font rendering in figures that are saved to png images is + very bad. This happens when the pdftocairo tool is not available and + ghostscript is used for the pdf to png conversion. + +.. _pgf-troubleshooting: + +Troubleshooting +=============== + +* Make sure what you are trying to do is possible in a LaTeX document, + that your LaTeX syntax is valid and that you are using raw strings + if necessary to avoid unintended escape sequences. + +* The ``pgf.preamble`` rc setting provides lots of flexibility, and lots of + ways to cause problems. When experiencing problems, try to minimalize or + disable the custom preamble before reporting problems. + +* If you still need help, please see :ref:`reporting-problems` + +.. _LaTeX: http://www.tug.org +.. _TeXLive: http://www.tug.org/texlive/ diff --git a/doc/users/plotting/examples/pgf_fonts.py b/doc/users/plotting/examples/pgf_fonts.py new file mode 100644 index 000000000000..ae260b151406 --- /dev/null +++ b/doc/users/plotting/examples/pgf_fonts.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +import matplotlib as mpl +mpl.use("pgf") +pgf_with_rc_fonts = { + "font.family": "serif", + "font.serif": [], # use latex default serif font + "font.sans-serif": ["DejaVu Sans"], # use a specific sans-serif font +} +mpl.rcParams.update(pgf_with_rc_fonts) + +import matplotlib.pyplot as plt +plt.figure(figsize=(4.5,2.5)) +plt.plot(range(5)) +plt.text(0.5, 3., "serif") +plt.text(0.5, 2., "monospace", family="monospace") +plt.text(2.5, 2., "sans-serif", family="sans-serif") +plt.text(2.5, 1., "comic sans", family="Comic Sans MS") +plt.xlabel(u"µ is not $\\mu$") +plt.tight_layout(.5) + +plt.savefig("pgf_fonts.pdf") +plt.savefig("pgf_fonts.png") diff --git a/doc/users/plotting/examples/pgf_preamble.py b/doc/users/plotting/examples/pgf_preamble.py new file mode 100644 index 000000000000..67c6283f6324 --- /dev/null +++ b/doc/users/plotting/examples/pgf_preamble.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +import matplotlib as mpl +mpl.use("pgf") +pgf_with_custom_preamble = { + "font.family": "serif", # use serif/main font for text elements + "text.usetex": True, # use inline math for ticks + "pgf.rcfonts": False, # don't setup fonts from rc parameters + "pgf.preamble": [ + r"\usepackage{units}", # load additional packages + r"\usepackage{metalogo}", # load additional packages + r"\usepackage{unicode-math}", # unicode math setup + r"\setmathfont{XITS Math}", + r"\setmainfont{DejaVu Serif}", # font setup via preamble + ] +} +mpl.rcParams.update(pgf_with_custom_preamble) + +import matplotlib.pyplot as plt +plt.figure(figsize=(4.5,2.5)) +plt.plot(range(5)) +plt.xlabel(u"unicode text: я, ψ, €, ü, \\unitfrac[10]{°}{µm}") +plt.ylabel(u"\\XeLaTeX") +plt.legend([u"unicode math: $λ=∑_i^∞ μ_i^2$"]) +plt.tight_layout(.5) + +plt.savefig("pgf_preamble.pdf") +plt.savefig("pgf_preamble.png") diff --git a/doc/users/plotting/examples/pgf_texsystem.py b/doc/users/plotting/examples/pgf_texsystem.py new file mode 100644 index 000000000000..88231348b5e2 --- /dev/null +++ b/doc/users/plotting/examples/pgf_texsystem.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +import matplotlib as mpl +mpl.use("pgf") +pgf_with_pdflatex = { + "pgf.texsystem": "pdflatex", + "pgf.preamble": [ + r"\usepackage[utf8x]{inputenc}", + r"\usepackage[T1]{fontenc}", + r"\usepackage{cmbright}", + ] +} +mpl.rcParams.update(pgf_with_pdflatex) + +import matplotlib.pyplot as plt +plt.figure(figsize=(4.5,2.5)) +plt.plot(range(5)) +plt.text(0.5, 3., "serif", family="serif") +plt.text(0.5, 2., "monospace", family="monospace") +plt.text(2.5, 2., "sans-serif", family="sans-serif") +plt.xlabel(u"µ is not $\\mu$") +plt.tight_layout(.5) + +plt.savefig("pgf_texsystem.pdf") +plt.savefig("pgf_texsystem.png") diff --git a/doc/users/whats_new.rst b/doc/users/whats_new.rst index b7a2f890662d..9ae7645f3a21 100644 --- a/doc/users/whats_new.rst +++ b/doc/users/whats_new.rst @@ -17,6 +17,15 @@ This page just covers the highlights -- for the full story, see the new in matplotlib-1.2 ===================== +PGF/TikZ backend +---------------- +Peter Würtz wrote a backend that allows matplotlib to export figures as +drawing commands for LaTeX that can be processed by PdfLaTeX, XeLaTeX or +LuaLaTeX using the PGF/TikZ package. Usage examples and documentation are +found in :ref:`pgf-tutorial`. + +.. image:: /_static/pgf_preamble.* + Locator interface ----------------- diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 232ac22618cf..a1a170766be8 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1056,6 +1056,7 @@ def tk_window_focus(): 'matplotlib.tests.test_agg', 'matplotlib.tests.test_axes', 'matplotlib.tests.test_backend_svg', + 'matplotlib.tests.test_backend_pgf', 'matplotlib.tests.test_basic', 'matplotlib.tests.test_cbook', 'matplotlib.tests.test_colorbar', diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 3beffdcc90e8..c0eeb0543db6 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1807,6 +1807,7 @@ def get_width_height(self): 'emf': 'Enhanced Metafile', 'eps': 'Encapsulated Postscript', 'pdf': 'Portable Document Format', + 'pgf': 'LaTeX PGF Figure', 'png': 'Portable Network Graphics', 'ps' : 'Postscript', 'raw': 'Raw RGBA bitmap', @@ -1841,6 +1842,11 @@ def print_pdf(self, *args, **kwargs): pdf = self.switch_backends(FigureCanvasPdf) return pdf.print_pdf(*args, **kwargs) + def print_pgf(self, *args, **kwargs): + from backends.backend_pgf import FigureCanvasPgf # lazy import + pgf = self.switch_backends(FigureCanvasPgf) + return pgf.print_pgf(*args, **kwargs) + def print_png(self, *args, **kwargs): from backends.backend_agg import FigureCanvasAgg # lazy import agg = self.switch_backends(FigureCanvasAgg) diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py new file mode 100644 index 000000000000..5684522efdc6 --- /dev/null +++ b/lib/matplotlib/backends/backend_pgf.py @@ -0,0 +1,803 @@ +from __future__ import division + +import math +import os +import sys +import re +import shutil +import tempfile +import codecs +import subprocess + +import matplotlib as mpl +from matplotlib.backend_bases import RendererBase, GraphicsContextBase,\ + FigureManagerBase, FigureCanvasBase +from matplotlib.figure import Figure +from matplotlib.text import Text +from matplotlib.path import Path +from matplotlib import _png, rcParams +from matplotlib import font_manager +from matplotlib.ft2font import FT2Font + +############################################################################### + +# create a list of system fonts, all of these should work with xe/lua-latex +system_fonts = [] +for f in font_manager.findSystemFonts(): + try: + system_fonts.append(FT2Font(str(f).family_name)) + except RuntimeError: + pass # some fonts on osx are known to fail, print? + except: + pass # unknown error, skip this font + +# get chosen TeX system from rc +def get_texcommand(): + texsystem_options = ["xelatex", "lualatex", "pdflatex"] + texsystem = rcParams.get("pgf.texsystem", "xelatex") + return texsystem if texsystem in texsystem_options else "xelatex" + +# build fontspec preamble from rc +def get_fontspec(): + latex_fontspec = [] + texcommand = get_texcommand() + + if texcommand is not "pdflatex": + latex_fontspec.append(r"\usepackage{fontspec}") + + if texcommand is not "pdflatex" and rcParams.get("pgf.rcfonts", True): + # try to find fonts from rc parameters + families = ["serif", "sans-serif", "monospace"] + fontspecs = [r"\setmainfont{%s}", r"\setsansfont{%s}", r"\setmonofont{%s}"] + for family, fontspec in zip(families, fontspecs): + matches = [f for f in rcParams["font."+family] if f in system_fonts] + if matches: + latex_fontspec.append(fontspec % matches[0]) + else: + pass # no fonts found, fallback to LaTeX defaule + + return "\n".join(latex_fontspec) + +# get LaTeX preamble from rc +def get_preamble(): + latex_preamble = rcParams.get("pgf.preamble", "") + if type(latex_preamble) == list: + latex_preamble = "\n".join(latex_preamble) + return latex_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"(? 3) and (rgbFace[3] != 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 (path_is_transparent or fill_is_transparent): + opacity = gc.get_alpha() * 1.0 if not fill_is_transparent else rgbFace[3] + writeln(self.fh, r"\pgfsetfillopacity{%f}" % opacity) + + # 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 gc.get_alpha() != 1.0: + writeln(self.fh, r"\pgfsetstrokeopacity{%f}" % gc.get_alpha()) + + # line style + ls = gc.get_linestyle(None) + if ls == "solid": + writeln(self.fh, r"\pgfsetdash{}{0pt}") + elif ls == "dashed": + writeln(self.fh, r"\pgfsetdash{{%fpt}{%fpt}}{0pt}" % (2.5*lw, 2.5*lw)) + elif ls == "dashdot": + writeln(self.fh, r"\pgfsetdash{{%fpt}{%fpt}{%fpt}{%fpt}}{0pt}" % (3*lw, 3*lw, 1*lw, 3*lw)) + elif "dotted": + writeln(self.fh, r"\pgfsetdash{{%fpt}{%fpt}}{0pt}" % (lw, 3*lw)) + + def _print_pgf_path(self, path, transform): + f = 1./self.dpi + # build path + for points, code in path.iter_segments(transform): + 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{\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{\pgfqpoint{%fin}{%fin}}{\pgfqpoint{%fin}{%fin}}{\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 draw_image(self, gc, x, y, im): + # TODO: Almost no documentation for the behavior of this function. + # Something missing? + + # save the images to png files + path = os.path.dirname(self.fh.name) + fname = os.path.splitext(os.path.basename(self.fh.name))[0] + fname_img = "%s-img%d.png" % (fname, self.image_counter) + self.image_counter += 1 + im.flipud_out() + rows, cols, buf = im.as_rgba_str() + _png.write_png(buf, cols, rows, os.path.join(path, fname_img)) + + # reference the image in the pgf picture + writeln(self.fh, r"\begin{pgfscope}") + self._print_pgf_clip(gc) + h, w = im.get_size_out() + f = 1./self.dpi # from display coords to inch + writeln(self.fh, r"\pgftext[at=\pgfqpoint{%fin}{%fin},left,bottom]{\pgfimage[interpolate=true,width=%fin,height=%fin]{%s}}" % (x*f, y*f, w*f, h*f, fname_img)) + writeln(self.fh, r"\end{pgfscope}") + + def draw_tex(self, gc, x, y, s, prop, angle, ismath="TeX!"): + self.draw_text(gc, x, y, s, prop, angle, ismath) + + def draw_text(self, gc, x, y, s, prop, angle, ismath=False): + s = common_texification(s) + + # apply font properties + prop_cmds = _font_properties_str(prop) + s = ur"{%s %s}" % (prop_cmds, s) + + # draw text at given coordinates + x = x * 1./self.dpi + y = y * 1./self.dpi + 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) + stroke_rgb = tuple(gc.get_rgb())[:3] + if stroke_rgb != (0, 0, 0): + writeln(self.fh, r"\definecolor{textcolor}{rgb}{%f,%f,%f}" % stroke_rgb) + writeln(self.fh, r"\pgfsetstrokecolor{textcolor}") + writeln(self.fh, r"\pgfsetfillcolor{textcolor}") + writeln(self.fh, "\\pgftext[left,bottom,x=%fin,y=%fin,rotate=%f]{%s}\n" % (x,y,angle,s)) + writeln(self.fh, r"\end{pgfscope}") + + def get_text_width_height_descent(self, s, prop, ismath): + # 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 = self.latexManager.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): + return False + + def get_canvas_width_height(self): + return self.figure.get_figwidth(), self.figure.get_figheight() + + def points_to_pixels(self, points): + return points * mpl_pt_to_in * self.dpi + + def new_gc(self): + return GraphicsContextPgf() + + +class GraphicsContextPgf(GraphicsContextBase): + pass + +######################################################################## + +def draw_if_interactive(): + pass + +def new_figure_manager(num, *args, **kwargs): + """ + Create a new figure manager instance + """ + # if a main-level app must be created, this is the usual place to + # do it -- see backend_wx, backend_wxagg and backend_tkagg for + # examples. Not all GUIs require explicit instantiation of a + # main-level app (egg backend_gtk, backend_gtkagg) for pylab + FigureClass = kwargs.pop('FigureClass', Figure) + thisFig = FigureClass(*args, **kwargs) + canvas = FigureCanvasPgf(thisFig) + manager = FigureManagerPgf(canvas, num) + return manager + + +class FigureCanvasPgf(FigureCanvasBase): + filetypes = {"pgf": "LaTeX PGF picture", + "pdf": "LaTeX compiled PGF picture", + "png": "Portable Network Graphics",} + + def __init__(self, *args): + FigureCanvasBase.__init__(self, *args) + + def get_default_filetype(self): + return 'pdf' + + def print_pgf(self, filename, *args, **kwargs): + """ + Output pgf commands for drawing the figure so it can be included and + rendered in latex documents. + """ + + header_text = r"""%% Creator: Matplotlib, PGF backend +%% +%% To include the figure in your LaTeX document, write +%% \input{.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{}{.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() + + # start a pgfpicture environment and set a bounding box + with codecs.open(filename, "w", encoding="utf-8") as fh: + 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}") + + renderer = RendererPgf(self.figure, fh) + self.figure.draw(renderer) + + # end the pgfpicture environment + writeln(fh, r"\end{pgfpicture}") + writeln(fh, r"\makeatother") + writeln(fh, r"\endgroup") + + def print_pdf(self, filename, *args, **kwargs): + """ + Use LaTeX to compile a Pgf generated figure to PDF. + """ + w, h = self.figure.get_figwidth(), self.figure.get_figheight() + + target = os.path.abspath(filename) + + try: + tmpdir = tempfile.mkdtemp() + cwd = os.getcwd() + os.chdir(tmpdir) + self.print_pgf("figure.pgf") + + latex_preamble = get_preamble() + latex_fontspec = get_fontspec() + latexcode = r""" +\documentclass[12pt]{minimal} +\usepackage[paperwidth=%fin, paperheight=%fin, margin=0in]{geometry} +%s +%s +\usepackage{pgf} + +\begin{document} +\centering +\input{figure.pgf} +\end{document}""" % (w, h, latex_preamble, latex_fontspec) + with codecs.open("figure.tex", "w", "utf-8") as fh: + fh.write(latexcode) + + texcommand = get_texcommand() + cmdargs = [texcommand, "-interaction=nonstopmode", "-halt-on-error", "figure.tex"] + try: + stdout = subprocess.check_output(cmdargs, universal_newlines=True, stderr=subprocess.STDOUT) + except: + raise RuntimeError("%s was not able to process your file.\n\nFull log:\n%s" % (texcommand, stdout)) + shutil.copyfile("figure.pdf", target) + finally: + os.chdir(cwd) + try: + shutil.rmtree(tmpdir) + except: + sys.stderr.write("could not delete tmp directory %s\n" % tmpdir) + + def print_png(self, filename, *args, **kwargs): + """ + Use LaTeX to compile a pgf figure to pdf and convert it to png. + """ + + converter = make_pdf_to_png_converter() + + target = os.path.abspath(filename) + try: + tmpdir = tempfile.mkdtemp() + cwd = os.getcwd() + os.chdir(tmpdir) + self.print_pdf("figure.pdf") + converter("figure.pdf", "figure.png", dpi=self.figure.dpi) + shutil.copyfile("figure.png", target) + finally: + os.chdir(cwd) + try: + shutil.rmtree(tmpdir) + except: + sys.stderr.write("could not delete tmp directory %s\n" % tmpdir) + + def _render_texts_pgf(self, fh): + # TODO: currently unused code path + + # alignment anchors + valign = {"top": "top", "bottom": "bottom", "baseline": "base", "center": ""} + halign = {"left": "left", "right": "right", "center": ""} + # alignment anchors for 90deg. rotated labels + rvalign = {"top": "left", "bottom": "right", "baseline": "right", "center": ""} + rhalign = {"left": "top", "right": "bottom", "center": ""} + + # TODO: matplotlib does not hide unused tick labels yet, workaround + for tick in self.figure.findobj(mpl.axis.Tick): + tick.label1.set_visible(tick.label1On) + tick.label2.set_visible(tick.label2On) + # TODO: strange, first legend label is always "None", workaround + for legend in self.figure.findobj(mpl.legend.Legend): + labels = legend.findobj(mpl.text.Text) + labels[0].set_visible(False) + # TODO: strange, legend child labels are duplicated, + # find a list of unique text objects as workaround + texts = self.figure.findobj(match=Text, include_self=False) + texts = list(set(texts)) + + # draw text elements + for text in texts: + s = text.get_text() + if not s or not text.get_visible(): continue + + s = common_texification(s) + + fontsize = text.get_fontsize() + angle = text.get_rotation() + transform = text.get_transform() + x, y = transform.transform_point(text.get_position()) + x = x * 1.0 / self.figure.dpi + y = y * 1.0 / self.figure.dpi + # TODO: positioning behavior unknown for rotated elements + # right now only the alignment for 90deg rotations is correct + if angle == 90.: + align = rvalign[text.get_va()] + "," + rhalign[text.get_ha()] + else: + align = valign[text.get_va()] + "," + halign[text.get_ha()] + + s = ur"{\fontsize{%f}{%f}\selectfont %s}" % (fontsize, fontsize*1.2, s) + writeln(fh, ur"\pgftext[%s,x=%fin,y=%fin,rotate=%f]{%s}" % (align,x,y,angle,s)) + + def get_renderer(self): + return RendererPgf(self.figure, None) + +class FigureManagerPgf(FigureManagerBase): + def __init__(self, *args): + FigureManagerBase.__init__(self, *args) + +######################################################################## + +FigureManager = FigureManagerPgf diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index ec9c3e7136ca..224708d2dc00 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -30,7 +30,7 @@ non_interactive_bk = ['agg', 'cairo', 'emf', 'gdk', - 'pdf', 'ps', 'svg', 'template'] + 'pdf', 'pgf', 'ps', 'svg', 'template'] all_backends = interactive_bk + non_interactive_bk @@ -319,6 +319,9 @@ def validate_hinting(s): return s.lower() raise ValueError("hinting should be 'auto', 'native', 'either' or 'none'") +validate_pgf_texsystem = ValidateInStrings('pgf.texsystem', + ['xelatex', 'lualatex', 'pdflatex']) + validate_movie_writer = ValidateInStrings('animation.writer', ['ffmpeg', 'ffmpeg_file', 'mencoder', 'mencoder_file']) @@ -574,6 +577,12 @@ def __call__(self, s): 'pdf.use14corefonts' : [False, validate_bool], # use only the 14 PDF core fonts # embedded in every PDF viewing application 'pdf.fonttype' : [3, validate_fonttype], # 3 (Type3) or 42 (Truetype) + + 'pgf.debug' : [False, validate_bool], # output debug information + 'pgf.texsystem' : ['xelatex', validate_pgf_texsystem], # choose latex application for creating pdf files (xelatex/lualatex) + 'pgf.rcfonts' : [True, validate_bool], # use matplotlib rc settings for font configuration + 'pgf.preamble' : [[''], validate_stringlist], # provide a custom preamble for the latex process + 'svg.image_inline' : [True, validate_bool], # write raster image data directly into the svg file 'svg.image_noscale' : [False, validate_bool], # suppress scaling of raster data embedded in SVG 'svg.embed_char_paths' : [True, deprecate_svg_embed_char_paths], # True to save all characters as paths in the SVG diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_pdflatex.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_pdflatex.pdf new file mode 100644 index 000000000000..4ffa77797d1d Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_pdflatex.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_rcupdate1.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_rcupdate1.pdf new file mode 100644 index 000000000000..955aad2fefd2 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_rcupdate1.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_rcupdate2.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_rcupdate2.pdf new file mode 100644 index 000000000000..e41ec043584c Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_rcupdate2.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_xelatex.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_xelatex.pdf new file mode 100644 index 000000000000..96a6d5217929 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_xelatex.pdf differ diff --git a/lib/matplotlib/tests/test_backend_pgf.py b/lib/matplotlib/tests/test_backend_pgf.py new file mode 100644 index 000000000000..10993927fe5b --- /dev/null +++ b/lib/matplotlib/tests/test_backend_pgf.py @@ -0,0 +1,110 @@ +# -*- encoding: utf-8 -*- + +import os +import shutil +import subprocess +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt +from matplotlib.testing.compare import compare_images, ImageComparisonFailure +from matplotlib.testing.decorators import _image_directories, knownfailureif + +baseline_dir, result_dir = _image_directories(lambda: 'dummy func') + +def run(*args): + try: + subprocess.check_output(args) + return True + except: + return False + +def switch_backend(backend): + import nose + + def switch_backend_decorator(func): + def backend_switcher(*args, **kwargs): + try: + prev_backend = mpl.get_backend() + mpl.rcdefaults() + plt.switch_backend(backend) + result = func(*args, **kwargs) + finally: + plt.switch_backend(prev_backend) + return result + + return nose.tools.make_decorator(func)(backend_switcher) + return switch_backend_decorator + +def compare_figure(fname): + actual = os.path.join(result_dir, fname) + plt.savefig(actual) + + expected = os.path.join(result_dir, "expected_%s" % fname) + shutil.copyfile(os.path.join(baseline_dir, fname), expected) + err = compare_images(expected, actual, tol=1e-4) + if err: + raise ImageComparisonFailure('images not close: %s vs. %s' % (actual, expected)) + +############################################################################### + +def create_figure(): + plt.figure() + x = np.linspace(0, 1, 15) + plt.plot(x, x**2, "b-") + plt.plot(x, 1-x**2, "g>") + plt.plot([0.9], [0.5], "ro", markersize=3) + plt.text(0.9, 0.5, u'unicode (ü, °, µ) and math ($\\mu_i = x_i^2$)', ha='right', fontsize=20) + plt.ylabel(u'sans-serif with math $\\frac{\\sqrt{x}}{y^2}$..', family='sans-serif') + + +# test compiling a figure to pdf with xelatex +@knownfailureif(not run('xelatex', '-v'), msg="xelatex is required for this test") +@switch_backend('pgf') +def test_xelatex(): + rc_xelatex = {'font.family': 'serif', + 'pgf.rcfonts': False,} + mpl.rcParams.update(rc_xelatex) + create_figure() + compare_figure('pgf_xelatex.pdf') + + +# test compiling a figure to pdf with pdflatex +@knownfailureif(not run('pdflatex', '-v'), msg="pdflatex is required for this test") +@switch_backend('pgf') +def test_pdflatex(): + rc_pdflatex = {'font.family': 'serif', + 'pgf.rcfonts': False, + 'pgf.texsystem': 'pdflatex', + 'pgf.preamble': [r'\usepackage[utf8x]{inputenc}', + r'\usepackage[T1]{fontenc}']} + mpl.rcParams.update(rc_pdflatex) + create_figure() + compare_figure('pgf_pdflatex.pdf') + + +# test updating the rc parameters for each figure +@knownfailureif(not run('pdflatex', '-v') or not run('xelatex', '-v'), + msg="xelatex and pdflatex are required for this test") +@switch_backend('pgf') +def test_rcupdate(): + rc_sets = [] + rc_sets.append({'font.family': 'sans-serif', + 'font.size': 30, + 'figure.subplot.left': .2, + 'lines.markersize': 10, + 'pgf.rcfonts': False, + 'pgf.texsystem': 'xelatex'}) + rc_sets.append({'font.family': 'monospace', + 'font.size': 10, + 'figure.subplot.left': .1, + 'lines.markersize': 20, + 'pgf.rcfonts': False, + 'pgf.texsystem': 'pdflatex', + 'pgf.preamble': [r'\usepackage[utf8x]{inputenc}', + r'\usepackage[T1]{fontenc}', + r'\usepackage{sfmath}']}) + + for i, rc_set in enumerate(rc_sets): + mpl.rcParams.update(rc_set) + create_figure() + compare_figure('pgf_rcupdate%d.pdf' % (i+1))