1818from matplotlib import _png , rcParams
1919from matplotlib import font_manager
2020from matplotlib .ft2font import FT2Font
21+ from matplotlib .cbook import is_string_like , is_writable_file_like
2122
2223###############################################################################
2324
@@ -220,15 +221,33 @@ def _build_latex_header():
220221 # Create LaTeX header with some content, else LaTeX will load some
221222 # math fonts later when we don't expect the additional output on stdout.
222223 # 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" )
230248
231- return latex_header
249+ def _expect_prompt (self ):
250+ return self ._expect ("\n *" )
232251
233252 def __init__ (self ):
234253 self .texcommand = get_texcommand ()
@@ -238,27 +257,23 @@ def __init__(self):
238257 # test the LaTeX setup to ensure a clean startup of the subprocess
239258 latex = subprocess .Popen ([self .texcommand , "-halt-on-error" ],
240259 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" ) )
244263 if latex .returncode != 0 :
245264 raise LatexError ("LaTeX returned an error, probably missing font or error in preamble:\n %s" % stdout )
246265
247- # open LaTeX process
266+ # open LaTeX process for real work
248267 latex = subprocess .Popen ([self .texcommand , "-halt-on-error" ],
249268 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 )
259270 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 ()
262277
263278 # cache for strings already processed
264279 self .str_cache = {}
@@ -267,8 +282,8 @@ def __del__(self):
267282 if rcParams .get ("pgf.debug" , False ):
268283 print "deleting LatexManager"
269284 try :
270- self .latex . terminate ()
271- self .latex .wait ()
285+ self .latex_stdin_utf8 . close ()
286+ self .latex .communicate ()
272287 except :
273288 pass
274289 try :
@@ -277,19 +292,6 @@ def __del__(self):
277292 except :
278293 pass
279294
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-
293295 def get_width_height_descent (self , text , prop ):
294296 """
295297 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):
298300
299301 # apply font properties and define textbox
300302 prop_cmds = _font_properties_str (prop )
301- textbox = u "\\ sbox0{%s %s}\n " % (prop_cmds , text )
303+ textbox = "\\ sbox0{%s %s}" % (prop_cmds , text )
302304
303305 # check cache
304306 if textbox in self .str_cache :
305307 return self .str_cache [textbox ]
306308
307309 # 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 )
310311 try :
311- self ._wait_for_prompt ()
312+ self ._expect_prompt ()
312313 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 )
314315 raise ValueError (msg )
315316
316317 # 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}" )
320319 # read answer from latex and advance to the next prompt
321320 try :
322- answer = self ._wait_for_prompt ()
321+ answer = self ._expect_prompt ()
323322 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 )
325324 raise ValueError (msg )
326325
327326 # parse metrics from the answer string
@@ -625,12 +624,7 @@ def __init__(self, *args):
625624 def get_default_filetype (self ):
626625 return 'pdf'
627626
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 ):
634628 header_text = r"""%% Creator: Matplotlib, PGF backend
635629%%
636630%% To include the figure in your LaTeX document, write
@@ -660,37 +654,50 @@ def print_pgf(self, filename, *args, **kwargs):
660654 # get figure size in inch
661655 w , h = self .figure .get_figwidth (), self .figure .get_figheight ()
662656
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 ):
683675 """
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.
685678 """
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" )
687690
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 ()
689693
690694 try :
695+ # create and switch to temporary directory
691696 tmpdir = tempfile .mkdtemp ()
692697 cwd = os .getcwd ()
693698 os .chdir (tmpdir )
699+
700+ # print figure to pgf and compile it with latex
694701 self .print_pgf ("figure.pgf" )
695702
696703 latex_preamble = get_preamble ()
@@ -706,45 +713,72 @@ def print_pdf(self, filename, *args, **kwargs):
706713\centering
707714\input{figure.pgf}
708715\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 )
711718
712719 texcommand = get_texcommand ()
713720 cmdargs = [texcommand , "-interaction=nonstopmode" , "-halt-on-error" , "figure.tex" ]
714721 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 )
719729 finally :
720730 os .chdir (cwd )
721731 try :
722732 shutil .rmtree (tmpdir )
723733 except :
724734 sys .stderr .write ("could not delete tmp directory %s\n " % tmpdir )
725735
726- def print_png (self , filename , * args , ** kwargs ):
736+ def print_pdf (self , fname_or_fh , * args , ** kwargs ):
727737 """
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 .
729739 """
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" )
730748
749+ def _print_png_to_fh (self , fh ):
731750 converter = make_pdf_to_png_converter ()
732751
733- target = os .path .abspath (filename )
734752 try :
753+ # create and switch to temporary directory
735754 tmpdir = tempfile .mkdtemp ()
736755 cwd = os .getcwd ()
737756 os .chdir (tmpdir )
757+ # create pdf and try to convert it to png
738758 self .print_pdf ("figure.pdf" )
739759 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 )
741763 finally :
742764 os .chdir (cwd )
743765 try :
744766 shutil .rmtree (tmpdir )
745767 except :
746768 sys .stderr .write ("could not delete tmp directory %s\n " % tmpdir )
747769
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+
748782 def _render_texts_pgf (self , fh ):
749783 # TODO: currently unused code path
750784
0 commit comments