#-----------------------------------------------------------------------------# 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__importannotationsimportlogging# isort:skiplog=logging.getLogger(__name__)#-----------------------------------------------------------------------------# Imports#-----------------------------------------------------------------------------# Standard library importsimportioimportosfromcontextlibimportcontextmanagerfromos.pathimportabspath,expanduser,splitextfromtempfileimportmkstempfromtypingimport(TYPE_CHECKING,Any,Iterator,cast,)ifTYPE_CHECKING:fromselenium.webdriver.remote.webdriverimportWebDriver# Bokeh importsfrom..core.typesimportPathLikefrom..documentimportDocumentfrom..embedimportfile_htmlfrom..resourcesimportINLINE,Resourcesfrom..themesimportThemefrom..util.warningsimportwarnfrom.stateimportState,curstatefrom.utilimportdefault_filenameifTYPE_CHECKING:fromPILimportImagefrom..models.plotsimportPlotfrom..models.uiimportUIElement#-----------------------------------------------------------------------------# Globals and constants#-----------------------------------------------------------------------------__all__=('export_png','export_svg','export_svgs','get_layout_html','get_screenshot_as_png','get_svgs',)#-----------------------------------------------------------------------------# General API#-----------------------------------------------------------------------------
[docs]defexport_png(obj:UIElement|Document,*,filename:PathLike|None=None,width:int|None=None,height:int|None=None,scale_factor:float=1,webdriver:WebDriver|None=None,timeout:int=5,state:State|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) : 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). 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,width=width,height=height,scale_factor=scale_factor,driver=webdriver,timeout=timeout,state=state)iffilenameisNone:filename=default_filename("png")ifimage.width==0orimage.height==0:raiseValueError("unable to save an empty image")filename=os.fspath(filename)# XXX: Image.save() doesn't fully support PathLikeimage.save(filename)returnabspath(expanduser(filename))
[docs]defexport_svg(obj:UIElement|Document,*,filename:PathLike|None=None,width:int|None=None,height:int|None=None,webdriver:WebDriver|None=None,timeout:int=5,state:State|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). 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_svg(obj,width=width,height=height,driver=webdriver,timeout=timeout,state=state)return_write_collection(svgs,filename,"svg")
[docs]defexport_svgs(obj:UIElement|Document,*,filename:str|None=None,width:int|None=None,height:int|None=None,webdriver:WebDriver|None=None,timeout:int=5,state:State|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). 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)iflen(svgs)==0:log.warning("No SVG Plots were found.")return[]return_write_collection(svgs,filename,"svg")
#-----------------------------------------------------------------------------# Dev API#-----------------------------------------------------------------------------
[docs]defget_screenshot_as_png(obj:UIElement|Document,*,driver:WebDriver|None=None,timeout:int=5,resources:Resources=INLINE,width:int|None=None,height:int|None=None,scale_factor:float=1,state:State|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) : 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. 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). Returns: 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. '''from.webdriverimport(get_web_driver_device_pixel_ratio,scale_factor_less_than_web_driver_device_pixel_ratio,webdriver_control,)with_tmp_html()astmp:theme=(stateorcurstate()).document.themehtml=get_layout_html(obj,resources=resources,width=width,height=height,theme=theme)withopen(tmp.path,mode="w",encoding="utf-8")asfile:file.write(html)ifdriverisnotNone:web_driver=driverifnotscale_factor_less_than_web_driver_device_pixel_ratio(scale_factor,web_driver):device_pixel_ratio=get_web_driver_device_pixel_ratio(web_driver)raiseValueError(f'Expected the web driver to have a device pixel ratio greater than {scale_factor}. 'f'Was given a web driver with a device pixel ratio of {device_pixel_ratio}.')else:web_driver=webdriver_control.get(scale_factor=scale_factor)web_driver.maximize_window()web_driver.get(f"file://{tmp.path}")wait_until_render_complete(web_driver,timeout)[width,height,dpr]=_maximize_viewport(web_driver)png=web_driver.get_screenshot_as_png()fromPILimportImagereturn(Image.open(io.BytesIO(png)).convert("RGBA").crop((0,0,width*dpr,height*dpr)).resize((int(width*scale_factor),int(height*scale_factor))))
[docs]defget_layout_html(obj:UIElement|Document,*,resources:Resources=INLINE,width:int|None=None,height:int|None=None,theme:Theme|None=None)->str:''' '''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 %} """defhtml()->str:returnfile_html(obj,resources=resources,title="",template=template,theme=theme,suppress_callback_warning=True,_always_new=True,)ifwidthisnotNoneorheightisnotNone:# Defer this import, it is expensivefrom..models.plotsimportPlotifnotisinstance(obj,Plot):warn("Export method called with width or height argument on a non-Plot model. The size values will be ignored.")else:with_resized(obj,width,height):returnhtml()returnhtml()
defwait_until_render_complete(driver:WebDriver,timeout:int)->None:''' '''fromselenium.common.exceptionsimportTimeoutExceptionfromselenium.webdriver.support.waitimportWebDriverWaitdefis_bokeh_loaded(driver:WebDriver)->bool:returncast(bool,driver.execute_script(''' return typeof Bokeh !== "undefined" && Bokeh.documents != null && Bokeh.documents.length != 0 '''))try:WebDriverWait(driver,timeout,poll_frequency=0.1).until(is_bokeh_loaded)exceptTimeoutExceptionase:_log_console(driver)raiseRuntimeError('Bokeh was not loaded in time. Something may have gone wrong.')fromedriver.execute_script(_WAIT_SCRIPT)defis_bokeh_render_complete(driver:WebDriver)->bool:returncast(bool,driver.execute_script('return window._bokeh_render_complete;'))try:WebDriverWait(driver,timeout,poll_frequency=0.1).until(is_bokeh_render_complete)exceptTimeoutException: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:_log_console(driver)#-----------------------------------------------------------------------------# Private API#-----------------------------------------------------------------------------@contextmanagerdef_resized(obj:Plot,width:int|None,height:int|None)->Iterator[None]:old_width=obj.widthold_height=obj.heightifwidthisnotNone:obj.width=widthifheightisnotNone:obj.height=heightyieldobj.width=old_widthobj.height=old_heightdef_write_collection(items:list[str],filename:PathLike|None,ext:str)->list[str]:iffilenameisNone:filename=default_filename(ext)filename=os.fspath(filename)filenames:list[str]=[]def_indexed(name:str,i:int)->str:basename,ext=splitext(name)returnf"{basename}_{i}{ext}"fori,iteminenumerate(items):fname=filenameifi==0else_indexed(filename,i)withopen(fname,mode="w",encoding="utf-8")asf:f.write(item)filenames.append(fname)returnfilenamesdef_log_console(driver:WebDriver)->None:levels={'WARNING','ERROR','SEVERE'}try:logs=driver.get_log('browser')exceptException:returnmessages=[log.get("message")forloginlogsiflog.get('level')inlevels]iflen(messages)>0:log.warning("There were browser warnings and/or errors that may have affected your export")formessageinmessages:log.warning(message)def_maximize_viewport(web_driver:WebDriver)->tuple[int,int,int]:calculate_viewport_size="""\ const root_view = Bokeh.index.roots[0] const {width, height} = root_view.el.getBoundingClientRect() return [Math.round(width), Math.round(height), window.devicePixelRatio] """viewport_size:tuple[int,int,int]=web_driver.execute_script(calculate_viewport_size)calculate_window_size="""\ const [width, height, dpr] = arguments return [ // XXX: outer{Width,Height} can be 0 in headless mode under certain window managers Math.round(Math.max(0, window.outerWidth - window.innerWidth) + width*dpr), Math.round(Math.max(0, window.outerHeight - window.innerHeight) + height*dpr), ] """[width,height]=web_driver.execute_script(calculate_window_size,*viewport_size)eps=100# XXX: can't set window size exactly in certain window managers, crop it to size laterweb_driver.set_window_size(width+eps,height+eps)returnviewport_size# TODO: consider UIElement like Pane_SVGS_SCRIPT="""const {LayoutDOMView} = Bokeh.require("models/layouts/layout_dom")const {PlotView} = Bokeh.require("models/plots/plot")function* collect_svgs(views) { for (const view of views) { if (view instanceof LayoutDOMView) { yield* collect_svgs(view.child_views.values()) } if (view instanceof PlotView && view.model.output_backend == "svg") { const {ctx} = view.canvas_view.compose() yield ctx.get_serialized_svg(true) } }}return [...collect_svgs(Bokeh.index)]"""_SVG_SCRIPT="""\function* export_svgs(views) { for (const view of views) { // TODO: use to_blob() API in future const {ctx} = view.export("svg") yield ctx.get_serialized_svg(true) }}return [...export_svgs(Bokeh.index)]"""_WAIT_SCRIPT="""// add private window prop to check that render is completewindow._bokeh_render_complete = false;function done() { window._bokeh_render_complete = true;}const doc = Bokeh.documents[0];if (doc.is_idle) done();else doc.idle.connect(done);"""class_TempFile:_closed:bool=Falsefd:intpath:strdef__init__(self,*,prefix:str="tmp",suffix:str="")->None:# XXX: selenium has issues with /tmp directory (or equivalent), so try using the# current directory first, if writable, and otherwise fall back to the system# default tmp directory.try:self.fd,self.path=mkstemp(prefix=prefix,suffix=suffix,dir=os.getcwd())exceptOSError:self.fd,self.path=mkstemp(prefix=prefix,suffix=suffix)def__enter__(self)->_TempFile:returnselfdef__exit__(self,exc:Any,value:Any,tb:Any)->None:self.close()def__del__(self)->None:self.close()defclose(self)->None:ifself._closed:returntry:os.close(self.fd)exceptOSError:passtry:os.unlink(self.path)exceptOSError:passself._closed=Truedef_tmp_html()->_TempFile:return_TempFile(prefix="bokeh",suffix=".html")#-----------------------------------------------------------------------------# Code#-----------------------------------------------------------------------------