Skip to content

Commit

Permalink
Add qtpng exporter
Browse files Browse the repository at this point in the history
  • Loading branch information
davidbrochart committed Aug 10, 2021
1 parent 2d6302d commit 87dae45
Show file tree
Hide file tree
Showing 10 changed files with 179 additions and 110 deletions.
1 change: 1 addition & 0 deletions nbconvert/exporters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .pdf import PDFExporter
from .webpdf import WebPDFExporter
from .qtpdf import QtPDFExporter
from .qtpng import QtPNGExporter
from .python import PythonExporter
from .rst import RSTExporter
from .exporter import Exporter, FilenameExtension
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()
106 changes: 3 additions & 103 deletions nbconvert/exporters/qtpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.

import tempfile, os
from traitlets import Bool

from traitlets import Bool, default
from jupyter_core.paths import jupyter_path
from .qt_exporter import QtExporter

from .html import HTMLExporter


class QtPDFExporter(HTMLExporter):
class QtPDFExporter(QtExporter):
"""Writer designed to write to PDF files.
This inherits from :class:`HTMLExporter`. It creates the HTML using the
Expand All @@ -31,100 +28,3 @@ class QtPDFExporter(HTMLExporter):
).tag(config=True)

output_mimetype = "application/pdf"

@default('file_extension')
def _file_extension_default(self):
return '.pdf'

@default('template_name')
def _template_name_default(self):
return 'qtpdf'

@default('template_data_paths')
def _template_data_paths_default(self):
return jupyter_path("nbconvert", "templates", "qtpdf")

def _check_launch_reqs(self):
try:
from PyQt5.QtWidgets import QApplication
from PyQt5 import QtCore
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings
from PyQt5.QtGui import QPageLayout, QPageSize
except ModuleNotFoundError as e:
raise RuntimeError("PyQtWebEngine is not installed to support Qt PDF conversion. "
"Please install `nbconvert[qtpdf]` to enable.") from e
return QApplication, QtCore, QWebEngineView, QWebEngineSettings, QPageLayout, QPageSize

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

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

try:
QApplication, QtCore, QWebEngineView, QWebEngineSettings, QPageLayout, QPageSize = self._check_launch_reqs()

class Screenshot(QWebEngineView):
def __init__(self, app, paginate):
super().__init__()
self.app = app
self.paginate = paginate
def capture(self, url, output_file):
self.output_file = output_file
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)
self.page().pdfPrintingFinished.connect(
lambda *args: self.app.exit())
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.take_screenshot)
def take_screenshot(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(pdf_filename, pageLayout=page_layout)

app = QApplication([""])
s = Screenshot(app, self.paginate)
s.capture(f"file://{temp_file.name}", pdf_filename)
finally:
# Ensure the file is deleted even if pyqtwebengine raises an exception
os.unlink(temp_file.name)
pdf_data = b""
if os.path.exists(pdf_filename):
with open(pdf_filename, "rb") as f:
pdf_data = f.read()
os.unlink(pdf_filename)
return pdf_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('Building PDF')
pdf_data = self.run_pyqtwebengine(html)
self.log.info('PDF successfully created')

# convert output extension to pdf
# the writer above required it to be html
resources['output_extension'] = '.pdf'

return pdf_data, resources
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"
8 changes: 1 addition & 7 deletions nbconvert/exporters/tests/test_qtpdf.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
"""Tests for the latex preprocessor"""
"""Tests for the qtpdf preprocessor"""

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

import io
import pytest

from unittest.mock import patch

from .base import ExportersTestsBase
from ..qtpdf import QtPDFExporter

Expand All @@ -16,7 +11,6 @@ class TestQtPDFExporter(ExportersTestsBase):

exporter_class = QtPDFExporter

@pytest.mark.network
def test_export(self):
"""
Can a TemplateExporter export something?
Expand Down
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
4 changes: 4 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,9 @@ def get_data_files():
'qtpdf': [
'pyqtwebengine>=5.15'
],
'qtpng': [
'pyqtwebengine>=5.15'
],
'docs': [
'sphinx>=1.5.1',
'sphinx_rtd_theme',
Expand All @@ -271,6 +274,7 @@ def get_data_files():
'pdf=nbconvert.exporters:PDFExporter',
'webpdf=nbconvert.exporters:WebPDFExporter',
'qtpdf=nbconvert.exporters:QtPDFExporter',
'qtpng=nbconvert.exporters:QtPNGExporter',
'markdown=nbconvert.exporters:MarkdownExporter',
'python=nbconvert.exporters:PythonExporter',
'rst=nbconvert.exporters:RSTExporter',
Expand Down
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 87dae45

Please sign in to comment.