Skip to content
This repository
Browse code

Merge pull request #1124 from pwuertz/pgf-backend

PGF backend, fix #1116, #1118 and #1128
  • Loading branch information...
commit c981774e742c8ca48023ba8f24a23c16b4c6f70d 2 parents 354721b + 138d55d
Michael Droettboom authored August 22, 2012
208  lib/matplotlib/backends/backend_pgf.py
@@ -18,6 +18,7 @@
18 18
 from matplotlib import _png, rcParams
19 19
 from matplotlib import font_manager
20 20
 from matplotlib.ft2font import FT2Font
  21
+from matplotlib.cbook import is_string_like, is_writable_file_like
21 22
 
22 23
 ###############################################################################
23 24
 
@@ -220,15 +221,33 @@ def _build_latex_header():
220 221
         # Create LaTeX header with some content, else LaTeX will load some
221 222
         # math fonts later when we don't expect the additional output on stdout.
222 223
         # TODO: is this sufficient?
223  
-        latex_header = u"""\\documentclass{minimal}
224  
-%s
225  
-%s
226  
-\\begin{document}
227  
-text $math \mu$ %% force latex to load fonts now
228  
-\\typeout{pgf_backend_query_start}
229  
-""" % (latex_preamble, latex_fontspec)
  224
+        latex_header = [r"\documentclass{minimal}",
  225
+                        latex_preamble,
  226
+                        latex_fontspec,
  227
+                        r"\begin{document}",
  228
+                        r"text $math \mu$", # force latex to load fonts now
  229
+                        r"\typeout{pgf_backend_query_start}"]
  230
+        return "\n".join(latex_header)
  231
+
  232
+    def _stdin_writeln(self, s):
  233
+        self.latex_stdin_utf8.write(s)
  234
+        self.latex_stdin_utf8.write("\n")
  235
+        self.latex_stdin_utf8.flush()
  236
+
  237
+    def _expect(self, s):
  238
+        exp = s.encode("utf8")
  239
+        buf = bytearray()
  240
+        while True:
  241
+            b = self.latex.stdout.read(1)
  242
+            buf += b
  243
+            if buf[-len(exp):] == exp:
  244
+                break
  245
+            if not len(b):
  246
+                raise LatexError("LaTeX process halted", buf.decode("utf8"))
  247
+        return buf.decode("utf8")
230 248
 
231  
-        return latex_header
  249
+    def _expect_prompt(self):
  250
+        return self._expect("\n*")
232 251
 
233 252
     def __init__(self):
234 253
         self.texcommand = get_texcommand()
@@ -238,27 +257,23 @@ def __init__(self):
238 257
         # test the LaTeX setup to ensure a clean startup of the subprocess
239 258
         latex = subprocess.Popen([self.texcommand, "-halt-on-error"],
240 259
                                   stdin=subprocess.PIPE,
241  
-                                  stdout=subprocess.PIPE,
242  
-                                  universal_newlines=True)
243  
-        stdout, stderr = latex.communicate(self.latex_header + latex_end)
  260
+                                  stdout=subprocess.PIPE)
  261
+        test_input = self.latex_header + latex_end
  262
+        stdout, stderr = latex.communicate(test_input.encode("utf-8"))
244 263
         if latex.returncode != 0:
245 264
             raise LatexError("LaTeX returned an error, probably missing font or error in preamble:\n%s" % stdout)
246 265
 
247  
-        # open LaTeX process
  266
+        # open LaTeX process for real work
248 267
         latex = subprocess.Popen([self.texcommand, "-halt-on-error"],
249 268
                                   stdin=subprocess.PIPE,
250  
-                                  stdout=subprocess.PIPE,
251  
-                                  universal_newlines=True)
252  
-        latex.stdin.write(self.latex_header)
253  
-        latex.stdin.flush()
254  
-        # read all lines until our 'pgf_backend_query_start' token appears
255  
-        while not latex.stdout.readline().startswith("*pgf_backend_query_start"):
256  
-            pass
257  
-        while latex.stdout.read(1) != '*':
258  
-            pass
  269
+                                  stdout=subprocess.PIPE)
259 270
         self.latex = latex
260  
-        self.latex_stdin = codecs.getwriter("utf-8")(latex.stdin)
261  
-        self.latex_stdout = codecs.getreader("utf-8")(latex.stdout)
  271
+        self.latex_stdin_utf8 = codecs.getwriter("utf8")(self.latex.stdin)
  272
+        # write header with 'pgf_backend_query_start' token
  273
+        self._stdin_writeln(self._build_latex_header())
  274
+        # read all lines until our 'pgf_backend_query_start' token appears
  275
+        self._expect("*pgf_backend_query_start")
  276
+        self._expect_prompt()
262 277
 
263 278
         # cache for strings already processed
264 279
         self.str_cache = {}
@@ -267,8 +282,8 @@ def __del__(self):
267 282
         if rcParams.get("pgf.debug", False):
268 283
             print "deleting LatexManager"
269 284
         try:
270  
-            self.latex.terminate()
271  
-            self.latex.wait()
  285
+            self.latex_stdin_utf8.close()
  286
+            self.latex.communicate()
272 287
         except:
273 288
             pass
274 289
         try:
@@ -277,19 +292,6 @@ def __del__(self):
277 292
         except:
278 293
             pass
279 294
 
280  
-    def _wait_for_prompt(self):
281  
-        """
282  
-        Read all bytes from LaTeX stdout until a new line starts with a *.
283  
-        """
284  
-        buf = [""]
285  
-        while True:
286  
-            buf.append(self.latex_stdout.read(1))
287  
-            if buf[-1] == "*" and buf[-2] == "\n":
288  
-                break
289  
-            if buf[-1] == "":
290  
-                raise LatexError("LaTeX process halted", u"".join(buf))
291  
-        return "".join(buf)
292  
-
293 295
     def get_width_height_descent(self, text, prop):
294 296
         """
295 297
         Get the width, total height and descent for a text typesetted by the
@@ -298,30 +300,27 @@ def get_width_height_descent(self, text, prop):
298 300
 
299 301
         # apply font properties and define textbox
300 302
         prop_cmds = _font_properties_str(prop)
301  
-        textbox = u"\\sbox0{%s %s}\n" % (prop_cmds, text)
  303
+        textbox = "\\sbox0{%s %s}" % (prop_cmds, text)
302 304
 
303 305
         # check cache
304 306
         if textbox in self.str_cache:
305 307
             return self.str_cache[textbox]
306 308
 
307 309
         # send textbox to LaTeX and wait for prompt
308  
-        self.latex_stdin.write(unicode(textbox))
309  
-        self.latex_stdin.flush()
  310
+        self._stdin_writeln(textbox)
310 311
         try:
311  
-            self._wait_for_prompt()
  312
+            self._expect_prompt()
312 313
         except LatexError as e:
313  
-            msg = u"Error processing '%s'\nLaTeX Output:\n%s" % (text, e.latex_output)
  314
+            msg = "Error processing '%s'\nLaTeX Output:\n%s" % (text, e.latex_output)
314 315
             raise ValueError(msg)
315 316
 
316 317
         # typeout width, height and text offset of the last textbox
317  
-        query = "\\typeout{\\the\\wd0,\\the\\ht0,\\the\\dp0}\n"
318  
-        self.latex_stdin.write(query)
319  
-        self.latex_stdin.flush()
  318
+        self._stdin_writeln(r"\typeout{\the\wd0,\the\ht0,\the\dp0}")
320 319
         # read answer from latex and advance to the next prompt
321 320
         try:
322  
-            answer = self._wait_for_prompt()
  321
+            answer = self._expect_prompt()
323 322
         except LatexError as e:
324  
-            msg = u"Error processing '%s'\nLaTeX Output:\n%s" % (text, e.latex_output)
  323
+            msg = "Error processing '%s'\nLaTeX Output:\n%s" % (text, e.latex_output)
325 324
             raise ValueError(msg)
326 325
 
327 326
         # parse metrics from the answer string
@@ -625,12 +624,7 @@ def __init__(self, *args):
625 624
     def get_default_filetype(self):
626 625
         return 'pdf'
627 626
 
628  
-    def print_pgf(self, filename, *args, **kwargs):
629  
-        """
630  
-        Output pgf commands for drawing the figure so it can be included and
631  
-        rendered in latex documents.
632  
-        """
633  
-
  627
+    def _print_pgf_to_fh(self, fh):
634 628
         header_text = r"""%% Creator: Matplotlib, PGF backend
635 629
 %%
636 630
 %% To include the figure in your LaTeX document, write
@@ -660,37 +654,50 @@ def print_pgf(self, filename, *args, **kwargs):
660 654
         # get figure size in inch
661 655
         w, h = self.figure.get_figwidth(), self.figure.get_figheight()
662 656
 
663  
-        # start a pgfpicture environment and set a bounding box
664  
-        with codecs.open(filename, "w", encoding="utf-8") as fh:
665  
-            fh.write(header_text)
666  
-            fh.write(header_info_preamble)
667  
-            fh.write("\n")
668  
-            writeln(fh, r"\begingroup")
669  
-            writeln(fh, r"\makeatletter")
670  
-            writeln(fh, r"\begin{pgfpicture}")
671  
-            writeln(fh, r"\pgfpathrectangle{\pgfpointorigin}{\pgfqpoint{%fin}{%fin}}" % (w,h))
672  
-            writeln(fh, r"\pgfusepath{use as bounding box}")
673  
-
674  
-            renderer = RendererPgf(self.figure, fh)
675  
-            self.figure.draw(renderer)
676  
-
677  
-            # end the pgfpicture environment
678  
-            writeln(fh, r"\end{pgfpicture}")
679  
-            writeln(fh, r"\makeatother")
680  
-            writeln(fh, r"\endgroup")
681  
-
682  
-    def print_pdf(self, filename, *args, **kwargs):
  657
+        # create pgfpicture environment and write the pgf code
  658
+        fh.write(header_text)
  659
+        fh.write(header_info_preamble)
  660
+        fh.write("\n")
  661
+        writeln(fh, r"\begingroup")
  662
+        writeln(fh, r"\makeatletter")
  663
+        writeln(fh, r"\begin{pgfpicture}")
  664
+        writeln(fh, r"\pgfpathrectangle{\pgfpointorigin}{\pgfqpoint{%fin}{%fin}}" % (w,h))
  665
+        writeln(fh, r"\pgfusepath{use as bounding box}")
  666
+        renderer = RendererPgf(self.figure, fh)
  667
+        self.figure.draw(renderer)
  668
+
  669
+        # end the pgfpicture environment
  670
+        writeln(fh, r"\end{pgfpicture}")
  671
+        writeln(fh, r"\makeatother")
  672
+        writeln(fh, r"\endgroup")
  673
+
  674
+    def print_pgf(self, fname_or_fh, *args, **kwargs):
683 675
         """
684  
-        Use LaTeX to compile a Pgf generated figure to PDF.
  676
+        Output pgf commands for drawing the figure so it can be included and
  677
+        rendered in latex documents.
685 678
         """
686  
-        w, h = self.figure.get_figwidth(), self.figure.get_figheight()
  679
+        if kwargs.get("dryrun", False): return
  680
+
  681
+        # figure out where the pgf is to be written to
  682
+        if is_string_like(fname_or_fh):
  683
+            with codecs.open(fname_or_fh, "w", encoding="utf-8") as fh:
  684
+                self._print_pgf_to_fh(fh)
  685
+        elif is_writable_file_like(fname_or_fh):
  686
+            raise ValueError("saving pgf to a stream is not supported, " + \
  687
+            "consider using the pdf option of the pgf-backend")
  688
+        else:
  689
+            raise ValueError("filename must be a path")
687 690
 
688  
-        target = os.path.abspath(filename)
  691
+    def _print_pdf_to_fh(self, fh):
  692
+        w, h = self.figure.get_figwidth(), self.figure.get_figheight()
689 693
 
690 694
         try:
  695
+            # create and switch to temporary directory
691 696
             tmpdir = tempfile.mkdtemp()
692 697
             cwd = os.getcwd()
693 698
             os.chdir(tmpdir)
  699
+
  700
+            # print figure to pgf and compile it with latex
694 701
             self.print_pgf("figure.pgf")
695 702
 
696 703
             latex_preamble = get_preamble()
@@ -706,16 +713,19 @@ def print_pdf(self, filename, *args, **kwargs):
706 713
 \centering
707 714
 \input{figure.pgf}
708 715
 \end{document}""" % (w, h, latex_preamble, latex_fontspec)
709  
-            with codecs.open("figure.tex", "w", "utf-8") as fh:
710  
-                fh.write(latexcode)
  716
+            with codecs.open("figure.tex", "w", "utf-8") as fh_tex:
  717
+                fh_tex.write(latexcode)
711 718
 
712 719
             texcommand = get_texcommand()
713 720
             cmdargs = [texcommand, "-interaction=nonstopmode", "-halt-on-error", "figure.tex"]
714 721
             try:
715  
-                stdout = subprocess.check_output(cmdargs, universal_newlines=True, stderr=subprocess.STDOUT)
716  
-            except:
717  
-                raise RuntimeError("%s was not able to process your file.\n\nFull log:\n%s" % (texcommand, stdout))
718  
-            shutil.copyfile("figure.pdf", target)
  722
+                subprocess.check_output(cmdargs, stderr=subprocess.STDOUT)
  723
+            except subprocess.CalledProcessError as e:
  724
+                raise RuntimeError("%s was not able to process your file.\n\nFull log:\n%s" % (texcommand, e.output))
  725
+
  726
+            # copy file contents to target
  727
+            with open("figure.pdf", "rb") as fh_src:
  728
+                shutil.copyfileobj(fh_src, fh)
719 729
         finally:
720 730
             os.chdir(cwd)
721 731
             try:
@@ -723,21 +733,33 @@ def print_pdf(self, filename, *args, **kwargs):
723 733
             except:
724 734
                 sys.stderr.write("could not delete tmp directory %s\n" % tmpdir)
725 735
 
726  
-    def print_png(self, filename, *args, **kwargs):
  736
+    def print_pdf(self, fname_or_fh, *args, **kwargs):
727 737
         """
728  
-        Use LaTeX to compile a pgf figure to pdf and convert it to png.
  738
+        Use LaTeX to compile a Pgf generated figure to PDF.
729 739
         """
  740
+        # figure out where the pdf is to be written to
  741
+        if is_string_like(fname_or_fh):
  742
+            with open(fname_or_fh, "wb") as fh:
  743
+                self._print_pdf_to_fh(fh)
  744
+        elif is_writable_file_like(fname_or_fh):
  745
+            self._print_pdf_to_fh(fname_or_fh)
  746
+        else:
  747
+            raise ValueError("filename must be a path or a file-like object")
730 748
 
  749
+    def _print_png_to_fh(self, fh):
731 750
         converter = make_pdf_to_png_converter()
732 751
 
733  
-        target = os.path.abspath(filename)
734 752
         try:
  753
+            # create and switch to temporary directory
735 754
             tmpdir = tempfile.mkdtemp()
736 755
             cwd = os.getcwd()
737 756
             os.chdir(tmpdir)
  757
+            # create pdf and try to convert it to png
738 758
             self.print_pdf("figure.pdf")
739 759
             converter("figure.pdf", "figure.png", dpi=self.figure.dpi)
740  
-            shutil.copyfile("figure.png", target)
  760
+            # copy file contents to target
  761
+            with open("figure.png", "rb") as fh_src:
  762
+                shutil.copyfileobj(fh_src, fh)
741 763
         finally:
742 764
             os.chdir(cwd)
743 765
             try:
@@ -745,6 +767,18 @@ def print_png(self, filename, *args, **kwargs):
745 767
             except:
746 768
                 sys.stderr.write("could not delete tmp directory %s\n" % tmpdir)
747 769
 
  770
+    def print_png(self, fname_or_fh, *args, **kwargs):
  771
+        """
  772
+        Use LaTeX to compile a pgf figure to pdf and convert it to png.
  773
+        """
  774
+        if is_string_like(fname_or_fh):
  775
+            with open(fname_or_fh, "wb") as fh:
  776
+                self._print_png_to_fh(fh)
  777
+        elif is_writable_file_like(fname_or_fh):
  778
+            self._print_png_to_fh(fname_or_fh)
  779
+        else:
  780
+            raise ValueError("filename must be a path or a file-like object")
  781
+
748 782
     def _render_texts_pgf(self, fh):
749 783
         # TODO: currently unused code path
750 784
 
41  lib/matplotlib/tests/test_backend_pgf.py
@@ -4,22 +4,32 @@
4 4
 import shutil
5 5
 import subprocess
6 6
 import numpy as np
  7
+import nose
  8
+from nose.plugins.skip import SkipTest
7 9
 import matplotlib as mpl
8 10
 import matplotlib.pyplot as plt
9 11
 from matplotlib.testing.compare import compare_images, ImageComparisonFailure
10  
-from matplotlib.testing.decorators import _image_directories, knownfailureif
  12
+from matplotlib.testing.decorators import _image_directories
11 13
 
12 14
 baseline_dir, result_dir = _image_directories(lambda: 'dummy func')
13 15
 
14  
-def run(*args):
15  
-    try:
16  
-        subprocess.check_output(args)
17  
-        return True
18  
-    except:
19  
-        return False
  16
+def check_for(texsystem):
  17
+    header = r"""
  18
+    \documentclass{minimal}
  19
+    \usepackage{pgf}
  20
+    \begin{document}
  21
+    \typeout{pgfversion=\pgfversion}
  22
+    \makeatletter
  23
+    \@@end
  24
+    """
  25
+    latex = subprocess.Popen(["xelatex", "-halt-on-error"],
  26
+                             stdin=subprocess.PIPE,
  27
+                             stdout=subprocess.PIPE)
  28
+    stdout, stderr = latex.communicate(header.encode("utf8"))
  29
+
  30
+    return latex.returncode == 0
20 31
 
21 32
 def switch_backend(backend):
22  
-    import nose
23 33
 
24 34
     def switch_backend_decorator(func):
25 35
         def backend_switcher(*args, **kwargs):
@@ -41,7 +51,7 @@ def compare_figure(fname):
41 51
 
42 52
     expected = os.path.join(result_dir, "expected_%s" % fname)
43 53
     shutil.copyfile(os.path.join(baseline_dir, fname), expected)
44  
-    err = compare_images(expected, actual, tol=1e-4)
  54
+    err = compare_images(expected, actual, tol=5e-3)
45 55
     if err:
46 56
         raise ImageComparisonFailure('images not close: %s vs. %s' % (actual, expected))
47 57
 
@@ -58,9 +68,11 @@ def create_figure():
58 68
 
59 69
 
60 70
 # test compiling a figure to pdf with xelatex
61  
-@knownfailureif(not run('xelatex', '-v'), msg="xelatex is required for this test")
62 71
 @switch_backend('pgf')
63 72
 def test_xelatex():
  73
+    if not check_for('xelatex'):
  74
+        raise SkipTest('xelatex + pgf is required')
  75
+
64 76
     rc_xelatex = {'font.family': 'serif',
65 77
                    'pgf.rcfonts': False,}
66 78
     mpl.rcParams.update(rc_xelatex)
@@ -69,9 +81,11 @@ def test_xelatex():
69 81
 
70 82
 
71 83
 # test compiling a figure to pdf with pdflatex
72  
-@knownfailureif(not run('pdflatex', '-v'), msg="pdflatex is required for this test")
73 84
 @switch_backend('pgf')
74 85
 def test_pdflatex():
  86
+    if not check_for('pdflatex'):
  87
+        raise SkipTest('pdflatex + pgf is required')
  88
+
75 89
     rc_pdflatex = {'font.family': 'serif',
76 90
                    'pgf.rcfonts': False,
77 91
                    'pgf.texsystem': 'pdflatex',
@@ -83,10 +97,11 @@ def test_pdflatex():
83 97
 
84 98
 
85 99
 # test updating the rc parameters for each figure
86  
-@knownfailureif(not run('pdflatex', '-v') or not run('xelatex', '-v'),
87  
-                msg="xelatex and pdflatex are required for this test")
88 100
 @switch_backend('pgf')
89 101
 def test_rcupdate():
  102
+    if not check_for('xelatex') or not check_for('pdflatex'):
  103
+        raise SkipTest('xelatex and pdflatex + pgf required')
  104
+
90 105
     rc_sets = []
91 106
     rc_sets.append({'font.family': 'sans-serif',
92 107
                     'font.size': 30,

0 notes on commit c981774

Please sign in to comment.
Something went wrong with that request. Please try again.