Source code for bokeh.io.export

#-----------------------------------------------------------------------------
# Copyright (c) Anaconda, Inc., and Bokeh Contributors.
# All rights reserved.
#
# The full license is in the file LICENSE.txt, distributed with this software.
#-----------------------------------------------------------------------------
'''

'''

#-----------------------------------------------------------------------------
# Boilerplate
#-----------------------------------------------------------------------------
from __future__ import annotations

import logging # isort:skip
log = logging.getLogger(__name__)

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------

# Standard library imports
import os
from os.path import abspath, expanduser, splitext
from types import ModuleType
from typing import TYPE_CHECKING, Literal

# Bokeh imports
from ..resources import INLINE
from ..settings import settings
from ..util.dependencies import import_optional
from . import browser as _playwright_backend, webdriver as _selenium_backend
from .util import default_filename, get_layout_html

if TYPE_CHECKING:
    from PIL import Image
    from selenium.webdriver.remote.webdriver import WebDriver

    try:
        from playwright.sync_api import Browser, BrowserContext
    except ImportError:
        from typing import (  # type: ignore[assignment]
            Any as Browser,
            Any as BrowserContext,
        )

    DriverLike = WebDriver | Browser | BrowserContext

    from ..core.types import PathLike
    from ..document import Document
    from ..models.ui import UIElement
    from ..resources import Resources
    from .state import State

#-----------------------------------------------------------------------------
# Globals and constants
#-----------------------------------------------------------------------------

ExportBackendType = Literal["selenium", "playwright"]

__all__ = (
    'export_png',
    'export_svg',
    'export_svgs',
    'get_layout_html',
    'get_screenshot_as_png',
    'get_svgs',
)

#-----------------------------------------------------------------------------
# General API
#-----------------------------------------------------------------------------

[docs] def export_png(obj: UIElement | Document, *, filename: PathLike | None = None, width: int | None = None, height: int | None = None, scale_factor: float = 1, webdriver: DriverLike | None = None, timeout: int = 5, state: State | None = None, backend: ExportBackendType | None = None) -> str: ''' Export the ``UIElement`` object or document as a PNG. If the filename is not given, it is derived from the script name (e.g. ``/foo/myplot.py`` will create ``/foo/myplot.png``) Args: obj (UIElement or Document) : a Layout (Row/Column), Plot or Widget object or Document to export. filename (PathLike, e.g. str, Path, optional) : filename to save document under (default: None) If None, infer from the filename. width (int) : the desired width of the exported layout obj only if it's a Plot instance. Otherwise the width kwarg is ignored. height (int) : the desired height of the exported layout obj only if it's a Plot instance. Otherwise the height kwarg is ignored. scale_factor (float, optional) : A factor to scale the output PNG by, providing a higher resolution while maintaining element relative scales. webdriver (selenium.webdriver or playwright Browser/BrowserContext) : A browser instance to use for export. Accepts a Selenium ``WebDriver`` or a Playwright ``Browser`` / ``BrowserContext`` (e.g. from ``playwright.chromium.launch()`` or ``launch_persistent_context()``). The backend is auto-detected from the type of object passed. timeout (int) : the maximum amount of time (in seconds) to wait for Bokeh to initialize (default: 5) (Added in 1.1.1). state (State, optional) : A :class:`State` object. If None, then the current default implicit state is used. (default: None). backend (ExportBackendType, optional) : Which browser backend to use for export. If None, uses the ``BOKEH_EXPORT_BACKEND`` setting (default: auto-detect). Passing a ``webdriver`` instance overrides this setting. Returns: str : the filename where the static file is saved. If you would like to access an Image object directly, rather than save a file to disk, use the lower-level :func:`~bokeh.io.export.get_screenshot_as_png` function. .. warning:: Responsive sizing_modes may generate layouts with unexpected size and aspect ratios. It is recommended to use the default ``fixed`` sizing mode. ''' image = get_screenshot_as_png(obj, width=width, height=height, scale_factor=scale_factor, driver=webdriver, timeout=timeout, state=state, backend=backend) if filename is None: filename = default_filename("png") if image.width == 0 or image.height == 0: raise ValueError("unable to save an empty image") filename = os.fspath(filename) # XXX: Image.save() doesn't fully support PathLike image.save(filename) return abspath(expanduser(filename))
[docs] def export_svg(obj: UIElement | Document, *, filename: PathLike | None = None, width: int | None = None, height: int | None = None, webdriver: DriverLike | None = None, timeout: int = 5, state: State | None = None, backend: ExportBackendType | None = None) -> list[str]: ''' Export a layout as SVG file or a document as a set of SVG files. If the filename is not given, it is derived from the script name (e.g. ``/foo/myplot.py`` will create ``/foo/myplot.svg``) Args: obj (UIElement object) : a Layout (Row/Column), Plot or Widget object to display filename (PathLike, e.g. str, Path, optional) : filename to save document under (default: None) If None, infer from the filename. width (int) : the desired width of the exported layout obj only if it's a Plot instance. Otherwise the width kwarg is ignored. height (int) : the desired height of the exported layout obj only if it's a Plot instance. Otherwise the height kwarg is ignored. webdriver (selenium.webdriver) : a selenium webdriver instance to use to export the image. timeout (int) : the maximum amount of time (in seconds) to wait for Bokeh to initialize (default: 5) state (State, optional) : A :class:`State` object. If None, then the current default implicit state is used. (default: None). backend (ExportBackendType, optional) : Which browser backend to use for export. If None, uses the ``BOKEH_EXPORT_BACKEND`` setting (default: auto-detect). Passing a ``webdriver`` instance forces the Selenium backend. Returns: list[str] : the list of filenames where the SVGs files are saved. .. warning:: Responsive sizing_modes may generate layouts with unexpected size and aspect ratios. It is recommended to use the default ``fixed`` sizing mode. ''' svgs = get_svg(obj, width=width, height=height, driver=webdriver, timeout=timeout, state=state, backend=backend) return _write_collection(svgs, filename, "svg")
[docs] def export_svgs(obj: UIElement | Document, *, filename: str | None = None, width: int | None = None, height: int | None = None, webdriver: DriverLike | None = None, timeout: int = 5, state: State | None = None, backend: ExportBackendType | None = None) -> list[str]: ''' Export the SVG-enabled plots within a layout. Each plot will result in a distinct SVG file. If the filename is not given, it is derived from the script name (e.g. ``/foo/myplot.py`` will create ``/foo/myplot.svg``) Args: obj (UIElement object) : a Layout (Row/Column), Plot or Widget object to display filename (str, optional) : filename to save document under (default: None) If None, infer from the filename. width (int) : the desired width of the exported layout obj only if it's a Plot instance. Otherwise the width kwarg is ignored. height (int) : the desired height of the exported layout obj only if it's a Plot instance. Otherwise the height kwarg is ignored. webdriver (selenium.webdriver) : a selenium webdriver instance to use to export the image. timeout (int) : the maximum amount of time (in seconds) to wait for Bokeh to initialize (default: 5) (Added in 1.1.1). state (State, optional) : A :class:`State` object. If None, then the current default implicit state is used. (default: None). backend (ExportBackendType, optional) : Which browser backend to use for export. If None, uses the ``BOKEH_EXPORT_BACKEND`` setting (default: auto-detect). Passing a ``webdriver`` instance forces the Selenium backend. Returns: filenames (list(str)) : the list of filenames where the SVGs files are saved. .. warning:: Responsive sizing_modes may generate layouts with unexpected size and aspect ratios. It is recommended to use the default ``fixed`` sizing mode. ''' svgs = get_svgs(obj, width=width, height=height, driver=webdriver, timeout=timeout, state=state, backend=backend) if len(svgs) == 0: log.warning("No SVG Plots were found.") return [] return _write_collection(svgs, filename, "svg")
#----------------------------------------------------------------------------- # Dev API #----------------------------------------------------------------------------- def _is_playwright_browser(obj: object) -> bool: '''Return True if ``obj`` is a Playwright Browser or BrowserContext. If Playwright is not installed, the object cannot be one of its types, so this unconditionally returns False. Otherwise a proper ``isinstance`` check is performed. ''' sync_api = import_optional("playwright.sync_api") if sync_api is None: return False return isinstance(obj, (sync_api.Browser, sync_api.BrowserContext)) def _resolve_backend(driver: DriverLike | None, backend: ExportBackendType | None) -> ModuleType: '''Determine which browser backend module to use. Returns the backend module itself (``bokeh.io.browser`` for playwright or ``bokeh.io.webdriver`` for selenium) so callers can dispatch directly without having to branch on a string. Priority order: 1. If a Playwright ``Browser`` or ``BrowserContext`` is passed as *driver*, always use the playwright backend. 2. If any other (Selenium) ``driver`` is passed, always use the selenium backend. 3. If ``backend`` is explicitly specified, use that. 4. Fall back to the ``BOKEH_EXPORT_BACKEND`` setting. 5. If set to "auto" (default), try selenium first, then playwright. This preserves existing behaviour for users who already have selenium installed. ''' if driver is not None: if _is_playwright_browser(driver): return _playwright_backend return _selenium_backend if backend is not None: if backend == "playwright": return _playwright_backend if backend == "selenium": return _selenium_backend raise ValueError(f"Invalid export backend: {backend!r}. Must be 'selenium' or 'playwright'.") configured = settings.export_backend() if configured == "playwright": return _playwright_backend if configured == "selenium": return _selenium_backend if configured != "auto": raise ValueError( f"Invalid export backend: {configured!r}. Must be one of 'auto', 'selenium', or 'playwright'.", ) # "auto" — try selenium first (preserves existing behaviour), then playwright if import_optional("selenium") is not None: return _selenium_backend if import_optional("playwright") is not None: return _playwright_backend raise RuntimeError( "Neither Selenium nor Playwright is installed. Install one of:\n" " pip install playwright && playwright install chromium\n" " pip install selenium (+ browser driver on PATH)", )
[docs] def get_screenshot_as_png(obj: UIElement | Document, *, driver: DriverLike | None = None, timeout: int = 5, resources: Resources = INLINE, width: int | None = None, height: int | None = None, scale_factor: float = 1, state: State | None = None, backend: ExportBackendType | None = None) -> Image.Image: ''' Get a screenshot of a ``UIElement`` object. Args: obj (UIElement or Document) : a Layout (Row/Column), Plot or Widget object or Document to export. driver (selenium.webdriver or playwright Browser/BrowserContext) : A browser instance to use for export. The backend is auto-detected from the type of object passed. timeout (int) : the maximum amount of time to wait for initialization. It will be used as a timeout for loading Bokeh, then when waiting for the layout to be rendered. scale_factor (float, optional) : A factor to scale the output PNG by, providing a higher resolution while maintaining element relative scales. state (State, optional) : A :class:`State` object. If None, then the current default implicit state is used. (default: None). backend ("selenium" or "playwright", optional) : Which browser backend to use. If None, auto-detected. Passing a ``driver`` overrides this setting. Returns: PIL.Image.Image : a pillow image loaded from PNG. .. warning:: Responsive sizing_modes may generate layouts with unexpected size and aspect ratios. It is recommended to use the default ``fixed`` sizing mode. ''' backend_module = _resolve_backend(driver, backend) return backend_module.get_screenshot_as_png( obj, driver=driver, timeout=timeout, resources=resources, width=width, height=height, scale_factor=scale_factor, state=state, )
def get_svg(obj: UIElement | Document, *, driver: DriverLike | None = None, timeout: int = 5, resources: Resources = INLINE, width: int | None = None, height: int | None = None, state: State | None = None, backend: ExportBackendType | None = None) -> list[str]: backend_module = _resolve_backend(driver, backend) return backend_module.get_svg( obj, driver=driver, timeout=timeout, resources=resources, width=width, height=height, state=state, ) def get_svgs(obj: UIElement | Document, *, driver: DriverLike | None = None, timeout: int = 5, resources: Resources = INLINE, width: int | None = None, height: int | None = None, state: State | None = None, backend: ExportBackendType | None = None) -> list[str]: backend_module = _resolve_backend(driver, backend) return backend_module.get_svgs( obj, driver=driver, timeout=timeout, resources=resources, width=width, height=height, state=state, ) #----------------------------------------------------------------------------- # Private API #----------------------------------------------------------------------------- def _write_collection(items: list[str], filename: PathLike | None, ext: str) -> list[str]: if filename is None: filename = default_filename(ext) filename = os.fspath(filename) filenames: list[str] = [] def _indexed(name: str, i: int) -> str: basename, ext = splitext(name) return f"{basename}_{i}{ext}" for i, item in enumerate(items): fname = filename if i == 0 else _indexed(filename, i) with open(fname, mode="w", encoding="utf-8") as f: f.write(item) filenames.append(fname) return filenames #----------------------------------------------------------------------------- # Code #-----------------------------------------------------------------------------