Skip to content

Commit

Permalink
Add qtpdf and qtpng exporters
Browse files Browse the repository at this point in the history
  • Loading branch information
davidbrochart committed Jun 17, 2022
1 parent 8afd354 commit 3133478
Show file tree
Hide file tree
Showing 14 changed files with 234 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
shell: bash
run: |
pip install codecov --user
pip install -e ".[execute,serve,test]" --user
pip install -e ".[execute,serve,qtpdf,qtpng,test]" --user
python -m ipykernel.kernelspec --user
# many things installed in --users need this to be in $PATH
echo "/Users/runner/.local/bin" >> $GITHUB_PATH
Expand Down
2 changes: 2 additions & 0 deletions nbconvert/exporters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from .notebook import NotebookExporter
from .pdf import PDFExporter
from .python import PythonExporter
from .qtpdf import QtPDFExporter
from .qtpng import QtPNGExporter
from .rst import RSTExporter
from .script import ScriptExporter
from .slides import SlidesExporter
Expand Down
76 changes: 76 additions & 0 deletions nbconvert/exporters/qt_exporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import tempfile, os

from jupyter_core.paths import jupyter_path
from traitlets import default

from .html import HTMLExporter


class QtExporter(HTMLExporter):

paginate = None

def __init__(self, *args, **kwargs):
self.format = self.output_mimetype.split("/")[-1]
super().__init__(*args, **kwargs)

@default('file_extension')
def _file_extension_default(self):
return "." + self.format

@default('template_name')
def _template_name_default(self):
return "qt" + self.format

@default('template_data_paths')
def _template_data_paths_default(self):
return jupyter_path("nbconvert", "templates", "qt" + self.format)

def _check_launch_reqs(self):
try:
from PyQt5.QtWidgets import QApplication
from .qt_screenshot import QtScreenshot
except ModuleNotFoundError as e:
raise RuntimeError(f"PyQtWebEngine is not installed to support Qt {self.format.upper()} conversion. "
f"Please install `nbconvert[qt{self.format}]` to enable.") from e
return QApplication, QtScreenshot

def run_pyqtwebengine(self, html):
"""Run pyqtwebengine."""

ext = ".html"
temp_file = tempfile.NamedTemporaryFile(suffix=ext, delete=False)
filename = f"{temp_file.name[:-len(ext)]}.{self.format}"
with temp_file:
temp_file.write(html.encode('utf-8'))

try:
QApplication, QtScreenshot = self._check_launch_reqs()
app = QApplication([""])
s = QtScreenshot(app)
s.capture(f"file://{temp_file.name}", filename, self.paginate)
finally:
# Ensure the file is deleted even if pyqtwebengine raises an exception
os.unlink(temp_file.name)
data = b""
if os.path.exists(filename):
with open(filename, "rb") as f:
data = f.read()
os.unlink(filename)
return data

def from_notebook_node(self, nb, resources=None, **kw):
self._check_launch_reqs()
html, resources = super().from_notebook_node(
nb, resources=resources, **kw
)

self.log.info(f"Building {self.format.upper()}")
data = self.run_pyqtwebengine(html)
self.log.info(f"{self.format.upper()} successfully created")

# convert output extension
# the writer above required it to be html
resources['output_extension'] = f".{self.format}"

return data, resources
51 changes: 51 additions & 0 deletions nbconvert/exporters/qt_screenshot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from PyQt5.QtWidgets import QApplication
from PyQt5 import QtCore
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings
from PyQt5.QtGui import QPageLayout, QPageSize


class QtScreenshot(QWebEngineView):

def __init__(self, app):
super().__init__()
self.app = app

def capture(self, url, output_file, paginate):
self.output_file = output_file
self.paginate = paginate
self.load(QtCore.QUrl(url))
self.loadFinished.connect(self.on_loaded)
# Create hidden view without scrollbars
self.setAttribute(QtCore.Qt.WA_DontShowOnScreen)
self.page().settings().setAttribute(
QWebEngineSettings.ShowScrollBars, False)
if output_file.endswith(".pdf"):
self.export = self.export_pdf
self.page().pdfPrintingFinished.connect(lambda *args: self.app.exit())
elif output_file.endswith(".png"):
self.export = self.export_png
else:
raise RuntimeError(f"Export file extension not supported: {output_file}")
self.show()
self.app.exec()

def on_loaded(self):
self.size = self.page().contentsSize().toSize()
self.resize(self.size)
# Wait for resize
QtCore.QTimer.singleShot(1000, self.export)

def export_pdf(self):
if self.paginate:
page_size = QPageSize(QPageSize.A4)
page_layout = QPageLayout(page_size, QPageLayout.Portrait, QtCore.QMarginsF())
else:
factor = 0.75
page_size = QPageSize(QtCore.QSizeF(self.size.width() * factor, self.size.height() * factor), QPageSize.Point)
page_layout = QPageLayout(page_size, QPageLayout.Portrait, QtCore.QMarginsF())

self.page().printToPdf(self.output_file, pageLayout=page_layout)

def export_png(self):
self.grab().save(self.output_file, b"PNG")
self.app.quit()
30 changes: 30 additions & 0 deletions nbconvert/exporters/qtpdf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Export to PDF via a headless browser"""

# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.

from traitlets import Bool

from .qt_exporter import QtExporter


class QtPDFExporter(QtExporter):
"""Writer designed to write to PDF files.
This inherits from :class:`HTMLExporter`. It creates the HTML using the
template machinery, and then uses pyqtwebengine to create a pdf.
"""
export_from_notebook = "PDF via HTML"

paginate = Bool(
True,
help="""
Split generated notebook into multiple pages.
If False, a PDF with one long page will be generated.
Set to True to match behavior of LaTeX based PDF generator
"""
).tag(config=True)

output_mimetype = "application/pdf"
17 changes: 17 additions & 0 deletions nbconvert/exporters/qtpng.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Export to PNG via a headless browser"""

# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.

from .qt_exporter import QtExporter


class QtPNGExporter(QtExporter):
"""Writer designed to write to PNG files.
This inherits from :class:`HTMLExporter`. It creates the HTML using the
template machinery, and then uses pyqtwebengine to create a png.
"""
export_from_notebook = "PNG via HTML"

output_mimetype = "image/png"
19 changes: 19 additions & 0 deletions nbconvert/exporters/tests/test_qtpdf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Tests for the qtpdf preprocessor"""

# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.

from .base import ExportersTestsBase
from ..qtpdf import QtPDFExporter

class TestQtPDFExporter(ExportersTestsBase):
"""Contains test functions for qtpdf.py"""

exporter_class = QtPDFExporter

def test_export(self):
"""
Can a TemplateExporter export something?
"""
(output, resources) = QtPDFExporter().from_filename(self._get_notebook())
assert len(output) > 0
19 changes: 19 additions & 0 deletions nbconvert/exporters/tests/test_qtpng.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Tests for the qtpng preprocessor"""

# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.

from .base import ExportersTestsBase
from ..qtpng import QtPNGExporter

class TestQtPNGExporter(ExportersTestsBase):
"""Contains test functions for qtpng.py"""

exporter_class = QtPNGExporter

def test_export(self):
"""
Can a TemplateExporter export something?
"""
(output, resources) = QtPNGExporter().from_filename(self._get_notebook())
assert len(output) > 0
1 change: 1 addition & 0 deletions nbconvert/nbconvertapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,7 @@ def initialize(self, argv=None):
self.config.TemplateExporter.exclude_input_prompt = True
self.config.ExecutePreprocessor.enabled = True
self.config.WebPDFExporter.paginate = False
self.config.QtPDFExporter.paginate = False

super().initialize(argv)

Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ html = "nbconvert.exporters:HTMLExporter"
slides = "nbconvert.exporters:SlidesExporter"
latex = "nbconvert.exporters:LatexExporter"
pdf = "nbconvert.exporters:PDFExporter"
qtpdf = "nbconvert.exporters:QtPDFExporter"
qtpng = "nbconvert.exporters:QtPNGExporter"
webpdf = "nbconvert.exporters:WebPDFExporter"
markdown = "nbconvert.exporters:MarkdownExporter"
python = "nbconvert.exporters:PythonExporter"
Expand All @@ -64,6 +66,8 @@ test = [
"pyppeteer>=1,<1.1",
]
serve = ["tornado>=6.1"]
qtpdf = ["pyqtwebengine>=5.15"]
qtpng = ["pyqtwebengine>=5.15"]
webpdf = ["pyppeteer>=1,<1.1"]
docs = [
"sphinx>=1.5.1",
Expand Down
6 changes: 6 additions & 0 deletions share/jupyter/nbconvert/templates/qtpdf/conf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"base_template": "lab",
"mimetypes": {
"application/pdf": true
}
}
1 change: 1 addition & 0 deletions share/jupyter/nbconvert/templates/qtpdf/index.pdf.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{%- extends 'lab/index.html.j2' -%}
6 changes: 6 additions & 0 deletions share/jupyter/nbconvert/templates/qtpng/conf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"base_template": "lab",
"mimetypes": {
"image/png": true
}
}
1 change: 1 addition & 0 deletions share/jupyter/nbconvert/templates/qtpng/index.png.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{%- extends 'lab/index.html.j2' -%}

0 comments on commit 3133478

Please sign in to comment.