omezarr-tilesource 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,33 +38,42 @@ import { OMEZarrTileSource } from "omezarr-tilesource";
38
38
 
39
39
  const url = ...;
40
40
 
41
- // configuration with URL (only works with zipped OME-Zarr URLs ending with .ozx)
41
+ // register the tile source and its data type converters with OpenSeadragon
42
+ // (required for inline configurations and URLs, see "Data pipeline" below)
43
+ OMEZarrTileSource.enable(OpenSeadragon);
44
+
45
+ // configuration with URL (only works with zipped OME-Zarr URLs, path ending in .ozx)
42
46
  const tileSource1 = url;
43
47
 
44
- // inline configuration with options object (requires prior enabling, see below)
45
- OMEZarrTileSource.enable(OpenSeadragon);
48
+ // inline configuration with options object
46
49
  const tileSource2 = {
47
50
  type: "ome-zarr",
48
51
  url: url,
49
- // zip: undefined, // undefined = OME-Zarr ZIP auto-detection based on .ozx suffix
50
- // c: undefined, // undefined = composite of all active channels (requires dataType "context2d")
51
- // z: undefined, // undefined = omero rdefs default (middle z-slice if missing)
52
+ // zip: undefined, // undefined = OME-Zarr ZIP auto-detection based on .ozx path suffix
52
53
  // t: undefined, // undefined = omero rdefs default (middle timepoint if missing)
53
- // dataType: undefined, // "context2d" (default, rendered tiles) or "ome-zarr" (raw single-channel chunks)
54
+ // z: undefined, // undefined = omero rdefs default (middle z-slice if missing)
55
+ // c: undefined, // channel index or array of indices; undefined = all active channels
56
+ // range: undefined, // contrast limits [min, max] or array thereof; undefined = omero windows
57
+ // color: undefined, // RGB color or array of colors; undefined = omero channel colors
58
+ // lutOrColorMap: undefined, // LUT/color map or array thereof; undefined = omero LUTs
59
+ // inverted: undefined, // boolean or array of booleans; undefined = omero inversion
54
60
  // autoBoost: undefined // boost brightness of dark tiles (default false)
55
61
  };
56
62
 
57
63
  // direct instantiation with URL (works with any OME-Zarr storage backend)
58
64
  const tileSource3 = new OMEZarrTileSource(url);
59
65
 
60
- // direct instantiation with options object (no prior enabling required)
66
+ // direct instantiation with options object
61
67
  const tileSource4 = new OMEZarrTileSource({
62
68
  url: url,
63
- // zip: undefined, // undefined = OME-Zarr ZIP auto-detection based on .ozx suffix
64
- // c: undefined, // undefined = composite of all active channels (requires dataType "context2d")
65
- // z: undefined, // undefined = omero rdefs default (middle z-slice if missing)
69
+ // zip: undefined, // undefined = OME-Zarr ZIP auto-detection based on .ozx path suffix
66
70
  // t: undefined, // undefined = omero rdefs default (middle timepoint if missing)
67
- // dataType: undefined, // "context2d" (default, rendered tiles) or "ome-zarr" (raw single-channel chunks)
71
+ // z: undefined, // undefined = omero rdefs default (middle z-slice if missing)
72
+ // c: undefined, // channel index or array of indices; undefined = all active channels
73
+ // range: undefined, // contrast limits [min, max] or array thereof; undefined = omero windows
74
+ // color: undefined, // RGB color or array of colors; undefined = omero channel colors
75
+ // lutOrColorMap: undefined, // LUT/color map or array thereof; undefined = omero LUTs
76
+ // inverted: undefined, // boolean or array of booleans; undefined = omero inversion
68
77
  // autoBoost: undefined // boost brightness of dark tiles (default false)
69
78
  });
70
79
 
@@ -79,81 +88,185 @@ const viewer = OpenSeadragon({
79
88
  });
80
89
  ```
81
90
 
91
+ ### Options
92
+
93
+ | Option | Type | Default | Description |
94
+ | --------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
95
+ | `url` | `string \| URL` | (required) | URL of the OME-Zarr image (group) or of a zipped OME-Zarr file, relative to the document base URL |
96
+ | `zip` | `boolean` | `true` if the `url` path ends with `.ozx` | Whether `url` points to a zipped OME-Zarr file |
97
+ | `t` | `number` | omero `rdefs.defaultT`, else middle plane | Timepoint index (0-based) |
98
+ | `z` | `number` | omero `rdefs.defaultZ`, else middle plane | Z-slice index (0-based) |
99
+ | `c` | `number \| number[]` | all channels marked active in omero | Channel index or indices (0-based) to render, composited in the given order. Arrays must be non-empty |
100
+ | `range` | `[number, number] \| ([number, number] \| undefined)[]` | omero channel windows (`window.start`, `window.end`), else the data type range | Contrast limits (`[min, max]`) to render the channels with. Ignored for channels rendered with a color map |
101
+ | `color` | `Color \| (Color \| undefined)[]` | colors of the rendered omero channels, else white | RGB color or colors (`[r, g, b]`, 0-255) to render the channels with. Ignored for channels rendered with a LUT or color map |
102
+ | `lutOrColorMap` | `LUTOrColorMap \| (LUTOrColorMap \| undefined)[]` | omero channel LUTs and color maps, else none | Color LUT (a color per scaled value) or color map (a color per raw value, `Map`) to render the channels with; colors may carry alpha. A color map also overrides `range` and `inverted` |
103
+ | `inverted` | `boolean \| (boolean \| undefined)[]` | omero channel inversion, else `false` | Whether to invert the channels. Ignored for channels rendered with a color map |
104
+ | `autoBoost` | `boolean` | `false` | Boost the brightness of dark tiles (ome-zarr.js `renderChunks` option) |
105
+
106
+ `range`, `color`, `lutOrColorMap` and `inverted` take a single value, which
107
+ applies to all rendered channels, or one entry per rendered channel. An
108
+ `undefined` entry falls back to the omero metadata of that channel, as does the
109
+ option as a whole. The `ranges`, `colors`, `lutsOrColorMaps` and `inverteds`
110
+ getters return the resolved per-channel arrays.
111
+
112
+ `t`, `z` and `c` are validated against the image shape when the OME-Zarr
113
+ metadata is loaded; invalid indices fail with `open-failed`. Images without
114
+ `c` fail if the omero metadata marks no channel as active. An invalid `c`,
115
+ `url` or rendering setting is rejected by the constructor, which throws
116
+ synchronously before any request is made; the length of a rendering setting
117
+ array is only checked against `c` if that is configured too.
118
+
119
+ `url` is resolved against the document base URL when the tile source is
120
+ created; the resolved absolute URL is available as `tileSource.url` (always a
121
+ string) and is used for loading, for the tile cache keys and for comparing tile
122
+ sources.
123
+
82
124
  ### Accessing OME-Zarr metadata
83
125
 
84
126
  Directly instantiated tile sources start loading the OME-Zarr metadata
85
127
  immediately. Await `whenReady()` (or use the `OMEZarrTileSource.open` shortcut)
86
- to access the `NgffImage` instance (ome-zarr.js) and the opened zarrita arrays
87
- (one per resolution level) before adding the tile source to a viewer:
128
+ to access the loaded image (`loaded.image`, an ome-zarr.js `NgffImage`) and the
129
+ opened zarrita arrays (`loaded.arrays`, one per resolution level, highest
130
+ resolution first) before adding the tile source to a viewer:
88
131
 
89
132
  ```javascript
90
133
  const tileSource = await OMEZarrTileSource.open({ url: url, c: 0 });
91
134
  // equivalent: await new OMEZarrTileSource({ url: url, c: 0 }).whenReady();
92
135
 
93
- console.log(tileSource.image.getAxesNames()); // e.g. ["t", "z", "c", "y", "x"]
94
- console.log(tileSource.image.omero?.channels); // omero channel metadata
95
- console.log(tileSource.arrays[0].shape); // full-resolution array shape
136
+ console.log(tileSource.loaded.image.getAxesNames()); // e.g. ["t", "c", "z", "y", "x"]
137
+ console.log(tileSource.loaded.image.omero?.channels); // omero channel metadata
138
+ console.log(tileSource.loaded.arrays[0].shape); // full-resolution array shape
139
+
140
+ console.log(tileSource.t, tileSource.z, tileSource.cs); // resolved indices
141
+ console.log(tileSource.channels); // omero channels of the rendered channel indices
142
+ console.log(tileSource.ranges, tileSource.colors); // resolved rendering settings
143
+ console.log(tileSource.lutsOrColorMaps, tileSource.inverteds);
144
+ console.log(tileSource.getWidth(), tileSource.getHeight()); // full resolution
145
+ console.log(tileSource.getWidth(0), tileSource.getHeight(0)); // lowest resolution
96
146
 
97
147
  viewer.addTiledImage({ tileSource: tileSource }); // no second metadata request
98
148
  ```
99
149
 
100
- The metadata is loaded once per tile source instance: OpenSeadragon reuses a
101
- tile source instance passed to it as-is (waiting for it to become ready if
102
- necessary), whereas a URL or an inline configuration object makes OpenSeadragon
103
- create (and load) a new instance. `whenReady()` rejects (and `image`/`arrays`
104
- throw) if loading fails.
150
+ `OMEZarrTileSource.open` accepts an `AbortSignal` as `signal` in its third
151
+ argument to cancel loading. The metadata is loaded once per tile source
152
+ instance: OpenSeadragon reuses a tile source instance passed to it as-is
153
+ (waiting for it to become ready if necessary), whereas a URL or an inline
154
+ configuration object makes OpenSeadragon create (and load) a new instance.
155
+ `whenReady()` rejects (and `loaded` throws) if loading or validation fails.
156
+
157
+ The `t`, `z` and `cs` getters return the resolved values (the omero defaults or
158
+ active channels if the option was not given); `cs` is always an array (the
159
+ `c` option may be a single index), and `channels` returns the omero channel
160
+ objects for `cs`. Before the tile source is ready, they return the configured
161
+ options. The `ranges`, `colors`, `lutsOrColorMaps` and `inverteds` getters
162
+ return the resolved rendering settings of the rendered channels, merging the
163
+ configured values with the omero metadata per channel: `undefined` before the
164
+ tile source is ready or if the image has no omero metadata, like `channels`.
165
+ Entries that are still `undefined` fall back at render time to the data type
166
+ range, white, no LUT and not inverted.
105
167
 
106
168
  ### Sharing OME-Zarr metadata between tile sources
107
169
 
108
- A loaded `NgffImage` can be reused by other directly instantiated tile sources
109
- for the same URL (e.g. one tile source per channel) by passing it as the second
110
- constructor argument (also supported by `OMEZarrTileSource.open`). This skips
111
- loading the OME-Zarr metadata (the `zip` option is ignored) and reuses the
112
- opened zarrita arrays, which ome-zarr.js caches on the `NgffImage` instance.
113
- The tile source never modifies the `NgffImage`, so sharing is safe:
170
+ The loaded image and arrays (`OMEZarr`) can be reused by other directly
171
+ instantiated tile sources for the same URL (e.g. one tile source per channel)
172
+ by passing them as the second constructor argument (also supported by
173
+ `OMEZarrTileSource.open`). This skips loading the OME-Zarr metadata (the `zip`
174
+ option is ignored) and reuses the opened zarrita arrays. The tile source never
175
+ modifies the loaded image, so sharing is safe:
114
176
 
115
177
  ```javascript
116
- import { NgffImage } from "ome-zarr.js";
178
+ // load once with the tile source's loader ...
179
+ const loaded = await OMEZarrTileSource.loadOMEZarr(url);
180
+ const tileSources = loaded.image.omero.channels.map(
181
+ (_, c) => new OMEZarrTileSource({ url: url, c: c }, loaded),
182
+ );
117
183
 
118
- // reuse the image loaded by a first tile source
184
+ // ... or reuse what a first tile source has loaded
119
185
  const tileSource1 = await OMEZarrTileSource.open({ url: url, c: 0 });
120
186
  const tileSource2 = new OMEZarrTileSource(
121
187
  { url: url, c: 1 },
122
- tileSource1.image,
188
+ tileSource1.loaded,
123
189
  );
190
+ ```
124
191
 
125
- // or load the image with the app's own ome-zarr.js import
126
- const image = await NgffImage.load(url);
127
- const tileSource3 = new OMEZarrTileSource({ url: url, c: 2 }, image);
192
+ `loadOMEZarr(url, zip?, { signal })` loads the metadata with ome-zarr.js and
193
+ opens the arrays of all resolution levels. An `OMEZarr` can also be assembled
194
+ from an `NgffImage` loaded by the app, as long as `arrays` lists the
195
+ opened arrays of `image.paths` in order. It is not checked against the URL of
196
+ the tile source it is passed to.
197
+
198
+ ### Loading chunks directly
199
+
200
+ `loadChunks(level, tile, { signal })` returns the (y, x) zarrita chunks of the
201
+ rendered channels at the rendered timepoint and z-slice, either for one tile
202
+ (`{ x, y }` tile coordinates, as passed to OpenSeadragon) or for the whole
203
+ level plane (`tile` `undefined`):
204
+
205
+ ```javascript
206
+ const chunks = await tileSource.loadChunks(tileSource.maxLevel, { x: 0, y: 0 });
207
+ const plane = await tileSource.loadChunks(0, undefined); // lowest resolution
208
+ ```
209
+
210
+ `OMEZarrTileSource.render(tileData)` composites such chunks into a 2D canvas
211
+ context, the same way the `ome-zarr` to `context2d` converter does:
212
+
213
+ ```javascript
214
+ import { getDataTypeRange } from "omezarr-tilesource";
215
+
216
+ const ctx = OMEZarrTileSource.render({
217
+ chunks: plane,
218
+ // the tile data holds the renderChunks arguments, one per chunk, with the
219
+ // defaults of the tile source getters applied
220
+ ranges: plane.map(
221
+ (chunk, c) => tileSource.ranges?.[c] ?? getDataTypeRange(chunk),
222
+ ),
223
+ colors: plane.map((_, c) => tileSource.colors?.[c] ?? [255, 255, 255]),
224
+ lutsOrColorMaps: plane.map((_, c) => tileSource.lutsOrColorMaps?.[c]),
225
+ inverteds: plane.map((_, c) => tileSource.inverteds?.[c] ?? false),
226
+ autoBoost: tileSource.autoBoost,
227
+ });
128
228
  ```
129
229
 
130
230
  ## Data pipeline
131
231
 
132
- By default (`dataType: "context2d"`), tiles are rendered by the tile source and
133
- passed to OpenSeadragon as 2D canvas contexts, using the rendering settings
134
- (color, color LUT/map, contrast limits, inversion) from the omero metadata. For
135
- multi-channel images without `c`, all active channels are rendered into a
136
- composite image.
137
-
138
- Contrast limits are taken from the omero channel windows (`window.start`,
139
- `window.end`). Channels without them are rendered using the data type range
140
- for integer types (e.g. `[0, 65535]` for `uint16`) and `[0, 1]` for floating
141
- point types.
142
-
143
- With `dataType: "ome-zarr"`, tiles are instead downloaded as raw single-channel
144
- zarrita chunks and passed to OpenSeadragon with the data type `ome-zarr` (see
145
- the `OMEZarrTileData` type). Multi-channel images therefore require the `c`
146
- option. A converter from `ome-zarr` to `context2d` is registered on
147
- `OpenSeadragon.converter` when the module is imported (and by
148
- `OMEZarrTileSource.enable`). It reads the rendering settings at conversion time
149
- from the omero channel referenced by the tile data (`OMEZarrTileData.channel`).
150
- The channel object is shared by all tiles of a tile source, so advanced users
151
- may modify it (e.g. from a `tile-invalidated` handler) and re-render the cached
152
- tiles using `viewer.requestInvalidate()`.
232
+ Tiles are downloaded as raw zarrita chunks, one per rendered channel, together
233
+ with the rendering settings resolved for that tile, and passed to OpenSeadragon
234
+ with the data type `ome-zarr` (see the `OMEZarrTileData` type: `chunks`,
235
+ `ranges`, `colors`, `lutsOrColorMaps`, `inverteds` and `autoBoost` — the
236
+ arguments of ome-zarr.js `renderChunks`, one entry per chunk). A converter from
237
+ `ome-zarr` to `context2d` calls `OMEZarrTileSource.render` on demand, which
238
+ composites them into a 2D canvas context. Because the raw chunks stay in the
239
+ tile cache, tiles can be re-rendered without re-downloading them.
240
+
241
+ Settings that are neither configured nor in the omero metadata are resolved
242
+ when the tile is downloaded: to the data type range for integer types (e.g.
243
+ `[0, 65535]` for `uint16`) and `[0, 1]` for floating point types, to white, to
244
+ no LUT and to not inverted — so an image without omero metadata is rendered in
245
+ white over its full data type range.
246
+
247
+ The converters are registered on `OpenSeadragon.converter` when the module is
248
+ imported (for the OpenSeadragon instance it imports, and for a global
249
+ `OpenSeadragon` if present) and by `OMEZarrTileSource.enable`. Tiles cannot be
250
+ rendered without them, so call `enable` if the app uses a different
251
+ OpenSeadragon instance than the one resolved by this module (e.g. a separately
252
+ bundled copy).
253
+
254
+ All rendering settings in `OMEZarrTileData` are plain values, resolved from the
255
+ tile source configuration and the omero metadata when the tile is downloaded.
256
+ Editing the omero channels of a loaded image therefore does not affect tiles
257
+ that are already cached, with or without `viewer.requestInvalidate()`; render
258
+ them differently by creating a tile source with the corresponding options
259
+ instead. Tile sources that configure rendering settings do not share cached
260
+ tiles with tile sources that configure different ones.
153
261
 
154
262
  ## Example
155
263
 
156
- [Example](https://tissuumaps.github.io/OMEZarrTileSource)
264
+ [https://tissuumaps.github.io/OMEZarrTileSource?url=https://livingobjects.ebi.ac.uk/idr/zarr/v0.5/idr0062A/6001240_labels.zarr](https://tissuumaps.github.io/OMEZarrTileSource?url=https%3A%2F%2Flivingobjects.ebi.ac.uk%2Fidr%2Fzarr%2Fv0.5%2Fidr0062A%2F6001240_labels.zarr)
265
+
266
+ The example page takes the URL of the OME-Zarr image from the mandatory `url`
267
+ query parameter, and the timepoint index, the z-slice index and the channel
268
+ indices from the optional `t`, `z` and `c` query parameters (repeat `c` for
269
+ multiple channels).
157
270
 
158
271
  [Source code](index.html)
159
272
 
@@ -3,58 +3,540 @@ import { default as default_2 } from 'openseadragon';
3
3
  import { NgffImage } from 'ome-zarr.js';
4
4
  import * as zarr from 'zarrita';
5
5
 
6
+ /** An RGB color, each component in the range 0-255. */
7
+ export declare type Color = [number, number, number];
8
+
9
+ /**
10
+ * 32-bit FNV-1a hash of a string, over its UTF-16 code units.
11
+ *
12
+ * @param text - Text to hash
13
+ * @returns The hash, as an unsigned 32-bit integer
14
+ */
15
+ export declare function fnv1a(text: string): number;
16
+
17
+ /**
18
+ * Value range of a chunk's data type, used when a channel has no window.
19
+ *
20
+ * @param chunk - Chunk to get the data type range of
21
+ * @returns The minimum and maximum value of the data type (`[0, 1]` for
22
+ * floating point and boolean data)
23
+ */
24
+ export declare function getDataTypeRange(chunk: zarr.Chunk<zarr.NumberDataType | zarr.BigintDataType>): [number, number];
25
+
26
+ /**
27
+ * Whether a URL points to a zipped OME-Zarr file (`.ozx` path suffix, ignoring
28
+ * any query and fragment).
29
+ *
30
+ * @param url - URL to check, as a string or a `URL`
31
+ * @returns Whether the URL path ends in `.ozx`
32
+ */
33
+ export declare function isOZX(url: string | URL): boolean;
34
+
35
+ /**
36
+ * A color LUT (a color per scaled value, e.g. 256 entries) or a color map (a
37
+ * color per raw pixel value), as used by the omero channels.
38
+ *
39
+ * Unlike {@link Color}, the colors may carry an alpha component, e.g. to render
40
+ * unmapped label values transparently.
41
+ */
42
+ export declare type LUTOrColorMap = (Color | [number, number, number, number])[] | Map<number, Color | [number, number, number, number]>;
43
+
44
+ /**
45
+ * A loaded OME-Zarr image: the ome-zarr.js {@link NgffImage} holding the
46
+ * metadata and the opened zarrita arrays of its multiscale pyramid.
47
+ *
48
+ * Created by {@link OMEZarrTileSource.loadOMEZarr} and exposed by
49
+ * {@link OMEZarrTileSource.loaded}. Can be passed to the constructor or to
50
+ * {@link OMEZarrTileSource.open} to share one metadata load across several
51
+ * tile sources for the same URL. The tile source never modifies it.
52
+ */
53
+ export declare type OMEZarr = {
54
+ /** OME-Zarr metadata (multiscales, axes, omero) */
55
+ image: NgffImage;
56
+ /** One array per resolution level, highest resolution first */
57
+ arrays: zarr.Array<zarr.NumberDataType | zarr.BigintDataType>[];
58
+ };
59
+
60
+ /**
61
+ * Tile data of the OpenSeadragon data type `ome-zarr`.
62
+ *
63
+ * Every tile is finished with this raw data type; the converter registered by
64
+ * {@link OMEZarrTileSource.enable} (and on import) renders it to `context2d`
65
+ * on demand, so cached tiles can be re-rendered without re-downloading them.
66
+ *
67
+ * The rendering settings are the arguments of ome-zarr.js `renderChunks`, one
68
+ * entry per chunk, fully resolved from the tile source configuration and the
69
+ * omero metadata when the tile was downloaded — so changing either requires
70
+ * re-downloading the tiles.
71
+ */
6
72
  export declare interface OMEZarrTileData {
7
- chunk: zarr.Chunk<zarr.NumberDataType | zarr.BigintDataType>;
8
- channel: Channel;
9
- autoBoost?: boolean;
73
+ /** One (y, x) chunk per rendered channel, in {@link OMEZarrTileSource.cs} order */
74
+ chunks: zarr.Chunk<zarr.NumberDataType | zarr.BigintDataType>[];
75
+ /**
76
+ * The contrast limits (`[min, max]`) of each chunk
77
+ * ({@link OMEZarrTileSource.ranges}), falling back to the data type range of
78
+ * the chunk. `min` is always less than `max`.
79
+ */
80
+ ranges: [number, number][];
81
+ /**
82
+ * The RGB color of each chunk ({@link OMEZarrTileSource.colors}), falling
83
+ * back to white. Unused for chunks with a LUT or a color map.
84
+ */
85
+ colors: Color[];
86
+ /**
87
+ * The color LUT or color map of each chunk
88
+ * ({@link OMEZarrTileSource.lutsOrColorMaps}). `undefined` entries are
89
+ * rendered with their {@link colors} entry instead.
90
+ */
91
+ lutsOrColorMaps: (LUTOrColorMap | undefined)[];
92
+ /**
93
+ * Whether to invert each chunk ({@link OMEZarrTileSource.inverteds}), falling
94
+ * back to `false`.
95
+ */
96
+ inverteds: boolean[];
97
+ /** Boost the brightness of dark tiles (ome-zarr.js `renderChunks` option) */
98
+ autoBoost: boolean;
10
99
  }
11
100
 
101
+ /**
102
+ * OpenSeadragon tile source for the OME-Zarr bioimage file format.
103
+ *
104
+ * Tiles correspond to the chunks of the multiscale pyramid stored in the
105
+ * OME-Zarr image; OpenSeadragon level 0 is the lowest resolution and
106
+ * `maxLevel` the highest. Tiles are delivered as raw `ome-zarr` data (see
107
+ * {@link OMEZarrTileData}) and rendered by a registered converter.
108
+ *
109
+ * Constructing a tile source starts loading the OME-Zarr metadata
110
+ * asynchronously (unless an {@link OMEZarr} is passed). Await
111
+ * {@link OMEZarrTileSource.whenReady} or use {@link OMEZarrTileSource.open}
112
+ * before accessing {@link OMEZarrTileSource.loaded} and the resolved
113
+ * {@link OMEZarrTileSource.t}, {@link OMEZarrTileSource.z},
114
+ * {@link OMEZarrTileSource.cs}, {@link OMEZarrTileSource.channels},
115
+ * {@link OMEZarrTileSource.ranges}, {@link OMEZarrTileSource.colors},
116
+ * {@link OMEZarrTileSource.lutsOrColorMaps} and
117
+ * {@link OMEZarrTileSource.inverteds}.
118
+ *
119
+ * Importing this module registers the `ome-zarr` converters on the imported
120
+ * OpenSeadragon instance (and on a global `OpenSeadragon`, if present). Call
121
+ * {@link OMEZarrTileSource.enable} for any other OpenSeadragon instance, and to
122
+ * register the tile source for inline configurations.
123
+ */
12
124
  export declare class OMEZarrTileSource extends default_2.TileSource {
13
125
  readonly url: string;
14
- readonly zip?: boolean;
15
- readonly c?: number;
16
- readonly z?: number;
17
- readonly t?: number;
18
- readonly dataType: "ome-zarr" | "context2d";
19
- readonly autoBoost?: boolean;
126
+ readonly zip: boolean;
127
+ private readonly _t?;
128
+ private readonly _z?;
129
+ private readonly _c?;
130
+ private readonly _renderSettings;
131
+ private readonly _renderSettingsHash;
20
132
  width: number;
21
133
  height: number;
22
- private _image?;
23
- private _arrays?;
134
+ private _loaded?;
24
135
  private readonly _readyPromise;
25
- static open(config: string | OMEZarrTileSourceOptions, image?: NgffImage): Promise<OMEZarrTileSource>;
26
- constructor(url: string, image?: NgffImage);
27
- constructor(options: OMEZarrTileSourceOptions, image?: NgffImage);
28
- get image(): NgffImage;
29
- get arrays(): zarr.Array<zarr.NumberDataType | zarr.BigintDataType>[];
136
+ /**
137
+ * Registers the tile source and its `ome-zarr` data type converters with an
138
+ * OpenSeadragon instance.
139
+ *
140
+ * Required for inline configurations (`{ type: "ome-zarr", ... }`) and for
141
+ * rendering tiles with an OpenSeadragon instance other than the one imported
142
+ * by this module.
143
+ *
144
+ * @param os - The OpenSeadragon module to register with
145
+ */
146
+ static enable(os?: typeof default_2): void;
147
+ /**
148
+ * Loads the OME-Zarr metadata and opens the arrays of all resolution levels.
149
+ *
150
+ * The result can be passed to the constructor or to {@link open} to share one
151
+ * metadata load across several tile sources for the same URL.
152
+ *
153
+ * @param url - URL of the OME-Zarr image or zipped OME-Zarr file, as a string
154
+ * or a `URL`; relative URLs are resolved against the document base URL
155
+ * @param zip - Whether the URL points to a zipped OME-Zarr file; defaults to
156
+ * `true` for URLs whose path ends in `.ozx`
157
+ * @param options - `signal` aborts the load
158
+ * @returns The loaded image and its arrays, highest resolution first
159
+ * @throws If the URL is relative and there is no document base URL
160
+ */
161
+ static loadOMEZarr(url: string | URL, zip?: boolean, options?: {
162
+ signal?: AbortSignal;
163
+ }): Promise<OMEZarr>;
164
+ /**
165
+ * Constructs a tile source and waits until its OME-Zarr metadata is loaded.
166
+ *
167
+ * Equivalent to `new OMEZarrTileSource(config, loaded).whenReady()`, except
168
+ * that loading the metadata can be aborted with `signal`.
169
+ *
170
+ * @param config - URL or {@link OMEZarrTileSourceOptions}
171
+ * @param loaded - Previously loaded image of the same URL to reuse instead of
172
+ * loading it
173
+ * @param options - `signal` aborts the load
174
+ * @returns The ready tile source; rejects if loading or validation fails
175
+ */
176
+ static open(config: string | URL | OMEZarrTileSourceOptions, loaded?: OMEZarr, options?: {
177
+ signal?: AbortSignal;
178
+ }): Promise<OMEZarrTileSource>;
179
+ /**
180
+ * Renders `ome-zarr` tile data into a composite 2D canvas context.
181
+ *
182
+ * Called by the `ome-zarr` to `context2d` converter registered by
183
+ * {@link enable} (and on import), and usable directly for rendering chunks
184
+ * loaded with {@link loadChunks} outside of OpenSeadragon.
185
+ *
186
+ * The tile data holds the rendering settings exactly as passed to ome-zarr.js
187
+ * `renderChunks`: each chunk is scaled to its {@link OMEZarrTileData.ranges}
188
+ * entry, colorized with its {@link OMEZarrTileData.colors} entry or with its
189
+ * {@link OMEZarrTileData.lutsOrColorMaps} entry, inverted if its
190
+ * {@link OMEZarrTileData.inverteds} entry is set, and the results are blended
191
+ * additively. Color maps ignore the range and the inversion.
192
+ *
193
+ * @param tileData - Chunks and rendering settings of one tile
194
+ * @returns A 2D canvas context of the chunk size holding the composite
195
+ * @throws If the chunks are empty or no 2D canvas context is available
196
+ */
197
+ static render(tileData: OMEZarrTileData): CanvasRenderingContext2D;
198
+ /**
199
+ * Creates a tile source and starts loading the OME-Zarr metadata (unless
200
+ * `loaded` is given), raising `ready` or `open-failed` asynchronously.
201
+ *
202
+ * @param url - URL of the OME-Zarr image, as a string or a `URL`, resolved
203
+ * against the document base URL; zipped files are detected by the `.ozx`
204
+ * path suffix
205
+ * @param loaded - Previously loaded image of the same URL to reuse instead of
206
+ * loading it
207
+ * @throws If the URL cannot be resolved
208
+ */
209
+ constructor(url: string | URL, loaded?: OMEZarr);
210
+ /**
211
+ * Creates a tile source and starts loading the OME-Zarr metadata (unless
212
+ * `loaded` is given), raising `ready` or `open-failed` asynchronously.
213
+ *
214
+ * @param options - Tile source configuration
215
+ * @param loaded - Previously loaded image of the same URL to reuse instead of
216
+ * loading it
217
+ * @throws If the configuration is invalid or the URL cannot be resolved */
218
+ constructor(options: OMEZarrTileSourceOptions, loaded?: OMEZarr);
219
+ /**
220
+ * The loaded OME-Zarr image and arrays.
221
+ *
222
+ * @throws If the tile source is not ready yet (or failed to load)
223
+ */
224
+ get loaded(): OMEZarr;
225
+ /**
226
+ * The rendered timepoint index: the configured `t`, otherwise the omero
227
+ * `rdefs.defaultT` once loaded, otherwise `undefined` (middle timepoint).
228
+ */
229
+ get t(): number | undefined;
230
+ /**
231
+ * The rendered z-slice index: the configured `z`, otherwise the omero
232
+ * `rdefs.defaultZ` once loaded, otherwise `undefined` (middle z-slice).
233
+ */
234
+ get z(): number | undefined;
235
+ /**
236
+ * The rendered channel indices: the configured `c` option (as an array, even if a
237
+ * single index was configured), otherwise the indices of all channels marked
238
+ * active in the omero metadata once loaded, otherwise `undefined` (all
239
+ * channels).
240
+ */
241
+ get cs(): number[] | undefined;
242
+ /**
243
+ * The omero channels of the rendered channels ({@link cs}): one entry per
244
+ * rendered channel, `undefined` for channels that the omero metadata does
245
+ * not cover.
246
+ *
247
+ * `undefined` instead of the whole array if `c` was not configured and the
248
+ * tile source is not ready or the image has no omero metadata.
249
+ */
250
+ get channels(): (Channel | undefined)[] | undefined;
251
+ /**
252
+ * The contrast limits (`[min, max]`) of the rendered channels ({@link cs}):
253
+ * the configured `ranges` (as an array, even if a single range was
254
+ * configured), otherwise the windows of the corresponding omero
255
+ * {@link channels}, and `undefined` wherever those are.
256
+ *
257
+ * Channels without contrast limits (`undefined` entries, or `undefined`
258
+ * instead of the whole array) are rendered with their data type range.
259
+ */
260
+ get ranges(): ([number, number] | undefined)[] | undefined;
261
+ /**
262
+ * The RGB colors of the rendered channels ({@link cs}): the configured
263
+ * `colors` (as an array, even if a single color was configured), otherwise
264
+ * the colors of the corresponding omero {@link channels}, and `undefined`
265
+ * wherever those are.
266
+ *
267
+ * Channels without a color (`undefined` entries, or `undefined` instead of
268
+ * the whole array) are rendered in white.
269
+ */
270
+ get colors(): (Color | undefined)[] | undefined;
271
+ /**
272
+ * The color LUTs and color maps of the rendered channels ({@link cs}): the
273
+ * configured `lutsOrColorMaps` (as an array, even if a single LUT or color
274
+ * map was configured), otherwise those of the corresponding omero
275
+ * {@link channels}, and `undefined` wherever those are.
276
+ *
277
+ * Channels without a LUT or color map (`undefined` entries, or `undefined`
278
+ * instead of the whole array) are rendered with their {@link colors} entry.
279
+ */
280
+ get lutsOrColorMaps(): (LUTOrColorMap | undefined)[] | undefined;
281
+ /**
282
+ * Whether the rendered channels ({@link cs}) are inverted: the configured
283
+ * `inverteds` (as an array, even if a single value was configured),
284
+ * otherwise the inversion of the corresponding omero {@link channels}, and
285
+ * `undefined` wherever those are.
286
+ *
287
+ * Channels without an inversion (`undefined` entries, or `undefined` instead
288
+ * of the whole array) are not inverted.
289
+ */
290
+ get inverteds(): (boolean | undefined)[] | undefined;
291
+ /** Whether the brightness of dark tiles is boosted (the configured `autoBoost`) */
292
+ get autoBoost(): boolean;
293
+ /**
294
+ * Waits until the OME-Zarr metadata is loaded and validated.
295
+ *
296
+ * @returns The tile source once `ready` has been raised; rejects with the
297
+ * `open-failed` message otherwise
298
+ */
30
299
  whenReady(): Promise<this>;
300
+ /**
301
+ * Width in pixels of a resolution level.
302
+ *
303
+ * @param level - OpenSeadragon level (0 = lowest resolution); defaults to
304
+ * `maxLevel` (full resolution)
305
+ * @throws If the tile source is not ready or the level is out of bounds
306
+ */
307
+ getWidth(level?: number): number;
308
+ /**
309
+ * Height in pixels of a resolution level.
310
+ *
311
+ * @param level - OpenSeadragon level (0 = lowest resolution); defaults to
312
+ * `maxLevel` (full resolution)
313
+ * @throws If the tile source is not ready or the level is out of bounds
314
+ */
315
+ getHeight(level?: number): number;
316
+ /**
317
+ * Loads the (y, x) chunks of the rendered channels ({@link cs}) at the rendered
318
+ * timepoint and z-slice ({@link t}, {@link z}).
319
+ *
320
+ * @param level - OpenSeadragon level (0 = lowest resolution)
321
+ * @param tile - Tile coordinates within the level, or `undefined` to load the
322
+ * whole level plane
323
+ * @param options - `signal` aborts the chunk requests
324
+ * @returns One chunk per rendered channel, in {@link cs} order
325
+ * @throws If the tile source is not ready or the level or tile is out of
326
+ * bounds
327
+ */
328
+ loadChunks(level: number, tile: {
329
+ x: number;
330
+ y: number;
331
+ } | undefined, options?: {
332
+ signal?: AbortSignal;
333
+ }): Promise<zarr.Chunk<zarr.NumberDataType | zarr.BigintDataType>[]>;
334
+ /**
335
+ * Whether OpenSeadragon should use this tile source for a configuration:
336
+ * URLs whose path ends in `.ozx` and objects with `type: "ome-zarr"`.
337
+ */
31
338
  supports(data: string | object | object[] | Document): boolean;
339
+ /**
340
+ * Normalizes an OpenSeadragon configuration into
341
+ * {@link OMEZarrTileSourceOptions}.
342
+ *
343
+ * @throws For array, XML document and POST data configurations
344
+ */
32
345
  configure(data: string | object | object[] | Document, _url: string, postData?: string | null): OMEZarrTileSourceOptions;
346
+ /**
347
+ * Whether another tile source renders the same data: the URL, `zip`, the
348
+ * resolved {@link t}, {@link z} and {@link cs}, and the hash of the
349
+ * configured rendering settings.
350
+ *
351
+ * These are the components of {@link getTileHashKey}, so tile sources compare
352
+ * equal exactly when they share cached tiles. The resolved indices depend on
353
+ * the loaded metadata, so tile sources that are not ready yet compare by
354
+ * their configured indices.
355
+ */
33
356
  equals(other: default_2.TileSource): boolean;
357
+ /**
358
+ * Loads (or reuses) the OME-Zarr metadata, validates it against the
359
+ * configuration and raises `ready`, or raises `open-failed` on error.
360
+ *
361
+ * Called asynchronously by the OpenSeadragon `TileSource` constructor.
362
+ */
34
363
  getImageInfo(url: string): void;
364
+ /** Chunk width of a resolution level in pixels. */
35
365
  getTileWidth(level: number): number;
366
+ /** Chunk height of a resolution level in pixels. */
36
367
  getTileHeight(level: number): number;
368
+ /** Width of a resolution level relative to the full resolution. */
37
369
  getLevelScale(level: number): number;
370
+ /** Number of tiles (chunks) per axis of a resolution level. */
371
+ getNumTiles(level: number): default_2.Point;
372
+ /**
373
+ * Encodes the tile coordinates as `level=…&x=…&y=…` for
374
+ * {@link downloadTileStart}; no network URL is involved.
375
+ */
38
376
  getTileUrl(level: number, x: number, y: number): string;
377
+ /**
378
+ * Cache key of a tile: the image URL plus the resolved data parameters
379
+ * (`zip`, {@link t}, {@link z}, {@link cs}) and tile coordinates, and an
380
+ * FNV-1a hash of the rendering settings (the configured `ranges`, `colors`,
381
+ * `lutsOrColorMaps` and `inverteds`, and `autoBoost`), so that tile sources
382
+ * rendering the same data share cached tiles.
383
+ *
384
+ * Rendering settings that are not configured are omitted from the hash, as
385
+ * they resolve to the same omero values for the same image.
386
+ */
39
387
  getTileHashKey(level: number, x: number, y: number): string;
388
+ /**
389
+ * Loads the chunks of a tile and finishes the job with `ome-zarr` data
390
+ * ({@link OMEZarrTileData}); fails the job if loading fails.
391
+ */
40
392
  downloadTileStart(context: default_2.ImageJob): void;
393
+ /** Aborts the chunk requests of a pending tile download. */
41
394
  downloadTileAbort(context: default_2.ImageJob): void;
42
- static enable(os?: typeof default_2): void;
395
+ /**
396
+ * Normalizes a configured rendering setting into one entry per rendered
397
+ * channel.
398
+ *
399
+ * @param options - The tile source configuration
400
+ * @param name - Name of the rendering setting to normalize
401
+ * @param isValue - Whether a value is a single value rather than an array of
402
+ * values
403
+ * @param c - The normalized `c` option, to validate the length against and to
404
+ * repeat a single value for
405
+ * @returns One entry per rendered channel (`undefined` entries fall back to
406
+ * the omero metadata), or `undefined` if the setting is not configured; a
407
+ * single value is repeated for every channel in `c`, or kept as the only
408
+ * entry if `c` is not configured, in which case the getters repeat it
409
+ * @throws If the setting is neither a value nor a non-empty array of values,
410
+ * or if its length does not match `c`
411
+ */
412
+ private static _normalize;
413
+ /**
414
+ * Spreads a normalized rendering setting over the rendered channels.
415
+ *
416
+ * @param values - The normalized rendering setting, if configured
417
+ * @param channels - The omero channels of the rendered channels, if known
418
+ * @returns One entry per rendered channel: the configured entries, a single
419
+ * configured entry repeated for every rendered channel, or one `undefined`
420
+ * entry per rendered channel if the setting is not configured
421
+ */
422
+ private static _repeat;
423
+ /** Registers the `ome-zarr` → `context2d` and copy converters (once). */
43
424
  private static _learnConverters;
44
- private _getActiveChannelIndices;
45
- private static _render;
46
- private static _getDataTypeRange;
425
+ /** Size of a named axis at a resolution level (`1` if the axis is absent). */
426
+ private static _getAxisSize;
47
427
  }
48
428
 
429
+ /**
430
+ * Configuration of an {@link OMEZarrTileSource}.
431
+ *
432
+ * Also accepted by OpenSeadragon as an inline tile source configuration
433
+ * (`tileSources: { type: "ome-zarr", url, ... }`) once the tile source has been
434
+ * registered with {@link OMEZarrTileSource.enable}.
435
+ */
49
436
  export declare interface OMEZarrTileSourceOptions {
437
+ /** Tile source type for inline OpenSeadragon configurations */
50
438
  type?: "ome-zarr";
51
- url: string;
439
+ /**
440
+ * URL of the OME-Zarr image (group) or of a zipped OME-Zarr file, as a
441
+ * string or a `URL`.
442
+ *
443
+ * Relative URLs are resolved against the document base URL.
444
+ */
445
+ url: string | URL;
446
+ /**
447
+ * Whether the URL points to a zipped OME-Zarr file.
448
+ *
449
+ * Defaults to `true` for URLs whose path ends in `.ozx` (ignoring any query
450
+ * and fragment) and `false` otherwise.
451
+ */
52
452
  zip?: boolean;
53
- c?: number;
54
- z?: number;
453
+ /**
454
+ * Timepoint index (0-based) to render.
455
+ *
456
+ * Defaults to the omero `rdefs.defaultT`, or to the middle timepoint if the
457
+ * metadata has none. Validated against the image shape when the image is
458
+ * loaded.
459
+ */
55
460
  t?: number;
56
- dataType?: "ome-zarr" | "context2d";
461
+ /**
462
+ * Z-slice index (0-based) to render.
463
+ *
464
+ * Defaults to the omero `rdefs.defaultZ`, or to the middle z-slice if the
465
+ * metadata has none. Validated against the image shape when the image is
466
+ * loaded.
467
+ */
468
+ z?: number;
469
+ /**
470
+ * Channel index or indices (0-based) to render, composited in the given
471
+ * order.
472
+ *
473
+ * A single index is equivalent to an array with one element; arrays must be
474
+ * non-empty. Defaults to all channels marked active in the omero metadata
475
+ * (or to all channels if the metadata has no omero section). Validated
476
+ * against the image shape when the image is loaded.
477
+ */
478
+ c?: number | number[];
479
+ /**
480
+ * Contrast limits (`[min, max]`) to render the channels ({@link c}) with,
481
+ * overriding the omero channel windows.
482
+ *
483
+ * A single range applies to all rendered channels; arrays must have one
484
+ * entry per rendered channel, which is only checked against {@link c} if
485
+ * that is configured too. `undefined` entries fall back to the omero channel
486
+ * window (`window.start`, `window.end`), as does the default, and to the
487
+ * data type range if the metadata has none. Ignored for channels rendered
488
+ * with a color map.
489
+ */
490
+ range?: [number, number] | ([number, number] | undefined)[];
491
+ /**
492
+ * RGB color to render the channels ({@link c}) with, overriding the omero
493
+ * channel colors.
494
+ *
495
+ * A single color applies to all rendered channels; arrays must have one
496
+ * color per rendered channel, which is only checked against {@link c} if
497
+ * that is configured too. `undefined` entries fall back to the omero channel
498
+ * color, as does the default, and to white if the metadata has none. Ignored
499
+ * for channels rendered with a LUT or color map.
500
+ */
501
+ color?: Color | (Color | undefined)[];
502
+ /**
503
+ * Color LUT or color map to render the channels ({@link c}) with, overriding
504
+ * the omero channel LUTs and color maps.
505
+ *
506
+ * A single LUT or color map applies to all rendered channels; arrays must
507
+ * have one entry per rendered channel, which is only checked against
508
+ * {@link c} if that is configured too. `undefined` entries fall back to the
509
+ * omero channel LUT or color map, as does the default, and to rendering with
510
+ * {@link color} if the metadata has none. A color map also overrides
511
+ * {@link range} and {@link inverted}.
512
+ */
513
+ lutOrColorMap?: LUTOrColorMap | (LUTOrColorMap | undefined)[];
514
+ /**
515
+ * Whether to invert the channels ({@link c}), overriding the omero channel
516
+ * inversion.
517
+ *
518
+ * A single value applies to all rendered channels; arrays must have one
519
+ * entry per rendered channel, which is only checked against {@link c} if
520
+ * that is configured too. `undefined` entries fall back to the omero channel
521
+ * inversion, as does the default, and to `false` if the metadata has none.
522
+ * Ignored for channels rendered with a color map.
523
+ */
524
+ inverted?: boolean | (boolean | undefined)[];
525
+ /**
526
+ * Boost the brightness of dark tiles (ome-zarr.js `renderChunks` option).
527
+ *
528
+ * Defaults to `false`.
529
+ */
57
530
  autoBoost?: boolean;
58
531
  }
59
532
 
533
+ /**
534
+ * Resolves a URL against the document base URL.
535
+ *
536
+ * @param url - URL of the OME-Zarr image, as a string or a `URL`
537
+ * @returns The absolute URL
538
+ * @throws If the URL is relative and there is no document base URL
539
+ */
540
+ export declare function resolveUrl(url: string | URL): URL;
541
+
60
542
  export { }
@@ -2,43 +2,158 @@ import e from "@zarrita/storage/zip";
2
2
  import { NgffImage as t, getSlices as n, renderChunks as r } from "ome-zarr.js";
3
3
  import i from "openseadragon";
4
4
  import * as a from "zarrita";
5
+ //#region src/utils.ts
6
+ function o(e) {
7
+ let t = 2166136261;
8
+ for (let n = 0; n < e.length; n++) t = Math.imul(t ^ e.charCodeAt(n), 16777619);
9
+ return t >>> 0;
10
+ }
11
+ function s(e) {
12
+ return new URL(e, globalThis.document?.baseURI);
13
+ }
14
+ function c(e) {
15
+ try {
16
+ return s(e).pathname.endsWith(".ozx");
17
+ } catch {
18
+ return e.toString().endsWith(".ozx");
19
+ }
20
+ }
21
+ function l(e) {
22
+ let { data: t } = e;
23
+ return t instanceof Int8Array ? [-128, 127] : t instanceof Uint8Array ? [0, 255] : t instanceof Int16Array ? [-32768, 32767] : t instanceof Uint16Array ? [0, 65535] : t instanceof Int32Array ? [-2147483648, 2147483647] : t instanceof Uint32Array ? [0, 4294967295] : t instanceof BigInt64Array ? [-(2 ** 63), 2 ** 63 - 1] : t instanceof BigUint64Array ? [0, 2 ** 64 - 1] : [0, 1];
24
+ }
25
+ //#endregion
5
26
  //#region src/OMEZarrTileSource.ts
6
- var o = class o extends i.TileSource {
7
- zip;
8
- c;
9
- z;
10
- t;
11
- dataType;
12
- autoBoost;
27
+ var u = class u extends i.TileSource {
28
+ zip = !1;
29
+ _t;
30
+ _z;
31
+ _c;
32
+ _renderSettings;
33
+ _renderSettingsHash;
13
34
  width = 10;
14
35
  height = 10;
15
- _image;
16
- _arrays;
36
+ _loaded;
17
37
  _readyPromise = new Promise((e, t) => {
18
38
  this.addOnceHandler("ready", () => e(this)), this.addOnceHandler("open-failed", (e) => t(Error(e.message)));
19
39
  });
20
40
  static {
21
- o._learnConverters(i);
41
+ u._learnConverters(i);
42
+ }
43
+ static enable(e = i) {
44
+ Object.assign(e, { OMEZarrTileSource: u }), u._learnConverters(e);
45
+ }
46
+ static async loadOMEZarr(n, r, i) {
47
+ let { signal: a } = i ?? {};
48
+ a?.throwIfAborted(), n = s(n), r ??= c(n);
49
+ let o = await t.load(r ? e.fromUrl(n) : n.toString(), { signal: a });
50
+ return {
51
+ image: o,
52
+ arrays: await Promise.all(o.paths.map((e) => o.openArray(e, { signal: a })))
53
+ };
22
54
  }
23
- static open(e, t) {
24
- return new o(e, t).whenReady();
55
+ static async open(e, t, n) {
56
+ let { signal: r } = n ?? {};
57
+ return r?.throwIfAborted(), t ??= typeof e == "string" || e instanceof URL ? await u.loadOMEZarr(e, void 0, { signal: r }) : await u.loadOMEZarr(e.url, e.zip, { signal: r }), new u(e, t).whenReady();
58
+ }
59
+ static render(e) {
60
+ let { chunks: t, ranges: n, colors: i, lutsOrColorMaps: a, inverteds: o, autoBoost: s } = e, c = t[0].shape[1], l = t[0].shape[0], u = document.createElement("canvas");
61
+ u.width = c, u.height = l;
62
+ let d = u.getContext("2d");
63
+ if (d === null) throw Error("failed to get 2D canvas context");
64
+ let f = r(t, n, i, a.map((e) => Array.isArray(e) ? [...e] : e), o, s), p = new ImageData(f, c, l);
65
+ return d.putImageData(p, 0, 0), d;
25
66
  }
26
67
  constructor(e, t) {
27
- typeof e == "string" ? (super(e), this.url = e, this.dataType = "context2d") : (super(e.url), this.url = e.url, this.zip = e.zip, this.c = e.c, this.z = e.z, this.t = e.t, this.dataType = e.dataType ?? "context2d", this.autoBoost = e.autoBoost), this._image = t, this._readyPromise.catch(() => {});
68
+ let n = typeof e == "string" || e instanceof URL ? { url: e } : e, r = s(n.url), i;
69
+ if (n.c !== void 0) if (Array.isArray(n.c)) {
70
+ if (n.c.length === 0) throw Error("c array must be non-empty");
71
+ i = n.c;
72
+ } else i = [n.c];
73
+ let a = u._normalize(n, "range", (e) => Array.isArray(e) && e.length === 2 && e.every((e) => typeof e == "number"), i), l = u._normalize(n, "color", (e) => Array.isArray(e) && e.length === 3 && e.every((e) => typeof e == "number"), i), d = u._normalize(n, "lutOrColorMap", (e) => e instanceof Map || Array.isArray(e) && e.length > 0 && e.every((e) => Array.isArray(e) && (e.length === 3 || e.length === 4) && e.every((e) => typeof e == "number")), i), f = u._normalize(n, "inverted", (e) => typeof e == "boolean", i);
74
+ super(r.toString()), this.url = r.toString(), this.zip = n.zip ?? c(r), this._t = n.t, this._z = n.z, this._c = i, this._renderSettings = {
75
+ ranges: a,
76
+ colors: l,
77
+ lutsOrColorMaps: d,
78
+ inverteds: f,
79
+ autoBoost: n.autoBoost ?? !1
80
+ }, this._renderSettingsHash = o(JSON.stringify({
81
+ ...this._renderSettings,
82
+ lutsOrColorMaps: d?.map((e) => e instanceof Map ? [...e].sort(([e], [t]) => e - t) : e)
83
+ })), this._loaded = t, this._readyPromise.catch(() => {});
84
+ }
85
+ get loaded() {
86
+ if (this._loaded === void 0) throw Error("tile source not ready");
87
+ return this._loaded;
88
+ }
89
+ get t() {
90
+ return this._t === void 0 ? this._loaded?.image.omero?.rdefs?.defaultT : this._t;
91
+ }
92
+ get z() {
93
+ return this._z === void 0 ? this._loaded?.image.omero?.rdefs?.defaultZ : this._z;
94
+ }
95
+ get cs() {
96
+ return this._c === void 0 ? this._loaded?.image.omero?.channels?.flatMap((e, t) => e.active === !1 ? [] : [t]) : this._c;
97
+ }
98
+ get channels() {
99
+ return this._c === void 0 ? this._loaded?.image.omero?.channels?.filter((e) => e.active !== !1) : this._c.map((e) => this._loaded?.image.omero?.channels?.[e]);
100
+ }
101
+ get ranges() {
102
+ let e = this.channels, t = (e) => {
103
+ if (e?.window.start !== void 0 && e.window.end !== void 0) return [e.window.start, e.window.end];
104
+ };
105
+ return u._repeat(this._renderSettings.ranges, e)?.map((n, r) => n ?? t(e?.[r]));
106
+ }
107
+ get colors() {
108
+ let e = this.channels, t = (e) => {
109
+ if (e === void 0) return;
110
+ let t = e.color.replace(/^#/, "");
111
+ return [
112
+ parseInt(t.slice(0, 2), 16),
113
+ parseInt(t.slice(2, 4), 16),
114
+ parseInt(t.slice(4, 6), 16)
115
+ ];
116
+ };
117
+ return u._repeat(this._renderSettings.colors, e)?.map((n, r) => n ?? t(e?.[r]));
118
+ }
119
+ get lutsOrColorMaps() {
120
+ let e = this.channels, t = (e) => e?.lut ?? e?.colorMap;
121
+ return u._repeat(this._renderSettings.lutsOrColorMaps, e)?.map((n, r) => n ?? t(e?.[r]));
28
122
  }
29
- get image() {
30
- if (this._image === void 0) throw Error("tile source not ready");
31
- return this._image;
123
+ get inverteds() {
124
+ let e = this.channels;
125
+ return u._repeat(this._renderSettings.inverteds, e)?.map((t, n) => t ?? e?.[n]?.inverted);
32
126
  }
33
- get arrays() {
34
- if (this._arrays === void 0) throw Error("tile source not ready");
35
- return this._arrays;
127
+ get autoBoost() {
128
+ return this._renderSettings.autoBoost;
36
129
  }
37
130
  whenReady() {
38
131
  return this._readyPromise;
39
132
  }
133
+ getWidth(e = this.maxLevel) {
134
+ if (e < 0 || e > this.maxLevel) throw Error("level out of bounds");
135
+ return u._getAxisSize(this.loaded, "x", this.maxLevel - e);
136
+ }
137
+ getHeight(e = this.maxLevel) {
138
+ if (e < 0 || e > this.maxLevel) throw Error("level out of bounds");
139
+ return u._getAxisSize(this.loaded, "y", this.maxLevel - e);
140
+ }
141
+ loadChunks(e, t, r) {
142
+ let i = {
143
+ t: this.t,
144
+ z: this.z
145
+ };
146
+ if (t !== void 0) {
147
+ let { x: n, y: r } = this.getNumTiles(e);
148
+ if (t.x < 0 || t.x >= n || t.y < 0 || t.y >= r) throw Error("tile out of bounds");
149
+ let a = this.getTileWidth(e), o = this.getTileHeight(e);
150
+ i.x = [t.x * a, (t.x + 1) * a], i.y = [t.y * o, (t.y + 1) * o];
151
+ }
152
+ let o = u._getAxisSize(this.loaded, "c"), s = n(this.cs ?? Array.from({ length: o }, (e, t) => t), this.loaded.arrays[this.maxLevel - e].shape, this.loaded.image.getAxesNames(), i);
153
+ return Promise.all(s.map((t) => a.get(this.loaded.arrays[this.maxLevel - e], t, r)));
154
+ }
40
155
  supports(e) {
41
- return Array.isArray(e) || e instanceof Document ? !1 : typeof e == "string" ? e.endsWith(".ozx") : "type" in e && e.type === "ome-zarr";
156
+ return Array.isArray(e) || e instanceof Document ? !1 : typeof e == "string" ? c(e) : "type" in e && e.type === "ome-zarr";
42
157
  }
43
158
  configure(e, t, n = null) {
44
159
  if (Array.isArray(e)) throw Error("configuration from array is not supported");
@@ -53,11 +168,11 @@ var o = class o extends i.TileSource {
53
168
  };
54
169
  }
55
170
  equals(e) {
56
- return e instanceof o && this.url === e.url && this.zip === e.zip && this.c === e.c && this.z === e.z && this.t === e.t && this.dataType === e.dataType && this.autoBoost === e.autoBoost;
171
+ return e instanceof u && this.url === e.url && this.zip === e.zip && this.t === e.t && this.z === e.z && this.cs?.join(",") === e.cs?.join(",") && this._renderSettingsHash === e._renderSettingsHash;
57
172
  }
58
- getImageInfo(n) {
59
- Promise.resolve(this._image ?? t.load(this.zip || this.zip === void 0 && n.endsWith(".ozx") ? e.fromUrl(n) : n)).then(async (e) => {
60
- let t = e.getAxesNames();
173
+ getImageInfo(e) {
174
+ Promise.resolve(this._loaded ?? u.loadOMEZarr(e, this.zip)).then((e) => {
175
+ let t = e.image.getAxesNames();
61
176
  for (let e of t) if (![
62
177
  "t",
63
178
  "c",
@@ -67,38 +182,39 @@ var o = class o extends i.TileSource {
67
182
  ].includes(e)) throw Error(`unsupported axis: ${e}`);
68
183
  if (!t.includes("x") || !t.includes("y")) throw Error("missing X or Y axis");
69
184
  if (t.indexOf("y") > t.indexOf("x")) throw Error("X axis must come after Y axis");
70
- let n = await Promise.all(e.paths.map((t) => e.openArray(t))), r = t.indexOf("t"), i = r >= 0 ? n[0].shape[r] : 1;
71
- if (this.t !== void 0 && (this.t < 0 || this.t >= i)) throw Error(`Invalid t index ${this.t} for image with ${i} timepoints`);
72
- let a = t.indexOf("z"), o = a >= 0 ? n[0].shape[a] : 1;
73
- if (this.z !== void 0 && (this.z < 0 || this.z >= o)) throw Error(`Invalid z index ${this.z} for image with ${o} z-slices`);
74
- let s = t.indexOf("c"), c = s >= 0 ? n[0].shape[s] : 1;
75
- if (this.c !== void 0 && (this.c < 0 || this.c >= c)) throw Error(`Invalid c index ${this.c} for image with ${c} channels`);
76
- if (this.dataType !== "context2d" && this.c === void 0 && c > 1) throw Error(`Multi-channel image with ${c} channels; specify c or render as "context2d"`);
77
- let l = e.checkChannelIndex(this.c ?? 0);
78
- if (l.channels.length !== c) throw Error(`OME-Zarr metadata lists ${l.channels.length} channels, but the image has ${c}`);
79
- if (this._getActiveChannelIndices(l).length === 0) throw Error("No active channels; specify c or activate channels in the OME-Zarr metadata");
80
- this._image = e, this._arrays = n, this.width = n[0].shape[t.indexOf("x")], this.height = n[0].shape[t.indexOf("y")], this.maxLevel = n.length - 1, this.raiseEvent("ready", { tileSource: this });
81
- }).catch((e) => {
82
- this._image = void 0, this._arrays = void 0, this.width = 10, this.height = 10, this.maxLevel = 0, this.raiseEvent("open-failed", {
83
- message: `failed to get image info for ${n}: ${e}`,
84
- source: n
185
+ let n = u._getAxisSize(e, "t");
186
+ if (this._t !== void 0 && (this._t < 0 || this._t >= n)) throw Error(`Invalid t index ${this._t} for image with ${n} timepoints`);
187
+ let r = u._getAxisSize(e, "z");
188
+ if (this._z !== void 0 && (this._z < 0 || this._z >= r)) throw Error(`Invalid z index ${this._z} for image with ${r} z-slices`);
189
+ let i = u._getAxisSize(e, "c");
190
+ if (this._c !== void 0 && this._c.some((e) => e < 0 || e >= i)) throw Error(`Invalid c indices [${this._c?.join(", ")}] for image with ${i} channels`);
191
+ if (e.image.omero !== void 0 && e.image.omero.channels !== void 0 && e.image.omero.channels.length !== i) throw Error(`OME-Zarr metadata lists ${e.image.omero.channels.length} channels, but the image has ${i}`);
192
+ if (this._c === void 0 && e.image.omero !== void 0 && e.image.omero.channels !== void 0 && !e.image.omero.channels.some((e) => e.active !== !1)) throw Error("No active channels; specify c or activate channels in the OME-Zarr metadata");
193
+ this._loaded = e, this.width = e.arrays[0].shape[t.indexOf("x")], this.height = e.arrays[0].shape[t.indexOf("y")], this.maxLevel = e.arrays.length - 1, this.raiseEvent("ready", { tileSource: this });
194
+ }).catch((t) => {
195
+ this._loaded = void 0, this.width = 10, this.height = 10, this.maxLevel = 0, this.raiseEvent("open-failed", {
196
+ message: `failed to get image info for ${e}: ${t}`,
197
+ source: e
85
198
  });
86
199
  });
87
200
  }
88
201
  getTileWidth(e) {
89
- if (this._image === void 0 || this._arrays === void 0) throw Error("tile source not ready");
202
+ let { image: t, arrays: n } = this.loaded;
90
203
  if (e < 0 || e > this.maxLevel) throw Error("level out of bounds");
91
- return this._arrays[this.maxLevel - e].chunks[this._image.getAxesNames().indexOf("x")];
204
+ return n[this.maxLevel - e].chunks[t.getAxesNames().indexOf("x")];
92
205
  }
93
206
  getTileHeight(e) {
94
- if (this._image === void 0 || this._arrays === void 0) throw Error("tile source not ready");
207
+ let { image: t, arrays: n } = this.loaded;
95
208
  if (e < 0 || e > this.maxLevel) throw Error("level out of bounds");
96
- return this._arrays[this.maxLevel - e].chunks[this._image.getAxesNames().indexOf("y")];
209
+ return n[this.maxLevel - e].chunks[t.getAxesNames().indexOf("y")];
97
210
  }
98
211
  getLevelScale(e) {
99
- if (this._image === void 0 || this._arrays === void 0) throw Error("tile source not ready");
100
212
  if (e < 0 || e > this.maxLevel) throw Error("level out of bounds");
101
- return this._arrays[this.maxLevel - e].shape[this._image.getAxesNames().indexOf("x")] / this.width;
213
+ return this.getWidth(e) / this.width;
214
+ }
215
+ getNumTiles(e) {
216
+ if (e < 0 || e > this.maxLevel) throw Error("level out of bounds");
217
+ return new i.Point(Math.ceil(this.getWidth(e) / this.getTileWidth(e)), Math.ceil(this.getHeight(e) / this.getTileHeight(e)));
102
218
  }
103
219
  getTileUrl(e, t, n) {
104
220
  let r = new URLSearchParams();
@@ -106,84 +222,77 @@ var o = class o extends i.TileSource {
106
222
  }
107
223
  getTileHashKey(e, t, n) {
108
224
  let r = new URL(this.url);
109
- return r.searchParams.append("level", e.toString()), r.searchParams.append("x", t.toString()), r.searchParams.append("y", n.toString()), this.zip !== void 0 && r.searchParams.append("zip", this.zip.toString()), this.c !== void 0 && r.searchParams.append("c", this.c.toString()), this.z !== void 0 && r.searchParams.append("z", this.z.toString()), this.t !== void 0 && r.searchParams.append("t", this.t.toString()), r.searchParams.append("dataType", this.dataType), this.autoBoost !== void 0 && r.searchParams.append("autoBoost", this.autoBoost.toString()), r.toString();
225
+ return r.searchParams.append("zip", this.zip.toString()), this.t !== void 0 && r.searchParams.append("t", this.t.toString()), this.z !== void 0 && r.searchParams.append("z", this.z.toString()), this.cs !== void 0 && r.searchParams.append("c", this.cs.join(",")), r.searchParams.append("y", n.toString()), r.searchParams.append("x", t.toString()), r.searchParams.append("level", e.toString()), r.searchParams.append("render", this._renderSettingsHash.toString(16)), r.toString();
110
226
  }
111
227
  downloadTileStart(e) {
112
- let t = e.userData, r = new AbortController();
113
- t.abortController = r;
114
- let i = new URLSearchParams(e.src), s = +i.get("level"), c = +i.get("x"), l = +i.get("y");
228
+ let t = e.userData, n = new AbortController();
229
+ t.abortController = n;
230
+ let r = new URLSearchParams(e.src), i = +r.get("level"), a = +r.get("x"), o = +r.get("y");
115
231
  try {
116
- if (this._image === void 0 || this._arrays === void 0) throw Error("tile source not ready");
117
- let t = this._arrays[this.maxLevel - s], i = this._image.checkChannelIndex(this.c ?? 0), u = this._getActiveChannelIndices(i), d = u.map((e) => i.channels[e]), f = this.getTileWidth(s), p = this.getTileHeight(s), m = n(u, t.shape, this._image.getAxesNames(), {
118
- x: [c * f, (c + 1) * f],
119
- y: [l * p, (l + 1) * p],
120
- z: this.z ?? i.rdefs?.defaultZ,
121
- t: this.t ?? i.rdefs?.defaultT
122
- });
123
- Promise.all(m.map((e) => a.get(t, e, { signal: r.signal }))).then((t) => {
124
- if (this.dataType === "context2d") {
125
- let n = o._render(t, d, this.autoBoost);
126
- e.finish(n, null, "context2d");
127
- } else {
128
- let n = {
129
- chunk: t[0],
130
- channel: d[0],
131
- autoBoost: this.autoBoost
132
- };
133
- e.finish(n, null, "ome-zarr");
134
- }
135
- }).catch((t) => {
136
- let n = r.signal.aborted;
137
- r.abort(), n || e.fail(`failed to render tile for level=${s}, x=${c}, y=${l}: ${String(t)}`, null);
232
+ this.loadChunks(i, {
233
+ x: a,
234
+ y: o
235
+ }, { signal: n.signal }).then((n) => {
236
+ let r = this.ranges, i = this.colors, a = this.inverteds, o = this.lutsOrColorMaps, s = this.autoBoost, c = {
237
+ chunks: n,
238
+ ranges: n.map((e, t) => {
239
+ let [n, i] = r?.[t] ?? l(e);
240
+ return n < i ? [n, i] : [n, n + 1];
241
+ }),
242
+ colors: n.map((e, t) => i?.[t] ?? [
243
+ 255,
244
+ 255,
245
+ 255
246
+ ]),
247
+ lutsOrColorMaps: n.map((e, t) => o?.[t]),
248
+ inverteds: n.map((e, t) => a?.[t] ?? !1),
249
+ autoBoost: s
250
+ };
251
+ t.abortController = void 0, e.finish(c, null, "ome-zarr");
252
+ }, (t) => {
253
+ let r = n.signal.aborted;
254
+ n.abort(), r || e.fail(`failed to load chunks for level=${i}, x=${a}, y=${o}: ${String(t)}`, null);
138
255
  });
139
256
  } catch (t) {
140
- e.fail(`failed to download tile for level=${s}, x=${c}, y=${l}: ${String(t)}`, null);
257
+ e.fail(`failed to download tile for level=${i}, x=${a}, y=${o}: ${String(t)}`, null);
141
258
  }
142
259
  }
143
260
  downloadTileAbort(e) {
144
261
  let t = e.userData;
145
262
  t.abortController !== void 0 && (t.abortController.abort(), t.abortController = void 0);
146
263
  }
147
- static enable(e = i) {
148
- Object.assign(e, { OMEZarrTileSource: o }), o._learnConverters(e);
264
+ static _normalize(e, t, n, r) {
265
+ let i = e[t];
266
+ if (i !== void 0) {
267
+ if (n(i)) {
268
+ let e = i;
269
+ return r === void 0 ? [e] : r.map(() => e);
270
+ }
271
+ if (!Array.isArray(i) || !i.every((e) => e === void 0 || n(e))) throw Error(`${t} must be a value or an array of values`);
272
+ if (i.length === 0) throw Error(`${t} array must be non-empty`);
273
+ if (r !== void 0 && i.length !== r.length) throw Error(`${t} array length ${i.length} does not match number of channels ${r.length} in c`);
274
+ return i;
275
+ }
276
+ }
277
+ static _repeat(e, t) {
278
+ return e === void 0 ? t?.map(() => void 0) : e.length === 1 && t !== void 0 ? t.map(() => e[0]) : e;
149
279
  }
150
280
  static _learnConverters(e) {
151
- e.converter.existsType("ome-zarr") || (e.converter.learn("ome-zarr", "context2d", (e, { chunk: t, channel: n, autoBoost: r }) => o._render([t], [n], r), 1, 1), e.converter.learn("ome-zarr", "ome-zarr", (e, t) => ({
281
+ e.converter.existsType("ome-zarr") || (e.converter.learn("ome-zarr", "context2d", (e, t) => u.render(t), 1, 1), e.converter.learn("ome-zarr", "ome-zarr", (e, t) => ({
152
282
  ...t,
153
- chunk: {
154
- ...t.chunk,
155
- data: t.chunk.data.slice()
156
- }
283
+ chunks: t.chunks.map((e) => ({
284
+ ...e,
285
+ data: e.data.slice()
286
+ }))
157
287
  })));
158
288
  }
159
- _getActiveChannelIndices(e) {
160
- return this.c !== void 0 || this.dataType !== "context2d" ? [this.c ?? 0] : e.channels.flatMap((e, t) => e.active === !1 ? [] : [t]);
161
- }
162
- static _render(e, t, n) {
163
- let i = t.map((e) => {
164
- let t = e.color.replace(/^#/, "");
165
- return [
166
- parseInt(t.slice(0, 2), 16),
167
- parseInt(t.slice(2, 4), 16),
168
- parseInt(t.slice(4, 6), 16)
169
- ];
170
- }), a = r(e, t.map((t, n) => {
171
- let [r, i] = t.window.start !== void 0 && t.window.end !== void 0 ? [t.window.start, t.window.end] : o._getDataTypeRange(e[n]);
172
- return r < i ? [r, i] : [r, r + 1];
173
- }), i, t.map((e) => e.lut ?? e.colorMap), t.map((e) => e.inverted === !0), n), s = e[0].shape[1], c = e[0].shape[0], l = document.createElement("canvas");
174
- l.width = s, l.height = c;
175
- let u = l.getContext("2d");
176
- if (u === null) throw Error("failed to get 2D canvas context");
177
- let d = new ImageData(a, s, c);
178
- return u.putImageData(d, 0, 0), u;
179
- }
180
- static _getDataTypeRange(e) {
181
- let { data: t } = e;
182
- return t instanceof Int8Array ? [-128, 127] : t instanceof Uint8Array ? [0, 255] : t instanceof Int16Array ? [-32768, 32767] : t instanceof Uint16Array ? [0, 65535] : t instanceof Int32Array ? [-2147483648, 2147483647] : t instanceof Uint32Array ? [0, 4294967295] : t instanceof BigInt64Array ? [-(2 ** 63), 2 ** 63 - 1] : t instanceof BigUint64Array ? [0, 2 ** 64 - 1] : [0, 1];
289
+ static _getAxisSize(e, t, n = 0) {
290
+ let r = e.image.getAxesNames().indexOf(t);
291
+ return r >= 0 ? e.arrays[n].shape[r] : 1;
183
292
  }
184
293
  };
185
294
  //#endregion
186
295
  //#region src/main.ts
187
- globalThis.OpenSeadragon && o.enable(globalThis.OpenSeadragon);
296
+ globalThis.OpenSeadragon && u.enable(globalThis.OpenSeadragon);
188
297
  //#endregion
189
- export { o as OMEZarrTileSource };
298
+ export { u as OMEZarrTileSource, o as fnv1a, l as getDataTypeRange, c as isOZX, s as resolveUrl };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omezarr-tilesource",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "An OpenSeadragon tile source for the OME-Zarr bioimage file format",
5
5
  "homepage": "https://github.com/TissUUmaps/OMEZarrTileSource#readme",
6
6
  "bugs": "https://github.com/TissUUmaps/OMEZarrTileSource/issues",