diff --git a/itkwidgets/standalone/index.html b/itkwidgets/standalone/index.html
index bd84a241..a9f437b9 100644
--- a/itkwidgets/standalone/index.html
+++ b/itkwidgets/standalone/index.html
@@ -44,6 +44,7 @@
}
async function setupViewerForImJoy(server) {
const version = itkVtkViewer.version
+
await server.registerService({
name: 'itkwidgets-service',
id: 'itkwidgets_service',
@@ -59,13 +60,16 @@
cover: ['https://kitware.github.io/itk-vtk-viewer/docs/howToUse.jpg', 'https://kitware.github.io/itk-vtk-viewer/docs/imjoy.png'],
});
+
itkVtkViewer.imJoyCodecs.forEach((codec) => {
server.registerCodec(codec)
})
+
const remoteID = `${workspace}/itkwidgets-server`;
let remoteService = await server.getService(`${remoteID}:itkwidgets-input-obj`);
const data = await remoteService.inputObject();
const viewer = await itkVtkViewer.createViewer(container, data.data);
+
await server.registerService({
name: "itk_vtk_viewer",
id: "itk-vtk-viewer",
@@ -75,8 +79,26 @@
require_context: false,
run_in_executor: true,
},
- viewer: () => viewer
+ viewer: () => viewer,
+ capture_screenshot: async () => {
+ const viewProxy = viewer.getViewProxy();
+ const representationProxy = viewProxy.getRepresentations()[0];
+ let mapper = null
+ if (representationProxy && representationProxy.getClassName() === 'vtkVolumeRepresentationProxy') {
+ mapper = representationProxy.getMapper();
+ mapper.setAutoAdjustSampleDistances(false)
+ }
+
+ const capturedImage = await viewer.captureImage()
+
+ if (representationProxy && representationProxy.getClassName() === 'vtkVolumeRepresentationProxy') {
+ mapper.setAutoAdjustSampleDistances(true)
+ }
+
+ return capturedImage
+ },
});
+
await server.registerService({
name: "setLabelImage",
id: "set-label-image",
@@ -89,9 +111,10 @@
set_label_image: async (uuid) => {
remoteService = await server.getService(`${remoteID}:fetch-zarr-store`);
let labelImage = await remoteService.fetchZarrStore(uuid);
- await viewer.setLabelImage(labelImage);
+ viewer.setLabelImage(labelImage);
}
});
+
remoteService = await server.getService(`${remoteID}:itkwidgets-viewer-ready`);
await remoteService.viewerReady(viewer);
}
diff --git a/itkwidgets/standalone_server.py b/itkwidgets/standalone_server.py
index f2c3ba37..f85fa540 100644
--- a/itkwidgets/standalone_server.py
+++ b/itkwidgets/standalone_server.py
@@ -10,10 +10,14 @@
import sys
import time
from pathlib import Path
-
+from base64 import b64decode
import webbrowser
+from playwright.sync_api import sync_playwright
import imjoy_rpc
+import numpy as np
+import PIL
+from imgcat import imgcat
from imjoy_rpc.hypha import connect_to_server_sync
from itkwidgets.standalone.config import SERVER_HOST, SERVER_PORT, VIEWER_HTML
@@ -31,6 +35,7 @@
from urllib3 import PoolManager, exceptions
logging.getLogger("urllib3").setLevel(logging.ERROR)
+logging.getLogger("websocket-client").setLevel(logging.ERROR)
def find_port(port=SERVER_PORT):
@@ -47,6 +52,7 @@ def find_port(port=SERVER_PORT):
OPTS = None
EVENT = threading.Event()
ZARR_STORE = {}
+BROWSER = None
def standalone_viewer(url):
@@ -71,6 +77,18 @@ def input_dict():
data = build_init_data(user_input)
ui = user_input.get("ui", "reference")
data["config"] = build_config(ui)
+
+ if data["view_mode"] is not None:
+ vm = data["view_mode"]
+ if vm == "x":
+ data["view_mode"] = "XPlane"
+ elif vm == "y":
+ data["view_mode"] = "YPlane"
+ elif vm == "z":
+ data["view_mode"] = "ZPlane"
+ elif vm == "v":
+ data["view_mode"] = "Volume"
+
return {"data": data}
@@ -83,7 +101,11 @@ def read_files():
if reader:
reader = ConversionBackend(reader)
else:
- reader = detect_cli_io_backend(input)
+ reader = detect_cli_io_backend([input])
+ if not input.find('://') == -1 and not Path(input).exists():
+ sys.stderr.write(f"File not found: {input}\n")
+ # hack
+ raise KeyboardInterrupt
ngff_image = cli_input_to_ngff_image(reader, [input])
user_input[param] = ngff_image
return user_input
@@ -179,11 +201,7 @@ def start_viewer(server_url):
}
)
- workspace = server.config.workspace
- token = server.generate_token()
- params = urlencode({"workspace": workspace, "token": token})
- webbrowser.open_new_tab(f"{server_url}/itkwidgets/index.html?{params}")
- return server
+ return server, input_obj
def main():
@@ -222,19 +240,82 @@ def main():
timeout -= 0.1
time.sleep(0.1)
- server = start_viewer(server_url)
+ server, input_obj = start_viewer(server_url)
+ workspace = server.config.workspace
+ token = server.generate_token()
+ params = urlencode({"workspace": workspace, "token": token})
+ url = f"{server_url}/itkwidgets/index.html?{params}"
+
+ # Updates for resolution progression
+ rate = 1.0
+ fast_rate = 0.05
+ if OPTS.rotate:
+ rate = fast_rate
+
+ if OPTS.browser:
+ sys.stdout.write(f"Viewer url:\n\n {url}\n\n")
+ webbrowser.open_new_tab(f"{server_url}/itkwidgets/index.html?{params}")
+ else:
+ playwright = sync_playwright().start()
+ args = [
+ "--enable-unsafe-webgpu",
+ ]
+ browser = playwright.chromium.launch(args=args)
+ BROWSER = browser
+ page = browser.new_page()
+
+ terminal_size = os.get_terminal_size()
+ width = terminal_size.columns * 10
+ is_tmux = 'TMUX' in os.environ and 'tmux' in os.environ['TMUX']
+ # https://github.com/tmux/tmux/issues/1502
+ if is_tmux:
+ width = min(width, 400)
+ else:
+ width = min(width, 1024)
+ height = width
+ page.set_viewport_size({"width": width, "height": height})
+
+ response = page.goto(url, timeout=0, wait_until="load")
+ assert response.status == 200, (
+ "Failed to start browser app instance, "
+ f"status: {response.status}, url: {url}"
+ )
+
+ input_data = input_obj["data"]
+ if not input_data["use2D"]:
+ if input_data["x_slice"] is None and input_data["view_mode"] == "XPlane":
+ page.locator('label[itk-vtk-tooltip-content="X plane play scroll"]').click()
+ rate = fast_rate
+ elif input_data["y_slice"] is None and input_data["view_mode"] == "YPlane":
+ page.locator('label[itk-vtk-tooltip-content="Y plane play scroll"]').click()
+ rate = fast_rate
+ elif input_data["y_slice"] is None and input_data["view_mode"] == "ZPlane":
+ page.locator('label[itk-vtk-tooltip-content="Z plane play scroll"]').click()
+ rate = fast_rate
+
+ EVENT.wait() # Wait until viewer is created before launching REPL
+ workspace = server.config.workspace
+ svc = server.get_service(f"{workspace}/itkwidgets-client:itk-vtk-viewer")
+ viewer = view(itk_viewer=svc.viewer(), server=server)
+ if not OPTS.browser:
+ terminal_height = min(terminal_size.lines - 1, terminal_size.columns // 3)
+
+
+ while True:
+ png_bin = b64decode(svc.capture_screenshot()[22:])
+ imgcat(png_bin, height=terminal_height)
+ time.sleep(rate)
+ CSI = b'\033['
+ sys.stdout.buffer.write(CSI + str(terminal_height).encode() + b"F")
+
if OPTS.repl:
- EVENT.wait() # Wait until viewer is created before launching REPL
- workspace = server.config.workspace
- svc = server.get_service(f"{workspace}/itkwidgets-client:itk-vtk-viewer")
- viewer = view(itk_viewer=svc.viewer(), server=server)
banner = f"""
Welcome to the itkwidgets command line tool! Press CTRL+D or
run `exit()` to terminate the REPL session. Use the `viewer`
object to manipulate the viewer.
"""
- exitmsg = "Exiting REPL. Press CTRL+C to teminate CLI tool."
- code.interact(banner=banner, local={"viewer": viewer}, exitmsg=exitmsg)
+ exitmsg = "Exiting REPL. Press CTRL+C to exit the viewer."
+ code.interact(banner=banner, local={"viewer": viewer, "svc": svc, "server": server}, exitmsg=exitmsg)
def cli_entrypoint():
@@ -243,11 +324,11 @@ def cli_entrypoint():
parser = argparse.ArgumentParser()
parser.add_argument("data", nargs="?", type=str, help="Path to a data file.")
- parser.add_argument("--image", type=str, help="Path to an image data file.")
+ parser.add_argument("-i", "--image", dest="image", type=str, help="Path to an image data file.")
parser.add_argument(
- "--label-image", type=str, help="Path to a label image data file."
+ "-l", "--label-image", dest="label_image", type=str, help="Path to a label image data file."
)
- parser.add_argument("--point-set", type=str, help="Path to a point set data file.")
+ parser.add_argument("-p", "--point-set", dest="point_set", type=str, help="Path to a point set data file.")
parser.add_argument(
"--use2D", dest="use2D", action="store_true", default=False, help="Image is 2D."
)
@@ -264,6 +345,13 @@ def cli_entrypoint():
default=False,
help="Print all log messages to stdout.",
)
+ parser.add_argument(
+ "-b", "--browser",
+ dest="browser",
+ action="store_true",
+ default=False,
+ help="Render to a browser tab instead of the terminal.",
+ )
parser.add_argument(
"--repl",
dest="repl",
@@ -273,7 +361,7 @@ def cli_entrypoint():
)
# General Interface
parser.add_argument(
- "--rotate",
+ "-r", "--rotate",
dest="rotate",
action="store_true",
default=False,
@@ -307,6 +395,7 @@ def cli_entrypoint():
"--bg-color",
type=tuple,
nargs="+",
+ default=(0.0, 0.0, 0.0),
help="Background color: (red, green, blue) tuple, components from 0.0 to 1.0.",
)
# Images
@@ -400,9 +489,9 @@ def cli_entrypoint():
help="Whether to used gradient-based shadows in the volume rendering.",
)
parser.add_argument(
- "--view-mode",
+ "-m", "--view-mode",
type=str,
- choices=["XPlane", "YPlane", "ZPlane", "Volume"],
+ choices=["x", "y", "z", "v"],
help="Only relevant for 3D scenes.",
)
parser.add_argument(
@@ -427,8 +516,18 @@ def cli_entrypoint():
OPTS = parser.parse_args()
- main()
+ try:
+ main()
+ except KeyboardInterrupt:
+ if BROWSER:
+ BROWSER.close()
+ if not OPTS.browser:
+ # Clear `^C%`
+ CSI = b'\033['
+ sys.stdout.buffer.write(CSI + b"1K")
+ sys.stdout.buffer.write(b"\n")
+ sys.exit(0)
if __name__ == "__main__":
- cli_entrypoint()
+ cli_entrypoint()
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index b61edf8d..a24f3eca 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -71,6 +71,8 @@ cli = [
"hypha >= 0.15.28",
"IPython >= 8.4.0",
"itk-io >= 5.3.0",
+ "ngff-zarr[cli]",
+ "playwright",
]
notebook = [