Skip to content

Add support for playwright based pdf export #5913

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 67 additions & 17 deletions panel/io/save.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import io
import os
import tempfile

from typing import (
IO, TYPE_CHECKING, Any, Dict, Iterable, List, Optional,
Expand Down Expand Up @@ -56,8 +57,73 @@
doc.idle.connect(done);
"""

_SAVE_TEMPLATE = r"""\
{% block preamble %}
<style>
html, body {
box-sizing: border-box;
width: 100%;
height: 100%;
margin: 0;
border: 0;
padding: 0;
overflow: hidden;
}
</style>
{% endblock %}
"""

bokeh.io.export._WAIT_SCRIPT = _WAIT_SCRIPT

def save_pdf(
model: Model, filename: str, resources=CDN, template=None,
template_variables=None, timeout: int = 5
) -> None:
"""
Saves a bokeh model to pdf

Arguments
---------
model: bokeh.model.Model
Model to save to PDF
filename: str
Filename to save to
resources: str
Resources
template:
template file, as used by bokeh.file_html. If None will use bokeh defaults
template_variables:
template_variables file dict, as used by bokeh.file_html
timeout: int
The maximum amount of time (in seconds) to wait for
"""
if not isinstance(model, Model):
model = model.get_root()
from playwright.sync_api import sync_playwright
resources = Resources.from_bokeh(resources)
html = file_html(
model, resources, title="", template=template,
template_variables=template_variables or {},
_always_new=True
)
with tempfile.NamedTemporaryFile(suffix='.html') as nf:
nf.write(html.encode('utf-8'))
nf.flush()
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
context = browser.new_context()
page = context.new_page()
page.goto(f"file://{nf.name}")
page.emulate_media(media="screen")
out = page.pdf(print_background=True)

if hasattr(filename, 'write'):
filename.write(out)
else:
with open(filename, 'wb') as f:
f.write(out)


def save_png(
model: Model, filename: str, resources=CDN, template=None,
template_variables=None, timeout: int = 5
Expand Down Expand Up @@ -86,22 +152,7 @@ def save_png(

webdriver = state.webdriver

if template is None:
template = r"""\
{% block preamble %}
<style>
html, body {
box-sizing: border-box;
width: 100%;
height: 100%;
margin: 0;
border: 0;
padding: 0;
overflow: hidden;
}
</style>
{% endblock %}
"""
template = template or _SAVE_TEMPLATE

try:
def get_layout_html(obj, resources, width, height, **kwargs):
Expand All @@ -114,7 +165,6 @@ def get_layout_html(obj, resources, width, height, **kwargs):
old_layout_fn = bokeh.io.export.get_layout_html
bokeh.io.export.get_layout_html = get_layout_html
img = get_screenshot_as_png(model, driver=webdriver, timeout=timeout, resources=resources)

if img.width == 0 or img.height == 0:
raise ValueError("unable to save an empty image")

Expand Down