Skip to content

Commit

Permalink
feat(bindgen): Add python-web-demo interface
Browse files Browse the repository at this point in the history
  • Loading branch information
thewtex committed Jul 25, 2023
1 parent 79b81eb commit 1d48471
Show file tree
Hide file tree
Showing 40 changed files with 1,878 additions and 213 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"build:emscripten:compress-stringify": "node ./src/itk-wasm-cli.js -s packages/compress-stringify -b emscripten-build build ",
"build:bindgen:typescript:compress-stringify": "./src/itk-wasm-cli.js -s packages/compress-stringify -b emscripten-build bindgen --package-version 0.5.0 --package-name @itk-wasm/compress-stringify --package-description \"Zstandard compression and decompression and base64 encoding and decoding in WebAssembly.\" --repository 'https://github.com/InsightSoftwareConsortium/itk-wasm'",
"build:bindgen:python:compress-stringify": "./src/itk-wasm-cli.js -s packages/compress-stringify -b wasi-build bindgen --interface python --package-name itkwasm-compress-stringify --package-description \"Zstandard compression and decompression and base64 encoding and decoding in WebAssembly.\" --package-version 0.5.0 --repository 'https://github.com/InsightSoftwareConsortium/itk-wasm'",
"build:bindgen:python-web-demo:compress-stringify": "./src/itk-wasm-cli.js -s packages/compress-stringify -b emscripten-build bindgen --interface python-web-demo --package-name itkwasm-compress-stringify --package-description \"Zstandard compression and decompression and base64 encoding and decoding in WebAssembly.\" --package-version 0.5.0 --repository 'https://github.com/InsightSoftwareConsortium/itk-wasm'",
"build:emscripten:dicom": "node ./src/itk-wasm-cli.js -s packages/dicom -b emscripten-build build -- -DCMAKE_BUILD_TYPE:STRING=Debug",
"build:bindgen:typescript:dicom": "./src/itk-wasm-cli.js -s packages/dicom -b emscripten-build bindgen --package-version 2.1.0 --package-name @itk-wasm/dicom --package-description \"Read files and images related to DICOM file format.\" --repository 'https://github.com/InsightSoftwareConsortium/itk-wasm'",
"build:bindgen:python:dicom": "./src/itk-wasm-cli.js -s packages/dicom -b wasi-build bindgen --package-version 2.1.0 --interface python --package-name itkwasm-dicom --package-description \"Read files and images related to DICOM file format.\" --repository 'https://github.com/InsightSoftwareConsortium/itk-wasm'",
Expand Down
17 changes: 17 additions & 0 deletions packages/compress-stringify/python-web-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# itkwasm-compress-stringify

[![PyPI version](https://badge.fury.io/py/itkwasm-compress-stringify.svg)](https://badge.fury.io/py/itkwasm-compress-stringify)

> API Demo: Zstandard compression and decompression and base64 encoding and decoding in WebAssembly.
This is a simple web browser app built with Python, HTML, and CSS that demonstrates the [itkwasm-compress-stringify](https://badge.fury.io/py/itkwasm-compress-stringify) Python package.

## Development

The app uses the Python package published on pypi.org and the corresponding JavaScript package published npmjs.com.

The web app can be developed locally with any http server that serves static files. For example:

```sh
python -m http.server
```
112 changes: 112 additions & 0 deletions packages/compress-stringify/python-web-demo/compress_stringify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Generated file. To retain edits, remove this comment.

from dataclasses import dataclass
from typing import Any, Dict

import numpy as np

import js
from pyodide.ffi.wrappers import add_event_listener
import pyodide

from itkwasm_compress_stringify import compress_stringify_async
from compress_stringify_load_sample_inputs import load_sample_inputs

@dataclass
class CompressStringifyModel:
inputs: Dict['str', Any]
options: Dict['str', Any]
outputs: Dict['str', Any]

class CompressStringifyController:

def __init__(self, load_sample_inputs):
self.model = CompressStringifyModel({}, {}, {})

self.load_sample_inputs = load_sample_inputs
if load_sample_inputs is not None:
load_sample_inputs_button = js.document.querySelector("#compress_stringify-inputs [name=load-sample-inputs]")
load_sample_inputs_button.setAttribute('style', 'display: block-inline;')
add_event_listener(load_sample_inputs_button, 'click', self.on_load_sample_inputs_click)

# Inputs
input_element = js.document.querySelector('#compress_stringify-inputs input[name=input-file]')
add_event_listener(input_element, 'change', self.on_input_change)

# Options
stringify_element = js.document.querySelector('#compress_stringify-inputs sl-checkbox[name=stringify]')
self.stringify_element = stringify_element
add_event_listener(stringify_element, 'sl-change', self.on_stringify_change)

compression_level_element = js.document.querySelector('#compress_stringify-inputs sl-input[name=compression-level]')
self.compression_level_element = compression_level_element
add_event_listener(compression_level_element, 'sl-change', self.on_compression_level_change)

data_url_prefix_element = js.document.querySelector('#compress_stringify-inputs sl-input[name=data-url-prefix]')
self.data_url_prefix_element = data_url_prefix_element
add_event_listener(data_url_prefix_element, 'sl-change', self.on_data_url_prefix_change)

# Outputs
output_download_element = js.document.querySelector('#compress_stringify-outputs sl-button[name=output-download]')
self.output_download_element = output_download_element
add_event_listener(output_download_element, 'click', self.on_output_click)

# Run
run_button = js.document.querySelector('#compress_stringify-inputs sl-button[name="run"]')
self.run_button = run_button
add_event_listener(run_button, 'click', self.on_run)

def on_load_sample_inputs_click(self, event):
self.model = self.load_sample_inputs(self.model)

async def on_input_change(self, event):
files = event.target.files
array_buffer = await files.item(0).arrayBuffer()
input_bytes = array_buffer.to_bytes()
self.model.inputs['input'] = input_bytes
input_element = js.document.querySelector("#compress_stringify-inputs sl-input[name=input]")
input_element.value = str(np.frombuffer(input_bytes[:50], dtype=np.uint8)) + ' ...'

def on_stringify_change(self, event):
self.model.options['stringify'] = self.stringify_element.checked

def on_compression_level_change(self, event):
self.model.options['compression_level'] = int(self.compression_level_element.value)

def on_data_url_prefix_change(self, event):
self.model.options['data_url_prefix'] = self.data_url_prefix_element.value

def on_output_click(self, event):
if 'output' not in self.model.outputs:
return
output = pyodide.ffi.to_js(self.model.outputs['output'])
js.globalThis.downloadFile(output, 'output.bin')

async def on_run(self, event):
event.preventDefault()
event.stopPropagation()

if 'input' not in self.model.inputs:

js.globalThis.notify("Error while running pipeline", "Missing input 'input'", "danger", "exclamation-octagon")
return

self.run_button.loading = True
try:
t0 = js.performance.now()
output = await compress_stringify_async(self.model.inputs['input'], **self.model.options)
t1 = js.performance.now()
js.globalThis.notify("compress_stringify successfully completed", f"in {t1 - t0} milliseconds.", "success", "rocket-fill")
self.model.outputs["output"] = output
self.output_download_element.variant = "success"
self.output_download_element.disabled = False
output_element = js.document.querySelector('#compress_stringify-outputs sl-textarea[name=output]')
output_element.value = str(np.frombuffer(output[:200], dtype=np.uint8)) + ' ...'

except Exception as error:
js.globalThis.notify("Error while running pipeline", str(error), "danger", "exclamation-octagon")
raise error
finally:
self.run_button.loading = False

compress_stringify_controller = CompressStringifyController(load_sample_inputs)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import js

# load_sample_inputs = None

def load_sample_inputs(model):
sample_input = bytes([222, 173, 190, 239])
model.inputs['input'] = sample_input
input_element = js.document.querySelector('#compress_stringify-inputs sl-input[name=input]')
input_element.value = str(sample_input)

stringify = True
model.options['stringify'] = stringify
stringify_element = js.document.querySelector('#compress_stringify-inputs sl-checkbox[name=stringify]')
stringify_element.checked = stringify

compression_level = 5
model.options['compression_level'] = compression_level
compression_level_element = js.document.querySelector('#compress_stringify-inputs sl-input[name=compression-level]')
compression_level_element.value = compression_level

data_url_prefix = 'data:application/iwi+cbor+zstd;base64,'
model.options['data_url_prefix'] = data_url_prefix
data_url_prefix_element = js.document.querySelector('#compress_stringify-inputs sl-input[name=data-url-prefix]')
data_url_prefix_element.value = data_url_prefix

return model
108 changes: 108 additions & 0 deletions packages/compress-stringify/python-web-demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="./style.css" />

<link
rel="stylesheet"
media="(prefers-color-scheme:light)"
href="https://cdn.jsdelivr.net/npm/@shoelace-style/[email protected]/cdn/themes/light.css"
/>
<link
rel="stylesheet"
media="(prefers-color-scheme:dark)"
href="https://cdn.jsdelivr.net/npm/@shoelace-style/[email protected]/cdn/themes/dark.css"
onload="document.documentElement.classList.add('sl-theme-dark');"
/>
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/[email protected]/cdn/shoelace-autoloader.js"></script>

<script src="https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.js"></script>

<title>itkwasm-compress-stringify<img src="./python-logo.svg" alt="Python logo" class="language-logo"/></title>
</head>
<body>

<script src="./utilities.js"></script>

<!-- https://tholman.com/github-corners/ -->
<a href="https://github.com/InsightSoftwareConsortium/itk-wasm" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>

<div id="app">
<a href="https://wasm.itk.org/" target="_blank">
<img src="./logo.svg" class="logo vanilla" alt="ITK Wasm logo" />
</a>

<h2>itkwasm-compress-stringify<img src="./python-logo.svg" alt="Python logo" class="language-logo"/></h2>
<i>Zstandard compression and decompression and base64 encoding and decoding in WebAssembly.</i>
<br /><br />

<sl-tab-group>
<sl-tab slot="nav" panel="compress_stringify-panel">compress_stringify</sl-tab>
<sl-tab slot="nav" panel="parse_string_decompress-panel">parse_string_decompress</sl-tab>


<sl-tab-panel name="compress_stringify-panel">

<small><i>Given a binary, compress and optionally base64 encode.</i></small><br /><br />

<div id="compress_stringify-inputs"><form action="">
<sl-input required name="input" type="text" label="input" help-text="Input binary" disabled></sl-input>
<label for="input-file"><sl-button required variant="primary" outline onclick="this.parentElement.nextElementSibling.click()">Upload</sl-button></label><input type="file" name="input-file" style="display: none"/>
<br /><br />
<sl-checkbox name="stringify">stringify - <i>Stringify the output</i></sl-checkbox>
<br /><br />
<sl-input name="compression-level" type="number" value="3" label="compression_level" help-text="Compression level, typically 1-9"></sl-input>
<br />
<sl-input name="data-url-prefix" type="text" label="data_url_prefix" help-text="dataURL prefix"></sl-input>
<sl-divider></sl-divider>
<br /><sl-button name="load-sample-inputs" variant="default" style="display: none;">Load sample inputs</sl-button>
<sl-button type="button" variant="success" name="run">Run</sl-button><br /><br />

</form></div>
<sl-divider></sl-divider>

<div id="compress_stringify-outputs">
<sl-textarea name="output" label="output" help-text="Output compressed binary"><sl-skeleton effect="none"></sl-skeleton></sl-textarea>
<sl-button variant="neutral" outline name="output-download" disabled>Download</sl-button>
<br /><br />
</div>

</sl-tab-panel>


<sl-tab-panel name="parse_string_decompress-panel">

<small><i>Given a binary or string produced with compress-stringify, decompress and optionally base64 decode.</i></small><br /><br />

<div id="parse_string_decompress-inputs"><form action="">
<sl-input required name="input" type="text" label="input" help-text="Compressed input" disabled></sl-input>
<label for="input-file"><sl-button required variant="primary" outline onclick="this.parentElement.nextElementSibling.click()">Upload</sl-button></label><input type="file" name="input-file" style="display: none"/>
<br /><br />
<sl-checkbox name="parse-string">parse_string - <i>Parse the input string before decompression</i></sl-checkbox>
<br /><br />
<sl-divider></sl-divider>
<br /><sl-button name="load-sample-inputs" variant="default" style="display: none;">Load sample inputs</sl-button>
<sl-button type="submit" variant="success" name="run">Run</sl-button><br /><br />

</form></div>
<sl-divider></sl-divider>

<div id="parse_string_decompress-outputs">
<sl-textarea name="output" label="output" help-text="Output decompressed binary"><sl-skeleton effect="none"></sl-skeleton></sl-textarea>
<sl-button variant="neutral" outline name="output-download" disabled>Download</sl-button>
<br /><br />
</div>

</sl-tab-panel>


</sl-tab-group>
</div>

<script type="module" src="./index.js"></script>

</body>
</html>
39 changes: 39 additions & 0 deletions packages/compress-stringify/python-web-demo/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Generated file. To retain edits, remove this comment.

function basename(path) {
return path.replace(/.*\//, '');
}

async function savePythonScript(pyodide, scriptPath) {
await pyodide.runPythonAsync(`
from pyodide.http import pyfetch
from pathlib import Path
response = await pyfetch("${scriptPath}")
with open(Path("${scriptPath}").name, "wb") as f:
f.write(await response.bytes())
`)
pyodide.pyimport(basename(scriptPath).replace('.py', ''))
}

async function main(){
globalThis.disableInputs('compress_stringify-inputs')
globalThis.disableInputs('parse_string_decompress-inputs')

const pyodide = await loadPyodide()
await pyodide.loadPackage("numpy")
await pyodide.loadPackage("micropip")
const micropip = pyodide.pyimport("micropip")
await micropip.install("itkwasm-compress-stringify")

await savePythonScript(pyodide, './compress_stringify_load_sample_inputs.py')
await pyodide.runPythonAsync(await (await fetch("./compress_stringify.py")).text())
await savePythonScript(pyodide, './parse_string_decompress_load_sample_inputs.py')
await pyodide.runPythonAsync(await (await fetch("./parse_string_decompress.py")).text())

globalThis.enableInputs('compress_stringify-inputs')
globalThis.enableInputs('parse_string_decompress-inputs')

globalThis.pyodide = pyodide
return pyodide
}
const pyodideReady = main()
1 change: 1 addition & 0 deletions packages/compress-stringify/python-web-demo/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 1d48471

Please sign in to comment.