1from __future__ import (absolute_import, division, print_function, 2 unicode_literals) 3 4import six 5 6import atexit 7import codecs 8import errno 9import math 10import os 11import re 12import shutil 13import sys 14import tempfile 15import warnings 16import weakref 17 18import matplotlib as mpl 19from matplotlib import _png, rcParams 20from matplotlib.backend_bases import ( 21 _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase, 22 RendererBase) 23from matplotlib.backends.backend_mixed import MixedModeRenderer 24from matplotlib.cbook import is_writable_file_like 25from matplotlib.compat import subprocess 26from matplotlib.compat.subprocess import check_output 27from matplotlib.path import Path 28 29 30############################################################################### 31 32# create a list of system fonts, all of these should work with xe/lua-latex 33system_fonts = [] 34if sys.platform.startswith('win'): 35 from matplotlib import font_manager 36 for f in font_manager.win32InstalledFonts(): 37 try: 38 system_fonts.append(font_manager.get_font(str(f)).family_name) 39 except: 40 pass # unknown error, skip this font 41else: 42 # assuming fontconfig is installed and the command 'fc-list' exists 43 try: 44 # list scalable (non-bitmap) fonts 45 fc_list = check_output([str('fc-list'), ':outline,scalable', 'family']) 46 fc_list = fc_list.decode('utf8') 47 system_fonts = [f.split(',')[0] for f in fc_list.splitlines()] 48 system_fonts = list(set(system_fonts)) 49 except: 50 warnings.warn('error getting fonts from fc-list', UserWarning) 51 52def get_texcommand(): 53 """Get chosen TeX system from rc.""" 54 texsystem_options = ["xelatex", "lualatex", "pdflatex"] 55 texsystem = rcParams["pgf.texsystem"] 56 return texsystem if texsystem in texsystem_options else "xelatex" 57 58 59def get_fontspec(): 60 """Build fontspec preamble from rc.""" 61 latex_fontspec = [] 62 texcommand = get_texcommand() 63 64 if texcommand != "pdflatex": 65 latex_fontspec.append("\\usepackage{fontspec}") 66 67 if texcommand != "pdflatex" and rcParams["pgf.rcfonts"]: 68 # try to find fonts from rc parameters 69 families = ["serif", "sans-serif", "monospace"] 70 fontspecs = [r"\setmainfont{%s}", r"\setsansfont{%s}", 71 r"\setmonofont{%s}"] 72 for family, fontspec in zip(families, fontspecs): 73 matches = [f for f in rcParams["font." + family] 74 if f in system_fonts] 75 if matches: 76 latex_fontspec.append(fontspec % matches[0]) 77 else: 78 pass # no fonts found, fallback to LaTeX defaule 79 80 return "\n".join(latex_fontspec) 81 82 83def get_preamble(): 84 """Get LaTeX preamble from rc.""" 85 return "\n".join(rcParams["pgf.preamble"]) 86 87############################################################################### 88 89# This almost made me cry!!! 90# In the end, it's better to use only one unit for all coordinates, since the 91# arithmetic in latex seems to produce inaccurate conversions. 92latex_pt_to_in = 1. / 72.27 93latex_in_to_pt = 1. / latex_pt_to_in 94mpl_pt_to_in = 1. / 72. 95mpl_in_to_pt = 1. / mpl_pt_to_in 96 97############################################################################### 98# helper functions 99 100NO_ESCAPE = r"(?<!\\)(?:\\\\)*" 101re_mathsep = re.compile(NO_ESCAPE + r"\$") 102re_escapetext = re.compile(NO_ESCAPE + "([_^$%])") 103repl_escapetext = lambda m: "\\" + m.group(1) 104re_mathdefault = re.compile(NO_ESCAPE + r"(\\mathdefault)") 105repl_mathdefault = lambda m: m.group(0)[:-len(m.group(1))] 106 107 108def common_texification(text): 109 """ 110 Do some necessary and/or useful substitutions for texts to be included in 111 LaTeX documents. 112 """ 113 114 # Sometimes, matplotlib adds the unknown command \mathdefault. 115 # Not using \mathnormal instead since this looks odd for the latex cm font. 116 text = re_mathdefault.sub(repl_mathdefault, text) 117 118 # split text into normaltext and inline math parts 119 parts = re_mathsep.split(text) 120 for i, s in enumerate(parts): 121 if not i % 2: 122 # textmode replacements 123 s = re_escapetext.sub(repl_escapetext, s) 124 else: 125 # mathmode replacements 126 s = r"\(\displaystyle %s\)" % s 127 parts[i] = s 128 129 return "".join(parts) 130 131 132def writeln(fh, line): 133 # every line of a file included with \\input must be terminated with % 134 # if not, latex will create additional vertical spaces for some reason 135 fh.write(line) 136 fh.write("%\n") 137 138 139def _font_properties_str(prop): 140 # translate font properties to latex commands, return as string 141 commands = [] 142 143 families = {"serif": r"\rmfamily", "sans": r"\sffamily", 144 "sans-serif": r"\sffamily", "monospace": r"\ttfamily"} 145 family = prop.get_family()[0] 146 if family in families: 147 commands.append(families[family]) 148 elif family in system_fonts and get_texcommand() != "pdflatex": 149 commands.append(r"\setmainfont{%s}\rmfamily" % family) 150 else: 151 pass # print warning? 152 153 size = prop.get_size_in_points() 154 commands.append(r"\fontsize{%f}{%f}" % (size, size * 1.2)) 155 156 styles = {"normal": r"", "italic": r"\itshape", "oblique": r"\slshape"} 157 commands.append(styles[prop.get_style()]) 158 159 boldstyles = ["semibold", "demibold", "demi", "bold", "heavy", 160 "extra bold", "black"] 161 if prop.get_weight() in boldstyles: 162 commands.append(r"\bfseries") 163 164 commands.append(r"\selectfont") 165 return "".join(commands) 166 167 168def make_pdf_to_png_converter(): 169 """ 170 Returns a function that converts a pdf file to a png file. 171 """ 172 173 tools_available = [] 174 # check for pdftocairo 175 try: 176 check_output([str("pdftocairo"), "-v"], stderr=subprocess.STDOUT) 177 tools_available.append("pdftocairo") 178 except: 179 pass 180 # check for ghostscript 181 gs, ver = mpl.checkdep_ghostscript() 182 if gs: 183 tools_available.append("gs") 184 185 # pick converter 186 if "pdftocairo" in tools_available: 187 def cairo_convert(pdffile, pngfile, dpi): 188 cmd = [str("pdftocairo"), "-singlefile", "-png", "-r", "%d" % dpi, 189 pdffile, os.path.splitext(pngfile)[0]] 190 check_output(cmd, stderr=subprocess.STDOUT) 191 return cairo_convert 192 elif "gs" in tools_available: 193 def gs_convert(pdffile, pngfile, dpi): 194 cmd = [str(gs), 195 '-dQUIET', '-dSAFER', '-dBATCH', '-dNOPAUSE', '-dNOPROMPT', 196 '-dUseCIEColor', '-dTextAlphaBits=4', 197 '-dGraphicsAlphaBits=4', '-dDOINTERPOLATE', 198 '-sDEVICE=png16m', '-sOutputFile=%s' % pngfile, 199 '-r%d' % dpi, pdffile] 200 check_output(cmd, stderr=subprocess.STDOUT) 201 return gs_convert 202 else: 203 raise RuntimeError("No suitable pdf to png renderer found.") 204 205 206class LatexError(Exception): 207 def __init__(self, message, latex_output=""): 208 Exception.__init__(self, message) 209 self.latex_output = latex_output 210 211 212class LatexManagerFactory(object): 213 previous_instance = None 214 215 @staticmethod 216 def get_latex_manager(): 217 texcommand = get_texcommand() 218 latex_header = LatexManager._build_latex_header() 219 prev = LatexManagerFactory.previous_instance 220 221 # Check if the previous instance of LatexManager can be reused. 222 if (prev and prev.latex_header == latex_header 223 and prev.texcommand == texcommand): 224 if rcParams["pgf.debug"]: 225 print("reusing LatexManager") 226 return prev 227 else: 228 if rcParams["pgf.debug"]: 229 print("creating LatexManager") 230 new_inst = LatexManager() 231 LatexManagerFactory.previous_instance = new_inst 232 return new_inst 233 234 235class LatexManager(object): 236 """ 237 The LatexManager opens an instance of the LaTeX application for 238 determining the metrics of text elements. The LaTeX environment can be 239 modified by setting fonts and/or a custem preamble in the rc parameters. 240 """ 241 _unclean_instances = weakref.WeakSet() 242 243 @staticmethod 244 def _build_latex_header(): 245 latex_preamble = get_preamble() 246 latex_fontspec = get_fontspec() 247 # Create LaTeX header with some content, else LaTeX will load some math 248 # fonts later when we don't expect the additional output on stdout. 249 # TODO: is this sufficient? 250 latex_header = [r"\documentclass{minimal}", 251 latex_preamble, 252 latex_fontspec, 253 r"\begin{document}", 254 r"text $math \mu$", # force latex to load fonts now 255 r"\typeout{pgf_backend_query_start}"] 256 return "\n".join(latex_header) 257 258 @staticmethod 259 def _cleanup_remaining_instances(): 260 unclean_instances = list(LatexManager._unclean_instances) 261 for latex_manager in unclean_instances: 262 latex_manager._cleanup() 263 264 def _stdin_writeln(self, s): 265 self.latex_stdin_utf8.write(s) 266 self.latex_stdin_utf8.write("\n") 267 self.latex_stdin_utf8.flush() 268 269 def _expect(self, s): 270 exp = s.encode("utf8") 271 buf = bytearray() 272 while True: 273 b = self.latex.stdout.read(1) 274 buf += b 275 if buf[-len(exp):] == exp: 276 break 277 if not len(b): 278 raise LatexError("LaTeX process halted", buf.decode("utf8")) 279 return buf.decode("utf8") 280 281 def _expect_prompt(self): 282 return self._expect("\n*") 283 284 def __init__(self): 285 # store references for __del__ 286 self._os_path = os.path 287 self._shutil = shutil 288 self._debug = rcParams["pgf.debug"] 289 290 # create a tmp directory for running latex, remember to cleanup 291 self.tmpdir = tempfile.mkdtemp(prefix="mpl_pgf_lm_") 292 LatexManager._unclean_instances.add(self) 293 294 # test the LaTeX setup to ensure a clean startup of the subprocess 295 self.texcommand = get_texcommand() 296 self.latex_header = LatexManager._build_latex_header() 297 latex_end = "\n\\makeatletter\n\\@@end\n" 298 try: 299 latex = subprocess.Popen([str(self.texcommand), "-halt-on-error"], 300 stdin=subprocess.PIPE, 301 stdout=subprocess.PIPE, 302 cwd=self.tmpdir) 303 except OSError as e: 304 if e.errno == errno.ENOENT: 305 raise RuntimeError( 306 "Latex command not found. Install %r or change " 307 "pgf.texsystem to the desired command." % self.texcommand) 308 else: 309 raise RuntimeError( 310 "Error starting process %r" % self.texcommand) 311 test_input = self.latex_header + latex_end 312 stdout, stderr = latex.communicate(test_input.encode("utf-8")) 313 if latex.returncode != 0: 314 raise LatexError("LaTeX returned an error, probably missing font " 315 "or error in preamble:\n%s" % stdout) 316 317 # open LaTeX process for real work 318 latex = subprocess.Popen([str(self.texcommand), "-halt-on-error"], 319 stdin=subprocess.PIPE, stdout=subprocess.PIPE, 320 cwd=self.tmpdir) 321 self.latex = latex 322 self.latex_stdin_utf8 = codecs.getwriter("utf8")(self.latex.stdin) 323 # write header with 'pgf_backend_query_start' token 324 self._stdin_writeln(self._build_latex_header()) 325 # read all lines until our 'pgf_backend_query_start' token appears 326 self._expect("*pgf_backend_query_start") 327 self._expect_prompt() 328 329 # cache for strings already processed 330 self.str_cache = {} 331 332 def _cleanup(self): 333 if not self._os_path.isdir(self.tmpdir): 334 return 335 try: 336 self.latex.communicate() 337 self.latex_stdin_utf8.close() 338 self.latex.stdout.close() 339 except: 340 pass 341 try: 342 self._shutil.rmtree(self.tmpdir) 343 LatexManager._unclean_instances.discard(self) 344 except: 345 sys.stderr.write("error deleting tmp directory %s\n" % self.tmpdir) 346 347 def __del__(self): 348 if self._debug: 349 print("deleting LatexManager") 350 self._cleanup() 351 352 def get_width_height_descent(self, text, prop): 353 """ 354 Get the width, total height and descent for a text typesetted by the 355 current LaTeX environment. 356 """ 357 358 # apply font properties and define textbox 359 prop_cmds = _font_properties_str(prop) 360 textbox = "\\sbox0{%s %s}" % (prop_cmds, text) 361 362 # check cache 363 if textbox in self.str_cache: 364 return self.str_cache[textbox] 365 366 # send textbox to LaTeX and wait for prompt 367 self._stdin_writeln(textbox) 368 try: 369 self._expect_prompt() 370 except LatexError as e: 371 raise ValueError("Error processing '{}'\nLaTeX Output:\n{}" 372 .format(text, e.latex_output)) 373 374 # typeout width, height and text offset of the last textbox 375 self._stdin_writeln(r"\typeout{\the\wd0,\the\ht0,\the\dp0}") 376 # read answer from latex and advance to the next prompt 377 try: 378 answer = self._expect_prompt() 379 except LatexError as e: 380 raise ValueError("Error processing '{}'\nLaTeX Output:\n{}" 381 .format(text, e.latex_output)) 382 383 # parse metrics from the answer string 384 try: 385 width, height, offset = answer.splitlines()[0].split(",") 386 except: 387 raise ValueError("Error processing '{}'\nLaTeX Output:\n{}" 388 .format(text, answer)) 389 w, h, o = float(width[:-2]), float(height[:-2]), float(offset[:-2]) 390 391 # the height returned from LaTeX goes from base to top. 392 # the height matplotlib expects goes from bottom to top. 393 self.str_cache[textbox] = (w, h + o, o) 394 return w, h + o, o 395 396 397class RendererPgf(RendererBase): 398 399 def __init__(self, figure, fh, dummy=False): 400 """ 401 Creates a new PGF renderer that translates any drawing instruction 402 into text commands to be interpreted in a latex pgfpicture environment. 403 404 Attributes 405 ---------- 406 figure : `matplotlib.figure.Figure` 407 Matplotlib figure to initialize height, width and dpi from. 408 fh : file-like 409 File handle for the output of the drawing commands. 410 411 """ 412 RendererBase.__init__(self) 413 self.dpi = figure.dpi 414 self.fh = fh 415 self.figure = figure 416 self.image_counter = 0 417 418 # get LatexManager instance 419 self.latexManager = LatexManagerFactory.get_latex_manager() 420 421 if dummy: 422 # dummy==True deactivate all methods 423 nop = lambda *args, **kwargs: None 424 for m in RendererPgf.__dict__: 425 if m.startswith("draw_"): 426 self.__dict__[m] = nop 427 else: 428 # if fh does not belong to a filename, deactivate draw_image 429 if not hasattr(fh, 'name') or not os.path.exists(fh.name): 430 warnings.warn("streamed pgf-code does not support raster " 431 "graphics, consider using the pgf-to-pdf option", 432 UserWarning) 433 self.__dict__["draw_image"] = lambda *args, **kwargs: None 434 435 def draw_markers(self, gc, marker_path, marker_trans, path, trans, 436 rgbFace=None): 437 writeln(self.fh, r"\begin{pgfscope}") 438 439 # convert from display units to in 440 f = 1. / self.dpi 441 442 # set style and clip 443 self._print_pgf_clip(gc) 444 self._print_pgf_path_styles(gc, rgbFace) 445 446 # build marker definition 447 bl, tr = marker_path.get_extents(marker_trans).get_points() 448 coords = bl[0] * f, bl[1] * f, tr[0] * f, tr[1] * f 449 writeln(self.fh, 450 r"\pgfsys@defobject{currentmarker}" 451 r"{\pgfqpoint{%fin}{%fin}}{\pgfqpoint{%fin}{%fin}}{" % coords) 452 self._print_pgf_path(None, marker_path, marker_trans) 453 self._pgf_path_draw(stroke=gc.get_linewidth() != 0.0, 454 fill=rgbFace is not None) 455 writeln(self.fh, r"}") 456 457 # draw marker for each vertex 458 for point, code in path.iter_segments(trans, simplify=False): 459 x, y = point[0] * f, point[1] * f 460 writeln(self.fh, r"\begin{pgfscope}") 461 writeln(self.fh, r"\pgfsys@transformshift{%fin}{%fin}" % (x, y)) 462 writeln(self.fh, r"\pgfsys@useobject{currentmarker}{}") 463 writeln(self.fh, r"\end{pgfscope}") 464 465 writeln(self.fh, r"\end{pgfscope}") 466 467 def draw_path(self, gc, path, transform, rgbFace=None): 468 writeln(self.fh, r"\begin{pgfscope}") 469 # draw the path 470 self._print_pgf_clip(gc) 471 self._print_pgf_path_styles(gc, rgbFace) 472 self._print_pgf_path(gc, path, transform, rgbFace) 473 self._pgf_path_draw(stroke=gc.get_linewidth() != 0.0, 474 fill=rgbFace is not None) 475 writeln(self.fh, r"\end{pgfscope}") 476 477 # if present, draw pattern on top 478 if gc.get_hatch(): 479 writeln(self.fh, r"\begin{pgfscope}") 480 self._print_pgf_path_styles(gc, rgbFace) 481 482 # combine clip and path for clipping 483 self._print_pgf_clip(gc) 484 self._print_pgf_path(gc, path, transform, rgbFace) 485 writeln(self.fh, r"\pgfusepath{clip}") 486 487 # build pattern definition 488 writeln(self.fh, 489 r"\pgfsys@defobject{currentpattern}" 490 r"{\pgfqpoint{0in}{0in}}{\pgfqpoint{1in}{1in}}{") 491 writeln(self.fh, r"\begin{pgfscope}") 492 writeln(self.fh, 493 r"\pgfpathrectangle" 494 r"{\pgfqpoint{0in}{0in}}{\pgfqpoint{1in}{1in}}") 495 writeln(self.fh, r"\pgfusepath{clip}") 496 scale = mpl.transforms.Affine2D().scale(self.dpi) 497 self._print_pgf_path(None, gc.get_hatch_path(), scale) 498 self._pgf_path_draw(stroke=True) 499 writeln(self.fh, r"\end{pgfscope}") 500 writeln(self.fh, r"}") 501 # repeat pattern, filling the bounding rect of the path 502 f = 1. / self.dpi 503 (xmin, ymin), (xmax, ymax) = \ 504 path.get_extents(transform).get_points() 505 xmin, xmax = f * xmin, f * xmax 506 ymin, ymax = f * ymin, f * ymax 507 repx, repy = int(math.ceil(xmax-xmin)), int(math.ceil(ymax-ymin)) 508 writeln(self.fh, 509 r"\pgfsys@transformshift{%fin}{%fin}" % (xmin, ymin)) 510 for iy in range(repy): 511 for ix in range(repx): 512 writeln(self.fh, r"\pgfsys@useobject{currentpattern}{}") 513 writeln(self.fh, r"\pgfsys@transformshift{1in}{0in}") 514 writeln(self.fh, r"\pgfsys@transformshift{-%din}{0in}" % repx) 515 writeln(self.fh, r"\pgfsys@transformshift{0in}{1in}") 516 517 writeln(self.fh, r"\end{pgfscope}") 518 519 def _print_pgf_clip(self, gc): 520 f = 1. / self.dpi 521 # check for clip box 522 bbox = gc.get_clip_rectangle() 523 if bbox: 524 p1, p2 = bbox.get_points() 525 w, h = p2 - p1 526 coords = p1[0] * f, p1[1] * f, w * f, h * f 527 writeln(self.fh, 528 r"\pgfpathrectangle" 529 r"{\pgfqpoint{%fin}{%fin}}{\pgfqpoint{%fin}{%fin}}" 530 % coords) 531 writeln(self.fh, r"\pgfusepath{clip}") 532 533 # check for clip path 534 clippath, clippath_trans = gc.get_clip_path() 535 if clippath is not None: 536 self._print_pgf_path(gc, clippath, clippath_trans) 537 writeln(self.fh, r"\pgfusepath{clip}") 538 539 def _print_pgf_path_styles(self, gc, rgbFace): 540 # cap style 541 capstyles = {"butt": r"\pgfsetbuttcap", 542 "round": r"\pgfsetroundcap", 543 "projecting": r"\pgfsetrectcap"} 544 writeln(self.fh, capstyles[gc.get_capstyle()]) 545 546 # join style 547 joinstyles = {"miter": r"\pgfsetmiterjoin", 548 "round": r"\pgfsetroundjoin", 549 "bevel": r"\pgfsetbeveljoin"} 550 writeln(self.fh, joinstyles[gc.get_joinstyle()]) 551 552 # filling 553 has_fill = rgbFace is not None 554 555 if gc.get_forced_alpha(): 556 fillopacity = strokeopacity = gc.get_alpha() 557 else: 558 strokeopacity = gc.get_rgb()[3] 559 fillopacity = rgbFace[3] if has_fill and len(rgbFace) > 3 else 1.0 560 561 if has_fill: 562 writeln(self.fh, 563 r"\definecolor{currentfill}{rgb}{%f,%f,%f}" 564 % tuple(rgbFace[:3])) 565 writeln(self.fh, r"\pgfsetfillcolor{currentfill}") 566 if has_fill and fillopacity != 1.0: 567 writeln(self.fh, r"\pgfsetfillopacity{%f}" % fillopacity) 568 569 # linewidth and color 570 lw = gc.get_linewidth() * mpl_pt_to_in * latex_in_to_pt 571 stroke_rgba = gc.get_rgb() 572 writeln(self.fh, r"\pgfsetlinewidth{%fpt}" % lw) 573 writeln(self.fh, 574 r"\definecolor{currentstroke}{rgb}{%f,%f,%f}" 575 % stroke_rgba[:3]) 576 writeln(self.fh, r"\pgfsetstrokecolor{currentstroke}") 577 if strokeopacity != 1.0: 578 writeln(self.fh, r"\pgfsetstrokeopacity{%f}" % strokeopacity) 579 580 # line style 581 dash_offset, dash_list = gc.get_dashes() 582 if dash_list is None: 583 writeln(self.fh, r"\pgfsetdash{}{0pt}") 584 else: 585 writeln(self.fh, 586 r"\pgfsetdash{%s}{%fpt}" 587 % ("".join(r"{%fpt}" % dash for dash in dash_list), 588 dash_offset)) 589 590 def _print_pgf_path(self, gc, path, transform, rgbFace=None): 591 f = 1. / self.dpi 592 # check for clip box / ignore clip for filled paths 593 bbox = gc.get_clip_rectangle() if gc else None 594 if bbox and (rgbFace is None): 595 p1, p2 = bbox.get_points() 596 clip = (p1[0], p1[1], p2[0], p2[1]) 597 else: 598 clip = None 599 # build path 600 for points, code in path.iter_segments(transform, clip=clip): 601 if code == Path.MOVETO: 602 x, y = tuple(points) 603 writeln(self.fh, 604 r"\pgfpathmoveto{\pgfqpoint{%fin}{%fin}}" % 605 (f * x, f * y)) 606 elif code == Path.CLOSEPOLY: 607 writeln(self.fh, r"\pgfpathclose") 608 elif code == Path.LINETO: 609 x, y = tuple(points) 610 writeln(self.fh, 611 r"\pgfpathlineto{\pgfqpoint{%fin}{%fin}}" % 612 (f * x, f * y)) 613 elif code == Path.CURVE3: 614 cx, cy, px, py = tuple(points) 615 coords = cx * f, cy * f, px * f, py * f 616 writeln(self.fh, 617 r"\pgfpathquadraticcurveto" 618 r"{\pgfqpoint{%fin}{%fin}}{\pgfqpoint{%fin}{%fin}}" 619 % coords) 620 elif code == Path.CURVE4: 621 c1x, c1y, c2x, c2y, px, py = tuple(points) 622 coords = c1x * f, c1y * f, c2x * f, c2y * f, px * f, py * f 623 writeln(self.fh, 624 r"\pgfpathcurveto" 625 r"{\pgfqpoint{%fin}{%fin}}" 626 r"{\pgfqpoint{%fin}{%fin}}" 627 r"{\pgfqpoint{%fin}{%fin}}" 628 % coords) 629 630 def _pgf_path_draw(self, stroke=True, fill=False): 631 actions = [] 632 if stroke: 633 actions.append("stroke") 634 if fill: 635 actions.append("fill") 636 writeln(self.fh, r"\pgfusepath{%s}" % ",".join(actions)) 637 638 def option_scale_image(self): 639 """ 640 pgf backend supports affine transform of image. 641 """ 642 return True 643 644 def option_image_nocomposite(self): 645 """ 646 return whether to generate a composite image from multiple images on 647 a set of axes 648 """ 649 return not rcParams['image.composite_image'] 650 651 def draw_image(self, gc, x, y, im, transform=None): 652 h, w = im.shape[:2] 653 if w == 0 or h == 0: 654 return 655 656 # save the images to png files 657 path = os.path.dirname(self.fh.name) 658 fname = os.path.splitext(os.path.basename(self.fh.name))[0] 659 fname_img = "%s-img%d.png" % (fname, self.image_counter) 660 self.image_counter += 1 661 _png.write_png(im[::-1], os.path.join(path, fname_img)) 662 663 # reference the image in the pgf picture 664 writeln(self.fh, r"\begin{pgfscope}") 665 self._print_pgf_clip(gc) 666 f = 1. / self.dpi # from display coords to inch 667 if transform is None: 668 writeln(self.fh, 669 r"\pgfsys@transformshift{%fin}{%fin}" % (x * f, y * f)) 670 w, h = w * f, h * f 671 else: 672 tr1, tr2, tr3, tr4, tr5, tr6 = transform.frozen().to_values() 673 writeln(self.fh, 674 r"\pgfsys@transformcm{%f}{%f}{%f}{%f}{%fin}{%fin}" % 675 (tr1 * f, tr2 * f, tr3 * f, tr4 * f, 676 (tr5 + x) * f, (tr6 + y) * f)) 677 w = h = 1 # scale is already included in the transform 678 interp = str(transform is None).lower() # interpolation in PDF reader 679 writeln(self.fh, 680 r"\pgftext[left,bottom]" 681 r"{\pgfimage[interpolate=%s,width=%fin,height=%fin]{%s}}" % 682 (interp, w, h, fname_img)) 683 writeln(self.fh, r"\end{pgfscope}") 684 685 def draw_tex(self, gc, x, y, s, prop, angle, ismath="TeX!", mtext=None): 686 self.draw_text(gc, x, y, s, prop, angle, ismath, mtext) 687 688 def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): 689 # prepare string for tex 690 s = common_texification(s) 691 prop_cmds = _font_properties_str(prop) 692 s = r"%s %s" % (prop_cmds, s) 693 694 695 writeln(self.fh, r"\begin{pgfscope}") 696 697 alpha = gc.get_alpha() 698 if alpha != 1.0: 699 writeln(self.fh, r"\pgfsetfillopacity{%f}" % alpha) 700 writeln(self.fh, r"\pgfsetstrokeopacity{%f}" % alpha) 701 rgb = tuple(gc.get_rgb())[:3] 702 if rgb != (0, 0, 0): 703 writeln(self.fh, r"\definecolor{textcolor}{rgb}{%f,%f,%f}" % rgb) 704 writeln(self.fh, r"\pgfsetstrokecolor{textcolor}") 705 writeln(self.fh, r"\pgfsetfillcolor{textcolor}") 706 s = r"\color{textcolor}" + s 707 708 f = 1.0 / self.figure.dpi 709 text_args = [] 710 if mtext and ( 711 (angle == 0 or 712 mtext.get_rotation_mode() == "anchor") and 713 mtext.get_va() != "center_baseline"): 714 # if text anchoring can be supported, get the original coordinates 715 # and add alignment information 716 x, y = mtext.get_transform().transform_point(mtext.get_position()) 717 text_args.append("x=%fin" % (x * f)) 718 text_args.append("y=%fin" % (y * f)) 719 720 halign = {"left": "left", "right": "right", "center": ""} 721 valign = {"top": "top", "bottom": "bottom", 722 "baseline": "base", "center": ""} 723 text_args.append(halign[mtext.get_ha()]) 724 text_args.append(valign[mtext.get_va()]) 725 else: 726 # if not, use the text layout provided by matplotlib 727 text_args.append("x=%fin" % (x * f)) 728 text_args.append("y=%fin" % (y * f)) 729 text_args.append("left") 730 text_args.append("base") 731 732 if angle != 0: 733 text_args.append("rotate=%f" % angle) 734 735 writeln(self.fh, r"\pgftext[%s]{%s}" % (",".join(text_args), s)) 736 writeln(self.fh, r"\end{pgfscope}") 737 738 def get_text_width_height_descent(self, s, prop, ismath): 739 # check if the math is supposed to be displaystyled 740 s = common_texification(s) 741 742 # get text metrics in units of latex pt, convert to display units 743 w, h, d = self.latexManager.get_width_height_descent(s, prop) 744 # TODO: this should be latex_pt_to_in instead of mpl_pt_to_in 745 # but having a little bit more space around the text looks better, 746 # plus the bounding box reported by LaTeX is VERY narrow 747 f = mpl_pt_to_in * self.dpi 748 return w * f, h * f, d * f 749 750 def flipy(self): 751 return False 752 753 def get_canvas_width_height(self): 754 return self.figure.get_figwidth(), self.figure.get_figheight() 755 756 def points_to_pixels(self, points): 757 return points * mpl_pt_to_in * self.dpi 758 759 def new_gc(self): 760 return GraphicsContextPgf() 761 762 763class GraphicsContextPgf(GraphicsContextBase): 764 pass 765 766######################################################################## 767 768 769class TmpDirCleaner(object): 770 remaining_tmpdirs = set() 771 772 @staticmethod 773 def add(tmpdir): 774 TmpDirCleaner.remaining_tmpdirs.add(tmpdir) 775 776 @staticmethod 777 def cleanup_remaining_tmpdirs(): 778 for tmpdir in TmpDirCleaner.remaining_tmpdirs: 779 try: 780 shutil.rmtree(tmpdir) 781 except: 782 sys.stderr.write("error deleting tmp directory %s\n" % tmpdir) 783 784 785class FigureCanvasPgf(FigureCanvasBase): 786 filetypes = {"pgf": "LaTeX PGF picture", 787 "pdf": "LaTeX compiled PGF picture", 788 "png": "Portable Network Graphics", } 789 790 def get_default_filetype(self): 791 return 'pdf' 792 793 def _print_pgf_to_fh(self, fh, *args, **kwargs): 794 if kwargs.get("dryrun", False): 795 renderer = RendererPgf(self.figure, None, dummy=True) 796 self.figure.draw(renderer) 797 return 798 799 header_text = """%% Creator: Matplotlib, PGF backend 800%% 801%% To include the figure in your LaTeX document, write 802%% \\input{<filename>.pgf} 803%% 804%% Make sure the required packages are loaded in your preamble 805%% \\usepackage{pgf} 806%% 807%% Figures using additional raster images can only be included by \\input if 808%% they are in the same directory as the main LaTeX file. For loading figures 809%% from other directories you can use the `import` package 810%% \\usepackage{import} 811%% and then include the figures with 812%% \\import{<path to file>}{<filename>.pgf} 813%% 814""" 815 816 # append the preamble used by the backend as a comment for debugging 817 header_info_preamble = ["%% Matplotlib used the following preamble"] 818 for line in get_preamble().splitlines(): 819 header_info_preamble.append("%% " + line) 820 for line in get_fontspec().splitlines(): 821 header_info_preamble.append("%% " + line) 822 header_info_preamble.append("%%") 823 header_info_preamble = "\n".join(header_info_preamble) 824 825 # get figure size in inch 826 w, h = self.figure.get_figwidth(), self.figure.get_figheight() 827 dpi = self.figure.get_dpi() 828 829 # create pgfpicture environment and write the pgf code 830 fh.write(header_text) 831 fh.write(header_info_preamble) 832 fh.write("\n") 833 writeln(fh, r"\begingroup") 834 writeln(fh, r"\makeatletter") 835 writeln(fh, r"\begin{pgfpicture}") 836 writeln(fh, 837 r"\pgfpathrectangle{\pgfpointorigin}{\pgfqpoint{%fin}{%fin}}" 838 % (w, h)) 839 writeln(fh, r"\pgfusepath{use as bounding box, clip}") 840 _bbox_inches_restore = kwargs.pop("bbox_inches_restore", None) 841 renderer = MixedModeRenderer(self.figure, w, h, dpi, 842 RendererPgf(self.figure, fh), 843 bbox_inches_restore=_bbox_inches_restore) 844 self.figure.draw(renderer) 845 846 # end the pgfpicture environment 847 writeln(fh, r"\end{pgfpicture}") 848 writeln(fh, r"\makeatother") 849 writeln(fh, r"\endgroup") 850 851 def print_pgf(self, fname_or_fh, *args, **kwargs): 852 """ 853 Output pgf commands for drawing the figure so it can be included and 854 rendered in latex documents. 855 """ 856 if kwargs.get("dryrun", False): 857 self._print_pgf_to_fh(None, *args, **kwargs) 858 return 859 860 # figure out where the pgf is to be written to 861 if isinstance(fname_or_fh, six.string_types): 862 with codecs.open(fname_or_fh, "w", encoding="utf-8") as fh: 863 self._print_pgf_to_fh(fh, *args, **kwargs) 864 elif is_writable_file_like(fname_or_fh): 865 fh = codecs.getwriter("utf-8")(fname_or_fh) 866 self._print_pgf_to_fh(fh, *args, **kwargs) 867 else: 868 raise ValueError("filename must be a path") 869 870 def _print_pdf_to_fh(self, fh, *args, **kwargs): 871 w, h = self.figure.get_figwidth(), self.figure.get_figheight() 872 873 try: 874 # create temporary directory for compiling the figure 875 tmpdir = tempfile.mkdtemp(prefix="mpl_pgf_") 876 fname_pgf = os.path.join(tmpdir, "figure.pgf") 877 fname_tex = os.path.join(tmpdir, "figure.tex") 878 fname_pdf = os.path.join(tmpdir, "figure.pdf") 879 880 # print figure to pgf and compile it with latex 881 self.print_pgf(fname_pgf, *args, **kwargs) 882 883 latex_preamble = get_preamble() 884 latex_fontspec = get_fontspec() 885 latexcode = """ 886\\documentclass[12pt]{minimal} 887\\usepackage[paperwidth=%fin, paperheight=%fin, margin=0in]{geometry} 888%s 889%s 890\\usepackage{pgf} 891 892\\begin{document} 893\\centering 894\\input{figure.pgf} 895\\end{document}""" % (w, h, latex_preamble, latex_fontspec) 896 with codecs.open(fname_tex, "w", "utf-8") as fh_tex: 897 fh_tex.write(latexcode) 898 899 texcommand = get_texcommand() 900 cmdargs = [str(texcommand), "-interaction=nonstopmode", 901 "-halt-on-error", "figure.tex"] 902 try: 903 check_output(cmdargs, stderr=subprocess.STDOUT, cwd=tmpdir) 904 except subprocess.CalledProcessError as e: 905 raise RuntimeError( 906 "%s was not able to process your file.\n\nFull log:\n%s" 907 % (texcommand, e.output)) 908 909 # copy file contents to target 910 with open(fname_pdf, "rb") as fh_src: 911 shutil.copyfileobj(fh_src, fh) 912 finally: 913 try: 914 shutil.rmtree(tmpdir) 915 except: 916 TmpDirCleaner.add(tmpdir) 917 918 def print_pdf(self, fname_or_fh, *args, **kwargs): 919 """ 920 Use LaTeX to compile a Pgf generated figure to PDF. 921 """ 922 if kwargs.get("dryrun", False): 923 self._print_pgf_to_fh(None, *args, **kwargs) 924 return 925 926 # figure out where the pdf is to be written to 927 if isinstance(fname_or_fh, six.string_types): 928 with open(fname_or_fh, "wb") as fh: 929 self._print_pdf_to_fh(fh, *args, **kwargs) 930 elif is_writable_file_like(fname_or_fh): 931 self._print_pdf_to_fh(fname_or_fh, *args, **kwargs) 932 else: 933 raise ValueError("filename must be a path or a file-like object") 934 935 def _print_png_to_fh(self, fh, *args, **kwargs): 936 converter = make_pdf_to_png_converter() 937 938 try: 939 # create temporary directory for pdf creation and png conversion 940 tmpdir = tempfile.mkdtemp(prefix="mpl_pgf_") 941 fname_pdf = os.path.join(tmpdir, "figure.pdf") 942 fname_png = os.path.join(tmpdir, "figure.png") 943 # create pdf and try to convert it to png 944 self.print_pdf(fname_pdf, *args, **kwargs) 945 converter(fname_pdf, fname_png, dpi=self.figure.dpi) 946 # copy file contents to target 947 with open(fname_png, "rb") as fh_src: 948 shutil.copyfileobj(fh_src, fh) 949 finally: 950 try: 951 shutil.rmtree(tmpdir) 952 except: 953 TmpDirCleaner.add(tmpdir) 954 955 def print_png(self, fname_or_fh, *args, **kwargs): 956 """ 957 Use LaTeX to compile a pgf figure to pdf and convert it to png. 958 """ 959 if kwargs.get("dryrun", False): 960 self._print_pgf_to_fh(None, *args, **kwargs) 961 return 962 963 if isinstance(fname_or_fh, six.string_types): 964 with open(fname_or_fh, "wb") as fh: 965 self._print_png_to_fh(fh, *args, **kwargs) 966 elif is_writable_file_like(fname_or_fh): 967 self._print_png_to_fh(fname_or_fh, *args, **kwargs) 968 else: 969 raise ValueError("filename must be a path or a file-like object") 970 971 def get_renderer(self): 972 return RendererPgf(self.figure, None, dummy=True) 973 974 975class FigureManagerPgf(FigureManagerBase): 976 def __init__(self, *args): 977 FigureManagerBase.__init__(self, *args) 978 979 980@_Backend.export 981class _BackendPgf(_Backend): 982 FigureCanvas = FigureCanvasPgf 983 FigureManager = FigureManagerPgf 984 985 986def _cleanup_all(): 987 LatexManager._cleanup_remaining_instances() 988 TmpDirCleaner.cleanup_remaining_tmpdirs() 989 990atexit.register(_cleanup_all) 991