Remove ImJoy as it is not maintained

This commit is contained in:
Julian Stirling 2025-07-06 18:57:40 +01:00
parent a0065931e4
commit 3d91a6ae3a
12 changed files with 26 additions and 1478 deletions

View file

@ -40,25 +40,6 @@
<hr v-if="item.divide" :key="'tab-divider-' + index" />
</template>
<tabIcon
v-for="imjoyTab in imjoyTabs"
:key="imjoyTab.id"
:tab-i-d="'ImJoy-Plugin-' + imjoyTab.id"
:title="imjoyTab.name"
:require-connection="false"
:current-tab="currentTab"
@set-tab="setTab"
>
<img
v-if="imjoyTab.iconURL"
style="filter: grayscale(100%);width: 22px;margin-top: 5px;margin-bottom: 8px;"
:src="imjoyTab.iconURL"
/>
<span v-if="!imjoyTab.iconURL" class="material-symbols-outlined">
{{ imjoyTab.iconName || "extension" }}
</span>
</tabIcon>
<hr id="extension-tab-divider" />
<!-- For each bottom tab -->
@ -98,15 +79,6 @@
<component :is="item.component"></component>
</tabContent>
<tabContent
v-for="imjoyTab in imjoyTabs"
:key="imjoyTab.id"
:tab-i-d="'ImJoy-Plugin-' + imjoyTab.id"
:require-connection="false"
:current-tab="currentTab"
>
<div :id="imjoyTab.window_id" class="window-container">Loading...</div>
</tabContent>
<!-- For each bottom tab -->
<tabContent
@ -124,7 +96,6 @@
</template>
<script>
import { mapState } from "vuex";
// Import generic components
import tabIcon from "./genericComponents/tabIcon";
@ -139,11 +110,6 @@ import settingsContent from "./tabContentComponents/settingsContent.vue";
import aboutContent from "./tabContentComponents/aboutContent.vue";
import loggingContent from "./tabContentComponents/loggingContent.vue";
import powerContent from "./tabContentComponents/powerContent.vue";
// ImJoy is loaded asynchronously to allow it to be disabled if needed
const imjoyContent = () =>
import(
/* webpackChunkName: "imjoy" */ "./tabContentComponents/imjoyContent.vue"
);
// Import modal components for device initialisation
import calibrationModal from "./modalComponents/calibrationModal.vue";
@ -166,7 +132,6 @@ export default {
loggingContent,
TabIcon,
powerContent,
imjoyContent
},
data: function() {
return {
@ -240,24 +205,12 @@ export default {
if (!this.$store.state.galleryEnabled) {
tabs = tabs.filter(tab => tab.id != "gallery");
}
if (this.$store.state.imjoyEnabled) {
tabs.push({
id: "imjoy",
iconURL: "https://imjoy.io/static/img/imjoy-icon.svg",
component: imjoyContent,
divide: true
});
}
return tabs;
},
currentTabIndex: function() {
return this.tabOrder.indexOf(this.currentTab);
},
// Map the tabs from ImJoy's store module so we can display them
...mapState("imjoy", { imjoyTabs: "tabs" }),
...mapState({ imjoyEnabled: "imjoyEnabled" })
}
},
mounted() {

View file

@ -1,62 +0,0 @@
<template>
<div
class="uk-card uk-card-hover uk-card-small uk-card-default uk-margin"
:class="{ 'uk-card-secondary': $store.state.darkMode }"
>
<div class="uk-card-body">
<div class="uk-grid-small uk-flex-middle" uk-grid>
<div class="uk-width-auto">
<img
v-if="iconURL"
style="height:50px"
:data-src="iconURL"
:alt="name + ' icon'"
uk-img
/>
<span
v-if="!iconURL"
class="material-symbols-outlined"
style="font-size:50px"
>
{{ iconName }}
</span>
</div>
<div class="uk-width-expand">
<h3 class="uk-card-title uk-margin-remove-bottom">{{ name }}</h3>
<p class="uk-text-meta uk-margin-remove-top">
<slot name="subtitle"></slot>
</p>
</div>
<div class="uk-width-auto">
<button class="uk-button" @click="$emit('click')">
<slot name="buttonText">Launch</slot>
</button>
</div>
</div>
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: "ImjoyPluginCard",
props: {
name: {
type: String,
required: true
},
iconURL: {
type: String,
required: false,
default: () => ""
},
iconName: {
type: String,
required: false,
default: () => "extension"
}
}
};
</script>

View file

@ -1,644 +0,0 @@
<template>
<!-- Grid managing tab content -->
<div class="uk-height-1-1 view-component">
<progress
ref="imjoyProgressbar"
style="height: 4px; margin-bottom:0!important"
class="uk-progress"
max="100"
></progress>
<div
class="uk-flex uk-flex-column uk-margin uk-margin-left uk-margin-right"
>
<imjoy-plugin-card
name="Load From URL"
icon-u-r-l="https://imjoy.io/static/img/imjoy-icon.svg"
@click="loadPluginDialog()"
>
<template v-slot:buttonText>Open</template>
</imjoy-plugin-card>
<imjoy-plugin-card
name="ImageJ.JS"
icon-u-r-l="https://ij.imjoy.io/assets/icons/chrome/chrome-installprocess-128-128.png"
@click="openImageJ()"
/>
<imjoy-plugin-card
name="Kaibu"
icon-u-r-l="https://kaibu.org/static/img/kaibu-icon.svg"
@click="startKaibu()"
/>
<imjoy-plugin-card
v-for="(p, name) in loadedPlugins"
:key="p.id"
:name="name"
icon-name="extension"
@click="runPlugin(p)"
/>
</div>
<div v-show="loading" class="progress uk-margin-small">
<div class="indeterminate"></div>
</div>
</div>
</template>
<script>
import * as imjoyCore from "imjoy-core";
import axios from "axios";
import UIkit from "uikit";
import { mapState } from "vuex";
import ImjoyPluginCard from "./imjoyComponents/imjoyPluginCard.vue";
// TODO: move this to a module or something...
async function pollAction(response) {
// Poll an action until its status is completed
// TODO: raise an error if it fails or is cancelled
// For ease, this accepts and returns a response object from
// axios.get
// FIXME: this could be an infinite loop if the task is cancelled/errored
let r = response;
while ((r.data.status == "running") | (r.data.status == "pending")) {
await new Promise(resolve => setTimeout(resolve, 100));
r = await axios.get(r.data.href);
}
return r;
}
/**
* Create a new Image element and wait for it to load.
*
* This returns a Promise, so you can "await" the image
* element rather than needing to add a callback.
*/
function loadImageFromUrl(src) {
// Create an Image, and return it once it has loaded
// see https://stackoverflow.com/a/66180709
// no need to declare async, as it returns a Promise
return new Promise((resolve, reject) => {
const img = new Image();
// We must set crossOrigin="anonymous" in order
// to avoid security errors when we get the pixel
// data
img.crossOrigin = "anonymous";
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
}
/**
* Retrieve an image from a URL, and return the RGB data
* in a format that can be cast to an ndarray
*
* This follows a slightly convoluted recipe, where we
* "draw" the image, and then extract the pixels from the
* 2D canvas.
*/
async function imageUrlToNdarray(url) {
// see https://stackoverflow.com/questions/934012/get-image-data-url-in-javascript
const img = await loadImageFromUrl(url);
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, img.width, img.height);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
const rgbaData = imageData.data; // This gets the RGBA values as an ArrayBuffer
// convert RGBA to RGB
const rgbData = new Uint8Array(new ArrayBuffer(img.height * img.width * 3));
for (let i = 0; i < img.height; i++) {
for (let j = 0; j < img.width; j++) {
const pos = i * img.width + j;
rgbData[pos * 3] = rgbaData[pos * 4];
rgbData[pos * 3 + 1] = rgbaData[pos * 4 + 1];
rgbData[pos * 3 + 2] = rgbaData[pos * 4 + 2];
}
}
return {
_rtype: "ndarray",
_rdtype: "uint8",
_rshape: [img.height, img.width, 3],
_rvalue: rgbData.buffer
};
}
export default {
name: "ImJoyContent",
components: { ImjoyPluginCard },
data() {
return {
loading: false,
loadedPlugins: {},
viewers: {}
};
},
computed: {
captureActionUri: function() {
return `${this.$store.getters.baseUri}/api/v2/actions/camera/capture`;
},
snapshotStreamUri: function() {
return `${this.$store.getters.baseUri}/api/v2/streams/snapshot`;
},
getPositionUri: function() {
return `${this.$store.getters.baseUri}/api/v2/instrument/state/stage/position`;
},
moveActionUri: function() {
return `${this.$store.getters.baseUri}/api/v2/actions/stage/move`;
},
baseUri: function() {
return this.$store.getters.baseUri;
},
...mapState("imjoy", { windows: "tabs" })
},
mounted() {
const self = this;
const imjoy = new imjoyCore.ImJoy({
// We will load the ImJoy plugin base frame via http (instead of https)
// such that imjoy plugins can access insecured urls
// however, some browser features won't work (e.g. webrtc)
// unless we explicitly set `base_frame` in the plugin to
// https://lib.imjoy.io/default_base_frame.html
default_base_frame: "http://lib.imjoy.io/default_base_frame.html",
imjoy_api: {
async showDialog(plugin, config, exta_config) {
return await imjoy.pm.createWindow(plugin, config, exta_config);
},
async showSnackbar(plugin, msg, duration) {
duration = duration || 5;
UIkit.notification.closeAll();
UIkit.notification({
message: msg.slice(0, 100),
status: "primary",
pos: "bottom-center",
timeout: duration * 1000
});
},
async showMessage(plugin, msg) {
imjoy.imjoy_api.showSnackbar(plugin, msg, 5);
},
async showStatus(plugin, status) {
imjoy.imjoy_api.showSnackbar(plugin, status, 5);
},
async showProgress(plugin, p) {
p = p || 0;
if (p < 1.0) p = p * 100;
if (p > 100) p = 100;
if (p) {
self.$refs.imjoyProgressbar.style.display = "block";
self.$refs.imjoyProgressbar.value = parseInt(p);
} else {
self.$refs.imjoyProgressbar.style.display = "none";
}
}
}
});
imjoy.start({ workspace: "default" }).then(async () => {
console.log("ImJoy started");
imjoy.event_bus.on("add_window", w => {
this.addWindow(w);
});
this.setupMicroscopeAPI();
this.setupViewers();
const origin = window.location.origin;
const localPlugins = [
"OpenFlexureSnapImageTemplate",
"OpenFlexureScriptEditor",
"OpenFlexureTestMoveStage",
"ListServices"
];
for (let fname of localPlugins) {
await this.loadPlugin(`${origin}/${fname}.imjoy.html`);
}
this.setupPluginEngine();
});
this.imjoy = imjoy;
},
methods: {
setupPluginEngine() {
// setup a plugin engine for Python plugins
this.imjoy.pm
.reloadPluginRecursively({
uri:
"https://imjoy-team.github.io/jupyter-engine-manager/Jupyter-Engine-Manager.imjoy.html"
})
.then(enginePlugin => {
// TODO: currently the url query are not preserved for ImJoy
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const engine = urlParams.get("engine");
const spec = urlParams.get("spec");
if (engine) {
enginePlugin.api
.createEngine({
name: "MyCustomEngine",
nbUrl: engine,
url: engine.split("?")[0]
})
.then(() => {
console.log("Jupyter Engine connected!");
})
.catch(e => {
console.error("Failed to connect to Jupyter Engine", e);
});
} else {
enginePlugin.api
.createEngine({
name: "MyBinder Engine",
url: "https://mybinder.org",
spec: spec || "oeway/imjoy-binder-image/master"
})
.then(() => {
console.log("Binder Engine connected!");
})
.catch(e => {
console.error("Failed to connect to MyBinder Engine", e);
});
}
});
},
async setupMicroscopeAPI() {
// Here we register a set of API for controlling the microscope as a service
// For interoperatability, it might be good to reuse a subset of the function names defined in MicroManager
// See here: https://valelab4.ucsf.edu/~MM/doc/MMCore/html/class_c_m_m_core.html
const snapshotUri = this.snapshotStreamUri;
const captureActionUri = this.captureActionUri;
const modalError = this.modalError;
const getPositionAsObject = this.getStagePosition;
const setPositionAsObject = this.setStagePosition;
const getInstrumentSettings = this.getInstrumentSettings;
const setInstrumentSettings = this.setInstrumentSettings;
const runAutofocus = this.runAutofocus;
const service = {
_rintf: true, // this will make sure the function can be called multiple times
type: "#microscope-control",
name: "OpenFlexure",
lastSnapResponse: null,
snapPreviewImage() {
// The "proper" capture method is more involved - this one pulls a frame out of the preview stream
return axios
.get(snapshotUri)
.then(response => {
return response.data;
})
.catch(modalError);
},
snapImage() {
// Do capture
return axios.post(captureActionUri, {}).then(response => {
service.lastSnapResponse = response;
// TODO: send the events to flash the stream and update captures
// Flash the stream (capture animation)
//this.$root.$emit("globalFlashStream");
// Update the global capture list
//this.$root.$emit("globalUpdateCaptures");
});
//.catch(modalError);
},
async getImage() {
// FIXME: handle nicely getImage() being called before snapImage()
// Wait until the capture has completed and we can retrieve it
service.lastSnapResponse = await pollAction(service.lastSnapResponse);
let capture = service.lastSnapResponse.data.output;
// TODO: check links.download.href exists and is JPEG
let jpeg = await axios
.get(capture.links.download.href, {
responseType: "arraybuffer",
headers: {
"Content-Type": "image/jpeg"
}
})
.then(r => r.data);
console.log(
"Downloaded ",
jpeg.byteLength,
" bytes of JPEG image data"
);
return jpeg; //TODO: what is the expected output format???
},
setPosition(z) {
// The C api suggests this defaults to just Z, and can accept a "label" to
// select the stage.
return setPositionAsObject({
z: z,
absolute: true
});
},
async getPosition() {
const { z } = await getPositionAsObject();
return z;
},
async setXYPosition(x, y) {
return setPositionAsObject({
x: x,
y: y,
absolute: true
});
},
async getXYPosition() {
const { x, y } = await getPositionAsObject();
return [x, y];
},
async getXYZPosition() {
const { x, y, z } = await getPositionAsObject();
return [x, y, z];
},
async setXYZPosition(x, y, z) {
return setPositionAsObject({
x: x,
y: y,
z: z,
absolute: true
});
},
async getExposure() {
const settings = await getInstrumentSettings();
if (settings.camera.picamera) {
if (settings.camera.picamera.shutter_speed) {
return settings.camera.picamera.shutter_speed / 1000;
}
}
return -1;
},
async setExposure(exposure) {
setInstrumentSettings({
camera: {
picamera: {
shutter_speed: exposure * 1000
}
}
});
},
async fullFocus() {
return runAutofocus();
}
};
const ref = await this.imjoy.pm.registerService(
{ type: "#microscope-control", name: "OpenFlexure" },
service
);
console.log("registered service");
console.log(ref);
},
closeWindow(w) {
if (w) {
w.api.close();
this.$store.commit("imjoy/removeTab", w);
}
},
/**
* Activate a window - you can pass the window object, its name, or its id.
*/
selectWindow(window) {
for (let w of this.windows) {
if ((w === window) | (w.name === window) | (w.id === window)) {
// Make the relevant window visible by switching to its tab
this.$root.$emit("globalSwitchTab", `ImJoy-Plugin-${w.id}`);
break;
}
}
},
setWindowIconUrl(window, iconURL) {
this.$store.commit("imjoy/setTabProperty", {
tab: window,
key: "iconURL",
value: iconURL
});
},
async startKaibu() {
this.viewers.kaibu = await this.imjoy.pm.createWindow(null, {
name: "Kaibu",
src: "https://kaibu.org/#/app"
});
this.viewers.kaibu.on("close", () => {
delete this.viewers.kaibu;
});
this.setWindowIconUrl(
this.viewers.kaibu.config.id,
"https://kaibu.org/static/img/kaibu-icon.svg"
);
this.viewers.kaibu.set_mode("full");
},
async startImageJ() {
this.viewers.imagej = await this.imjoy.pm.createWindow(null, {
name: "ImageJ.JS",
src: "https://ij.imjoy.io"
});
this.viewers.imagej.on("close", () => {
delete this.viewers.imagej;
});
this.setWindowIconUrl(
this.viewers.imagej.config.id,
"https://ij.imjoy.io/assets/icons/chrome/chrome-installprocess-128-128.png"
);
// load the ITK/VTK viewer for imagej.js
this.loadPlugin(
"https://gist.githubusercontent.com/oeway/e5c980fbf6582f25fde795262a7e33ec/raw/itk-vtk-viewer-imagej.imjoy.html"
);
},
/**
* Switch to the ImageJ window, starting it if necessary.
*/
async openImageJ() {
if (!this.viewers.imagej) {
await this.startImageJ();
}
this.selectWindow("ImageJ.JS");
return this.viewers.imagej;
},
/**
* Switch to the Kaibu window, starting it if necessary.
*/
async openKaibu() {
if (!this.viewers.kaibu) {
await this.startKaibu();
}
this.selectWindow("Kaibu"); // TODO: make this behave more nicely when we have multiple Kaibu windows
return this.viewers.kaibu;
},
async setupViewers() {
this.loading = true;
// TODO: improve this using the ImJoy `api.getServices`
// See here: https://imjoy.io/docs/#/api?id=apigetservices
// The idea is that plugins can register custom services via `api.registerService`
// For example, we can have a image visualization service
// Then other plugins can use `api.getServices` to get a list of services
// And this can be used for render our "Open In ImJoy" menu
try {
const self = this;
this.$store.commit("imjoy/addOpenImageItem", {
title: "Kaibu",
async callback(name, imageUrl) {
const viewer = await self.openKaibu();
await viewer.view_image(imageUrl, { type: "2d-image", name });
await viewer.add_shapes([]);
}
});
this.$store.commit("imjoy/addOpenImageItem", {
title: "ImageJ.JS",
async callback(name, imageUrl) {
const viewer = await self.openImageJ();
const response = await fetch(imageUrl);
const buffer = await response.arrayBuffer();
await viewer.viewImage(buffer, {
name: name.replace(".jpeg", ".jpg")
});
}
});
this.$store.commit("imjoy/addOpenImageItem", {
title: "ImageJ.JS [ndarray]",
async callback(name, imageUrl) {
const viewer = await self.openImageJ();
const ndarray = await imageUrlToNdarray(imageUrl);
await viewer.viewImage(ndarray, {
name: name.replace(".jpeg", "")
});
}
});
this.$store.commit("imjoy/addOpenScanItem", {
title: "ImageJ.JS [stack]",
async callback(name, allURLs) {
const viewer = await self.openImageJ();
// allURLs is a list of URLs for *capture objects*, so
// first we need to retrieve them, then extract image URLs
// and finally turn image URLs into "numpy arrays" (RGB data).
const imagesAsNdarrays = await Promise.all(
allURLs.map(async url => {
const r = await axios.get(url);
return await imageUrlToNdarray(r.data.links.download.href);
})
);
// Construct an array to hold all the images, based on the first image
// TODO: assert that _rdtype is uint8, _rshape is the same, _rtype is ndarray
const shape = imagesAsNdarrays[0]._rshape;
const totalSize = imagesAsNdarrays.reduce(
(total, image) => total + image._rvalue.byteLength,
0
);
const stackData = new Uint8Array(new ArrayBuffer(totalSize));
const transferredBytes = imagesAsNdarrays.reduce((total, image) => {
console.log(`inserting image at ${total} bytes`);
// TODO: is it bad to populate stackData as a "side effect"?
stackData.set(new Uint8Array(image._rvalue), total);
//assert image._rshape == shape;
return total + image._rvalue.byteLength;
}, 0);
const stackNdarray = {
_rtype: "ndarray",
_rdtype: "uint8",
_rshape: [imagesAsNdarrays.length, shape[0], shape[1], shape[2]],
_rvalue: stackData.buffer
};
console.log(
`Stack is ${transferredBytes} and will have shape: ${stackNdarray._rshape}`
);
await viewer.viewImage(stackNdarray, {
name
});
}
});
} catch (e) {
alert(`Failed to setup viewers, error: ${e}`);
} finally {
this.loading = false;
}
},
async addWindow(w) {
// Here we can add check w.standalone if we want to allow the plugin to decide whether to open in a new tab.
this.$store.commit("imjoy/addTab", w);
await this.$nextTick();
//this.$forceUpdate();
this.selectWindow(w);
},
loadPlugin(uri) {
return new Promise((resolve, reject) => {
this.loading = true;
this.imjoy.pm
.reloadPluginRecursively({
uri
})
.then(async plugin => {
this.loadedPlugins[plugin.name] = plugin;
this.loading = false;
resolve(plugin);
})
.catch(e => {
console.error(e);
this.loading = false;
reject(e);
alert(`failed to load the plugin, error: ${e}`);
});
});
},
async runPlugin(plugin) {
await plugin.api.run();
},
loadPluginDialog() {
const uri = prompt(
"Paste the ImJoy plugin url here",
"imjoy-team/imjoy-plugins:Welcome"
);
if (uri) {
this.loadPlugin(uri);
}
},
async getStagePosition() {
return axios.get(this.getPositionUri).then(r => r.data);
},
async setStagePosition(payload) {
return axios.post(this.moveActionUri, payload).then(pollAction);
},
async getInstrumentSettings() {
return axios
.get(`${this.baseUri}/api/v2/instrument/settings/`)
.then(r => r.data);
},
async setInstrumentSettings(payload) {
return axios.put(`${this.baseUri}/api/v2/instrument/settings/`, payload);
},
runAutofocus() {
// This mechanism is also used by the keybinding for autofocus.
// It means we don't have a way to poll, but that's not a problem.
this.$root.$emit("globalFastAutofocusEvent");
}
}
};
</script>
<style scoped>
.window-container {
width: 100%;
height: 100%;
}
.imjoy-container-card {
width: 100%;
height: 100%;
}
.window-title {
height: 26px;
text-align: center;
font-size: 1.2rem;
background-color: #efefef;
color: #482727;
font-weight: 500;
}
.plugins-button {
position: absolute;
right: 1px;
height: 26px;
line-height: 11px;
}
.close-button {
position: absolute;
right: 100px;
height: 26px;
}
</style>