Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c981774

Browse files
committed
Merge pull request #1124 from pwuertz/pgf-backend
PGF backend, fix #1116, #1118 and #1128
2 parents 354721b + 138d55d commit c981774

File tree

2 files changed

+149
-100
lines changed

2 files changed

+149
-100
lines changed

lib/matplotlib/backends/backend_pgf.py

Lines changed: 121 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from matplotlib import _png, rcParams
1919
from matplotlib import font_manager
2020
from 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'\nLaTeX Output:\n%s" % (text, e.latex_output)
314+
msg = "Error processing '%s'\nLaTeX 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'\nLaTeX Output:\n%s" % (text, e.latex_output)
323+
msg = "Error processing '%s'\nLaTeX 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\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)
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

Comments
 (0)