From bd79f8dda2c2093cdc3c80af0e4fa6c5b952824e Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Sat, 28 Nov 2015 08:37:52 -0500 Subject: [PATCH] refactoring to use nbbrowserpdf --- conda.recipe/meta.yaml | 2 - nbpresent/__init__.py | 20 +++- nbpresent/exporters/__init__.py | 2 - nbpresent/exporters/pdf.py | 63 ++---------- nbpresent/exporters/pdf_capture.py | 156 +++++++++-------------------- nbpresent/install.py | 50 +++++++-- nbpresent/present/__main__.py | 2 +- nbpresent/present/pdf.py | 2 +- 8 files changed, 112 insertions(+), 185 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index f264d4c..7093cbb 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -18,8 +18,6 @@ requirements: - python - notebook - funcsigs - - pypdf2 - - ghost.py test: imports: diff --git a/nbpresent/__init__.py b/nbpresent/__init__.py index ce42c47..492fcf0 100644 --- a/nbpresent/__init__.py +++ b/nbpresent/__init__.py @@ -4,11 +4,23 @@ def load_jupyter_server_extension(nbapp): from nbconvert.exporters.export import exporter_map - from .exporters import ( - PresentExporter, - PDFPresentExporter, - ) + from .exporters.html import PresentExporter + + nbapp.log.info("Enabling nbpresent HTML export") exporter_map.update( nbpresent=PresentExporter, + ) + + try: + from .exporters.pdf import PDFPresentExporter + except Exception as err: + nbapp.log.warn( + "nbbrowserpdf not available, PDF generation disabled: {}" + .format(err) + ) + return + + nbapp.log.info("Enabling nbpresent PDF export") + exporter_map.update( nbpresent_pdf=PDFPresentExporter ) diff --git a/nbpresent/exporters/__init__.py b/nbpresent/exporters/__init__.py index a648ab2..f250de6 100644 --- a/nbpresent/exporters/__init__.py +++ b/nbpresent/exporters/__init__.py @@ -3,5 +3,3 @@ ASSETS, APP_ROOT ) -from .html import PresentExporter -from .pdf import PDFPresentExporter diff --git a/nbpresent/exporters/pdf.py b/nbpresent/exporters/pdf.py index ca83594..1cfc5db 100644 --- a/nbpresent/exporters/pdf.py +++ b/nbpresent/exporters/pdf.py @@ -1,60 +1,11 @@ -import os -import shutil -import sys -from subprocess import check_call - -from ipython_genutils.tempdir import TemporaryWorkingDirectory -import nbformat - from .html import PresentExporter +from nbbrowserpdf.exporters.pdf import BrowserPDFExporter -class PDFPresentExporter(PresentExporter): - def __init__(self, *args, **kwargs): - super(PDFPresentExporter, self).__init__(*args, **kwargs) - - def from_notebook_node(self, nb, resources=None, **kw): - output, resources = super(PDFPresentExporter, self).from_notebook_node( - nb, resources=resources, **kw - ) - - with TemporaryWorkingDirectory() as td: - for path, res in resources.get("outputs", {}).items(): - dest = os.path.join(td, os.path.basename(path)) - shutil.copyfile(path, dest) - - index_html = os.path.join(td, "index.html") - - with open(index_html, "w+") as fp: - fp.write(output) - - ipynb = "notebook.ipynb" - - with open(os.path.join(td, ipynb), "w") as fp: - nbformat.write(nb, fp) - - self.log.info("Building PDF...") - check_call([ - sys.executable, - "-m", "nbpresent.exporters.pdf_capture", - td - ]) - - pdf_file = "notebook.pdf" - - if not os.path.isfile(pdf_file): - raise IOError("PDF creating failed") - - 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 +class PDFPresentExporter(PresentExporter, BrowserPDFExporter): + def pdf_capture_args(self): + return [ + "--capture-server-class", + "nbpresent.exporters.pdf_capture:SlideCaptureServer" + ] diff --git a/nbpresent/exporters/pdf_capture.py b/nbpresent/exporters/pdf_capture.py index 1503736..9eaa8d8 100644 --- a/nbpresent/exporters/pdf_capture.py +++ b/nbpresent/exporters/pdf_capture.py @@ -1,145 +1,83 @@ -from concurrent import futures -import os -import logging import time -import sys -from ghost import Ghost from ghost.bindings import ( - # QApplication, - # QImage, QPainter, QPrinter, - QtWebKit, QtCore, ) - -import tornado.web -from tornado.httpserver import HTTPServer - -from tornado.ioloop import IOLoop -from tornado.concurrent import run_on_executor - -import nbformat - from PyPDF2 import ( PdfFileReader, - PdfFileWriter, PdfFileMerger, ) -from .base import DEFAULT_STATIC_FILES_PATH +from nbbrowserpdf.exporters.pdf_capture import CaptureServer +VIEWPORT = (1920, 1080) -class CaptureServer(HTTPServer): - executor = futures.ThreadPoolExecutor(max_workers=1) - def __init__(self, *args, **kwargs): - super(CaptureServer, self).__init__(*args, **kwargs) - - @run_on_executor - def capture(self): - # DO SOME MAGIC - ghost = Ghost( - log_level=logging.DEBUG - ) - session = ghost.start( +class SlideCaptureServer(CaptureServer): + """ CaptureServer to generate multi-page PDF based on nbpresent metadata + """ + def init_session(self): + """ create a session with a some tweaked settings + """ + return self.ghost.start( # display=True, - viewport_size=(1920, 1080), + # TODO: read this off config + viewport_size=VIEWPORT, + show_scrollbars=False, ) - merger = PdfFileMerger() - join = lambda *bits: os.path.join(self.static_path, *bits) - session.open("http://localhost:9999/index.html") + def page_ready(self): + """ Wait until nbpresent-css gets created + """ + self.session.wait_for_page_loaded() + self.session.wait_for_selector("#nbpresent-css") + time.sleep(1) - try: - session.wait_for_selector("#nbpresent-css") - time.sleep(1) - except Exception as err: - print(err) + def print_to_pdf(self, path): + """ Custom print based on metadata: generate one per slide + """ + merger = PdfFileMerger() for i, slide in enumerate(self.notebook.metadata.nbpresent.slides): print("\n\n\nprinting slide", i, slide) - filename = join("notebook-{0:04d}.pdf".format(i)) - session.show() - screenshot(self.notebook, session, filename) + # science help you if you have 9999 slides + filename = self.in_static("notebook-{0:04d}.pdf".format(i)) + # this is weird, but it seems to always need it + self.session.show() + self.screenshot(filename) merger.append(PdfFileReader(filename, "rb")) - result, resources = session.evaluate( + + # advance the slides + result, resources = self.session.evaluate( """ console.log(window.nbpresent); console.log(window.nbpresent.mode.presenter.speaker.advance()); """) - time.sleep(1) - - merger.write(join("notebook-unmeta.pdf")) - - unmeta = PdfFileReader(join("notebook-unmeta.pdf"), "rb") - meta = PdfFileWriter() - meta.appendPagesFromReader(unmeta) - - ipynb = "notebook.ipynb" + # always seem to get some weirdness... perhaps could inttegrate + # ioloop... + time.sleep(1) - with open(join(ipynb), "rb") as fp: - meta.addAttachment(ipynb, fp.read()) + # all done! + merger.write(path) - with open(join("notebook.pdf"), "wb") as fp: - meta.write(fp) + def screenshot(self, filename): + """ do an individual slide screenshot + big thanks to https://gist.github.com/jmaupetit/4217925 + """ - raise KeyboardInterrupt() + printer = QPrinter(mode=QPrinter.ScreenResolution) + printer.setOutputFormat(QPrinter.PdfFormat) + printer.setPaperSize(QtCore.QSizeF(*reversed(VIEWPORT)), + QPrinter.DevicePixel) + printer.setOrientation(QPrinter.Landscape) + printer.setOutputFileName(filename) + printer.setPageMargins(0, 0, 0, 0, QPrinter.DevicePixel) -def screenshot(nb, session, dest, as_print=False): - """ - big thanks to https://gist.github.com/jmaupetit/4217925 - """ - - printer = QPrinter(mode=QPrinter.ScreenResolution) - printer.setOutputFormat(QPrinter.PdfFormat) - printer.setPaperSize(QtCore.QSizeF(1080, 1920), QPrinter.DevicePixel) - printer.setOrientation(QPrinter.Landscape) - printer.setOutputFileName(dest) - printer.setPageMargins(0, 0, 0, 0, QPrinter.DevicePixel) - - if as_print: - webview = QtWebKit.QWebView() - webview.setPage(session.page) - webview.print_(printer) - else: painter = QPainter(printer) painter.scale(1.45, 1.45) - session.main_frame.render(painter) + self.session.main_frame.render(painter) painter.end() - - -def pdf_capture(static_path): - settings = { - "static_path": static_path - } - - app = tornado.web.Application([ - (r"/components/(.*)", tornado.web.StaticFileHandler, { - "path": os.path.join(DEFAULT_STATIC_FILES_PATH, "components") - }), - (r"/(.*)", tornado.web.StaticFileHandler, { - "path": settings['static_path'] - }), - ], **settings) - - server = CaptureServer(app) - server.static_path = static_path - - with open(os.path.join(static_path, "notebook.ipynb")) as fp: - server.notebook = nbformat.read(fp, 4) - - ioloop = IOLoop() - ioloop.add_callback(server.capture) - server.listen(9999) - - try: - ioloop.start() - except KeyboardInterrupt: - print("stopped") - -if __name__ == "__main__": - pdf_capture(sys.argv[1]) diff --git a/nbpresent/install.py b/nbpresent/install.py index e97f5e1..2397b59 100644 --- a/nbpresent/install.py +++ b/nbpresent/install.py @@ -2,12 +2,20 @@ import argparse import os -from os.path import dirname, abspath, join, exists +from os.path import ( + abspath, + dirname, + exists, + join, +) +from pprint import pprint try: from inspect import signature except ImportError: from funcsigs import signature +from jupyter_core.paths import jupyter_config_dir + def install(enable=False, **kwargs): """Install the nbpresent nbextension assets and optionally enables the @@ -34,19 +42,41 @@ def install(enable=False, **kwargs): if "prefix" in kwargs: path = join(kwargs["prefix"], "etc", "jupyter") if not exists(path): + print("Making directory", path) os.makedirs(path) cm = ConfigManager(config_dir=path) - print("Enabling for", cm.config_dir) - print("Enabling nbpresent server component...") + print("Enabling nbpresent server component in", cm.config_dir) + cfg = cm.get("jupyter_notebook_config") + print("Existing config...") + pprint(cfg) + server_extensions = ( + cfg.setdefault("NotebookApp", {}) + .setdefault("server_extensions", []) + ) + if "nbpresent" not in server_extensions: + cfg["NotebookApp"]["server_extensions"] += ["nbpresent"] + + cm.update("jupyter_notebook_config", cfg) + print("New config...") + pprint(cm.get("jupyter_notebook_config")) + + cm = ConfigManager(config_dir=join(jupyter_config_dir(), "nbconfig")) + print( + "Enabling nbpresent nbextension at notebook launch in", + cm.config_dir + ) + + if not exists(cm.config_dir): + print("Making directory", cm.config_dir) + os.makedirs(cm.config_dir) + + cm.update( - "jupyter_notebook_config", { - "notebook": { - "load_extensions": {"nbpresent/nbpresent.min": True} + "notebook", { + "load_extensions": { + "nbpresent/nbpresent.min": True }, - "NotebookApp": { - "server_extensions": ["nbpresent"] - } } ) @@ -60,7 +90,7 @@ def install(enable=False, **kwargs): description="Installs nbpresent nbextension") parser.add_argument( "-e", "--enable", - help="Automatically load extension on notebook launch", + help="Automatically load server and nbextension on notebook launch", action="store_true") default_kwargs = dict( diff --git a/nbpresent/present/__main__.py b/nbpresent/present/__main__.py index 40ae999..cd99d32 100644 --- a/nbpresent/present/__main__.py +++ b/nbpresent/present/__main__.py @@ -2,9 +2,9 @@ import sys from ..exporters import ( - PresentExporter, APP_ROOT ) +from ..exporters.html import PresentExporter def main(nb): diff --git a/nbpresent/present/pdf.py b/nbpresent/present/pdf.py index f8c3fbc..4ac4414 100644 --- a/nbpresent/present/pdf.py +++ b/nbpresent/present/pdf.py @@ -2,9 +2,9 @@ import sys from ..exporters import ( - PDFPresentExporter, APP_ROOT ) +from ..exporters.pdf import PDFPresentExporter def main(nb):