-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from compute-tooling/add-screenshot
Add module for taking screenshots of renderable outputs
- Loading branch information
Showing
9 changed files
with
494 additions
and
62 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
include cs_storage/templates/index.html |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import asyncio | ||
import os | ||
import tempfile | ||
|
||
try: | ||
# These dependencies are optional. The storage component may be used | ||
# without the screenshot component. | ||
from jinja2 import Template | ||
from pyppeteer import launch | ||
from bokeh.resources import CDN | ||
|
||
BASE_ARGS = { | ||
"bokeh_scripts": {"cdn_js": CDN.js_files[0], "widget_js": CDN.js_files[1]} | ||
} | ||
SCREENSHOT_ENABLED = True | ||
|
||
except ImportError: | ||
SCREENSHOT_ENABLED = False | ||
Template = None | ||
launch = None | ||
CDN = None | ||
BASE_ARGS = {} | ||
|
||
import cs_storage | ||
|
||
|
||
CURRENT_DIR = os.path.abspath(os.path.dirname(__file__)) | ||
|
||
|
||
class ScreenshotError(Exception): | ||
pass | ||
|
||
|
||
def get_template(): | ||
if not SCREENSHOT_ENABLED: | ||
return None | ||
with open(f"{CURRENT_DIR}/templates/index.html", "r") as f: | ||
text = f.read() | ||
|
||
template = Template(text) | ||
|
||
return template | ||
|
||
|
||
TEMPLATE = get_template() | ||
|
||
|
||
def write_template(output): | ||
kwargs = {**BASE_ARGS, **{"output": output}} | ||
return TEMPLATE.render(**kwargs) | ||
|
||
|
||
async def _screenshot(template_path, pic_path): | ||
""" | ||
Use pyppeteer, a python port of puppeteer, to open the | ||
template at template_path and take a screenshot of the | ||
output that is rendered within it. | ||
The output is rendered within a Bootstrap card element. | ||
This element is only as big as the elements that it contains. | ||
Thus, we only need to get the dimensions of the bootstrap | ||
card to figure out which part of the screen we need to use | ||
for the screenshot! | ||
Note: pyppetter looks stale. If it continues to not be | ||
maintained well, then the extremely active, well-maintained | ||
puppeteer should be used for creating these screenshots. The | ||
downside of using puppeteer is that it is written in nodejs. | ||
""" | ||
browser = await launch(args=["--no-sandbox"]) | ||
page = await browser.newPage() | ||
await page.goto(f"file://{template_path}") | ||
await page.setViewport(dict(width=1920, height=1080)) | ||
await page.waitFor(1000) | ||
element = await page.querySelector("#output") | ||
if element is None: | ||
raise ScreenshotError("Unable to take screenshot.") | ||
boundingbox = await element.boundingBox() | ||
clip = dict( | ||
x=boundingbox["x"], | ||
y=boundingbox["y"], | ||
width=min(boundingbox["width"], 1920), | ||
height=min(boundingbox["height"], 1080), | ||
) | ||
await page.screenshot(path=f"{pic_path}", type="png", clip=clip) | ||
await browser.close() | ||
|
||
|
||
def screenshot(output, debug=False): | ||
""" | ||
Create screenshot of outputs. The intermediate results are | ||
written to temporary files and a picture, represented as a | ||
stream of bytes, is returned. | ||
""" | ||
if not SCREENSHOT_ENABLED: | ||
return None | ||
html = write_template(output) | ||
with tempfile.NamedTemporaryFile(suffix=".html") as temp: | ||
if debug: | ||
with open(f'{output["title"]}.html', "w") as f: | ||
f.write(html) | ||
temp.write(html.encode("utf-8")) | ||
temp.seek(0) | ||
template_path = temp.name | ||
with tempfile.NamedTemporaryFile(suffix=".png") as pic: | ||
pic_path = pic.name | ||
asyncio.get_event_loop().run_until_complete( | ||
_screenshot(template_path, pic_path) | ||
) | ||
pic_bytes = pic.read() | ||
return pic_bytes |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
|
||
<head> | ||
<title>Compute Studio</title> | ||
<script src="https://code.jquery.com/jquery-3.3.1.min.js" | ||
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script> | ||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" | ||
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" | ||
crossorigin="anonymous"></script> | ||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" | ||
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> | ||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" | ||
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" | ||
crossorigin="anonymous"></script> | ||
<!-- <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.5.0/css/all.css" integrity="sha384-B4dIYHKNBt8Bc12p+WXckhzcICo0wtJAoU8YZTY5qE0Id1GSseTk6S+L3BlXeVIU" crossorigin="anonymous"> --> | ||
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet"> | ||
|
||
<meta name="viewport" content="width=device-width, initial-scale=1"> | ||
|
||
|
||
<script type="text/javascript" src={{bokeh_scripts.cdn_js|safe}}></script> | ||
<script type="text/javascript" src={{bokeh_scripts.widget_js|safe}}></script> | ||
|
||
</head> | ||
|
||
<body> | ||
<div class="container mt-4"> | ||
<div class="col-md-12"> | ||
<div id="output" class="card card-body card-inner text-center" style="overflow:auto"> | ||
<h4>{{output.title}}</h4> | ||
{% if output.media_type == 'bokeh' %} | ||
<div style="margin:0 auto;">{{output.id}} | ||
<div id="{{output.id}}" data-root-id="" className="bk-root"></div> | ||
</div> | ||
{% elif output.media_type == 'table' %} | ||
<div style="margin:0 auto;"> | ||
{{output.data|safe}} | ||
</div> | ||
{% elif output.media_type == 'PNG' %} | ||
<div style="margin:0 auto;"> | ||
<img src="data:image/png;base64, {{ output.data|safe }}" /> | ||
</div> | ||
{% elif output.media_type == 'JPEG' %} | ||
<div style="margin:0 auto;"> | ||
<img src="data:image/jpeg;base64, {{ output.data|safe }}" /> | ||
</div> | ||
{% endif %} | ||
</div> | ||
</div> | ||
</div> | ||
</body> | ||
|
||
{% if output.media_type == "bokeh" %} | ||
<script> | ||
window.Bokeh.embed.embed_item({{ output.data | tojson | safe }}, "{{ output.id }}"); | ||
</script> | ||
{% endif %} | ||
|
||
</html> |
Oops, something went wrong.