diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index c322152a..19657001 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -9,6 +9,7 @@
"version": "2.4.0",
"license": "BSD-3-Clause",
"dependencies": {
+ "@bioimagetools/capability-manifest": "^0.1.0",
"@material-tailwind/react": "^3.0.0-beta.24",
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-table": "^8.21.3",
@@ -398,6 +399,15 @@
"node": ">=18"
}
},
+ "node_modules/@bioimagetools/capability-manifest": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@bioimagetools/capability-manifest/-/capability-manifest-0.1.0.tgz",
+ "integrity": "sha512-FvVN2J3BCCx1fsgL1aFpDnpoNSEH5MeZ3FkGOaOX+4p1cnpMl71nAERI4SOXM00UAZxuj/cDpiR81OHuc2r1jQ==",
+ "license": "ISC",
+ "dependencies": {
+ "js-yaml": "^4.1.1"
+ }
+ },
"node_modules/@emnapi/core": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
@@ -2437,7 +2447,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true,
"license": "Python-2.0"
},
"node_modules/aria-query": {
@@ -5863,7 +5872,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
diff --git a/frontend/package.json b/frontend/package.json
index 12a3c0ba..608b9bfe 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -30,6 +30,7 @@
"test": "vitest"
},
"dependencies": {
+ "@bioimagetools/capability-manifest": "^0.1.0",
"@material-tailwind/react": "^3.0.0-beta.24",
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-table": "^8.21.3",
diff --git a/frontend/src/hooks/useZarrMetadata.ts b/frontend/src/hooks/useZarrMetadata.ts
index 3df57288..5188d8a4 100644
--- a/frontend/src/hooks/useZarrMetadata.ts
+++ b/frontend/src/hooks/useZarrMetadata.ts
@@ -17,6 +17,61 @@ import {
} from '@/omezarr-helper';
import { buildUrl } from '@/utils';
import * as zarr from 'zarrita';
+import {
+ getCompatibleViewers,
+ type OmeZarrMetadata
+} from '@bioimagetools/capability-manifest';
+
+/**
+ * Convert Fileglancer's internal Metadata to the library's OmeZarrMetadata format
+ * Only converts if this is a valid OME-Zarr dataset (has version)
+ */
+function convertToOmeZarrMetadata(
+ metadata: ZarrMetadata
+): OmeZarrMetadata | null {
+ if (!metadata?.multiscale) {
+ return null;
+ }
+
+ // If version is null/undefined, this is not a proper OME-Zarr dataset
+ // Return null to fall back to legacy logic
+ const version = metadata.multiscale.version;
+ if (!version) {
+ return null;
+ }
+
+ // Convert axes from multiscale to the expected format, ensuring no null values
+ const axes =
+ metadata.multiscale.axes?.map((axis: any) => ({
+ name: axis.name,
+ type: axis.type ?? undefined,
+ unit: axis.unit ?? undefined
+ })) || [];
+
+ // Build the OmeZarrMetadata object
+ const omeZarrMetadata: OmeZarrMetadata = {
+ version: version as '0.4' | '0.5',
+ axes,
+ multiscales: [
+ {
+ ...metadata.multiscale,
+ version,
+ axes
+ }
+ ]
+ };
+
+ // Add optional fields if they exist
+ if (metadata.omero) {
+ omeZarrMetadata.omero = metadata.omero;
+ }
+
+ if (metadata.labels && metadata.labels.length > 0) {
+ omeZarrMetadata.labels = metadata.labels;
+ }
+
+ return omeZarrMetadata;
+}
export type { OpenWithToolUrls, ZarrMetadata };
export type PendingToolKey = keyof OpenWithToolUrls | null;
@@ -102,62 +157,93 @@ export default function useZarrMetadata() {
copy: url || ''
} as OpenWithToolUrls;
- // Determine which tools should be available based on metadata type
- if (metadata?.multiscale) {
- // OME-Zarr - all urls for v2; no avivator for v3
+ // Convert metadata to OmeZarrMetadata format and get compatible viewers
+ const omeZarrMetadata = convertToOmeZarrMetadata(metadata);
+ let compatibleViewers: string[] = [];
+
+ if (omeZarrMetadata) {
+ try {
+ compatibleViewers = getCompatibleViewers(omeZarrMetadata);
+ log.debug('Compatible viewers from library:', compatibleViewers);
+ } catch (error) {
+ log.error('Error getting compatible viewers:', error);
+ // Fall back to assuming it's OME-Zarr if we have multiscale
+ compatibleViewers = metadata?.multiscale ? ['Neuroglancer'] : [];
+ }
+ }
+
+ // Determine which tools should be available based on compatible viewers
+ const isOmeZarr = omeZarrMetadata !== null;
+
+ if (isOmeZarr) {
+ // OME-Zarr dataset
+ const hasNeuroglancer = compatibleViewers.includes('Neuroglancer');
+ const hasAvivator =
+ compatibleViewers.includes('Vizarr') ||
+ compatibleViewers.includes('Avivator');
+
if (url) {
- if (effectiveZarrVersion === 2) {
+ // Avivator/Vizarr - only for v2 and if compatible
+ if (effectiveZarrVersion === 2 && hasAvivator) {
openWithToolUrls.avivator = buildUrl(avivatorBaseUrl, null, {
image_url: url
});
} else {
openWithToolUrls.avivator = null;
}
- // Populate with actual URLs when proxied path is available
+
+ // Validator - always available for OME-Zarr
openWithToolUrls.validator = buildUrl(validatorBaseUrl, null, {
source: url
});
+
+ // Vol-E - keep as-is for now (not in library)
openWithToolUrls.vole = buildUrl(voleBaseUrl, null, {
url
});
- if (disableNeuroglancerStateGeneration) {
- openWithToolUrls.neuroglancer =
- neuroglancerBaseUrl +
- generateNeuroglancerStateForDataURL(url, effectiveZarrVersion);
- } else if (layerType) {
- try {
- openWithToolUrls.neuroglancer =
- neuroglancerBaseUrl +
- generateNeuroglancerStateForOmeZarr(
- url,
- effectiveZarrVersion,
- layerType,
- metadata.multiscale,
- metadata.arr,
- metadata.labels,
- metadata.omero,
- useLegacyMultichannelApproach
- );
- } catch (error) {
- log.error(
- 'Error generating Neuroglancer state for OME-Zarr:',
- error
- );
+
+ // Neuroglancer - if compatible
+ if (hasNeuroglancer) {
+ if (disableNeuroglancerStateGeneration) {
openWithToolUrls.neuroglancer =
neuroglancerBaseUrl +
generateNeuroglancerStateForDataURL(url, effectiveZarrVersion);
+ } else if (layerType && metadata.multiscale) {
+ try {
+ openWithToolUrls.neuroglancer =
+ neuroglancerBaseUrl +
+ generateNeuroglancerStateForOmeZarr(
+ url,
+ effectiveZarrVersion,
+ layerType,
+ metadata.multiscale,
+ metadata.arr,
+ metadata.labels,
+ metadata.omero,
+ useLegacyMultichannelApproach
+ );
+ } catch (error) {
+ log.error(
+ 'Error generating Neuroglancer state for OME-Zarr:',
+ error
+ );
+ openWithToolUrls.neuroglancer =
+ neuroglancerBaseUrl +
+ generateNeuroglancerStateForDataURL(url, effectiveZarrVersion);
+ }
+ } else {
+ openWithToolUrls.neuroglancer = '';
}
+ } else {
+ openWithToolUrls.neuroglancer = '';
}
} else {
- // No proxied URL - show all tools as available but empty
+ // No proxied URL - show compatible tools as available but empty
openWithToolUrls.validator = '';
openWithToolUrls.vole = '';
- // if this is a zarr version 2, then set the url to blank which will show
- // the icon before a data link has been generated. Setting it to null for
- // all other versions, eg zarr v3 means the icon will not be present before
- // a data link is generated.
- openWithToolUrls.avivator = effectiveZarrVersion === 2 ? '' : null;
- openWithToolUrls.neuroglancer = '';
+ openWithToolUrls.avivator =
+ effectiveZarrVersion === 2 && hasAvivator ? '' : null;
+ openWithToolUrls.neuroglancer = hasNeuroglancer ? '' : '';
}
} else {
// Non-OME Zarr - only Neuroglancer available
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 831d2026..2791c5fb 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -1,15 +1,33 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { QueryClientProvider } from '@tanstack/react-query';
+import { initializeViewerManifests } from '@bioimagetools/capability-manifest';
import './index.css';
import App from './App';
import { queryClient } from './queryClient';
-createRoot(document.getElementById('root')!).render(
-
-
-
-
-
-);
+// Expose initialization function for tests
+// This allows Playwright tests to call initializeViewerManifests via page.evaluate()
+// since it needs to run in the browser context (uses fetch API), not Node.js context
+declare global {
+ interface Window {
+ initializeViewerManifests?: typeof initializeViewerManifests;
+ }
+}
+
+window.initializeViewerManifests = initializeViewerManifests;
+
+const startApp = async () => {
+ await initializeViewerManifests();
+
+ createRoot(document.getElementById('root')!).render(
+
+
+
+
+
+ );
+};
+
+void startApp();
diff --git a/frontend/ui-tests/fixtures/fileglancer-fixture.ts b/frontend/ui-tests/fixtures/fileglancer-fixture.ts
index d2e783d9..f0cccdbe 100644
--- a/frontend/ui-tests/fixtures/fileglancer-fixture.ts
+++ b/frontend/ui-tests/fixtures/fileglancer-fixture.ts
@@ -22,6 +22,19 @@ const openFileglancer = async (page: Page) => {
// Wait for the app to be ready
await page.waitForSelector('text=Log In', { timeout: 10000 });
+ // Initialize viewer manifests before login
+ // We call this via page.evaluate() because initializeViewerManifests uses browser APIs
+ // (fetch) and must run in the browser context, not the Node.js context where Playwright runs
+ await page.evaluate(async () => {
+ // @ts-ignore - accessing window-scoped function from main.tsx
+ if (window.initializeViewerManifests) {
+ await window.initializeViewerManifests();
+ console.log('[Test] Viewer manifests initialized');
+ } else {
+ console.warn('[Test] initializeViewerManifests not found on window');
+ }
+ });
+
// Perform login
const loginForm = page.getByRole('textbox', { name: 'Username' });
const loginSubmitBtn = page.getByRole('button', { name: 'Log In' });