pdf.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. """Export to PDF via latex"""
  2. # Copyright (c) IPython Development Team.
  3. # Distributed under the terms of the Modified BSD License.
  4. import subprocess
  5. import os
  6. import sys
  7. from ipython_genutils.py3compat import which, cast_bytes_py2, getcwd
  8. from traitlets import Integer, List, Bool, Instance, Unicode, default
  9. from testpath.tempdir import TemporaryWorkingDirectory
  10. from .latex import LatexExporter
  11. class LatexFailed(IOError):
  12. """Exception for failed latex run
  13. Captured latex output is in error.output.
  14. """
  15. def __init__(self, output):
  16. self.output = output
  17. def __unicode__(self):
  18. return u"PDF creating failed, captured latex output:\n%s" % self.output
  19. def __str__(self):
  20. u = self.__unicode__()
  21. return cast_bytes_py2(u)
  22. def prepend_to_env_search_path(varname, value, envdict):
  23. """Add value to the environment variable varname in envdict
  24. e.g. prepend_to_env_search_path('BIBINPUTS', '/home/sally/foo', os.environ)
  25. """
  26. if not value:
  27. return # Nothing to add
  28. envdict[varname] = cast_bytes_py2(value) + os.pathsep + envdict.get(varname, '')
  29. class PDFExporter(LatexExporter):
  30. """Writer designed to write to PDF files.
  31. This inherits from :class:`LatexExporter`. It creates a LaTeX file in
  32. a temporary directory using the template machinery, and then runs LaTeX
  33. to create a pdf.
  34. """
  35. export_from_notebook="PDF via LaTeX"
  36. latex_count = Integer(3,
  37. help="How many times latex will be called."
  38. ).tag(config=True)
  39. latex_command = List([u"xelatex", u"{filename}", "-quiet"],
  40. help="Shell command used to compile latex."
  41. ).tag(config=True)
  42. bib_command = List([u"bibtex", u"{filename}"],
  43. help="Shell command used to run bibtex."
  44. ).tag(config=True)
  45. verbose = Bool(False,
  46. help="Whether to display the output of latex commands."
  47. ).tag(config=True)
  48. texinputs = Unicode(help="texinputs dir. A notebook's directory is added")
  49. writer = Instance("nbconvert.writers.FilesWriter", args=(), kw={'build_directory': '.'})
  50. output_mimetype = "application/pdf"
  51. _captured_output = List()
  52. @default('file_extension')
  53. def _file_extension_default(self):
  54. return '.pdf'
  55. def run_command(self, command_list, filename, count, log_function, raise_on_failure=None):
  56. """Run command_list count times.
  57. Parameters
  58. ----------
  59. command_list : list
  60. A list of args to provide to Popen. Each element of this
  61. list will be interpolated with the filename to convert.
  62. filename : unicode
  63. The name of the file to convert.
  64. count : int
  65. How many times to run the command.
  66. raise_on_failure: Exception class (default None)
  67. If provided, will raise the given exception for if an instead of
  68. returning False on command failure.
  69. Returns
  70. -------
  71. success : bool
  72. A boolean indicating if the command was successful (True)
  73. or failed (False).
  74. """
  75. command = [c.format(filename=filename) for c in command_list]
  76. # On windows with python 2.x there is a bug in subprocess.Popen and
  77. # unicode commands are not supported
  78. if sys.platform == 'win32' and sys.version_info < (3,0):
  79. #We must use cp1252 encoding for calling subprocess.Popen
  80. #Note that sys.stdin.encoding and encoding.DEFAULT_ENCODING
  81. # could be different (cp437 in case of dos console)
  82. command = [c.encode('cp1252') for c in command]
  83. # This will throw a clearer error if the command is not found
  84. cmd = which(command_list[0])
  85. if cmd is None:
  86. link = "https://nbconvert.readthedocs.io/en/latest/install.html#installing-tex"
  87. raise OSError("{formatter} not found on PATH, if you have not installed "
  88. "{formatter} you may need to do so. Find further instructions "
  89. "at {link}.".format(formatter=command_list[0], link=link))
  90. times = 'time' if count == 1 else 'times'
  91. self.log.info("Running %s %i %s: %s", command_list[0], count, times, command)
  92. shell = (sys.platform == 'win32')
  93. if shell:
  94. command = subprocess.list2cmdline(command)
  95. env = os.environ.copy()
  96. prepend_to_env_search_path('TEXINPUTS', self.texinputs, env)
  97. prepend_to_env_search_path('BIBINPUTS', self.texinputs, env)
  98. prepend_to_env_search_path('BSTINPUTS', self.texinputs, env)
  99. with open(os.devnull, 'rb') as null:
  100. stdout = subprocess.PIPE if not self.verbose else None
  101. for index in range(count):
  102. p = subprocess.Popen(command, stdout=stdout, stderr=subprocess.STDOUT,
  103. stdin=null, shell=shell, env=env)
  104. out, _ = p.communicate()
  105. if p.returncode:
  106. if self.verbose:
  107. # verbose means I didn't capture stdout with PIPE,
  108. # so it's already been displayed and `out` is None.
  109. out = u''
  110. else:
  111. out = out.decode('utf-8', 'replace')
  112. log_function(command, out)
  113. self._captured_output.append(out)
  114. if raise_on_failure:
  115. raise raise_on_failure(
  116. 'Failed to run "{command}" command:\n{output}'.format(
  117. command=command, output=out))
  118. return False # failure
  119. return True # success
  120. def run_latex(self, filename, raise_on_failure=LatexFailed):
  121. """Run xelatex self.latex_count times."""
  122. def log_error(command, out):
  123. self.log.critical(u"%s failed: %s\n%s", command[0], command, out)
  124. return self.run_command(self.latex_command, filename,
  125. self.latex_count, log_error, raise_on_failure)
  126. def run_bib(self, filename, raise_on_failure=False):
  127. """Run bibtex one time."""
  128. filename = os.path.splitext(filename)[0]
  129. def log_error(command, out):
  130. self.log.warning('%s had problems, most likely because there were no citations',
  131. command[0])
  132. self.log.debug(u"%s output: %s\n%s", command[0], command, out)
  133. return self.run_command(self.bib_command, filename, 1, log_error, raise_on_failure)
  134. def from_notebook_node(self, nb, resources=None, **kw):
  135. latex, resources = super(PDFExporter, self).from_notebook_node(
  136. nb, resources=resources, **kw
  137. )
  138. # set texinputs directory, so that local files will be found
  139. if resources and resources.get('metadata', {}).get('path'):
  140. self.texinputs = resources['metadata']['path']
  141. else:
  142. self.texinputs = getcwd()
  143. self._captured_outputs = []
  144. with TemporaryWorkingDirectory():
  145. notebook_name = 'notebook'
  146. resources['output_extension'] = '.tex'
  147. tex_file = self.writer.write(latex, resources, notebook_name=notebook_name)
  148. self.log.info("Building PDF")
  149. self.run_latex(tex_file)
  150. if self.run_bib(tex_file):
  151. self.run_latex(tex_file)
  152. pdf_file = notebook_name + '.pdf'
  153. if not os.path.isfile(pdf_file):
  154. raise LatexFailed('\n'.join(self._captured_output))
  155. self.log.info('PDF successfully created')
  156. with open(pdf_file, 'rb') as f:
  157. pdf_data = f.read()
  158. # convert output extension to pdf
  159. # the writer above required it to be tex
  160. resources['output_extension'] = '.pdf'
  161. # clear figure outputs, extracted by latex export,
  162. # so we don't claim to be a multi-file export.
  163. resources.pop('outputs', None)
  164. return pdf_data, resources