"""Export to PDF via latex""" # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. import subprocess import os import sys import shutil from traitlets import Integer, List, Bool, Instance, Unicode, default from testpath.tempdir import TemporaryWorkingDirectory from typing import Optional from .latex import LatexExporter class LatexFailed(IOError): """Exception for failed latex run Captured latex output is in error.output. """ def __init__(self, output): self.output = output def __unicode__(self): return u"PDF creating failed, captured latex output:\n%s" % self.output def __str__(self): u = self.__unicode__() return u def prepend_to_env_search_path(varname, value, envdict): """Add value to the environment variable varname in envdict e.g. prepend_to_env_search_path('BIBINPUTS', '/home/sally/foo', os.environ) """ if not value: return # Nothing to add envdict[varname] = value + os.pathsep + envdict.get(varname, '') class PDFExporter(LatexExporter): """Writer designed to write to PDF files. This inherits from `LatexExporter`. It creates a LaTeX file in a temporary directory using the template machinery, and then runs LaTeX to create a pdf. """ export_from_notebook="PDF via LaTeX" latex_count = Integer(3, help="How many times latex will be called." ).tag(config=True) latex_command = List([u"xelatex", u"{filename}", "-quiet"], help="Shell command used to compile latex." ).tag(config=True) bib_command = List([u"bibtex", u"{filename}"], help="Shell command used to run bibtex." ).tag(config=True) verbose = Bool(False, help="Whether to display the output of latex commands." ).tag(config=True) texinputs = Unicode(help="texinputs dir. A notebook's directory is added") writer = Instance("nbconvert.writers.FilesWriter", args=(), kw={'build_directory': '.'}) output_mimetype = "application/pdf" _captured_output = List() @default('file_extension') def _file_extension_default(self): return '.pdf' @default('template_extension') def _template_extension_default(self): return '.tex.j2' def run_command(self, command_list, filename, count, log_function, raise_on_failure=None): """Run command_list count times. Parameters ---------- command_list : list A list of args to provide to Popen. Each element of this list will be interpolated with the filename to convert. filename : unicode The name of the file to convert. count : int How many times to run the command. raise_on_failure: Exception class (default None) If provided, will raise the given exception for if an instead of returning False on command failure. Returns ------- success : bool A boolean indicating if the command was successful (True) or failed (False). """ command = [c.format(filename=filename) for c in command_list] # This will throw a clearer error if the command is not found cmd = shutil.which(command_list[0]) if cmd is None: link = "https://nbconvert.readthedocs.io/en/latest/install.html#installing-tex" raise OSError("{formatter} not found on PATH, if you have not installed " "{formatter} you may need to do so. Find further instructions " "at {link}.".format(formatter=command_list[0], link=link)) times = 'time' if count == 1 else 'times' self.log.info("Running %s %i %s: %s", command_list[0], count, times, command) shell = (sys.platform == 'win32') if shell: command = subprocess.list2cmdline(command) env = os.environ.copy() prepend_to_env_search_path('TEXINPUTS', self.texinputs, env) prepend_to_env_search_path('BIBINPUTS', self.texinputs, env) prepend_to_env_search_path('BSTINPUTS', self.texinputs, env) with open(os.devnull, 'rb') as null: stdout = subprocess.PIPE if not self.verbose else None for index in range(count): p = subprocess.Popen(command, stdout=stdout, stderr=subprocess.STDOUT, stdin=null, shell=shell, env=env) out, _ = p.communicate() if p.returncode: if self.verbose: # verbose means I didn't capture stdout with PIPE, # so it's already been displayed and `out` is None. out = u'' else: out = out.decode('utf-8', 'replace') log_function(command, out) self._captured_output.append(out) if raise_on_failure: raise raise_on_failure( 'Failed to run "{command}" command:\n{output}'.format( command=command, output=out)) return False # failure return True # success def run_latex(self, filename, raise_on_failure=LatexFailed): """Run xelatex self.latex_count times.""" def log_error(command, out): self.log.critical(u"%s failed: %s\n%s", command[0], command, out) return self.run_command(self.latex_command, filename, self.latex_count, log_error, raise_on_failure) def run_bib(self, filename, raise_on_failure=False): """Run bibtex one time.""" filename = os.path.splitext(filename)[0] def log_error(command, out): self.log.warning('%s had problems, most likely because there were no citations', command[0]) self.log.debug(u"%s output: %s\n%s", command[0], command, out) return self.run_command(self.bib_command, filename, 1, log_error, raise_on_failure) def from_notebook_node(self, nb, resources=None, **kw): latex, resources = super().from_notebook_node( nb, resources=resources, **kw ) # set texinputs directory, so that local files will be found if resources and resources.get('metadata', {}).get('path'): self.texinputs = resources['metadata']['path'] else: self.texinputs = os.getcwd() self._captured_outputs = [] with TemporaryWorkingDirectory(): notebook_name = 'notebook' resources['output_extension'] = '.tex' tex_file = self.writer.write(latex, resources, notebook_name=notebook_name) self.log.info("Building PDF") self.run_latex(tex_file) if self.run_bib(tex_file): self.run_latex(tex_file) pdf_file = notebook_name + '.pdf' if not os.path.isfile(pdf_file): raise LatexFailed('\n'.join(self._captured_output)) self.log.info('PDF successfully created') with open(pdf_file, 'rb') as f: pdf_data = f.read() # convert output extension to pdf # the writer above required it to be tex resources['output_extension'] = '.pdf' # clear figure outputs, extracted by latex export, # so we don't claim to be a multi-file export. resources.pop('outputs', None) return pdf_data, resources