XOpat — OpenSeadragon-based histology data visualizer
xOpat is a JavaScript application. Two reference backends ship in the repo and either may serve it:
server/node/— canonical Node.js backend (seeserver/node/README.md). Started withnpm run s-node(production) ornpm run dev(server/utils/node/dev-mode.js).server/php/— legacy PHP backend (entrypointserver/php/index.php→server/php/init.php).
Both backends inject the same runtime configuration into the browser and provide the proxy/auth/storage endpoints the client expects. A high-level integration story lives in ../INTEGRATION.md; operational/deployment docs at https://xopat.org.
Configuration
The viewer always boots from a single object (XOpatRuntimeConfig, see src/types/app.d.ts) carrying params, data, background, visualizations, plugins. The client resolves where that object comes from in this order — first hit wins (src/parse-input.js:94–209):
- POST body, field
visualization(legacy aliasvisualisationalso accepted). Canonical delivery for non-trivial sessions; the field carries either a JSON object or a JSON-encoded string. The server advertises POST support viaXOpatServerConfig.supportsPost(src/types/config.d.ts:113). - URL hash
#<urlencoded-json>— parsed locally. IfsupportsPostis true the viewer transparently rewrites the navigation into a self-POST (hidden form atparse-input.js:110–123) so refreshes/shares stay POST-backed and the address bar is clean. ?visualization=<urlencoded-json>query parameter — same parser as the hash path.?slides=id1,id2&masks=m1,m2shorthand — synthesizes one background per slide plus aheatmap-shader visualization per mask (parse-input.js:131–174). Convenient for quick links and CI tests.- Storage fallback —
localStorage["xoSessionCache"](orsessionStorage["xoSessionCache"]) restores the last successful session if it is < 30 minutes old. The restored config is marked__fromLocalStorage: trueso plugins can detect it. Every successful boot writes the current session back to both storages, so an auth-redirect round-trip never loses state.
A simple form that just POSTs a session JSON into the
visualizationfield is available at/dev_setupon both backends (server/node/index.js:438–473, 610–611,server/php/dev_setup.php, templateserver/templates/dev-setup.html). Use it during development; in production the embedding application supplies POST data directly.
Plugins may layer additional opening behavior on top of this pipeline — check the relevant plugin README.
Example session
{
"params": {
"sessionName": "Demo case 0042",
"locale": "en"
},
"data": [
{
"dataID": "path/to/tissue/scan.tif",
"microns": 0.001,
"protocol": "dzi",
"options": { "format": "jpeg" }
},
"path/to/annotation.tif",
"path/to/probability.tif"
],
"background": [
{ "dataReference": 0 }
],
"visualizations": [
{
"name": "A visualization setup 1",
"shaders": {
"shader_id_1": {
"name": "Advanced visualization layer",
"type": "edge",
"fixed": false,
"visible": 1,
"dataReferences": [2, 0],
"params": {}
},
"another_shader_id": {
"name": "Probability layer",
"type": "edge",
"visible": 1,
"dataReferences": [1],
"params": { "color": "#fa0058", "use_gamma": 1.0 }
}
}
}
],
"plugins": {
"recorder": {}
}
}
data — DataSpecification[] (required)
Each entry is either a bare DataID (string/object the image server understands — most often a UUID4 or file path; objects are used by sources like DICOM) or a DataOverride (src/types/app.d.ts:31–39):
dataID(required) — the underlyingDataID.options— generic map forwarded to the TileSource (SlideSourceOptions,src/types/app.d.ts:46–49). Standard keys:format.microns/micronsX/micronsY— pixel size in micrometers.protocol— name of a registered slide protocol (see Slide protocols below). In non-secure mode a backtick-template string is accepted for back-compat, but is rejected with a warning in secure mode.imageSmoothingEnabled— whenfalse, tiles for this data source are sampled withgl.NEAREST(blocky pixels at high zoom — useful for label maps or integer-coded segmentation layers). Whentrueor unset (default), tiles usegl.LINEAR. Honored by drawers that implementsetTiledImageSmoothingEnabled(currently FlexDrawer); silently ignored otherwise.tileSource— deprecated escape hatch for code-only consumers; not serializable.
params — viewer setup (optional)
Aligned with XOpatSetup in src/types/config.d.ts:53–87. initXOpat silently drops unknown keys with a console warning (src/app.ts:108–122), so typos vanish quietly — verify names against the type.
| Key | Type | Default | Notes |
|---|---|---|---|
sessionName |
string | — | Unique session id; overridable by background[i].sessionName. |
locale |
string | "en" |
i18next locale. |
theme |
"auto" \| "light" \| "dark" |
"auto" |
DaisyUI data-theme; "auto" follows the OS preference. ("dark_dimmed" / "dimmed" were never wired up in the v3 UI.) |
customBlending |
bool | false |
Allow user-programmed blending. |
debugMode |
bool | false |
Verbose runtime instrumentation. |
webglDebugMode |
bool | false |
Debug post-processing. |
webGlPreferredVersion |
string | — | Select WebGL backend version. |
valueInspectorEnabled |
bool | false |
Hover value inspector. |
visualizationInspectorEnabled |
bool | false |
Pixel/lens inspector overlay. |
visualizationInspectorMode |
string | — | Inspector mode (paired with UTILITIES.setVisualizationInspectorMode). |
visualizationInspectorRadiusPx |
number | — | Inspector radius. |
visualizationInspectorLensZoom |
number | — | Lens zoom factor. |
activeBackgroundIndex |
number | number[] | 0 |
Initial bg index; array for multi-view. |
viewport |
ViewportSetup \| ViewportSetup[] |
— | { point, zoomLevel, rotation? }; single value applies to all viewers or one per viewer in multi-view. |
preventNavigationShortcuts |
bool | false |
Disable xOpat navigation bindings (OSD defaults still apply). |
scrollRequiresCtrl |
bool | false |
Require Ctrl/Cmd + wheel to zoom; plain wheel scrolls the host page. Use for notebook / scrollable-host embeddings. A throttled toast nudges first-time users toward the modifier. |
scaleBar |
bool | true |
Deprecated, use ui.scaleBar. Requires microns to render. |
toolBar |
bool | — | Deprecated, use ui.toolBar. |
statusBar |
bool | — | Deprecated, use ui.statusBar. |
ui |
XOpatUiSetup |
— | Initial visibility of UI components — see table below. |
disablePluginsUi |
bool | false |
Hide plugin UI without unloading the plugins. |
disablePluginsAutoload |
bool | false |
Skip the _plugins cookie restore for this session. permaLoad plugins and plugins listed in config.plugins still load. Use to ignore the user's prior manual picks while respecting deployment defaults and session-declared plugins. |
grayscale |
bool | false |
Force grayscale transfer. |
tileCache |
bool | true |
Enable tile caching. |
maxImageCacheCount |
number | 1200 |
Tile cache size. |
preferredFormat |
string | — | Hint to the protocol; must be honored by the TileSource. |
background |
string | — | Hex #RGB/#RGBA clear color (e.g. fluorescence). Transparent if unset. |
permaLoadPlugins |
bool | true |
Remember loaded plugins across sessions. |
bypassCookies |
bool | false |
Skip cookie-backed user state. |
bypassCache |
bool | false |
Never reuse cached values. |
bypassCacheLoadTime |
bool | false |
Ignore cache at initial load only — avoids pulling cached content from a foreign session. |
historySize |
number | — | Cap on the history stack (src/classes/history.ts). |
isStaticPreview |
bool | false |
Disable interactive controls for thumbnail/preview embeds. |
maxMobileWidthPx |
number | — | Responsive breakpoint. |
params.ui — UI initial visibility
Each flag is the initial visible state at boot. false boots the component
collapsed, but the user can still bring it back via the settings menu, the
hide-UI button, or the relevant opener. Defaults to true for every key.
Reads go through APPLICATION_CONTEXT.getUiOption(key) which also honors the
legacy flat aliases (scaleBar / toolBar / statusBar) and the AppCache
of user-toggled settings — see XOpatUiSetup in src/types/config.d.ts.
Shorthand: set params.ui: false (or setup.ui: false for a deployment-wide
default) to hide every global UI component in one shot — handy for notebook
embeddings. params.ui: true is equivalent to leaving the field unset.
| Key | Affects |
|---|---|
scaleBar |
Per-viewer OSD scalebar overlay. Replaces legacy flat scaleBar. |
toolBar |
Top viewer toolbar. Replaces legacy flat toolBar. |
statusBar |
Bottom status bar (#viewer-status-bar). Replaces legacy flat statusBar. |
mainMenu |
Global menu (FullscreenMenus). false boots collapsed; menu-open buttons still work. |
navigator |
Per-viewer OSD navigator panel. |
appBar |
Top AppBar chrome — false is equivalent to the hide-UI button being pre-toggled. |
globalMenu |
Global right-side dock (window.LAYOUT) that hosts plugin tabs (chats, slide-switcher, questionnaire, …). false boots the dock closed; user opens/plugins focus still work. |
background — BackgroundItem[]
Each item is an image group rendered as one OSD layer (src/types/app.d.ts:76–90):
dataReference(required) — index intodata, or an inlineDataID/DataOverride. One reference per background entry.shaders(optional) — shader configuration array, same shape as visualization shaders (dataReferencesbecomes optional). When unset, the renderer synthesizes an implicitidentityshader keyed under the background'sid. As soon as any entry is set, the implicit identity is replaced.canonical-scene.tsmaterializes the implicit entry as[{ type: "identity", … }]when a tool edits it, so the change persists across reopens.id— unique id; derived from the data path if unset.name— tissue name shown in the UI.sessionName— overridesparams.sessionNamefor this background.visualizationIndex— index intovisualizationsselected when this background is mounted. Authoritative per-viewer viz binding — the slot's viz follows the bg entry through slot reordering / insertion / deletion. Passnullfor "no overlay". LegacygoalIndexis still accepted on read (folded with a one-time warning).options— forwarded to the TileSource.
Legacy fields
lossless,protocol,microns,micronsX,micronsYare still accepted at the background level for back-compat, but new code should put them on theDataOverrideinstead.
visualizations — VisualizationItem[]
WebGL composition goals over the data group (src/types/app.d.ts:109–116):
shaders(required) — map of shader id → layer spec:type(required) —color,edge,dual-color,identity,heatmap,none, or any custom-registered shader.dataReferences(required) — index array intodata.visible—1/0or boolean.name— UI label.fixed— iffalse, user can change the shader type; defaulttrue.params— shader-specific defaults; invalid entries fall back silently.
name— goal label.goalIndex— preferred index when this item is selected.
Legacy
losslessandprotocolare accepted at the visualization level for back-compat — preferDataOverride.
plugins
Plugin-id → plugin-config map; consult each plugin's README.
Advanced features
**Internal parameters.** The runtime augments visualization items at runtime with fields that show up in serialized sessions: - `order` — shader-id array on a visualization goal; sets render order. All referenced data with `visible=1` must be present and valid. - `cache` — per-shader, shader-type-dependent value bag (equivalent to default-value overrides). Type-sensitive: writing a wrong-type value will break rendering. **Slide protocols.** A protocol is a named entry in `ENV.client.slide_protocols` (see `src/types/config.d.ts:5–28`, registry implementation at `src/classes/slide-protocols.ts`). Each entry is either: - a URL template string with `data` in scope (non-secure mode only — rejected in secure mode), or - an object `{ url, proxy?, baseURL?, auth?, … }` whose extra fields are forwarded verbatim to `new HttpClient(...)`, so every metadata + tile request the resulting TileSource issues inherits proxy routing, CSRF tokens, and JWT/auth headers uniformly. `DataOverride.protocol` (and legacy `BackgroundItem.protocol` / `VisualizationItem.protocol`) reference an entry **by name** (`"dzi"`, `"dicomweb"`, …). Defaults come from `default_background_protocol` / `default_visualization_protocol`; the legacy `image_group_*` / `data_group_*` env keys are auto-migrated into synthesized `__legacy_bg` / `__legacy_viz` entries. Plugins register protocols at runtime via `window.SLIDE_PROTOCOLS.register({ id, createTileSource })`. Use this registry instead of hand-rolling URLs in `background.protocol`/`visualizations.protocol` — those evaluations are blocked in secure mode and lose proxy/auth integration.Structure
Each folder ships a README with more detail. The most up-to-date ones are this file, ../plugins/README.md, and ../modules/README.md.
../ (repo root)
index.htmland theserver/tree (Node + PHP entrypoints).package.json—s-node,s-node-test,dev,docker-node,docker-php.
./ (src/)
app.ts—initXOpat(...)entrypoint; buildsAPPLICATION_CONTEXT,VIEWER_MANAGER,SESSION,IO_PIPELINE.loader.ts— module/plugin loader and the global helpersplugin(id),singletonModule(id),viewerSingletonModule(className, viewerLike).parse-input.js— the precedence chain described in Configuration above.store.ts— pluggable storage middleware (KV drivers used by the IO pipeline).tile-source.ts— common TileSource scaffolding.classes/app/— viewer-open pipeline and canonical-scene round-trip (viewer-open-pipeline.ts,canonical-scene.ts,application-lifecycle-controller.ts,viewer-inspector-controller.ts).io/— IO pipeline implementation (seeIO_PIPELINE.md).session/— live-collaboration controller (seeSESSION.md).scripting/+scripting-manager.ts— sandboxed scripting API.slide-protocols.ts—SLIDE_PROTOCOLSregistry.http-client.ts—HttpClient(seeHTTP_CLIENT.md).history.ts,user.ts.
external/— always-loaded third-party libraries and OSD extensions (DZI ext tile source, scalebar, autocomplete, …).libs/— vendored libraries: jQuery, i18next, OpenSeadragon (openseadragon.js), Tailwind CSS, Monaco, FontAwesome, Phosphor Icons (phoshor-icons/), plusflex-renderer/(WebGL renderer). Do not editlibs/— upstream-only. Exception:phoshor-icons/fa-overrides.cssis xOpat-owned and should be edited to extend the Font Awesome → Phosphor mapping as we migrate.assets/—style.css, icons, and other static assets.types/— ambient TypeScript declarations (app.d.ts,config.d.ts,globals.d.ts,slide-protocols.d.ts,io.d.ts).
OpenSeadragon (v6+) is bundled under src/libs/openseadragon.js and configured via openSeadragonPrefix / openSeadragon in src/config.json. To run a debug build, point those values at an unminified copy.
../plugins/, ../modules/
User-facing features and shared libraries respectively; both are dynamically loadable via the loader. See their READMEs.
Available API
Make sure you've read ../INTEGRATION.md first.
Globals
Established by src/app.ts and src/loader.ts. These are the supported, ambiently-typed entrypoints:
| Global | Where it's set | Purpose |
|---|---|---|
window.APPLICATION_CONTEXT |
src/app.ts:159 |
Session, config accessors, open pipeline. |
window.VIEWER_MANAGER |
src/app.ts:224 |
Manager for all OSD viewer instances (single- and multi-view). |
window.USER_INTERFACE |
UI layer | Core generic UI operations (notifications, menus). |
window.UTILITIES |
UI / inspector controllers | System utilities (inspector toggles, serializers). |
window.HttpClient |
src/classes/http-client.ts:349 |
Auth-aware HTTP client (proxy, JWT, CSRF). |
window.SESSION |
src/app.ts:230 |
Live-collaboration SessionSyncController. |
window.IO_PIPELINE |
bootstrapIOPipeline() in src/app.ts:149 |
Save/load pipeline; also reachable as APPLICATION_CONTEXT.io. |
window.SLIDE_PROTOCOLS |
src/classes/slide-protocols.ts |
Slide-protocol registry. |
window.xmodules |
src/loader.ts |
Object store of module exports. Use the helpers below — don't reach in directly. |
plugin(id) |
src/loader.ts:298 |
Returns the plugin instance. |
singletonModule(id) |
src/loader.ts:313 |
Returns (and lazily instantiates) the module singleton. |
viewerSingletonModule(className, viewerLike) |
src/loader.ts:330 |
Returns a per-viewer XOpatViewerSingleton. |
window.VIEWERis not a stable handle — it tracks whichever viewer most recently took focus, which is the wrong instance whenever multi-view is active. Resolve the right viewer withVIEWER_MANAGER.get(...), withviewerSingletonModule(...), or frome.eventSourceon broadcast events. Likewise, do not store long-livedTiledImagereferences unless you own them, and preferVIEWER_MANAGERevents over reaching for the focused viewer. When you only need to retarget one viewer, useupdateViewerSelection(...)instead of rebuilding the whole session. SeeMULTI_VIEWPORTS.md.
Viewer Open API
xOpat treats viewer opening as an explicit transaction rather than a loose mix of config mutation and OpenSeadragon world edits. The runtime opening pipeline is class-based and lives under src/classes/app/ — viewer rebinding, visualization runtime checks, synthetic-open handling, inspector integration, and session lifecycle all stay there, and src/app.ts is intentionally reduced to bootstrap/composition. The public entrypoints exposed to plugins/modules remain global through window.APPLICATION_CONTEXT.
APPLICATION_CONTEXT.openViewerWith(data?, background?, visualizations?, bgSpec?, vizSpec?, opts?)- Main transaction entrypoint.
- Can replace or merge session
data/background. - Can create additional viewers when multiple backgrounds are targeted.
vizSpecarrays may contain explicitundefinedentries to mean "no visualization for this viewer"; omittedvizSpecstill means "keep the current selection".- Rebinds navigator title, scalebar reference, measurements, visualization menu, history, and synthetic open events.
APPLICATION_CONTEXT.updateViewerSelection(viewerIndex, { backgroundIndex?, visualizationIndex? }, opts?)- Use when one existing viewer should switch background and/or visualization without rebuilding unrelated viewers.
- Passing
visualizationIndex: nullclears the active visualization for that viewer. - Delegates to the same open pipeline, keeping history/session synchronization consistent.
APPLICATION_CONTEXT.replaceVisualizations(visualizations, newData?, activeVizIndex?)- Replaces the session visualization list while preserving the rest of the session.
- Preferred over the older
updateVisualization(...)name.
Options are ambiently typed as ViewerOpenOptions and per-viewer patches as ViewerSelectionPatch, so plugins/modules use them without importing from core.
Canonical Scene
src/classes/app/canonical-scene.ts is the single round-trip pair for full session state. Use it whenever you need to capture what is currently rendered and replay it later — playground Apply, session sync's heavy-apply path, scripting export/import, and draft persistence all go through it.
serializeScene()— capturescfg(data, background, visualizations, active indices) and merges per-shader runtime cache/state from every viewer's renderer back into the structural shader entries. Returns aCanonicalSceneJSON object.serializeSceneFromViewer(viewer, init, live?)— single-viewer slice, used by the playground page (passes its namespace-strippedliveso renderer ids match the structural ids).deserializeScene(scene, opts)— callsAPPLICATION_CONTEXT.openViewerWith(...)with the canonical cfg shape and forwardshistoryMode/historyLabel. The pipeline rebuilds renderers from the inlined cache — no second per-layer apply pass needed.backgroundShaderRendererIds(bg)/visualizationShaderRendererIds(viz)— single source of truth for renderer-id derivation. Bg shader ids followbgRef.idfor index 0 and${bgRef.id}-Nfor subsequent entries (mirrorsassemble-render-output.ts:149-150); viz shader ids are the structural map keys.
Devtools handle: window.__SCENE = { serialize, serializeFromViewer, deserialize, … }. Inspect the round-trip from the console — e.g. await __SCENE.deserialize(__SCENE.serialize(), { historyMode: "skip" }) should be a visual no-op.
Implicit identity rule. When cfg.background[i].shaders is unset, the renderer synthesizes an identity shader at bg.id. If a tool edits that implicit shader, the canonical-scene serializer materializes it as [{ type: "identity", cache: {…} }] so the change persists across reopens.
Session Restore and Lifecycle
Session bootstrap and restore live in ApplicationLifecycleController.
- Startup restores the last successful session from browser storage when no explicit POST/hash/query session is provided (see Configuration above).
beginApplicationLifecycle(...)loads required plugins, initializes layers, raisesbefore-app-init, and then opens the requested viewer state.- Inspector registration is centralized in
ViewerInspectorController(no longer mixed intoapp.ts).
IO Pipeline
window.IO_PIPELINE (also APPLICATION_CONTEXT.io) decouples what modules want to save/load from where it goes. Modules declare capabilities in their include.json (io.capabilities); admin config (ENV.client.io.bindings) binds those to concrete sinks. Plugin authors typically:
- Register bundle-level hooks via
this.initIO({ bundleScope, exportBundle, importBundle }). - Define per-element CRUD resources via
this.defineResource({ name, validate, serialize, deserialize }).
The pipeline queues sink dispatch per-resource, supports coalescing, and persists its outbox to IndexedDB. Bundle sinks include file-download, file-upload, post-data, http-rest. See IO_PIPELINE.md for the full design.
Session / Collaboration
window.SESSION is a SessionSyncController singleton enabling real-time peer-to-peer collaboration. Plugins/modules participate by calling window.SESSION?.registerProvider({ id, scope, snapshot, applySnapshot, subscribe, applyDelta }). The sessionCompatible flag in include.json declares participation: "provider" = actively syncs, true = safe but non-syncing, false = incompatible (undeclared plugins trigger a warnings modal). Hosts provision guest URLs via UTILITIES.serializeApp(...) so guests load the host's exact plugin set. Read meta.role in post-event handlers to avoid duplicate side effects on guests. See SESSION.md.
HttpClient
Never use native fetch or XMLHttpRequest for upstream calls — HttpClient (src/classes/http-client.ts) integrates with xOpatUser and injects JWT, CSRF, and proxy paths automatically. See HTTP_CLIENT.md.
const client = new HttpClient({
proxy: "cerit", // alias defined in server proxies
baseURL: "/api/v1",
auth: { contextId: "core", types: ["jwt"], required: true },
timeoutMs: 30000, // optional, default 30s
maxRetries: 3, // optional, default 3
});
const response = await client.request("data", {
method: "POST",
body: { object: "goes here" },
expect: "json", // "json" | "text" | "auto"
// query: { foo: "bar" },
});
Inspector Utilities
Ambiently typed, part of the supported runtime surface:
UTILITIES.toggleVisualizationInspector(enabled?)UTILITIES.setVisualizationInspectorRadius(radiusPx)UTILITIES.adjustVisualizationInspectorRadius(deltaPx)UTILITIES.setVisualizationInspectorMode(mode)UTILITIES.toggleValueInspector(enabled?)
Scripting
src/classes/scripting-manager.ts + src/classes/scripting/ is a Worker-based sandbox exposing scripting namespaces (XOpatApplicationScriptApi, XOpatViewerScriptApi, XOpatVisualizationScriptApi) to user/plugin scripts. Use it for advanced automation and LLM integration; not required for typical plugin development.
UI
Use the new UI components — see ../ui/README.md and ../ui/classes/README.md. Extend BaseComponent and rely on Van.js reactivity instead of manual jQuery DOM work. The CORE UI singletons (AppBar, FloatingManager, FullscreenMenus, GlobalTooltip, …) are listed in ../ui/services/README.md.
Reuse the existing components before pulling new dependencies. If you need a DaisyUI element that isn't already wrapped, add it under ui/classes/elements so other plugins can reuse it.
Localization
Driven by i18next. Use $.t('translation_key') at runtime; $.i18n holds the instance. Server-side i18n is available with limited capabilities. In spawned child windows, $.t(...) works but jquery-i18next is not bundled.
For plugin localization specifics, see the plugins README.
Embedding the viewer in a custom server
The two reference backends are the documentation:
- PHP —
server/php/init.phpshows the canonical wiring. The helpers inserver/php/inc/core.php(require_libs,require_openseadragon,require_external,require_core) andserver/php/inc/plugins.php(require_modules,require_plugins) are still the building blocks for embedding xOpat into a PHP host. The browser-side entry isinitXOpat(PLUGINS, MODULES, ENV, POST_DATA, PLUGINS_FOLDER, MODULES_FOLDER, VERSION, I18NCONFIG?)(src/app.ts:44). - Node —
server/node/index.jsandserver/node/README.mdcover the modern integration story: session-cookie CSRF, RPC for plugins/modules, dev-mode hot reload viaserver/utils/node/dev-mode.js.
Further reading
- Lifecycle events:
EVENTS.md - HTTP / proxies / token verifiers:
HTTP_CLIENT.md - IO pipeline (save/load):
IO_PIPELINE.md - Live collaboration:
SESSION.md - Multi-viewport rules:
MULTI_VIEWPORTS.md - NPM-packaged modules/plugins:
NPM_MODULES_PLUGINS.md - Plugin development:
../plugins/README.md - Module development:
../modules/README.md - UI design system:
../ui/README.md,../ui/classes/README.md,../ui/services/README.md