OME-Zarr whole-slide image · measured in your browser

Only the viewport.

Pan and zoom a multi-gigabyte OME-Zarr whole-slide image. PureJsImage reads only the metadata, shard indexes, and chunks needed for the viewport, directly from object storage. There is no application server or tile proxy.

This sample is a large remote sharded whole-slide image. The same reader also handles Z/T navigation, channel display metadata, labels, image collections, and HCS plates and wells.

Reader: NGFF 0.4 + 0.5Reader: Zarr v2 + v3Current sample: v3 shardedBrowser HTTP access0 runtime dependencies

Logical chunks from sharded arrays

OME-Zarr viewport

Single image
Channel mixerUp to three channels · window / gamma
Drag to pan · wheel or pinch to zoom
X - · Y - · display RGB -
Waiting for OME-Zarr metadata…
Opening OME-Zarr storeStarting the worker and connecting to object storage…

Starting the worker…

Viewport tile jobsPending Decoded Cancelled

Each chip is a render job, not an HTTP request. Actual object and range requests are counted separately in the instrument.

Read-only technical demonstration. Not validated for diagnostic use.

What's already verified

OME-Zarr 0.5 attribute-case conformance

This is attribute-case conformance plus a separate public-store compatibility corpus. It is not a claim of complete Zarr v3 or full hierarchy conformance.

OME-Zarr version tested
0.5
Upstream case-corpus revision
69b136f1e64e68fead11216ac8dd3f1155668d04
Normative attribute cases
70 / 70
Optional strict cases
10 / 11
Explicit exclusions
5
Public roots tested
9
Collections represented
6 · BioImage Archive · IDR · OME 2024 NGFF Challenge · OME 2024 NGFF Challenge / Jackson Laboratory · SSBD · Sanger Institute
Remote versions and layouts represented
OME-NGFF 0.4 / Zarr v2 · regular chunks · OME-NGFF 0.5 / Zarr v3 · sharding_indexed · bioformats2raw series layout

What it supports

What the reader covers, and what it does not.

The capability manifest and the OME-Zarr support doc have the full details on codecs, data types, storage, validation modes, and serving.

Implemented

  • OME-NGFF 0.4 and 0.5
  • Zarr v2 and v3
  • Regular and sharded reads
  • OMERO display metadata
  • Labels
  • Plates and wells
  • HTTP and ZIP stores
  • Documented codec stack

Not supported yet

  • Writer
  • OME-Zarr 0.6rc0
  • Storage transformers
  • Unsupported codecs and data types listed by the capability manifest
  • Large remote stores that cannot serve HTTP ranges

See the generated capability manifest and OME-Zarr support documentation for the full list.

How it fetches data

Read metadata, then only the chunks in view.

The worker owns the URL-backed store, OME-Zarr reader, channel composition, cancellation, and network counters. The main thread owns interaction, a bounded 192-bitmap cache, best-level selection, and canvas drawing.

1NGFF metadataList image fields, wells, labels, channels, and calibrated axes
2Shard + chunk rangesRead only indexes and chunks that overlap the current view
3Worker + canvasMix channels and labels, then transfer one ImageBitmap for the current view
Read-only, and it only fetches what it needs.

The viewer shows plate fields, non-spatial axes, a three-channel mixer, compatible label overlays, visible-tile histograms, and calibrated navigation without downloading the full store. It does not write annotations or data, authenticate private stores, or keep decoded chunks beyond the bounded bitmap cache. Label overlays need matching x/y pyramid geometry; unsupported datasets stay visible but disabled. For geographic Zarr conventions, use the GeoZarr Cube Lab.

Build it with PureJsImage

The viewport read happens in a worker.

The demo uses PureJsImage for OME-NGFF parsing, pyramid metadata, shard indexes, codecs, and bounded region reads. App code only fetches remote objects, chooses the visible level and region, and composites returned blocks for the canvas.

Implementation files
Choose an implementation file
import { createOmeZarrReader } from 'purejsimage/scientific/readers/ome-zarr'
import { createOmeZarrHttpContext } from 'purejsimage/scientific/browser'

// Public context: only allowed object names, a bounded source cache, cancellation,
// range validation, request coalescing, and measured network statistics.
const context = await createOmeZarrHttpContext(storeUrl, {
  maxOpenSources: 8,
  maxCacheBytesPerSource: 8_388_608,
})

const reader = createOmeZarrReader({
  limits: { rowsPerBlock: 1_024 },
  metadataValidation: 'compatible',
})
const document = await reader.open(context)

// Choose any displayable image or plate field. The UI persists these indices.
const summary = document.datasets.find(({ descriptor }) =>
  descriptor.axes.some(({ id }) => id === 'x') &&
  descriptor.axes.some(({ id }) => id === 'y'),
)
if (!summary) throw new Error('No x/y image dataset found')

const dataset = await document.openDataset(summary.id)
const fixedIndices = dataset.descriptor.axes
  .filter(({ id }) => id !== 'x' && id !== 'y')
  .map(({ id, length }) => ({ axisId: id, index: viewerAxisIndex(id, length) }))

// level and region come from the current canvas zoom and viewport.
for await (const block of dataset.readPlane({
  displayAxes: ['x', 'y'],
  fixedIndices,
  resolutionLevel: level,
  x,
  y,
  width,
  height,
  signal: request.signal,
})) {
  try {
    compositeIntoRgba(block)
  } finally {
    block.release?.()
  }
}

console.log(context.store.stats())
context.store.close()
import {
  createOmeZarrHttpContext,
  type OmeZarrHttpContext,
} from 'purejsimage/scientific/browser'

export const openRemoteOmeZarr = async (
  storeUrl: string,
  signal: AbortSignal,
): Promise<OmeZarrHttpContext> =>
  createOmeZarrHttpContext(storeUrl, {
    signal,
    maxOpenSources: 16,
    blockBytes: 262_144,
    maxCacheBytesPerSource: 2_097_152,
  })

// Pass the returned context directly to createOmeZarrReader().open().
// Keep context.store to inspect stats, reset counters, or close the session.