Source code for gofigr.backends.py3dmol
"""\
Copyright (c) 2023, Flagstaff Solutions, LLC
All rights reserved.
"""
import inspect
import os
import re
import sys
import tempfile
try:
import py3Dmol
except ImportError:
py3Dmol = None
try:
from html2image import Html2Image
except ImportError:
Html2Image = None
from gofigr.backends import GoFigrBackend, get_all_function_arguments
[docs]
class WatermarkedView(py3Dmol.view):
"""Overrides py3Dmol.view to include a GoFigr watermark"""
def __init__(self, wrapped_obj, extra_html):
super().__init__()
self.wrapped_obj = wrapped_obj
self.extra_html = extra_html
def _make_html(self):
# pylint: disable=protected-access
return self.wrapped_obj._make_html() + self.extra_html
[docs]
class Py3DmolBackend(GoFigrBackend):
"""Plotly backend for GoFigr"""
def __init__(self, *args, debug=False, image_size=(1920, 1080),
disable_logging=True, **kwargs):
super().__init__(*args, **kwargs)
self.image_size = image_size
self.hti = Html2Image(size=image_size, disable_logging=disable_logging)
self.debug = debug
def _debug(self, text):
if self.debug:
print(text)
def _infer_size(self, html):
m = re.search(r'position: relative; width: (\d+)px; height: (\d+)px', html)
if m is None:
return self.image_size
else:
return int(m.group(1)), int(m.group(2))
[docs]
def find_figures(self, shell, data):
self._debug("Looking for py3dmol figures...")
self._debug(f"Available MIME types: {data.keys()}")
frames = inspect.stack()
# Make sure there's an actual figure being published, as opposed to Plotly initialization scripts
if not any('3dmoljs' in x for x in data.keys()):
self._debug("Could not locate 3dmoljs MIME type. Stopping search early.")
return
else:
self._debug("3dmoljs MIME found")
# Walk through the stack in *reverse* order (from top to bottom), to find the first call
# in case display() was called recursively
for idx, f in enumerate(reversed(frames)):
self._debug(f"Frame {idx + 1}: function={f.function} file={f.filename}")
if ("show" in f.function or "repr_html" in f.function) and "py3dmol" in f.filename.lower():
self._debug("Found make_html call")
for arg_idx, arg_value in enumerate(get_all_function_arguments(f)):
self._debug(f"Inspecting argument {arg_idx + 1}: "
f"{arg_value.__class__ if not arg_value is None else None}")
if self.is_compatible(arg_value):
self._debug("Argument is compatible with GoFigr")
yield arg_value
else:
self._debug("Argument not compatible.")
break
# pylint: disable=useless-return
[docs]
def get_default_figure(self, silent=False):
if not silent:
print("py3Dmol does not have a default figure. Please specify a figure to publish.", file=sys.stderr)
return None
[docs]
def get_title(self, fig):
title = getattr(fig, "title", None)
if not isinstance(title, str):
return None
else:
return title
[docs]
def figure_to_bytes(self, fig, fmt, params):
if fmt == "png":
with tempfile.NamedTemporaryFile(suffix=".png", dir=self.hti.output_path) as ntf:
fig_html = self.figure_to_html(fig)
width, height = self._infer_size(fig_html)
self.hti.screenshot(html_str=fig_html, save_as=os.path.basename(ntf.name),
size=(width, height))
with open(ntf.name, 'rb') as f:
return f.read()
raise ValueError(f"Py3Dmol does not support {fmt.upper()}")
[docs]
def add_interactive_watermark(self, fig, rev, watermark):
watermark_html = f"<div><a href='{rev.revision_url}'>{rev.revision_url}</a></div>"
return WatermarkedView(fig, watermark_html)