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'\n LaTeX Output:\n %s" % (text , e .latex_output )
314
+ msg = "Error processing '%s'\n LaTeX 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'\n LaTeX Output:\n %s" % (text , e .latex_output )
323
+ msg = "Error processing '%s'\n LaTeX 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,45 +713,72 @@ 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 \n Full 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 \n Full 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 :
722
732
shutil .rmtree (tmpdir )
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 :
744
766
shutil .rmtree (tmpdir )
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
0 commit comments