Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add script to generate RRD vs. screenshots comparisons #3946

Merged
merged 10 commits into from
Oct 23, 2023
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,8 @@ screenshot*.png
# Web demo app build
web_demo

# Screenshot comparison build
/compare_screenshot

.nox/
*.rrd
51 changes: 51 additions & 0 deletions scripts/screenshot_compare/assets/static/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
html {
/* Remove touch delay: */
touch-action: manipulation;
}

body {
background: #0d1011;
}

html,
body {
overflow: hidden;
margin: 0 !important;
padding: 0 !important;
height: 100%;
width: 100%;
}

.screen_splitter {
position: absolute;
height: 100vh;
width: 50vw;
}

.screen_splitter img {
position: absolute;
left: 50vw;
width: 50vw;
}

iframe {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
box-sizing: border-box;
top: 0;
left: 0;
width: 50vw;
height: 100vh;

border: none;

/* canvas must be on top when visible */
z-index: 1000;
}

* {
font-family: sans-serif;
color: #cad8de;
}
30 changes: 30 additions & 0 deletions scripts/screenshot_compare/assets/templates/example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

<!-- Disable zooming: -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />

<head>
<title>Compare Screenshot</title>
<link rel="stylesheet" href="index.css" />
</head>

<body>
<div class="screen_splitter">
<iframe id="iframe" allowfullscreen src=""></iframe>
<img alt="" src="{{ example.screenshot_url }}" >
</div>



<script>
// set the iframe URL
let encodedUrl = encodeURIComponent(
window.location.protocol + "//" + window.location.host + "/examples/{{ example.name }}/data.rrd");
document.getElementById('iframe').src = "http://127.0.0.1:9090/?url=" + encodedUrl;
</script>
</body>
</html>


23 changes: 23 additions & 0 deletions scripts/screenshot_compare/assets/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

<!-- Disable zooming: -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />

<head>
<title>Screenshot compare</title>
<link rel="stylesheet" href="index.css" />
</head>

<body>

<ul>
{% for entry in examples %}
<li>
<a href="examples/{{ entry.name }}">{{ entry.name }}</a>
</li>
{% endfor %}
</ul>
</body>
</html>
280 changes: 280 additions & 0 deletions scripts/screenshot_compare/build_screenshot_compare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
#!/usr/bin/env python3

"""
Generate comparison between examples and their related screenshots.

This script builds/gather RRDs and corresponding screenshots and displays
them side-by-side. It pulls from the following sources:

- The screenshots listed in .fbs files (crates/re_types/definitions/rerun/**/*.fbs),
and the corresponding code examples in the docs (docs/code-examples/*.rs)
- The `demo.rerun.io` examples, as built by the `build_demo_app.py` script.

The comparisons are generated in the `compare_screenshot` directory. Use the `--serve`
option to show them in a browser.
"""

from __future__ import annotations

import argparse
import http.server
import json
import os
import shutil
import subprocess
import threading
from dataclasses import dataclass
from functools import partial
from io import BytesIO
from pathlib import Path
from typing import Any, Iterable

import requests
from jinja2 import Template
from PIL import Image

BASE_PATH = Path("compare_screenshot")


SCRIPT_DIR_PATH = Path(__file__).parent
STATIC_ASSETS = SCRIPT_DIR_PATH / "assets" / "static"
TEMPLATE_DIR = SCRIPT_DIR_PATH / "assets" / "templates"
INDEX_TEMPLATE = Template((TEMPLATE_DIR / "index.html").read_text())
EXAMPLE_TEMPLATE = Template((TEMPLATE_DIR / "example.html").read_text())
RERUN_DIR = SCRIPT_DIR_PATH.parent.parent
CODE_EXAMPLE_DIR = RERUN_DIR / "docs" / "code-examples"


def measure_thumbnail(url: str) -> Any:
"""Downloads `url` and returns its width and height."""
response = requests.get(url)
response.raise_for_status()
image = Image.open(BytesIO(response.content))
return image.size


def run(
args: list[str], *, env: dict[str, str] | None = None, timeout: int | None = None, cwd: str | Path | None = None
) -> None:
print(f"> {subprocess.list2cmdline(args)}")
result = subprocess.run(args, env=env, cwd=cwd, timeout=timeout, check=False, capture_output=True, text=True)
assert (
result.returncode == 0
), f"{subprocess.list2cmdline(args)} failed with exit-code {result.returncode}. Output:\n{result.stdout}\n{result.stderr}"


@dataclass
class Example:
name: str
title: str
rrd: Path
screenshot_url: str


def copy_static_assets(examples: list[Example]) -> None:
# copy root
dst = BASE_PATH
print(f"\nCopying static assets from {STATIC_ASSETS} to {dst}")
shutil.copytree(STATIC_ASSETS, dst, dirs_exist_ok=True)

# copy examples
for example in examples:
dst = os.path.join(BASE_PATH, f"examples/{example.name}")
shutil.copytree(
STATIC_ASSETS,
dst,
dirs_exist_ok=True,
ignore=shutil.ignore_patterns("index.html"),
)


def build_python_sdk() -> None:
print("Building Python SDK…")
run(["just", "py-build", "--features", "web_viewer"])


# ====================================================================================================
# CODE EXAMPLES
#
# We scrape FBS for screenshot URL and generate the corresponding code examples RRD with roundtrips.py
# ====================================================================================================


def extract_code_example_urls_from_fbs() -> dict[str, str]:
fbs_path = SCRIPT_DIR_PATH.parent.parent / "crates" / "re_types" / "definitions" / "rerun"

urls = {}
for fbs in fbs_path.glob("**/*.fbs"):
for line in fbs.read_text().splitlines():
if line.startswith(r"/// \example"):
name = line.split()[2]

idx = line.find('image="')
if idx != -1:
end_idx = line.find('"', idx + 8)
if end_idx == -1:
end_idx = len(line)
urls[name] = line[idx + 7 : end_idx]

return urls


CODE_EXAMPLE_URLS = extract_code_example_urls_from_fbs()


def build_code_examples() -> None:
cmd = [
str(CODE_EXAMPLE_DIR / "roundtrips.py"),
"--no-py",
"--no-cpp",
"--no-py-build",
"--no-cpp-build",
]

for name in CODE_EXAMPLE_URLS.keys():
run(cmd + [name], cwd=RERUN_DIR)


def collect_code_examples() -> Iterable[Example]:
for name in sorted(CODE_EXAMPLE_URLS.keys()):
rrd = CODE_EXAMPLE_DIR / f"{name}_rust.rrd"
assert rrd.exists(), f"Missing {rrd} for {name}"
yield Example(name=name, title=name, rrd=rrd, screenshot_url=CODE_EXAMPLE_URLS[name])


# ====================================================================================================
# DEMO EXAMPLES
#
# We run the `build_demo_app.py` script and scrap the output "web_demo" directory.
# ====================================================================================================


BUILD_DEMO_APP_SCRIPT = RERUN_DIR / "scripts" / "ci" / "build_demo_app.py"


def build_demo_examples(skip_example_build: bool = False) -> None:
cmd = [
str(BUILD_DEMO_APP_SCRIPT),
"--skip-build", # we handle that ourselves
]

if skip_example_build:
cmd.append("--skip-example-build")

run(cmd, cwd=RERUN_DIR)


def collect_demo_examples() -> Iterable[Example]:
web_demo_example_dir = SCRIPT_DIR_PATH.parent.parent / "web_demo" / "examples"
assert web_demo_example_dir.exists(), "Web demos have not been built yet."

manifest = json.loads((web_demo_example_dir / "manifest.json").read_text())

for example in manifest:
name = example["name"]
rrd = web_demo_example_dir / f"{name}" / "data.rrd"
assert rrd.exists(), f"Missing {rrd} for {name}"

yield Example(
name=name,
title=example["title"],
rrd=rrd,
screenshot_url=example["thumbnail"]["url"],
)


def collect_examples() -> Iterable[Example]:
yield from collect_code_examples()
yield from collect_demo_examples()


def render_index(examples: list[Example]) -> None:
BASE_PATH.mkdir(exist_ok=True)

index_path = BASE_PATH / "index.html"
print(f"Rendering index.html -> {index_path}")
index_path.write_text(INDEX_TEMPLATE.render(examples=examples))


def render_examples(examples: list[Example]) -> None:
print("Rendering examples")

for example in examples:
target_path = BASE_PATH / "examples" / example.name
target_path.mkdir(parents=True, exist_ok=True)
index_path = target_path / "index.html"
print(f"{example.name} -> {index_path}")
index_path.write_text(EXAMPLE_TEMPLATE.render(example=example, examples=examples))

shutil.copy(example.rrd, target_path / "data.rrd")


class CORSRequestHandler(http.server.SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header("Access-Control-Allow-Origin", "*")
super().end_headers()


def serve_files() -> None:
def serve() -> None:
print("\nServing examples at http://127.0.0.1:8080/\n")
server = http.server.HTTPServer(
server_address=("127.0.0.1", 8080),
RequestHandlerClass=partial(
CORSRequestHandler,
directory=BASE_PATH,
),
)
server.serve_forever()

def serve_wasm() -> None:
abey79 marked this conversation as resolved.
Show resolved Hide resolved
import rerun as rr

rr.init("Screenshot compare")
rr.serve(open_browser=False)

threading.Thread(target=serve, daemon=True).start()
threading.Thread(target=serve_wasm, daemon=True).start()


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument(
"--serve",
action="store_true",
help="Serve the app on this port after building [default: 8080]",
)
parser.add_argument("--skip-build", action="store_true", help="Skip building the Python SDK.")
parser.add_argument("--skip-example-build", action="store_true", help="Skip building the RRDs.")

args = parser.parse_args()

if not args.skip_build:
build_python_sdk()

if not args.skip_example_build:
build_code_examples()
build_demo_examples()

examples = list(collect_examples())
assert len(examples) > 0, "No examples found"

render_index(examples)
render_examples(examples)
copy_static_assets(examples)

if args.serve:
serve_files()

while True:
try:
print("Press enter to reload static files")
input()
render_examples(examples)
copy_static_assets(examples)
except KeyboardInterrupt:
break


if __name__ == "__main__":
main()