Skip to content
Open
3 changes: 3 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ Configuration

$ omero config append omero.web.open_with '["web_zarr_validator", "omero_web_zarr_index", {"supported_objects":["image"], "label": "NGFF validator", "script_url": "omero_web_zarr/openwith_validator.js"}]'

# To enable web-based import of public OME-Zarr images (requires `omero-zarr-pixel-buffer` on the server). Open via Dataset > Open with > "Import OME-Zarr"

$ omero config append omero.web.open_with '["web_zarr_import", "omero_web_zarr_import", {"supported_objects":["dataset"], "label": "Import OME-Zarr"}]'

Then you will be able to access OMERO Images in OME-NGFF format v0.3 or v0.4 with a URLs like::

Expand Down
358 changes: 358 additions & 0 deletions omero_web_zarr/templates/omero_web_zarr/zarr_import.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,358 @@
<html>
<head>
<title>OMERO Zarr Import</title>

<style>
body {
font-family: Arial, Helvetica, sans-serif;
}
h1 {
text-align: center;
}
#forms_wrapper {
width: fit-content;
margin: auto;
position: relative;
border: solid lightgrey 1px;
border-radius: 10px;
padding: 15px;
box-shadow: 10px 10px 20px rgba(212, 239, 253, 0.5);
display: flex;
flex-direction: column;
gap: 20px;
}
.thumbnail {
width: 64px;
height: 64px;
object-fit: contain;
}
input[name="url"] {
width: 600px;
width: 600px;
border: solid #ccc 1px;
padding: 10px;
font-size: 1.0em;
border-radius: 5px;
}
form {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 10px;
padding: 5px;
margin: 0;
}
.omero_thumb, .zarr_thumb {
position: relative;
}
/* Hide thumbnails initially */
.omero_thumb img, .zarr_thumb img {
visibility: hidden;
border-radius: 5px;
}
button[type="submit"] {
padding: 10px;
border-radius: 5px;
border: 1px solid darkgreen;
font-size: 1.0em;
background: darkgreen;
color: white;
}
button[type="submit"]:disabled {
opacity: 0.6;
}


@keyframes spinner {
to {
transform: rotate(360deg);
}
}

.spinner {
position: relative;
}
.failed {
position: relative;
width: 64px;
height: 64px;
display: inline-block
}
.failed img {
top: 0;
left: 0;
position: absolute;
}
.failed div {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 3em;
color: darkgrey;
}
.spinner img, .failed img {
background-color: #ddd;
width: 64px;
height: 64px;
}
.spinner:after {
content: "";
box-sizing: border-box;
position: absolute;
top: 50%;
left: 50%;
width: 40px;
height: 40px;
margin-top: -20px;
margin-left: -20px;
border-radius: 50%;
border: 3px solid rgba(180, 180, 180, 0.6);
border-top-color: rgba(0, 0, 0, 0.6);
animation: spinner 1s linear infinite;
}

.status {
position: absolute;
bottom: 0;
font-size: 0.9em;
color: #999;
visibility: hidden;
width: 500px;
}
.zarr_thumb .status {
left: calc(100% + 11px);
}
.omero_thumb .status {
right: calc(100% + 11px);
text-align: right;
}

#add_row {
font-size: 1.3em;
border-radius: 15px;
border: solid grey 1px;
background: darkgreen;
color: white;
position: absolute;
top: calc(100% + 15px);
left: 15px;
}
</style>
</head>
<body>
<h1>Import OME-Zarr</h1>
<p style="text-align: center">
Import OME-Zarr data from a public URL
{% if dataset_name %} into Dataset: <code>{{ dataset_name }}</code> {% endif %} <br>
e.g. https://storage.googleapis.com/jax-public-ngff/example_v2/LacZ_ctrl.zarr
</p>

<div id="forms_wrapper">
<button id="add_row" title="Add another import">+</button>
<form method="post" action="{% url 'omero_web_zarr_import' %}">
{% csrf_token %}
<input type="hidden" name="dataset" value="{{ dataset_id }}" />
<!-- Add spinner class to show spinner -->
<div class="zarr_thumb">
<img class="thumbnail" src="" alt="Zarr Thumbnail" />
<div class="status">Zarr Preview</div>
</div>
<input
type="text"
name="url"
required
value=""
placeholder="Enter public OME-Zarr URL"
autofocus
/>
<button type="submit" disabled>Import</button>
<div class="omero_thumb">
<img class="thumbnail" src="" alt="OMERO Thumbnail" />
<div class="status">Importing...</div>
</div>
</form>
</div>
</body>

<script type="module">
import * as omezarr from "https://cdn.jsdelivr.net/npm/ome-zarr.js@latest/+esm";

const WEBCLIENT_URL = "{% url 'webindex' %}";

// ALL events are delegated to formsWrapper so we don't need to add event listeners to each form
const formsWrapper = document.getElementById("forms_wrapper");

function loadZarrThumbnail(url, formElement, retrying = false) {
setState(formElement, LOADING_ZARR);
omezarr.renderThumbnail(url).then((src) => {
formElement.querySelector(".zarr_thumb img").src = src;
// if src is valid, we enable submit
if (src) {
setState(formElement, VALID_ZARR, src);
} else {
// Does this ever happen?
setState(formElement, INVALID_ZARR);
}
}).catch((error) => {
console.error("Error:", error);
if (error.message.indexOf("Cannot read properties of undefined (reading '0')") > -1 && !retrying) {
// multiscales not found in zarr.json - Might be bioformats2raw.layout
console.log("try /0/...");
loadZarrThumbnail(url + "0/", formElement, true);
} else if (error.message.indexOf("Lowest resolution") > -1) {
// handle e.g. Lowest resolution (1054 * 1328) is too large for Thumbnail. Limit is 1000 * 1000
setState(formElement, VALID_ZARR, error.message);
} else {
// e.g. NodeNotFoundError: Node not found: v3 array or group
let msg = error.message;
if (retrying) {
msg = "No OME-Zarr multiscales found";
} else if (msg.indexOf("Node not found") > -1) {
msg = "Not a valid Zarr URL";
}
setState(formElement, INVALID_ZARR, msg);
}
});
}

formsWrapper.addEventListener("input", function (event) {
// Check if the source element input...
if (event.target && event.target.nodeName === "INPUT") {
let url = event.target.value;
if (!url.endsWith("/")) {
url = url + "/";
}
const formElement = event.target.closest("form");
loadZarrThumbnail(url, formElement);
}
});

// Handle form submission...
formsWrapper.addEventListener("submit", function (event) {
if (event.target && event.target.nodeName === "FORM") {
event.preventDefault();
const formElement = event.target;

// submit form data by POST
const formData = new FormData(formElement);
fetch(event.target.action, {
method: "POST",
body: formData,
headers: {
"X-CSRFToken": formData.get("csrfmiddlewaretoken"),
},
})
.then((response) => response.json())
.then((data) => {
console.log("Success:", data);
setState(formElement, LOADING_THUMBNAIL);
// only expect a single image...
const image_ids = data.images.map((img) => img.id);
const src = `${WEBCLIENT_URL}render_thumbnail/${image_ids[0]}/`;
// load OMERO thumbnail...
formElement.querySelector(".omero_thumb img").onload = () => {
// when thumbnail loads, show it, hide spinner
setState(formElement, DONE, image_ids);
};
formElement.querySelector(".omero_thumb img").src = src;
})
.catch((error) => {
console.error("Error:", error);
setState(formElement, IMPORT_ERROR);
});
setState(formElement, IMPORTING);
}
});

// Clone the form before any changes, so we duplicate without changes
const firstForm = formsWrapper.querySelector("form");
const originalForm = firstForm.cloneNode(true);

document.getElementById("add_row").addEventListener("click", function () {
const newForm = originalForm.cloneNode(true);
formsWrapper.appendChild(newForm);
});

// states
const LOADING_ZARR = "LOADING_ZARR";
const INVALID_ZARR = "INVALID_ZARR";
const VALID_ZARR = "VALID_ZARR";
const IMPORTING = "IMPORTING";
const IMPORT_ERROR = "IMPORT_ERROR";
const LOADING_THUMBNAIL = "LOADING_THUMBNAIL";
const DONE = "DONE";

function setState(formElement, state, data) {
const $zarr = formElement.querySelector(".zarr_thumb");
const $zarrStatus = $zarr.querySelector(".status");
const $zarrImg = $zarr.querySelector("img");
const $urlInput = formElement.querySelector("input[name='url']");
const $omero = formElement.querySelector(".omero_thumb");
const $omeroStatus = $omero.querySelector(".status");
const $omeroImg = $omero.querySelector("img");
const $submit = formElement.querySelector("button[type='submit']");

let message = "";
switch (state) {
case LOADING_ZARR:
$zarr.classList.add("spinner");
$zarr.style.background = "transparent";
$zarrImg.style.visibility = "hidden";
$zarrStatus.style.visibility = "visible";
$zarrStatus.innerText = "Checking URL";
break;
case INVALID_ZARR:
$zarr.classList.remove("spinner");
$zarr.style.background = "transparent";
$submit.disabled = true;
if (data) {
message = data;
} else {
message = "Invalid Zarr";
}
$zarrStatus.innerText = message;
break;
case VALID_ZARR:
$zarr.classList.remove("spinner");
$zarrStatus.innerText = "URL valid";
$zarr.style.background = "transparent";
if (data && data.startsWith("data:")) {
// data is the thumbnail src
$zarrImg.src = data;
$zarrImg.style.visibility = "visible";
} else if (data) {
$zarr.style.background = "#ddd";
$zarr.title = data;
}
$submit.disabled = false;
break;
case IMPORTING:
$submit.disabled = true;
$omero.classList.add("spinner");
$omeroStatus.innerText = "Importing";
$omeroStatus.style.visibility = "visible";
$urlInput.disabled = true; // disable after submit or url won't get submitted!
break;
case IMPORT_ERROR:
$omero.classList.remove("spinner");
$omeroStatus.innerText = "Import Error";
break;
case LOADING_THUMBNAIL:
$omeroStatus.innerText = "Loading OMERO thumbnail";
break;
case DONE:
$omero.classList.remove("spinner");
$omeroImg.style.visibility = "visible";
let iids = data;
if (iids && iids.length) {
let view = iids.length > 1 ? `${iids.length} Images` : `Image: ${iids[0]}`;
let link = "image-" + iids.join("|image-");
$omeroStatus.innerHTML = `View <a target="_blank" href="${WEBCLIENT_URL}?show=${link}">${view}</a>`;
}

break;
}
}
</script>
</html>
2 changes: 2 additions & 0 deletions omero_web_zarr/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
re_path(r'^v(?P<version>0\.[3-4]+)/image/(?P<iid>[0-9]+).zarr/(?P<level>[0-9]+)/(?P<chunk>[0-9/]+)$', # noqa
views.image_chunk, name='zarr_image_chunk'),

re_path(r'^import/$', views.zarr_import, name="omero_web_zarr_import"),

# Delegate all /vizarr/ or /validator/ urls to statically-hosted files
re_path(r'^(?P<app>vizarr|validator)/(?P<url>.*)$', views.apps, name='zarr_app'), # noqa

Expand Down
Loading