#----------------------------------------------------------------------------- # Copyright (c) 2012 - 2019, 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 absolute_import, division, print_function, unicode_literals import logging log = logging.getLogger(__name__) #----------------------------------------------------------------------------- # Imports #----------------------------------------------------------------------------- # Standard library imports import os import io import warnings from os.path import abspath from tempfile import mkstemp # External imports from six import raise_from # Bokeh imports from ..embed import file_html from ..resources import INLINE from ..util.dependencies import import_required from ..util.string import decode_utf8 from .util import default_filename #----------------------------------------------------------------------------- # Globals and constants #----------------------------------------------------------------------------- __all__ = ( 'create_webdriver', 'export_png', 'export_svgs', 'get_layout_html', 'get_screenshot_as_png', 'get_svgs', 'terminate_webdriver', 'webdriver_control', ) #----------------------------------------------------------------------------- # General API #----------------------------------------------------------------------------- [docs]def export_png(obj, filename=None, height=None, width=None, webdriver=None, timeout=5): ''' Export the ``LayoutDOM`` 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 (LayoutDOM or Document) : a Layout (Row/Column), Plot or Widget object or Document to export. filename (str, optional) : filename to save document under (default: None) If None, infer from the filename. height (int) : the desired height of the exported layout obj only if it's a Plot instance. Otherwise the height kwarg is ignored. width (int) : the desired width of the exported layout obj only if it's a Plot instance. Otherwise the width 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). Returns: filename (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, height=height, width=width, driver=webdriver, timeout=timeout) if filename is None: filename = default_filename("png") if image.width == 0 or image.height == 0: raise ValueError("unable to save an empty image") image.save(filename) return abspath(filename) [docs]def export_svgs(obj, filename=None, height=None, width=None, webdriver=None, timeout=5): ''' 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 (LayoutDOM 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. height (int) : the desired height of the exported layout obj only if it's a Plot instance. Otherwise the height kwarg is ignored. width (int) : the desired width of the exported layout obj only if it's a Plot instance. Otherwise the width 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). 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, height=height, width=width, driver=webdriver, timeout=timeout) if len(svgs) == 0: log.warning("No SVG Plots were found.") return if filename is None: filename = default_filename("svg") filenames = [] for i, svg in enumerate(svgs): if i == 0: filename = filename else: idx = filename.find(".svg") filename = filename[:idx] + "_{}".format(i) + filename[idx:] with io.open(filename, mode="w", encoding="utf-8") as f: f.write(svg) filenames.append(filename) return filenames #----------------------------------------------------------------------------- # Dev API #----------------------------------------------------------------------------- # this is part of the API for this module from .webdriver import webdriver_control from .webdriver import terminate_webdriver # for back compat [docs]def create_webdriver(): ''' Create a new webdriver. .. note :: Here for compatibility. Prefer methods on the webdriver_control object. ''' return webdriver_control.create() [docs]def get_screenshot_as_png(obj, driver=None, timeout=5, **kwargs): ''' Get a screenshot of a ``LayoutDOM`` object. Args: obj (LayoutDOM or Document) : a Layout (Row/Column), Plot or Widget object or Document to export. driver (selenium.webdriver) : a selenium webdriver instance to use to export the image. 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. Returns: cropped_image (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. ''' Image = import_required('PIL.Image', 'To use bokeh.io.export_png you need pillow ' + '("conda install pillow" or "pip install pillow")') with _tmp_html() as tmp: html = get_layout_html(obj, **kwargs) with io.open(tmp.path, mode="w", encoding="utf-8") as file: file.write(decode_utf8(html)) web_driver = driver if driver is not None else webdriver_control.get() web_driver.get("file:///" + tmp.path) web_driver.maximize_window() ## resize for PhantomJS compat web_driver.execute_script("document.body.style.width = '100%';") wait_until_render_complete(web_driver, timeout) png = web_driver.get_screenshot_as_png() b_rect = web_driver.execute_script(_BOUNDING_RECT_SCRIPT) image = Image.open(io.BytesIO(png)) cropped_image = _crop_image(image, **b_rect) return cropped_image [docs]def get_svgs(obj, driver=None, timeout=5, **kwargs): ''' ''' with _tmp_html() as tmp: html = get_layout_html(obj, **kwargs) with io.open(tmp.path, mode="w", encoding="utf-8") as file: file.write(decode_utf8(html)) web_driver = driver if driver is not None else webdriver_control.get() web_driver.get("file:///" + tmp.path) wait_until_render_complete(web_driver, timeout) svgs = web_driver.execute_script(_SVG_SCRIPT) return svgs [docs]def get_layout_html(obj, resources=INLINE, **kwargs): ''' ''' resize = False if kwargs.get('height') is not None or kwargs.get('width') is not None: # Defer this import, it is expensive from ..models.plots import Plot if not isinstance(obj, Plot): warnings.warn("Export method called with height or width kwargs on a non-Plot layout. The size values will be ignored.") else: resize = True old_height = obj.plot_height old_width = obj.plot_width obj.plot_height = kwargs.get('height', old_height) obj.plot_width = kwargs.get('width', old_width) try: html = file_html(obj, resources, title="", suppress_callback_warning=True, _always_new=True) finally: if resize: obj.plot_height = old_height obj.plot_width = old_width return html def wait_until_render_complete(driver, timeout): ''' ''' from selenium.webdriver.support.ui import WebDriverWait from selenium.common.exceptions import TimeoutException from selenium.webdriver import Firefox def is_bokeh_loaded(driver): return driver.execute_script(''' const b = window.Bokeh; return b && b.documents && b.documents.length > 0; ''') try: WebDriverWait(driver, timeout, poll_frequency=0.1).until(is_bokeh_loaded) except TimeoutException as e: raise_from(RuntimeError('Bokeh was not loaded in time. Something may have gone wrong.'), e) driver.execute_script(_WAIT_SCRIPT) def is_bokeh_render_complete(driver): return driver.execute_script('return window._bokeh_render_complete;') try: WebDriverWait(driver, timeout, poll_frequency=0.1).until(is_bokeh_render_complete) except TimeoutException: log.warning("The webdriver raised a TimeoutException while waiting for " "a 'bokeh:idle' event to signify that the layout has rendered. " "Something may have gone wrong.") finally: # Firefox webdriver does not currently support logs if not isinstance(driver, Firefox): browser_logs = driver.get_log('browser') messages = [ l.get("message") for l in browser_logs if l.get('level') in ['WARNING', 'ERROR', 'SEVERE'] ] if len(messages) > 0: log.warning("There were browser warnings and/or errors that may have affected your export") for message in messages: log.warning(message) #----------------------------------------------------------------------------- # Private API #----------------------------------------------------------------------------- _BOUNDING_RECT_SCRIPT = """ return document.getElementsByClassName('bk-root')[0].children[0].getBoundingClientRect() """ _SVG_SCRIPT = """ var serialized_svgs = []; var svgs = document.getElementsByClassName('bk-root')[0].getElementsByTagName("svg"); for (var i = 0; i < svgs.length; i++) { var source = (new XMLSerializer()).serializeToString(svgs[i]); serialized_svgs.push(source); }; return serialized_svgs """ _WAIT_SCRIPT = """ // add private window prop to check that render is complete window._bokeh_render_complete = false; function done() { window._bokeh_render_complete = true; } var doc = window.Bokeh.documents[0]; if (doc.is_idle) done(); else doc.idle.connect(done); """ def _crop_image(image, left=0, top=0, right=0, bottom=0, **kwargs): ''' Crop the border from the layout ''' return image.crop((left, top, right, bottom)) class _TempFile(object): _closed = False def __init__(self, prefix="tmp", suffix=""): self.fd, self.path = mkstemp(prefix=prefix, suffix=suffix) def __enter__(self): return self def __exit__(self, exc, value, tb): self.close() def __del__(self): self.close() def close(self): if self._closed: return try: os.close(self.fd) except (OSError, IOError): pass finally: self.fd = None try: os.unlink(self.path) except (OSError, IOError): pass finally: self.path = None self._closed = True def _tmp_html(): return _TempFile(prefix="bokeh", suffix=".html") #----------------------------------------------------------------------------- # Code #-----------------------------------------------------------------------------