From 5c849a90e66a07d2d0f518c23f7d0207108f78c2 Mon Sep 17 00:00:00 2001 From: Allison Truhlar Date: Thu, 9 Apr 2026 13:31:38 +0000 Subject: [PATCH 01/12] feat: add canonical viewer manifests directory Add manifests/ directory with canonical capability manifests for Neuroglancer, Avivator, OME-Zarr Validator, and Vol-E. These can be loaded by URL so non-Fileglancer consumers can access them. Also fix neuroglancer omero_metadata to true in public/viewers/. --- manifests/avivator.yaml | 41 ++++++++++++++++++++++++++++++++ manifests/neuroglancer.yaml | 41 ++++++++++++++++++++++++++++++++ manifests/validator.yaml | 41 ++++++++++++++++++++++++++++++++ manifests/vole.yaml | 41 ++++++++++++++++++++++++++++++++ public/viewers/neuroglancer.yaml | 2 +- 5 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 manifests/avivator.yaml create mode 100644 manifests/neuroglancer.yaml create mode 100644 manifests/validator.yaml create mode 100644 manifests/vole.yaml diff --git a/manifests/avivator.yaml b/manifests/avivator.yaml new file mode 100644 index 0000000..3098d39 --- /dev/null +++ b/manifests/avivator.yaml @@ -0,0 +1,41 @@ +viewer: + name: "Avivator" + version: "0.16.1" + repo: "https://github.com/hms-dbmi/viv" + template_url: "https://avivator.gehlenborglab.org/?image_url={DATA_URL}" + +capabilities: + # Enumeration of OME-Zarr versions that can be loaded + ome_zarr_versions: [0.4] + + compression_codecs: ["blosc", "gzip"] + + # Which additional RFCs are supported? + rfcs_supported: [] + + # Are axis names and units respected? + axes: true + + # Are scaling factors respected on multiscales? + scale: true + + # Are translation factors respected on multiscales (including subpixel offsets for lower scale levels)? + translation: true + + # Does the tool support multiple channels? + channels: true + + # Does the tool support multiple timepoints? + timepoints: true + + # Are labels loaded when available? + labels: false + + # Are HCS plates loaded when available? + hcs_plates: false + + # Does the viewer handle multiple images in a bioformats2raw layout? + bioformats2raw_layout: false + + # Is the OMERO metadata used to e.g. color the channels? + omero_metadata: true diff --git a/manifests/neuroglancer.yaml b/manifests/neuroglancer.yaml new file mode 100644 index 0000000..4ccebd2 --- /dev/null +++ b/manifests/neuroglancer.yaml @@ -0,0 +1,41 @@ +viewer: + name: "Neuroglancer" + version: "2.41.2" + repo: "https://github.com/google/neuroglancer" + template_url: https://neuroglancer-demo.appspot.com/#!{"layers":[{"name":"image","source":"{DATA_URL}","type":"image"}]} + +capabilities: + # Enumeration of OME-Zarr versions that can be loaded + ome_zarr_versions: [0.4, 0.5] + + compression_codecs: ["blosc", "zstd", "zlib", "lz4", "gzip"] + + # Which additional RFCs are supported? + rfcs_supported: [] + + # Are axis names and units respected? + axes: true + + # Are scaling factors respected on multiscales? + scale: true + + # Are translation factors respected on multiscales (including subpixel offsets for lower scale levels)? + translation: true + + # Does the tool support multiple channels? + channels: true + + # Does the tool support multiple timepoints? + timepoints: true + + # Are labels loaded when available? + labels: false + + # Are HCS plates loaded when available? + hcs_plates: false + + # Does the viewer handle multiple images in a bioformats2raw layout? + bioformats2raw_layout: false + + # Is the OMERO metadata used to e.g. color the channels? + omero_metadata: true diff --git a/manifests/validator.yaml b/manifests/validator.yaml new file mode 100644 index 0000000..85e479c --- /dev/null +++ b/manifests/validator.yaml @@ -0,0 +1,41 @@ +viewer: + name: "OME-Zarr Validator" + version: "1.0.0" + repo: "https://github.com/ome/ome-ngff-validator" + template_url: "https://ome.github.io/ome-ngff-validator/?source={DATA_URL}" + +capabilities: + # Enumeration of OME-Zarr versions that can be loaded + ome_zarr_versions: [0.4, 0.5] + + compression_codecs: [] + + # Which additional RFCs are supported? + rfcs_supported: [] + + # Are axis names and units respected? + axes: true + + # Are scaling factors respected on multiscales? + scale: true + + # Are translation factors respected on multiscales (including subpixel offsets for lower scale levels)? + translation: true + + # Does the tool support multiple channels? + channels: true + + # Does the tool support multiple timepoints? + timepoints: true + + # Are labels loaded when available? + labels: true + + # Are HCS plates loaded when available? + hcs_plates: true + + # Does the viewer handle multiple images in a bioformats2raw layout? + bioformats2raw_layout: false + + # Is the OMERO metadata used to e.g. color the channels? + omero_metadata: true diff --git a/manifests/vole.yaml b/manifests/vole.yaml new file mode 100644 index 0000000..d0fbf0c --- /dev/null +++ b/manifests/vole.yaml @@ -0,0 +1,41 @@ +viewer: + name: "Vol-E" + version: "1.0.0" + repo: "https://github.com/allen-cell-animated/volume-viewer" + template_url: "https://volumeviewer.allencell.org/viewer?url={DATA_URL}" + +capabilities: + # Enumeration of OME-Zarr versions that can be loaded + ome_zarr_versions: [0.4, 0.5] + + compression_codecs: [] + + # Which additional RFCs are supported? + rfcs_supported: [] + + # Are axis names and units respected? + axes: true + + # Are scaling factors respected on multiscales? + scale: true + + # Are translation factors respected on multiscales (including subpixel offsets for lower scale levels)? + translation: true + + # Does the tool support multiple channels? + channels: true + + # Does the tool support multiple timepoints? + timepoints: true + + # Are labels loaded when available? + labels: false + + # Are HCS plates loaded when available? + hcs_plates: false + + # Does the viewer handle multiple images in a bioformats2raw layout? + bioformats2raw_layout: false + + # Is the OMERO metadata used to e.g. color the channels? + omero_metadata: false diff --git a/public/viewers/neuroglancer.yaml b/public/viewers/neuroglancer.yaml index 9b395a8..4ccebd2 100644 --- a/public/viewers/neuroglancer.yaml +++ b/public/viewers/neuroglancer.yaml @@ -38,4 +38,4 @@ capabilities: bioformats2raw_layout: false # Is the OMERO metadata used to e.g. color the channels? - omero_metadata: false + omero_metadata: true From 6aebf5d060f64d147c8092122cbc130391cef6b2 Mon Sep 17 00:00:00 2001 From: Allison Truhlar Date: Thu, 9 Apr 2026 13:31:45 +0000 Subject: [PATCH 02/12] docs: document manifest schema and validation behavior Expand README with: - Canonical manifests table linking to manifests/ directory - Full viewer section schema (name, version, repo, template_url) - Full capabilities section schema with all fields documented - Table showing how validateViewer() maps each capability check to errors vs warnings --- README.md | 113 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 103 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4324c47..5346edf 100644 --- a/README.md +++ b/README.md @@ -92,16 +92,109 @@ The library exports TypeScript types for all data structures: - `ValidationError`, `ValidationWarning` - Detailed validation messages - `AxisMetadata`, `MultiscaleMetadata` - Nested metadata types -## Manifest Specification (DRAFT) - -| Attribute | Description | -| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| ome_zarr_versions | List of OME-NGFF versions which are supported by the tool. When a Zarr group with multiscales metadata containing a version listed here is given to the tool, the tool promises to do something useful. However, it may not support every feature of the specification. | -| rfcs_supported | List of supported RFC numbers which have been implemented on top of the released OME-NGFF versions listed in ome_zarr_versions. Given test data produced for a given RFC listed here, the tool promises to do something useful. However, it may not support every feature of the RFC. | -| bioformats2raw_layout | A tool that advertises support for this will be able to open a Zarr that implements this transitional layout. | -| omero_metadata | A tool that advertises support for this will be able to open a Zarr that implements this transitional metadata, for example by defaulting channel colors with the provided color values. | -| labels | A tool that advertises support will open pixel-annotation metadata found in the "labels" group. | -| hcs_plates | A tool that advertises support will open high content screening datasets found in the "plate" group. | +## Canonical Manifests + +This repository hosts canonical capability manifests for well-known OME-Zarr viewers in the [`manifests/`](manifests/) directory: + +| Manifest | Viewer | OME-Zarr Versions | +| --- | --- | --- | +| [neuroglancer.yaml](manifests/neuroglancer.yaml) | Neuroglancer | 0.4, 0.5 | +| [avivator.yaml](manifests/avivator.yaml) | Avivator (Viv) | 0.4 | +| [validator.yaml](manifests/validator.yaml) | OME-Zarr Validator | 0.4, 0.5 | +| [vole.yaml](manifests/vole.yaml) | Vol-E | 0.4, 0.5 | + +Consumers can load these manifests by URL directly from GitHub (raw content URLs) or host copies on their own infrastructure. + +Viewer developers are encouraged to maintain their own manifests and submit PRs to update the canonical versions here when capabilities change. + +## Manifest Schema (DRAFT) + +A capability manifest is a YAML file with two top-level sections: `viewer` and `capabilities`. + +### `viewer` Section + +Identifies the tool and provides a URL template for launching it. + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | string | yes | Human-readable name of the viewer | +| `version` | string | yes | Version of the viewer these capabilities describe | +| `repo` | string | no | URL of the source code repository | +| `template_url` | string | no | URL template for opening a dataset. Use `{DATA_URL}` as a placeholder for the dataset URL — consumers replace it at runtime with the actual OME-Zarr location | + +Example: + +```yaml +viewer: + name: "Neuroglancer" + version: "2.41.2" + repo: "https://github.com/google/neuroglancer" + template_url: https://neuroglancer-demo.appspot.com/#!{"layers":[{"name":"image","source":"{DATA_URL}","type":"image"}]} +``` + +### `capabilities` Section + +Describes which OME-Zarr features the tool supports. All fields are optional — omitting a field means the capability is unknown/undeclared. + +| Field | Type | Description | +| --- | --- | --- | +| `ome_zarr_versions` | number[] | OME-NGFF specification versions the tool can load (e.g. `[0.4, 0.5]`). When a dataset's multiscales metadata contains a version listed here, the tool should be able to open it. | +| `compression_codecs` | string[] | Compression codecs the tool can decode (e.g. `["blosc", "zstd", "gzip"]`). An empty array `[]` means the tool does not declare codec support — compatibility is unknown rather than unsupported. | +| `rfcs_supported` | number[] | RFC numbers implemented on top of the released OME-NGFF versions. Given test data for a listed RFC, the tool should handle it. | +| `axes` | boolean | Whether axis names and units from the metadata are respected | +| `scale` | boolean | Whether scaling factors on multiscale datasets are respected | +| `translation` | boolean | Whether translation offsets (including subpixel offsets for lower scale levels) are respected | +| `channels` | boolean | Whether the tool supports datasets with multiple channels (c axis) | +| `timepoints` | boolean | Whether the tool supports datasets with multiple timepoints (t axis) | +| `labels` | boolean | Whether pixel-annotation metadata in the "labels" group is loaded | +| `hcs_plates` | boolean | Whether high content screening datasets in the "plate" group are loaded | +| `bioformats2raw_layout` | boolean | Whether the tool can open Zarr stores using the bioformats2raw transitional layout | +| `omero_metadata` | boolean | Whether the tool uses OMERO metadata (e.g. to set default channel colors) | + +Example: + +```yaml +capabilities: + ome_zarr_versions: [0.4, 0.5] + compression_codecs: ["blosc", "zstd", "gzip"] + rfcs_supported: [] + axes: true + scale: true + translation: true + channels: true + timepoints: true + labels: false + hcs_plates: false + bioformats2raw_layout: false + omero_metadata: true +``` + +### How `validateViewer()` Uses the Manifest + +The `validateViewer()` function checks a manifest's declared capabilities against a dataset's `OmeZarrMetadata` and returns a `ValidationResult`: + +```typescript +interface ValidationResult { + compatible: boolean; // true if no errors (viewer can open the data) + errors: ValidationError[]; // hard failures — data will not load + warnings: ValidationWarning[]; // soft issues — data loads but features may be missing +} +``` + +The checks performed, in order: + +| Check | Metadata field | Manifest field | Result if mismatch | +| --- | --- | --- | --- | +| OME-Zarr version | `version` or `multiscales[0].version` | `ome_zarr_versions` | **Error** — viewer cannot load this version | +| Compression codec | `compressor.id` | `compression_codecs` | **Error** if codec not listed; **Warning** if viewer declares no codecs (unknown support) | +| Axes metadata | `axes` | `axes` | **Warning** — axis names/units may be ignored | +| Channel support | `axes` contains c/channel | `channels` | **Error** — multi-channel data won't render | +| Timepoint support | `axes` contains t/time | `timepoints` | **Error** — time-series data won't render | +| Labels | `labels` array non-empty | `labels` | **Warning** — labels won't be displayed | +| HCS plates | `plate` present | `hcs_plates` | **Error** — plate data won't load | +| OMERO metadata | `omero` present | `omero_metadata` | **Warning** — channel colors etc. won't be applied | + +A viewer is considered **compatible** (`compatible: true`) when there are zero errors. Warnings indicate features that may be missing but don't prevent the data from loading. ## Prototype From 05940322dc09ceaf1854cba8f66f687a8fa39a96 Mon Sep 17 00:00:00 2001 From: Allison Truhlar Date: Tue, 14 Apr 2026 18:38:21 +0000 Subject: [PATCH 03/12] feat: distinguish data compatibility from data feature support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces `compatible: boolean` on `ValidationResult` with two separate fields: `dataCompatible` (hard gate — viewer is shown or hidden) and `dataFeaturesSupported` (soft gate — warnings are shown). Reclassifies `channels`, `timepoints`, and `hcs_plates` from hard errors to soft warnings, since viewers that don't support these features can still open the data. Fixes `getCompatibleViewersWithDetails()` to call `validateViewer()` once per manifest instead of twice. Fixes version comparison to use string equality via `.map(String)` instead of `parseFloat`, avoiding future breakage with versions like "0.10". --- src/app.ts | 2 +- src/index.test.ts | 33 ++++++++++--- src/index.ts | 7 +-- src/types.ts | 66 ++++++++++++++++---------- src/validator.test.ts | 107 +++++++++++++++++++++++------------------- src/validator.ts | 102 +++++++++++++++++++++------------------- 6 files changed, 183 insertions(+), 134 deletions(-) diff --git a/src/app.ts b/src/app.ts index e2b643b..9f72fec 100644 --- a/src/app.ts +++ b/src/app.ts @@ -193,7 +193,7 @@ function createLaunchButton( button.textContent = 'Launch'; button.target = '_blank'; - if (validation && !validation.compatible) { + if (validation && !validation.dataCompatible) { button.classList.add('disabled'); button.setAttribute('aria-disabled', 'true'); diff --git a/src/index.test.ts b/src/index.test.ts index 840917b..837cb38 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -44,9 +44,28 @@ describe('getCompatibleViewers', () => { expect(result).toEqual(['ViewerA', 'ViewerB']); }); - it('filters out incompatible viewers', () => { + it('filters out incompatible viewers by hard requirements', () => { const manifests = [ - createManifest('FullViewer', { ome_zarr_versions: [0.4, 0.5], channels: true }), + createManifest('FullViewer', { ome_zarr_versions: [0.4, 0.5] }), + createManifest('LimitedViewer', { ome_zarr_versions: [0.4] }) + ]; + const metadata: OmeZarrMetadata = { + version: '0.5', + axes: [ + { name: 'y', type: 'space' }, + { name: 'x', type: 'space' } + ] + }; + + const result = getCompatibleViewers(manifests, metadata); + + expect(result).toContain('FullViewer'); + expect(result).not.toContain('LimitedViewer'); + }); + + it('includes viewers with unsupported features (soft requirements)', () => { + const manifests = [ + createManifest('FullViewer', { ome_zarr_versions: [0.4], channels: true }), createManifest('LimitedViewer', { ome_zarr_versions: [0.4], channels: false }) ]; const metadata: OmeZarrMetadata = { @@ -61,7 +80,7 @@ describe('getCompatibleViewers', () => { const result = getCompatibleViewers(manifests, metadata); expect(result).toContain('FullViewer'); - expect(result).not.toContain('LimitedViewer'); + expect(result).toContain('LimitedViewer'); }); it('returns empty array when no viewers are compatible', () => { @@ -106,7 +125,8 @@ describe('getCompatibleViewersWithDetails', () => { expect(results).toHaveLength(1); expect(results[0].name).toBe('TestViewer'); - expect(results[0].validation).toHaveProperty('compatible'); + expect(results[0].validation).toHaveProperty('dataCompatible'); + expect(results[0].validation).toHaveProperty('dataFeaturesSupported'); expect(results[0].validation).toHaveProperty('errors'); expect(results[0].validation).toHaveProperty('warnings'); }); @@ -122,7 +142,7 @@ describe('getCompatibleViewersWithDetails', () => { expect(results).toHaveLength(1); expect(results[0].name).toBe('Compatible'); - expect(results[0].validation.compatible).toBe(true); + expect(results[0].validation.dataCompatible).toBe(true); expect(results[0].validation.errors).toHaveLength(0); }); @@ -146,7 +166,8 @@ describe('getCompatibleViewersWithDetails', () => { const results = getCompatibleViewersWithDetails(manifests, metadata); expect(results).toHaveLength(1); - expect(results[0].validation.compatible).toBe(true); + expect(results[0].validation.dataCompatible).toBe(true); + expect(results[0].validation.dataFeaturesSupported).toBe(false); expect(results[0].validation.warnings.length).toBeGreaterThan(0); }); diff --git a/src/index.ts b/src/index.ts index 16dc43b..bd7257f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,11 +45,8 @@ export function getCompatibleViewersWithDetails( metadata: OmeZarrMetadata ): Array<{ name: string; validation: ValidationResult }> { return manifests - .filter(viewer => isCompatible(viewer, metadata)) - .map(viewer => ({ - name: viewer.viewer.name, - validation: validateViewer(viewer, metadata) - })); + .map(viewer => ({ name: viewer.viewer.name, validation: validateViewer(viewer, metadata) })) + .filter(({ validation }) => validation.dataCompatible); } // Re-export loader diff --git a/src/types.ts b/src/types.ts index 070915b..c70f1a4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ // Schema types -export interface Schema { +export type Schema = { properties: { viewer: { properties: { @@ -12,7 +12,7 @@ export interface Schema { }; }; }; -} +}; export interface PropertyDefinition { type: string; @@ -28,7 +28,7 @@ export interface CapabilityDefinition extends PropertyDefinition { } // Viewer manifest types -export interface ViewerManifest { +export type ViewerManifest = { viewer: { name: string; version: string; @@ -49,46 +49,60 @@ export interface ViewerManifest { bioformats2raw_layout?: boolean; omero_metadata?: boolean; }; -} +}; // OME-Zarr metadata types -export interface OmeZarrMetadata { - version?: string; - axes?: AxisMetadata[]; - multiscales?: MultiscaleMetadata[]; - omero?: any; - labels?: string[]; - plate?: any; - compressor?: any; -} - -export interface AxisMetadata { +export type AxisMetadata = { name: string; type?: string; unit?: string; -} +}; -export interface MultiscaleMetadata { +export type MultiscaleMetadata = { version?: string; axes?: AxisMetadata[]; - datasets?: any[]; -} + datasets: { path: string }[]; + [key: string]: unknown; +}; + +export type OmeZarrMetadata = { + version?: string; + axes?: AxisMetadata[]; + bioformats2raw_layout?: boolean; + multiscales?: MultiscaleMetadata[]; + omero?: { [key: string]: unknown }; + labels?: string[]; + plate?: { [key: string]: unknown }; + well?: { [key: string]: unknown }; + compressor?: unknown; +}; // Validation types -export interface ValidationResult { - compatible: boolean; +export type ValidationResult = { + /** Can the viewer read and parse this data? Determines whether a viewer is shown. + * Determined by ome_zarr_versions, compression_codecs, and rfcs_supported. + */ + dataCompatible: boolean; + /** + * Does the viewer support all features in this data? Determines whether + * warnings are shown/logged, NOT whether the viewer is shown. A viewer may be + * compatible with a dataset but not support all features + */ + dataFeaturesSupported: boolean; + /** Data compatibility errors (e.g. unsupported version or codec) */ errors: ValidationError[]; + /** Unsupported data feature warnings (e.g. labels, channels not supported) */ warnings: ValidationWarning[]; -} +}; -export interface ValidationError { +export type ValidationError = { capability: string; message: string; required: any; found: any; -} +}; -export interface ValidationWarning { +export type ValidationWarning = { capability: string; message: string; -} +}; diff --git a/src/validator.test.ts b/src/validator.test.ts index b6b996f..3815c73 100644 --- a/src/validator.test.ts +++ b/src/validator.test.ts @@ -46,7 +46,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(true); + expect(result.dataCompatible).toBe(true); expect(result.errors).toHaveLength(0); }); @@ -56,10 +56,10 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(false); + expect(result.dataCompatible).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0].capability).toBe('ome_zarr_versions'); - expect(result.errors[0].required).toBe(0.5); + expect(result.errors[0].required).toBe('0.5'); expect(result.errors[0].found).toEqual([0.4]); }); @@ -72,7 +72,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(true); + expect(result.dataCompatible).toBe(true); expect(result.errors).toHaveLength(0); }); @@ -82,7 +82,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(false); + expect(result.dataCompatible).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0].message).toContain('does not specify OME-Zarr version support'); }); @@ -93,7 +93,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(false); + expect(result.dataCompatible).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0].message).toContain('does not specify OME-Zarr version support'); }); @@ -104,7 +104,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(false); + expect(result.dataCompatible).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0].capability).toBe('ome_zarr_versions'); expect(result.errors[0].message).toContain('Metadata does not specify an OME-Zarr version'); @@ -118,7 +118,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(true); + expect(result.dataCompatible).toBe(true); }); it('returns error when viewer does not support the codec', () => { @@ -127,7 +127,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(false); + expect(result.dataCompatible).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0].capability).toBe('compression_codecs'); expect(result.errors[0].required).toBe('zstd'); @@ -139,7 +139,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(false); + expect(result.dataCompatible).toBe(false); expect(result.errors[0].required).toBe('zstd'); }); @@ -149,7 +149,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(true); + expect(result.dataCompatible).toBe(true); }); it('returns warning when viewer has no codec list but data uses compression', () => { @@ -158,7 +158,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(true); + expect(result.dataCompatible).toBe(true); expect(result.warnings).toHaveLength(1); expect(result.warnings[0].capability).toBe('compression_codecs'); expect(result.warnings[0].message).toContain('zstd'); @@ -171,7 +171,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(true); + expect(result.dataCompatible).toBe(true); expect(result.warnings).toHaveLength(1); expect(result.warnings[0].capability).toBe('compression_codecs'); expect(result.warnings[0].message).toContain('blosc'); @@ -188,7 +188,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(true); + expect(result.dataCompatible).toBe(true); expect(result.warnings).toHaveLength(1); expect(result.warnings[0].capability).toBe('axes'); }); @@ -206,7 +206,7 @@ describe('validateViewer', () => { }); describe('channels support', () => { - it('returns error when data has channels but viewer does not support them', () => { + it('returns warning when data has channels but viewer does not support them', () => { const viewer = createViewer({ channels: false }); const metadata = createMetadata({ axes: [ @@ -218,9 +218,11 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(false); - expect(result.errors).toHaveLength(1); - expect(result.errors[0].capability).toBe('channels'); + expect(result.dataCompatible).toBe(true); + expect(result.dataFeaturesSupported).toBe(false); + expect(result.warnings).toContainEqual( + expect.objectContaining({ capability: 'channels' }) + ); }); it('detects channels by axis name "c"', () => { @@ -231,7 +233,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.errors.some(e => e.capability === 'channels')).toBe(true); + expect(result.warnings.some(w => w.capability === 'channels')).toBe(true); }); it('detects channels by axis type "channel"', () => { @@ -242,7 +244,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.errors.some(e => e.capability === 'channels')).toBe(true); + expect(result.warnings.some(w => w.capability === 'channels')).toBe(true); }); it('returns compatible when viewer supports channels', () => { @@ -253,12 +255,13 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(true); + expect(result.dataCompatible).toBe(true); + expect(result.dataFeaturesSupported).toBe(true); }); }); describe('timepoints support', () => { - it('returns error when data has timepoints but viewer does not support them', () => { + it('returns warning when data has timepoints but viewer does not support them', () => { const viewer = createViewer({ timepoints: false }); const metadata = createMetadata({ axes: [ @@ -270,9 +273,11 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(false); - expect(result.errors).toHaveLength(1); - expect(result.errors[0].capability).toBe('timepoints'); + expect(result.dataCompatible).toBe(true); + expect(result.dataFeaturesSupported).toBe(false); + expect(result.warnings).toContainEqual( + expect.objectContaining({ capability: 'timepoints' }) + ); }); it('detects timepoints by axis name "t"', () => { @@ -283,7 +288,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.errors.some(e => e.capability === 'timepoints')).toBe(true); + expect(result.warnings.some(w => w.capability === 'timepoints')).toBe(true); }); it('detects timepoints by axis type "time"', () => { @@ -294,7 +299,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.errors.some(e => e.capability === 'timepoints')).toBe(true); + expect(result.warnings.some(w => w.capability === 'timepoints')).toBe(true); }); it('returns compatible when viewer supports timepoints', () => { @@ -305,7 +310,8 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(true); + expect(result.dataCompatible).toBe(true); + expect(result.dataFeaturesSupported).toBe(true); }); }); @@ -318,7 +324,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(true); + expect(result.dataCompatible).toBe(true); expect(result.warnings).toContainEqual( expect.objectContaining({ capability: 'labels' }) ); @@ -332,7 +338,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(true); + expect(result.dataCompatible).toBe(true); }); it('skips label check when metadata has empty labels array', () => { @@ -343,7 +349,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(true); + expect(result.dataCompatible).toBe(true); }); it('skips label check when metadata has no labels', () => { @@ -354,12 +360,12 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(true); + expect(result.dataCompatible).toBe(true); }); }); describe('HCS plates support', () => { - it('returns error when data is HCS plate but viewer does not support them', () => { + it('returns warning when data is HCS plate but viewer does not support them', () => { const viewer = createViewer({ hcs_plates: false }); const metadata = createMetadata({ plate: { wells: [], columns: [] } @@ -367,12 +373,14 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(false); - expect(result.errors).toHaveLength(1); - expect(result.errors[0].capability).toBe('hcs_plates'); + expect(result.dataCompatible).toBe(true); + expect(result.dataFeaturesSupported).toBe(false); + expect(result.warnings).toContainEqual( + expect.objectContaining({ capability: 'hcs_plates' }) + ); }); - it('returns compatible when viewer supports HCS plates', () => { + it('returns fully supported when viewer supports HCS plates', () => { const viewer = createViewer({ hcs_plates: true }); const metadata = createMetadata({ plate: { wells: [], columns: [] } @@ -380,7 +388,8 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(true); + expect(result.dataCompatible).toBe(true); + expect(result.dataFeaturesSupported).toBe(true); }); it('skips HCS check when metadata has no plate', () => { @@ -391,7 +400,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(true); + expect(result.dataCompatible).toBe(true); }); }); @@ -404,7 +413,7 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(true); + expect(result.dataCompatible).toBe(true); expect(result.warnings).toHaveLength(1); expect(result.warnings[0].capability).toBe('omero_metadata'); }); @@ -422,7 +431,7 @@ describe('validateViewer', () => { }); describe('multiple validation issues', () => { - it('collects multiple errors', () => { + it('collects errors for hard requirements and warnings for soft requirements', () => { const viewer = createViewer({ ome_zarr_versions: [0.4], channels: false, @@ -440,11 +449,14 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(false); - expect(result.errors).toHaveLength(3); + expect(result.dataCompatible).toBe(false); + expect(result.errors).toHaveLength(1); + expect(result.errors[0].capability).toBe('ome_zarr_versions'); + expect(result.warnings.some(w => w.capability === 'channels')).toBe(true); + expect(result.warnings.some(w => w.capability === 'timepoints')).toBe(true); }); - it('collects both errors and warnings', () => { + it('collects warnings from both feature and metadata checks', () => { const viewer = createViewer({ channels: false, axes: false, @@ -457,9 +469,10 @@ describe('validateViewer', () => { const result = validateViewer(viewer, metadata); - expect(result.compatible).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - expect(result.warnings.length).toBeGreaterThan(0); + expect(result.dataCompatible).toBe(true); + expect(result.dataFeaturesSupported).toBe(false); + expect(result.errors).toHaveLength(0); + expect(result.warnings.length).toBeGreaterThanOrEqual(3); }); }); }); diff --git a/src/validator.ts b/src/validator.ts index ce35cc9..93aaf9a 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -3,8 +3,8 @@ import type { OmeZarrMetadata, ValidationResult, ValidationError, - ValidationWarning -} from './types.js'; + ValidationWarning, +} from "./types.js"; /** * Validates whether a viewer is compatible with a given OME-Zarr dataset. @@ -15,48 +15,50 @@ import type { */ export function validateViewer( viewer: ViewerManifest, - metadata: OmeZarrMetadata + metadata: OmeZarrMetadata, ): ValidationResult { const errors: ValidationError[] = []; const warnings: ValidationWarning[] = []; // Check version compatibility - this is critical - let dataVersion: number | null = null; + let dataVersion: string | null = null; // Try to extract version from metadata (could be in root or in multiscales) if (metadata.version) { - dataVersion = parseFloat(metadata.version); + dataVersion = metadata.version; } else if ( metadata.multiscales && metadata.multiscales.length > 0 && metadata.multiscales[0].version ) { - dataVersion = parseFloat(metadata.multiscales[0].version); + dataVersion = metadata.multiscales[0].version; } if (dataVersion === null) { errors.push({ - capability: 'ome_zarr_versions', - message: 'Metadata does not specify an OME-Zarr version', - required: 'version', - found: null + capability: "ome_zarr_versions", + message: "Metadata does not specify an OME-Zarr version", + required: "version", + found: null, }); } else if ( !viewer.capabilities.ome_zarr_versions || viewer.capabilities.ome_zarr_versions.length === 0 ) { errors.push({ - capability: 'ome_zarr_versions', + capability: "ome_zarr_versions", message: `Viewer does not specify OME-Zarr version support (data is v${dataVersion})`, required: dataVersion, - found: [] + found: [], }); - } else if (!viewer.capabilities.ome_zarr_versions.includes(dataVersion)) { + } else if ( + !viewer.capabilities.ome_zarr_versions.map(String).includes(dataVersion) + ) { errors.push({ - capability: 'ome_zarr_versions', - message: `Viewer does not support OME-Zarr v${dataVersion} (supports: ${viewer.capabilities.ome_zarr_versions.join(', ')})`, + capability: "ome_zarr_versions", + message: `Viewer does not support OME-Zarr v${dataVersion} (supports: ${viewer.capabilities.ome_zarr_versions.join(", ")})`, required: dataVersion, - found: viewer.capabilities.ome_zarr_versions + found: viewer.capabilities.ome_zarr_versions, }); } @@ -69,15 +71,15 @@ export function validateViewer( ) { // Viewer doesn't declare codec support - can't guarantee compatibility warnings.push({ - capability: 'compression_codecs', - message: `Data uses codec '${codec}' but viewer doesn't declare codec support - compatibility unknown` + capability: "compression_codecs", + message: `Data uses codec '${codec}' but viewer doesn't declare codec support - compatibility unknown`, }); } else if (!viewer.capabilities.compression_codecs.includes(codec)) { errors.push({ - capability: 'compression_codecs', + capability: "compression_codecs", message: `Viewer does not support compression codec: ${codec}`, required: codec, - found: viewer.capabilities.compression_codecs + found: viewer.capabilities.compression_codecs, }); } } @@ -85,65 +87,67 @@ export function validateViewer( // Check axes support if (metadata.axes && !viewer.capabilities.axes) { warnings.push({ - capability: 'axes', - message: 'Dataset has axis metadata but viewer may not respect it' + capability: "axes", + message: "Dataset has axis metadata but viewer may not respect it", }); } // Check for multiple channels const hasChannels = metadata.axes?.some( - ax => ax.name === 'c' || ax.type === 'channel' + (ax) => ax.name === "c" || ax.type === "channel", ); if (hasChannels && !viewer.capabilities.channels) { - errors.push({ - capability: 'channels', - message: 'Dataset has multiple channels but viewer does not support them', - required: true, - found: false + warnings.push({ + capability: "channels", + message: "Dataset has multiple channels but viewer may not support them", }); } // Check for timepoints - const hasTime = metadata.axes?.some(ax => ax.name === 't' || ax.type === 'time'); + const hasTime = metadata.axes?.some( + (ax) => ax.name === "t" || ax.type === "time", + ); if (hasTime && !viewer.capabilities.timepoints) { - errors.push({ - capability: 'timepoints', - message: 'Dataset has multiple timepoints but viewer does not support them', - required: true, - found: false + warnings.push({ + capability: "timepoints", + message: + "Dataset has multiple timepoints but viewer may not support them", }); } // Check for labels - if (metadata.labels && metadata.labels.length > 0 && !viewer.capabilities.labels) { + if ( + metadata.labels && + metadata.labels.length > 0 && + !viewer.capabilities.labels + ) { warnings.push({ - capability: 'labels', - message: 'Dataset has labels but viewer may not display them' + capability: "labels", + message: "Dataset has labels but viewer may not display them", }); } // Check for HCS plates if (metadata.plate && !viewer.capabilities.hcs_plates) { - errors.push({ - capability: 'hcs_plates', - message: 'Dataset is an HCS plate but viewer does not support plates', - required: true, - found: false + warnings.push({ + capability: "hcs_plates", + message: "Dataset is an HCS plate but viewer may not support plates", }); } // Check OMERO metadata if (metadata.omero && !viewer.capabilities.omero_metadata) { warnings.push({ - capability: 'omero_metadata', - message: 'Dataset has OMERO metadata but viewer may not use it' + capability: "omero_metadata", + message: "Dataset has OMERO metadata but viewer may not use it", }); } return { - compatible: errors.length === 0, + dataCompatible: errors.length === 0, + dataFeaturesSupported: warnings.length === 0, errors, - warnings + warnings, }; } @@ -153,11 +157,11 @@ export function validateViewer( * * @param viewer - The viewer manifest to check * @param metadata - The OME-Zarr metadata from the dataset - * @returns True if compatible (no errors), false otherwise + * @returns True if data compatible (no errors), false otherwise */ export function isCompatible( viewer: ViewerManifest, - metadata: OmeZarrMetadata + metadata: OmeZarrMetadata, ): boolean { - return validateViewer(viewer, metadata).compatible; + return validateViewer(viewer, metadata).dataCompatible; } From 410eb77afd2299d013ede3a4aafc360cee219cde Mon Sep 17 00:00:00 2001 From: Allison Truhlar Date: Tue, 14 Apr 2026 18:39:30 +0000 Subject: [PATCH 04/12] feat: add validation for scale, translation, and bioformats2raw_layout Adds CoordinateTransformation and DatasetMetadata types modelling the OME-NGFF coordinateTransformations spec (identity, scale, and translation each supporting inline list or path reference). Adds a hasTransformationType() helper and soft-requirement warning checks for scale, translation, and bioformats2raw_layout. Adds a tracked TODO for rfcs_supported explaining the spec-level gap that blocks implementation. --- src/types.ts | 15 ++++++- src/validator.test.ts | 94 +++++++++++++++++++++++++++++++++++++++++++ src/validator.ts | 56 ++++++++++++++++++++++++++ 3 files changed, 164 insertions(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index c70f1a4..69538b5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -58,10 +58,23 @@ export type AxisMetadata = { unit?: string; }; +export type CoordinateTransformation = + | { type: 'identity' } + | { type: 'scale'; scale: number[] } + | { type: 'scale'; path: string } + | { type: 'translation'; translation: number[] } + | { type: 'translation'; path: string }; + +export type DatasetMetadata = { + path: string; + coordinateTransformations?: CoordinateTransformation[]; +}; + export type MultiscaleMetadata = { version?: string; axes?: AxisMetadata[]; - datasets: { path: string }[]; + datasets: DatasetMetadata[]; + coordinateTransformations?: CoordinateTransformation[]; [key: string]: unknown; }; diff --git a/src/validator.test.ts b/src/validator.test.ts index 3815c73..6d41605 100644 --- a/src/validator.test.ts +++ b/src/validator.test.ts @@ -430,6 +430,100 @@ describe('validateViewer', () => { }); }); + describe('bioformats2raw_layout support', () => { + it('returns warning when data uses bioformats2raw layout but viewer does not support it', () => { + const viewer = createViewer({ bioformats2raw_layout: false }); + const metadata = createMetadata({ bioformats2raw_layout: true }); + const result = validateViewer(viewer, metadata); + expect(result.dataCompatible).toBe(true); + expect(result.dataFeaturesSupported).toBe(false); + expect(result.warnings).toHaveLength(1); + expect(result.warnings[0].capability).toBe('bioformats2raw_layout'); + }); + + it('returns no warning when viewer supports bioformats2raw layout', () => { + const viewer = createViewer({ bioformats2raw_layout: true }); + const metadata = createMetadata({ bioformats2raw_layout: true }); + const result = validateViewer(viewer, metadata); + expect(result.dataFeaturesSupported).toBe(true); + expect(result.warnings).toHaveLength(0); + }); + + it('skips bioformats2raw check when metadata does not use it', () => { + const viewer = createViewer({ bioformats2raw_layout: false }); + const metadata = createMetadata({ bioformats2raw_layout: false }); + const result = validateViewer(viewer, metadata); + expect(result.warnings).toHaveLength(0); + }); + }); + + describe('scale support', () => { + const metadataWithScale = createMetadata({ + multiscales: [{ + version: '0.4', + axes: [{ name: 'y', type: 'space' }, { name: 'x', type: 'space' }], + datasets: [{ path: '0', coordinateTransformations: [{ type: 'scale', scale: [0.5, 0.5] }] }] + }] + }); + + it('returns warning when data has scale transforms and viewer does not support them', () => { + const viewer = createViewer({ scale: false }); + const result = validateViewer(viewer, metadataWithScale); + expect(result.dataCompatible).toBe(true); + expect(result.dataFeaturesSupported).toBe(false); + expect(result.warnings[0].capability).toBe('scale'); + }); + + it('no warning when viewer supports scale', () => { + const viewer = createViewer({ scale: true }); + const result = validateViewer(viewer, metadataWithScale); + expect(result.warnings.filter(w => w.capability === 'scale')).toHaveLength(0); + }); + + it('returns warning when viewer does not declare scale (undefined)', () => { + const viewer = createViewer({ scale: undefined }); + const result = validateViewer(viewer, metadataWithScale); + expect(result.warnings.filter(w => w.capability === 'scale')).toHaveLength(1); + }); + }); + + describe('translation support', () => { + const metadataWithTranslation = createMetadata({ + multiscales: [{ + version: '0.4', + axes: [{ name: 'y', type: 'space' }, { name: 'x', type: 'space' }], + datasets: [{ path: '0', coordinateTransformations: [{ type: 'translation', translation: [10, 20] }] }] + }] + }); + + it('returns warning when data has translation transforms and viewer does not support them', () => { + const viewer = createViewer({ translation: false }); + const result = validateViewer(viewer, metadataWithTranslation); + expect(result.dataCompatible).toBe(true); + expect(result.dataFeaturesSupported).toBe(false); + expect(result.warnings[0].capability).toBe('translation'); + }); + + it('no warning when viewer supports translation', () => { + const viewer = createViewer({ translation: true }); + const result = validateViewer(viewer, metadataWithTranslation); + expect(result.warnings.filter(w => w.capability === 'translation')).toHaveLength(0); + }); + + it('no warning when metadata has no translation transforms', () => { + const viewer = createViewer({ translation: false }); + const metadataNoTranslation = createMetadata({ + multiscales: [{ + version: '0.4', + axes: [{ name: 'y', type: 'space' }, { name: 'x', type: 'space' }], + datasets: [{ path: '0', coordinateTransformations: [{ type: 'scale', scale: [1, 1] }] }] + }] + }); + const result = validateViewer(viewer, metadataNoTranslation); + expect(result.warnings.filter(w => w.capability === 'translation')).toHaveLength(0); + }); + }); + describe('multiple validation issues', () => { it('collects errors for hard requirements and warnings for soft requirements', () => { const viewer = createViewer({ diff --git a/src/validator.ts b/src/validator.ts index 93aaf9a..7dce7a4 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -1,11 +1,24 @@ import type { ViewerManifest, OmeZarrMetadata, + MultiscaleMetadata, ValidationResult, ValidationError, ValidationWarning, } from "./types.js"; +function hasTransformationType( + multiscales: MultiscaleMetadata[], + type: "scale" | "translation", +): boolean { + return multiscales.some( + (ms) => + ms.datasets?.some((ds) => + ds.coordinateTransformations?.some((ct) => ct.type === type), + ) || ms.coordinateTransformations?.some((ct) => ct.type === type), + ); +} + /** * Validates whether a viewer is compatible with a given OME-Zarr dataset. * @@ -84,6 +97,11 @@ export function validateViewer( } } + // TODO: Check rfcs_supported (hard compatibility requirement) + // Blocker: OME-NGFF metadata does not expose which RFCs a dataset requires. + // After determing how to implement this check, compare metadata.rfcs_required against + // viewer.capabilities.rfcs_supported and push to errors[] on mismatch. + // Check axes support if (metadata.axes && !viewer.capabilities.axes) { warnings.push({ @@ -92,6 +110,32 @@ export function validateViewer( }); } + // Check support for respecting scaling factors on multiscales + if ( + metadata.multiscales && + hasTransformationType(metadata.multiscales, "scale") && + !viewer.capabilities.scale + ) { + warnings.push({ + capability: "scale", + message: + "Dataset has coordinate scale transformations but viewer may not respect them", + }); + } + + // Check translation support + if ( + metadata.multiscales && + hasTransformationType(metadata.multiscales, "translation") && + !viewer.capabilities.translation + ) { + warnings.push({ + capability: "translation", + message: + "Dataset has coordinate translation offsets but viewer may not respect them", + }); + } + // Check for multiple channels const hasChannels = metadata.axes?.some( (ax) => ax.name === "c" || ax.type === "channel", @@ -135,6 +179,18 @@ export function validateViewer( }); } + // Check bioformats2raw layout support + if ( + metadata.bioformats2raw_layout && + !viewer.capabilities.bioformats2raw_layout + ) { + warnings.push({ + capability: "bioformats2raw_layout", + message: + "Dataset uses bioformats2raw layout but viewer may not support it", + }); + } + // Check OMERO metadata if (metadata.omero && !viewer.capabilities.omero_metadata) { warnings.push({ From be08d9f41d9bdeeb23e174ae9e132cb49cb910a2 Mon Sep 17 00:00:00 2001 From: Allison Truhlar Date: Tue, 14 Apr 2026 18:39:35 +0000 Subject: [PATCH 05/12] docs: update README to reflect two-level compatibility model Documents the dataCompatible/dataFeaturesSupported distinction, updates the checks table with a Level column showing which capabilities are hard compatibility requirements vs soft feature support, adds rows for scale/translation/bioformats2raw_layout, and notes the rfcs_supported spec-level gap. --- README.md | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 5346edf..25c1b01 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Returns whether a single viewer is compatible with the given metadata. #### `validateViewer(viewer: ViewerManifest, metadata: OmeZarrMetadata): ValidationResult` -Returns full validation details (compatible flag, errors, warnings) for a single viewer against the given metadata. +Returns full validation details (`dataCompatible`, `dataFeaturesSupported`, errors, warnings) for a single viewer against the given metadata. ### Types @@ -175,26 +175,37 @@ The `validateViewer()` function checks a manifest's declared capabilities agains ```typescript interface ValidationResult { - compatible: boolean; // true if no errors (viewer can open the data) - errors: ValidationError[]; // hard failures — data will not load + dataCompatible: boolean; // true if no errors (viewer can open the data) + dataFeaturesSupported: boolean; // true if no warnings (viewer fully supports all data features) + errors: ValidationError[]; // hard failures — data will not load warnings: ValidationWarning[]; // soft issues — data loads but features may be missing } ``` +Capabilities fall into two levels: + +- **Data compatibility** (`errors`): Hard requirements. If unmet, the viewer cannot open or render the data at all — it should not be shown. +- **Data support** (`warnings`): Soft requirements. If unmet, the viewer can still open the data but may not display certain features — it should still be shown, with warnings logged or surfaced to the user. + The checks performed, in order: -| Check | Metadata field | Manifest field | Result if mismatch | -| --- | --- | --- | --- | -| OME-Zarr version | `version` or `multiscales[0].version` | `ome_zarr_versions` | **Error** — viewer cannot load this version | -| Compression codec | `compressor.id` | `compression_codecs` | **Error** if codec not listed; **Warning** if viewer declares no codecs (unknown support) | -| Axes metadata | `axes` | `axes` | **Warning** — axis names/units may be ignored | -| Channel support | `axes` contains c/channel | `channels` | **Error** — multi-channel data won't render | -| Timepoint support | `axes` contains t/time | `timepoints` | **Error** — time-series data won't render | -| Labels | `labels` array non-empty | `labels` | **Warning** — labels won't be displayed | -| HCS plates | `plate` present | `hcs_plates` | **Error** — plate data won't load | -| OMERO metadata | `omero` present | `omero_metadata` | **Warning** — channel colors etc. won't be applied | - -A viewer is considered **compatible** (`compatible: true`) when there are zero errors. Warnings indicate features that may be missing but don't prevent the data from loading. +| Check | Metadata field | Manifest field | Level | Result if mismatch | +| --- | --- | --- | --- | --- | +| OME-Zarr version | `version` or `multiscales[0].version` | `ome_zarr_versions` | **Compatibility** | **Error** — viewer cannot load this version | +| Compression codec | `compressor.id` | `compression_codecs` | **Compatibility** | **Error** if codec not listed; **Warning** if viewer declares no codecs (unknown support) | +| Axes metadata | `axes` | `axes` | **Support** | **Warning** — axis names/units may be ignored | +| Channel support | `axes` contains c/channel | `channels` | **Support** | **Warning** — multi-channel data may not render correctly | +| Timepoint support | `axes` contains t/time | `timepoints` | **Support** | **Warning** — time-series data may not render correctly | +| Labels | `labels` array non-empty | `labels` | **Support** | **Warning** — labels won't be displayed | +| HCS plates | `plate` present | `hcs_plates` | **Support** | **Warning** — plate layout won't be shown | +| OMERO metadata | `omero` present | `omero_metadata` | **Support** | **Warning** — channel colors etc. won't be applied | +| Scale transforms | `multiscales[].datasets[].coordinateTransformations` type `scale` | `scale` | **Support** | **Warning** — scaling factors may be ignored | +| Translation offsets | `multiscales[].datasets[].coordinateTransformations` type `translation` | `translation` | **Support** | **Warning** — coordinate offsets may be ignored | +| bioformats2raw layout | `bioformats2raw_layout` | `bioformats2raw_layout` | **Support** | **Warning** — layout may not be traversed correctly | + +> **Note on `rfcs_supported`:** Although `rfcs_supported` is a hard compatibility requirement (it determines whether a viewer can parse RFC-mandated metadata structures), no validation check is currently implemented. OME-NGFF metadata does not yet expose which RFCs a dataset requires — this is a spec-level gap. When the spec defines a `rfcs_required` field, the validator will compare it against `viewer.capabilities.rfcs_supported` and produce an error on mismatch. + +A viewer is considered **data-compatible** (`dataCompatible: true`) when there are zero errors — it should be shown to the user. `dataFeaturesSupported` is `false` when there are warnings, indicating the viewer can open the data but may not display all features. ## Prototype From 9f89434874896031dc6d4815c6ced181cd47e756 Mon Sep 17 00:00:00 2001 From: Allison Truhlar Date: Tue, 14 Apr 2026 19:02:46 +0000 Subject: [PATCH 06/12] fix: handle Zarr v3 codecs array in addition to Zarr v2 compressor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zarr v2 stores compression info as `compressor: { id: string }` in array metadata; Zarr v3 uses `codecs: [{ name: string }]`. The previous check only handled v2 and had a broken fallback (`|| metadata.compressor`) for the case where `.id` was absent. Replaces the single-codec check with a collection step that reads from whichever field is present, then checks each codec individually — emitting one error per unsupported codec and a single warning when the viewer declares no codec support at all. --- src/types.ts | 3 ++- src/validator.test.ts | 52 ++++++++++++++++++++++++++++++++++++++----- src/validator.ts | 33 ++++++++++++++++++--------- 3 files changed, 71 insertions(+), 17 deletions(-) diff --git a/src/types.ts b/src/types.ts index 69538b5..5526012 100644 --- a/src/types.ts +++ b/src/types.ts @@ -87,7 +87,8 @@ export type OmeZarrMetadata = { labels?: string[]; plate?: { [key: string]: unknown }; well?: { [key: string]: unknown }; - compressor?: unknown; + compressor?: { id: string; [key: string]: unknown } | null; // Zarr v2 + codecs?: Array<{ name: string; configuration?: { [key: string]: unknown } }>; // Zarr v3 }; // Validation types diff --git a/src/validator.test.ts b/src/validator.test.ts index 6d41605..232a678 100644 --- a/src/validator.test.ts +++ b/src/validator.test.ts @@ -133,23 +133,65 @@ describe('validateViewer', () => { expect(result.errors[0].required).toBe('zstd'); }); - it('handles compressor as plain string', () => { + it('skips codec check when metadata has no compressor or codecs', () => { const viewer = createViewer({ compression_codecs: ['blosc'] }); - const metadata = createMetadata({ compressor: 'zstd' }); + const metadata = createMetadata({ compressor: undefined }); + + const result = validateViewer(viewer, metadata); + + expect(result.dataCompatible).toBe(true); + }); + + it('returns compatible when viewer supports a Zarr v3 codec', () => { + const viewer = createViewer({ compression_codecs: ['blosc', 'bytes'] }); + const metadata = createMetadata({ + codecs: [{ name: 'bytes' }, { name: 'blosc', configuration: { cname: 'lz4' } }] + }); + + const result = validateViewer(viewer, metadata); + + expect(result.dataCompatible).toBe(true); + }); + + it('returns error when viewer does not support a Zarr v3 codec', () => { + const viewer = createViewer({ compression_codecs: ['bytes'] }); + const metadata = createMetadata({ + codecs: [{ name: 'bytes' }, { name: 'zstd' }] + }); const result = validateViewer(viewer, metadata); expect(result.dataCompatible).toBe(false); + expect(result.errors).toHaveLength(1); + expect(result.errors[0].capability).toBe('compression_codecs'); expect(result.errors[0].required).toBe('zstd'); }); - it('skips codec check when metadata has no compressor', () => { - const viewer = createViewer({ compression_codecs: ['blosc'] }); - const metadata = createMetadata({ compressor: undefined }); + it('returns multiple errors when viewer does not support multiple Zarr v3 codecs', () => { + const viewer = createViewer({ compression_codecs: ['bytes'] }); + const metadata = createMetadata({ + codecs: [{ name: 'blosc' }, { name: 'bytes' }, { name: 'zstd' }] + }); + + const result = validateViewer(viewer, metadata); + + expect(result.dataCompatible).toBe(false); + expect(result.errors).toHaveLength(2); + expect(result.errors.map(e => e.required)).toEqual(expect.arrayContaining(['blosc', 'zstd'])); + }); + + it('returns warning when viewer declares no codecs and data uses Zarr v3 codecs', () => { + const viewer = createViewer({ compression_codecs: undefined }); + const metadata = createMetadata({ + codecs: [{ name: 'bytes' }, { name: 'blosc' }] + }); const result = validateViewer(viewer, metadata); expect(result.dataCompatible).toBe(true); + expect(result.warnings).toHaveLength(1); + expect(result.warnings[0].capability).toBe('compression_codecs'); + expect(result.warnings[0].message).toContain('compatibility unknown'); }); it('returns warning when viewer has no codec list but data uses compression', () => { diff --git a/src/validator.ts b/src/validator.ts index 7dce7a4..0b7cc97 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -75,25 +75,36 @@ export function validateViewer( }); } - // Check compression codecs - if (metadata.compressor) { - const codec = metadata.compressor.id || metadata.compressor; + // Collect codecs from Zarr v2 (compressor.id) or Zarr v3 (codecs[].name) + const dataCodecs: string[] = []; + if (metadata.compressor?.id) { + dataCodecs.push(metadata.compressor.id); + } else if (metadata.codecs && metadata.codecs.length > 0) { + dataCodecs.push(...metadata.codecs.map((c) => c.name)); + } + + if (dataCodecs.length > 0) { if ( !viewer.capabilities.compression_codecs || viewer.capabilities.compression_codecs.length === 0 ) { // Viewer doesn't declare codec support - can't guarantee compatibility + const codecList = dataCodecs.join("', '"); warnings.push({ capability: "compression_codecs", - message: `Data uses codec '${codec}' but viewer doesn't declare codec support - compatibility unknown`, - }); - } else if (!viewer.capabilities.compression_codecs.includes(codec)) { - errors.push({ - capability: "compression_codecs", - message: `Viewer does not support compression codec: ${codec}`, - required: codec, - found: viewer.capabilities.compression_codecs, + message: `Data uses codec '${codecList}' but viewer doesn't declare codec support - compatibility unknown`, }); + } else { + for (const codec of dataCodecs) { + if (!viewer.capabilities.compression_codecs.includes(codec)) { + errors.push({ + capability: "compression_codecs", + message: `Viewer does not support compression codec: ${codec}`, + required: codec, + found: viewer.capabilities.compression_codecs, + }); + } + } } } From 4128c3860f8aa544d537abe1da6e4034d2826ab3 Mon Sep 17 00:00:00 2001 From: Allison Truhlar Date: Tue, 14 Apr 2026 15:04:18 -0400 Subject: [PATCH 07/12] 0.4.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8a8ea2f..ca49687 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bioimagetools/capability-manifest", - "version": "0.3.3", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bioimagetools/capability-manifest", - "version": "0.3.3", + "version": "0.4.0", "license": "ISC", "dependencies": { "js-yaml": "^4.1.1" diff --git a/package.json b/package.json index dfafd51..554d51b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bioimagetools/capability-manifest", - "version": "0.3.3", + "version": "0.4.0", "description": "Library to determine OME-Zarr viewer compatibility based on capability manifests", "type": "module", "main": "./dist/index.js", From 59bef13a7fee14503c2bb14f811d0650c2377560 Mon Sep 17 00:00:00 2001 From: Allison Truhlar Date: Tue, 14 Apr 2026 20:15:48 +0000 Subject: [PATCH 08/12] feat: add logo field to viewer manifest schema --- README.md | 1 + public/schema.json | 6 ++++++ src/types.ts | 1 + 3 files changed, 8 insertions(+) diff --git a/README.md b/README.md index 25c1b01..0f2331a 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ Identifies the tool and provides a URL template for launching it. | `name` | string | yes | Human-readable name of the viewer | | `version` | string | yes | Version of the viewer these capabilities describe | | `repo` | string | no | URL of the source code repository | +| `logo` | string | no | URL to a logo image for the viewer. Optional override — consumers may derive a logo URL by convention (see [Icons](#icons)). Omit if the conventional path applies. | | `template_url` | string | no | URL template for opening a dataset. Use `{DATA_URL}` as a placeholder for the dataset URL — consumers replace it at runtime with the actual OME-Zarr location | Example: diff --git a/public/schema.json b/public/schema.json index 4553d8d..e4efdba 100644 --- a/public/schema.json +++ b/public/schema.json @@ -27,6 +27,12 @@ "description": "URL to the tool's source code repository", "examples": ["https://github.com/google/neuroglancer"] }, + "logo": { + "type": "string", + "format": "uri", + "description": "URL to a logo image for the viewer. Optional override — consumers may derive a logo URL from the viewer name by convention. Provide this field only if the conventional path does not apply.", + "examples": ["https://raw.githubusercontent.com/bioimagetools/capability-manifest/host-manifests-and-docs/public/icons/neuroglancer.png"] + }, "template_url": { "type": "string", "format": "uri", diff --git a/src/types.ts b/src/types.ts index 5526012..516943d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -33,6 +33,7 @@ export type ViewerManifest = { name: string; version: string; repo?: string; + logo?: string; template_url?: string; }; capabilities: { From a2d53e17c7bee3473132d98b0e195218dbbc93d2 Mon Sep 17 00:00:00 2001 From: Allison Truhlar Date: Tue, 14 Apr 2026 20:15:53 +0000 Subject: [PATCH 09/12] feat: add getLogoUrl utility for deriving viewer logo URLs --- src/index.ts | 3 +++ src/logo.test.ts | 50 +++++++++++++++++++++++++++++++++++++++++++++++ src/logo.ts | 27 +++++++++++++++++++++++++ tsconfig.lib.json | 1 + 4 files changed, 81 insertions(+) create mode 100644 src/logo.test.ts create mode 100644 src/logo.ts diff --git a/src/index.ts b/src/index.ts index bd7257f..97630d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -55,6 +55,9 @@ export { loadManifestsFromUrls } from './loader.js'; // Re-export validator functions export { validateViewer, isCompatible } from './validator.js'; +// Re-export logo utility +export { getLogoUrl } from './logo.js'; + // Re-export types for consumers export type { ViewerManifest, diff --git a/src/logo.test.ts b/src/logo.test.ts new file mode 100644 index 0000000..a06e870 --- /dev/null +++ b/src/logo.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest'; +import { getLogoUrl } from './logo.js'; +import type { ViewerManifest } from './types.js'; + +function createManifest( + name: string, + logo?: string +): ViewerManifest { + return { + viewer: { name, version: '1.0.0', ...(logo !== undefined ? { logo } : {}) }, + capabilities: { + ome_zarr_versions: [0.4, 0.5] + } + }; +} + +describe('getLogoUrl', () => { + it('returns the manifest logo field when provided', () => { + const manifest = createManifest('Neuroglancer', 'https://example.com/custom-logo.png'); + expect(getLogoUrl(manifest)).toBe('https://example.com/custom-logo.png'); + }); + + it('derives a URL from the viewer name when no logo field is set', () => { + const manifest = createManifest('Neuroglancer'); + expect(getLogoUrl(manifest)).toBe( + 'https://raw.githubusercontent.com/bioimagetools/capability-manifest/host-manifests-and-docs/public/icons/neuroglancer.png' + ); + }); + + it('replaces spaces with hyphens in the derived slug', () => { + const manifest = createManifest('OME-Zarr Validator'); + expect(getLogoUrl(manifest)).toBe( + 'https://raw.githubusercontent.com/bioimagetools/capability-manifest/host-manifests-and-docs/public/icons/ome-zarr-validator.png' + ); + }); + + it('handles multiple consecutive spaces', () => { + const manifest = createManifest('My Cool Viewer'); + expect(getLogoUrl(manifest)).toBe( + 'https://raw.githubusercontent.com/bioimagetools/capability-manifest/host-manifests-and-docs/public/icons/my-cool-viewer.png' + ); + }); + + it('does not use an empty string logo as an override', () => { + const manifest = createManifest('Avivator', ''); + expect(getLogoUrl(manifest)).toBe( + 'https://raw.githubusercontent.com/bioimagetools/capability-manifest/host-manifests-and-docs/public/icons/avivator.png' + ); + }); +}); diff --git a/src/logo.ts b/src/logo.ts new file mode 100644 index 0000000..8abc597 --- /dev/null +++ b/src/logo.ts @@ -0,0 +1,27 @@ +import type { ViewerManifest } from './types.js'; + +/** + * Default base URL for viewer icons hosted in the capability-manifest repository. + */ +const DEFAULT_ICONS_BASE_URL = + 'https://raw.githubusercontent.com/bioimagetools/capability-manifest/host-manifests-and-docs/public/icons'; + +/** + * Derive a logo URL for a viewer. + * + * If the manifest includes a `viewer.logo` field, that value is returned as-is + * (allowing per-viewer overrides). Otherwise a URL is constructed pointing to + * the capability-manifest icon repository, where the filename is the viewer + * name lowercased with spaces replaced by hyphens + * (e.g. "OME-Zarr Validator" → "ome-zarr-validator.png"). + * + * @param manifest - The viewer's capability manifest + * @returns URL string pointing to the viewer's logo + */ +export function getLogoUrl(manifest: ViewerManifest): string { + if (manifest.viewer.logo) { + return manifest.viewer.logo; + } + const slug = manifest.viewer.name.toLowerCase().replace(/\s+/g, '-'); + return `${DEFAULT_ICONS_BASE_URL}/${slug}.png`; +} diff --git a/tsconfig.lib.json b/tsconfig.lib.json index 75b9542..9fb9d9b 100644 --- a/tsconfig.lib.json +++ b/tsconfig.lib.json @@ -11,6 +11,7 @@ "src/index.ts", "src/loader.ts", "src/validator.ts", + "src/logo.ts", "src/types.ts" ], "exclude": ["src/app.ts"] From e71c1c9125a00e6d5c1c857ef2e943a2c4f5bec3 Mon Sep 17 00:00:00 2001 From: Allison Truhlar Date: Tue, 14 Apr 2026 20:15:57 +0000 Subject: [PATCH 10/12] docs: document icon hosting convention and logo override --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 0f2331a..4b25b67 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,18 @@ The library exports TypeScript types for all data structures: - `ValidationError`, `ValidationWarning` - Detailed validation messages - `AxisMetadata`, `MultiscaleMetadata` - Nested metadata types +## Icons + +Icons for canonical viewers are hosted in the [`public/icons/`](public/icons/) directory and served via GitHub Pages at: + +``` +https://raw.githubusercontent.com/bioimagetools/capability-manifest/host-manifests-and-docs/public/icons/{slug}.png +``` + +where `{slug}` is the viewer name lowercased with spaces replaced by hyphens (e.g. `"OME-Zarr Validator"` → `ome-zarr-validator.png`). Consumers can derive this URL automatically and fall back to a local placeholder when the icon is unavailable. + +The `logo` field in the `viewer` section is an optional override for cases where this convention does not apply. + ## Canonical Manifests This repository hosts canonical capability manifests for well-known OME-Zarr viewers in the [`manifests/`](manifests/) directory: From 40fd1511c5dbf8b17ffec5a6225c2fa899260a4a Mon Sep 17 00:00:00 2001 From: Allison Truhlar Date: Tue, 14 Apr 2026 16:18:07 -0400 Subject: [PATCH 11/12] chore: add icons --- public/icons/avivator.png | Bin 0 -> 5822 bytes public/icons/neuroglancer.png | Bin 0 -> 39096 bytes public/icons/ome-zarr-validator.png | Bin 0 -> 434 bytes public/icons/vol-e.png | Bin 0 -> 13174 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/icons/avivator.png create mode 100644 public/icons/neuroglancer.png create mode 100644 public/icons/ome-zarr-validator.png create mode 100644 public/icons/vol-e.png diff --git a/public/icons/avivator.png b/public/icons/avivator.png new file mode 100644 index 0000000000000000000000000000000000000000..2d07b8cfce868d0ebc0a8dc4c293c3e01d38c85f GIT binary patch literal 5822 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 literal 0 HcmV?d00001 diff --git a/public/icons/ome-zarr-validator.png b/public/icons/ome-zarr-validator.png new file mode 100644 index 0000000000000000000000000000000000000000..b96a530961a33770f81374545e375f4888b27449 GIT binary patch literal 434 zcmV;j0ZsmiP)Px$Y)M2xR7gw3m9Z{_K@i7(*LVPl&O1o1qtbc+LMT-T4K-?-6FQ}Y@Bm6GI*E7C zNj$(+@|T;-WV7?_ek);9$00iLXrn_s&SSYbd}DaMc*PiJQB! z@Q_^ro`B^BKq@W?iThGLcL8#BwK!_TWg7xiQ;DPz$*ER61emx*>mhx_LqH@V2f_Wy zW$5?A66GN2Jt&D!GuQ-fGPkG;QX?+OE!`sZ{P*CY^{u&&Qx9#Srd4=IZR7$?TZR6V znw=n8X}_s%b~4nUxovpSIhPF^JbyZHEE4tc%ZAyYj=X1s#>D<*==+*#R_I`(!`0#I c_ZgktH>+D!f9|!lB>(^b07*qoM6N<$g3c7d3jhEB literal 0 HcmV?d00001 diff --git a/public/icons/vol-e.png b/public/icons/vol-e.png new file mode 100644 index 0000000000000000000000000000000000000000..e5e00744c93a422586adc1df241fdee0423f9866 GIT binary patch literal 13174 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 Date: Tue, 14 Apr 2026 16:19:41 -0400 Subject: [PATCH 12/12] 0.5.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ca49687..0af67fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bioimagetools/capability-manifest", - "version": "0.4.0", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bioimagetools/capability-manifest", - "version": "0.4.0", + "version": "0.5.0", "license": "ISC", "dependencies": { "js-yaml": "^4.1.1" diff --git a/package.json b/package.json index 554d51b..b0f1547 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bioimagetools/capability-manifest", - "version": "0.4.0", + "version": "0.5.0", "description": "Library to determine OME-Zarr viewer compatibility based on capability manifests", "type": "module", "main": "./dist/index.js",