@fideus-labs/fidnii 0.2.0 → 0.4.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.
Files changed (45) hide show
  1. package/dist/BufferManager.d.ts +37 -4
  2. package/dist/BufferManager.d.ts.map +1 -1
  3. package/dist/BufferManager.js +70 -22
  4. package/dist/BufferManager.js.map +1 -1
  5. package/dist/OMEZarrNVImage.d.ts +26 -0
  6. package/dist/OMEZarrNVImage.d.ts.map +1 -1
  7. package/dist/OMEZarrNVImage.js +144 -24
  8. package/dist/OMEZarrNVImage.js.map +1 -1
  9. package/dist/RegionCoalescer.d.ts +16 -0
  10. package/dist/RegionCoalescer.d.ts.map +1 -1
  11. package/dist/RegionCoalescer.js +42 -5
  12. package/dist/RegionCoalescer.js.map +1 -1
  13. package/dist/ResolutionSelector.d.ts +14 -2
  14. package/dist/ResolutionSelector.d.ts.map +1 -1
  15. package/dist/ResolutionSelector.js +26 -16
  16. package/dist/ResolutionSelector.js.map +1 -1
  17. package/dist/fromTiff.d.ts +51 -0
  18. package/dist/fromTiff.d.ts.map +1 -0
  19. package/dist/fromTiff.js +64 -0
  20. package/dist/fromTiff.js.map +1 -0
  21. package/dist/index.d.ts +10 -4
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +7 -35
  24. package/dist/index.js.map +1 -1
  25. package/dist/normalize.d.ts +50 -0
  26. package/dist/normalize.d.ts.map +1 -0
  27. package/dist/normalize.js +95 -0
  28. package/dist/normalize.js.map +1 -0
  29. package/dist/types.d.ts +66 -1
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/types.js +66 -0
  32. package/dist/types.js.map +1 -1
  33. package/dist/utils/coordinates.d.ts.map +1 -1
  34. package/dist/utils/coordinates.js +20 -26
  35. package/dist/utils/coordinates.js.map +1 -1
  36. package/package.json +4 -4
  37. package/src/BufferManager.ts +83 -22
  38. package/src/OMEZarrNVImage.ts +190 -24
  39. package/src/RegionCoalescer.ts +45 -5
  40. package/src/ResolutionSelector.ts +32 -16
  41. package/src/fromTiff.ts +88 -0
  42. package/src/index.ts +18 -2
  43. package/src/normalize.ts +119 -0
  44. package/src/types.ts +95 -1
  45. package/src/utils/coordinates.ts +26 -24
@@ -93,51 +93,67 @@ export function selectResolution(
93
93
  }
94
94
 
95
95
  /**
96
- * Get the chunk shape for a volume.
96
+ * Get the chunk shape for a volume as [z, y, x].
97
+ *
98
+ * Looks up `"z"`, `"y"`, `"x"` dimensions by name. If `"z"` is absent
99
+ * (e.g., 2D images with `dims=["y", "x"]` or `["y", "x", "c"]`),
100
+ * the z chunk size defaults to 1. Non-spatial dimensions like `"c"` and
101
+ * `"t"` are ignored.
97
102
  *
98
103
  * @param ngffImage - The NgffImage to get chunk shape from
99
104
  * @returns Chunk shape as [z, y, x]
105
+ * @throws If neither `"y"` nor `"x"` can be found in dims
100
106
  */
101
107
  export function getChunkShape(ngffImage: NgffImage): [number, number, number] {
102
108
  const chunks = ngffImage.data.chunks
103
109
  const dims = ngffImage.dims
104
110
 
105
- // Find z, y, x indices in dims
106
- const zIdx = dims.indexOf("z")
107
111
  const yIdx = dims.indexOf("y")
108
112
  const xIdx = dims.indexOf("x")
109
113
 
110
- if (zIdx === -1 || yIdx === -1 || xIdx === -1) {
111
- // Fallback: assume last 3 dimensions are z, y, x
112
- const n = chunks.length
113
- return [chunks[n - 3] || 1, chunks[n - 2] || 1, chunks[n - 1] || 1]
114
+ if (yIdx === -1 || xIdx === -1) {
115
+ throw new Error(
116
+ `Cannot determine chunk shape: dims=[${dims.join(",")}] ` +
117
+ `is missing required "y" and/or "x" axes`,
118
+ )
114
119
  }
115
120
 
116
- return [chunks[zIdx], chunks[yIdx], chunks[xIdx]]
121
+ const zIdx = dims.indexOf("z")
122
+ const cz = zIdx !== -1 ? chunks[zIdx] : 1
123
+
124
+ return [cz, chunks[yIdx], chunks[xIdx]]
117
125
  }
118
126
 
119
127
  /**
120
- * Get the shape of a volume as [z, y, x].
128
+ * Get the spatial shape of a volume as [z, y, x].
129
+ *
130
+ * Looks up `"z"`, `"y"`, `"x"` dimensions by name. If `"z"` is absent
131
+ * (e.g., 2D images with `dims=["y", "x"]` or `["y", "x", "c"]`),
132
+ * the z size defaults to 1. Non-spatial dimensions like `"c"` and
133
+ * `"t"` are ignored.
121
134
  *
122
135
  * @param ngffImage - The NgffImage
123
136
  * @returns Shape as [z, y, x]
137
+ * @throws If neither `"y"` nor `"x"` can be found in dims
124
138
  */
125
139
  export function getVolumeShape(ngffImage: NgffImage): [number, number, number] {
126
140
  const shape = ngffImage.data.shape
127
141
  const dims = ngffImage.dims
128
142
 
129
- // Find z, y, x indices in dims
130
- const zIdx = dims.indexOf("z")
131
143
  const yIdx = dims.indexOf("y")
132
144
  const xIdx = dims.indexOf("x")
133
145
 
134
- if (zIdx === -1 || yIdx === -1 || xIdx === -1) {
135
- // Fallback: assume last 3 dimensions are z, y, x
136
- const n = shape.length
137
- return [shape[n - 3] || 1, shape[n - 2] || 1, shape[n - 1] || 1]
146
+ if (yIdx === -1 || xIdx === -1) {
147
+ throw new Error(
148
+ `Cannot determine volume shape: dims=[${dims.join(",")}] ` +
149
+ `is missing required "y" and/or "x" axes`,
150
+ )
138
151
  }
139
152
 
140
- return [shape[zIdx], shape[yIdx], shape[xIdx]]
153
+ const zIdx = dims.indexOf("z")
154
+ const sz = zIdx !== -1 ? shape[zIdx] : 1
155
+
156
+ return [sz, shape[yIdx], shape[xIdx]]
141
157
  }
142
158
 
143
159
  /**
@@ -0,0 +1,88 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) Fideus Labs LLC
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ /**
5
+ * Load TIFF files (OME-TIFF, pyramidal, plain) and produce a
6
+ * {@link Multiscales} object ready for {@link OMEZarrNVImage.create}.
7
+ *
8
+ * Uses {@link https://github.com/fideus-labs/fiff | @fideus-labs/fiff}
9
+ * to present TIFFs as zarrita-compatible stores, then delegates to
10
+ * {@link fromNgffZarr} for OME-Zarr metadata parsing.
11
+ */
12
+
13
+ import type { TiffStoreOptions } from "@fideus-labs/fiff"
14
+ import { TiffStore } from "@fideus-labs/fiff"
15
+ import type { Multiscales } from "@fideus-labs/ngff-zarr"
16
+ import type { FromNgffZarrOptions } from "@fideus-labs/ngff-zarr/browser"
17
+ import { fromNgffZarr } from "@fideus-labs/ngff-zarr/browser"
18
+ import type { Readable } from "zarrita"
19
+
20
+ /** Options for {@link fromTiff}. */
21
+ export interface FromTiffOptions {
22
+ /** Options forwarded to {@link TiffStore} (e.g. IFD offsets, HTTP headers). */
23
+ tiff?: TiffStoreOptions
24
+ /** Options forwarded to {@link fromNgffZarr} (e.g. validate, cache). */
25
+ ngffZarr?: Omit<FromNgffZarrOptions, "version">
26
+ }
27
+
28
+ /**
29
+ * Load a TIFF file and return a {@link Multiscales} object ready for
30
+ * {@link OMEZarrNVImage.create}.
31
+ *
32
+ * Supports OME-TIFF, pyramidal TIFF (SubIFDs / legacy / COG), and
33
+ * plain single-image TIFFs. Both local (Blob/ArrayBuffer) and
34
+ * remote (URL with HTTP range requests) sources are supported.
35
+ *
36
+ * @param source - A URL string, Blob/File, ArrayBuffer, or
37
+ * pre-built {@link TiffStore}.
38
+ * @param options - Optional {@link FromTiffOptions}.
39
+ * @returns A {@link Multiscales} object for use with
40
+ * {@link OMEZarrNVImage.create}.
41
+ * @throws If the source cannot be opened as a TIFF or if the
42
+ * synthesized OME-Zarr metadata is invalid.
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * // From a remote URL
47
+ * const ms = await fromTiff("https://example.com/image.ome.tif")
48
+ * const image = await OMEZarrNVImage.create({
49
+ * multiscales: ms,
50
+ * niivue: nv,
51
+ * })
52
+ *
53
+ * // From a File input
54
+ * const file = inputElement.files[0]
55
+ * const ms = await fromTiff(file)
56
+ * ```
57
+ */
58
+ export async function fromTiff(
59
+ source: string | Blob | ArrayBuffer | TiffStore,
60
+ options: FromTiffOptions = {},
61
+ ): Promise<Multiscales> {
62
+ let store: TiffStore
63
+ if (source instanceof TiffStore) {
64
+ store = source
65
+ } else if (typeof source === "string") {
66
+ store = await TiffStore.fromUrl(source, options.tiff)
67
+ } else if (source instanceof Blob) {
68
+ store = await TiffStore.fromBlob(source, options.tiff)
69
+ } else if (source instanceof ArrayBuffer) {
70
+ store = await TiffStore.fromArrayBuffer(source, options.tiff)
71
+ } else {
72
+ throw new Error(
73
+ "[fidnii] fromTiff: source must be a URL string, Blob, " +
74
+ "ArrayBuffer, or TiffStore",
75
+ )
76
+ }
77
+
78
+ // TiffStore implements zarrita's Readable interface structurally
79
+ // (its get() method accepts string keys including the leading "/"
80
+ // that zarrita uses for AbsolutePath). Since fromNgffZarr (v0.10+)
81
+ // accepts zarr.Readable directly, we only need a type assertion.
82
+ //
83
+ // TiffStore always produces OME-Zarr v0.5 metadata.
84
+ return fromNgffZarr(store as unknown as Readable, {
85
+ version: "0.5",
86
+ ...options.ngffZarr,
87
+ })
88
+ }
package/src/index.ts CHANGED
@@ -34,10 +34,15 @@
34
34
  * ```
35
35
  */
36
36
 
37
+ export type { TiffStoreOptions } from "@fideus-labs/fiff"
38
+ export { TiffStore } from "@fideus-labs/fiff"
37
39
  // Re-export Methods enum so consumers can check isLabelImage or compare method values
38
40
  export { Methods } from "@fideus-labs/ngff-zarr"
39
41
  // Worker pool lifecycle (re-exported from ngff-zarr)
40
- export { terminateWorkerPool } from "@fideus-labs/ngff-zarr/browser"
42
+ export {
43
+ terminateOmeroWorkerPool,
44
+ terminateWorkerPool,
45
+ } from "@fideus-labs/ngff-zarr/browser"
41
46
 
42
47
  // Buffer manager
43
48
  export { BufferManager } from "./BufferManager.js"
@@ -69,10 +74,16 @@ export type {
69
74
  } from "./events.js"
70
75
  // Event system (browser-native EventTarget API)
71
76
  export { OMEZarrNVImageEvent } from "./events.js"
77
+ export type { FromTiffOptions } from "./fromTiff.js"
78
+ // TIFF support (via @fideus-labs/fiff)
79
+ export { fromTiff } from "./fromTiff.js"
80
+ // RGB normalization utilities
81
+ export type { ChannelWindow } from "./normalize.js"
82
+ export { computeChannelMinMax, normalizeToUint8 } from "./normalize.js"
72
83
  // Main class
73
84
  export { OMEZarrNVImage } from "./OMEZarrNVImage.js"
74
85
  // Region coalescer
75
- export { RegionCoalescer } from "./RegionCoalescer.js"
86
+ export { buildSelection, RegionCoalescer } from "./RegionCoalescer.js"
76
87
  export type { OrthogonalAxis } from "./ResolutionSelector.js"
77
88
  // Resolution selector utilities
78
89
  export {
@@ -88,6 +99,7 @@ export {
88
99
  // Types
89
100
  export type {
90
101
  AttachedNiivueState,
102
+ ChannelInfo,
91
103
  ChunkAlignedRegion,
92
104
  ChunkCache,
93
105
  ClipPlane,
@@ -106,9 +118,13 @@ export type {
106
118
  // Type utilities
107
119
  export {
108
120
  getBytesPerPixel,
121
+ getChannelInfo,
109
122
  getNiftiDataType,
123
+ getRGBNiftiDataType,
110
124
  getTypedArrayConstructor,
125
+ isRGBImage,
111
126
  NiftiDataType,
127
+ needsRGBNormalization,
112
128
  parseZarritaDtype,
113
129
  SLICE_TYPE,
114
130
  } from "./types.js"
@@ -0,0 +1,119 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) Fideus Labs LLC
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import type { TypedArray } from "./types.js"
5
+
6
+ /**
7
+ * Per-channel display window for normalization.
8
+ * Maps source values in `[start, end]` to uint8 `[0, 255]`.
9
+ */
10
+ export interface ChannelWindow {
11
+ /** Lower bound of the display window (maps to 0) */
12
+ start: number
13
+ /** Upper bound of the display window (maps to 255) */
14
+ end: number
15
+ }
16
+
17
+ /**
18
+ * Normalize interleaved multi-component data to uint8.
19
+ *
20
+ * Each channel is linearly mapped from its `[start, end]` window to
21
+ * `[0, 255]`, with clamping at both ends. Source data is expected to
22
+ * be interleaved: `[R0, G0, B0, R1, G1, B1, ...]`.
23
+ *
24
+ * @param source - Interleaved multi-component data in any typed array
25
+ * @param components - Number of components per voxel (3 for RGB, 4 for RGBA)
26
+ * @param channelWindows - Per-channel display windows; must have
27
+ * `components` entries. If fewer are provided, remaining channels use
28
+ * the last window.
29
+ * @returns A new `Uint8Array` with normalized uint8 values
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * const src = new Uint16Array([0, 32768, 65535, 0, 0, 0])
34
+ * const windows = [
35
+ * { start: 0, end: 65535 },
36
+ * { start: 0, end: 65535 },
37
+ * { start: 0, end: 65535 },
38
+ * ]
39
+ * const result = normalizeToUint8(src, 3, windows)
40
+ * // result ≈ Uint8Array [0, 128, 255, 0, 0, 0]
41
+ * ```
42
+ */
43
+ export function normalizeToUint8(
44
+ source: TypedArray,
45
+ components: number,
46
+ channelWindows: ChannelWindow[],
47
+ ): Uint8Array {
48
+ const len = source.length
49
+ const output = new Uint8Array(len)
50
+ const numVoxels = len / components
51
+
52
+ // Pre-compute per-channel scale factors for performance
53
+ const scales = new Float64Array(components)
54
+ const offsets = new Float64Array(components)
55
+ for (let c = 0; c < components; c++) {
56
+ const win = channelWindows[Math.min(c, channelWindows.length - 1)]
57
+ const range = win.end - win.start
58
+ if (range > 0) {
59
+ scales[c] = 255 / range
60
+ offsets[c] = win.start
61
+ } else {
62
+ // Degenerate window: all values map to 0
63
+ scales[c] = 0
64
+ offsets[c] = 0
65
+ }
66
+ }
67
+
68
+ for (let v = 0; v < numVoxels; v++) {
69
+ const base = v * components
70
+ for (let c = 0; c < components; c++) {
71
+ const scaled = (source[base + c] - offsets[c]) * scales[c]
72
+ // Clamp to [0, 255] and round
73
+ output[base + c] =
74
+ scaled <= 0 ? 0 : scaled >= 255 ? 255 : (scaled + 0.5) | 0
75
+ }
76
+ }
77
+
78
+ return output
79
+ }
80
+
81
+ /**
82
+ * Compute per-channel min/max from interleaved multi-component data.
83
+ *
84
+ * Use this as a fallback when OMERO window metadata is not available.
85
+ * The result can be passed directly to {@link normalizeToUint8}.
86
+ *
87
+ * @param data - Interleaved multi-component data
88
+ * @param components - Number of components per voxel (3 for RGB, 4 for RGBA)
89
+ * @returns Per-channel windows with `start = min` and `end = max`
90
+ */
91
+ export function computeChannelMinMax(
92
+ data: TypedArray,
93
+ components: number,
94
+ ): ChannelWindow[] {
95
+ const windows: ChannelWindow[] = Array.from({ length: components }, () => ({
96
+ start: Infinity,
97
+ end: -Infinity,
98
+ }))
99
+
100
+ const numVoxels = data.length / components
101
+ for (let v = 0; v < numVoxels; v++) {
102
+ const base = v * components
103
+ for (let c = 0; c < components; c++) {
104
+ const val = data[base + c]
105
+ if (val < windows[c].start) windows[c].start = val
106
+ if (val > windows[c].end) windows[c].end = val
107
+ }
108
+ }
109
+
110
+ // Handle empty data: set degenerate windows to [0, 0]
111
+ for (let c = 0; c < components; c++) {
112
+ if (windows[c].start === Infinity) {
113
+ windows[c].start = 0
114
+ windows[c].end = 0
115
+ }
116
+ }
117
+
118
+ return windows
119
+ }
package/src/types.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // SPDX-FileCopyrightText: Copyright (c) Fideus Labs LLC
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
- import type { Multiscales } from "@fideus-labs/ngff-zarr"
4
+ import type { Multiscales, NgffImage } from "@fideus-labs/ngff-zarr"
5
5
  import type { Niivue, NVImage } from "@niivue/niivue"
6
6
  import { SLICE_TYPE } from "@niivue/niivue"
7
7
 
@@ -154,6 +154,17 @@ export interface OMEZarrNVImageOptions {
154
154
  * @see {@link ChunkCache}
155
155
  */
156
156
  cache?: ChunkCache
157
+ /**
158
+ * Flip the y-axis in the NIfTI affine for 2D images (default: true).
159
+ *
160
+ * 2D images (e.g., PNGs converted to OME-Zarr) store pixel data with
161
+ * y=0 at the top (increasing downward). NiiVue expects a negative
162
+ * y-scale in the affine to render these right-side up. Set to `false`
163
+ * if your 2D data already has bottom-to-top y ordering.
164
+ *
165
+ * This option has no effect on 3D volumes (images with a `"z"` axis).
166
+ */
167
+ flipY2D?: boolean
157
168
  }
158
169
 
159
170
  /**
@@ -307,14 +318,97 @@ export const NiftiDataType = {
307
318
  INT32: 8,
308
319
  FLOAT32: 16,
309
320
  FLOAT64: 64,
321
+ RGB24: 128,
310
322
  INT8: 256,
311
323
  UINT16: 512,
312
324
  UINT32: 768,
325
+ RGBA32: 2304,
313
326
  } as const
314
327
 
315
328
  export type NiftiDataTypeCode =
316
329
  (typeof NiftiDataType)[keyof typeof NiftiDataType]
317
330
 
331
+ /**
332
+ * Information about a channel (component) dimension in the image.
333
+ */
334
+ export interface ChannelInfo {
335
+ /** Index of the `"c"` dimension in `ngffImage.dims` */
336
+ channelAxis: number
337
+ /** Number of components (e.g. 3 for RGB, 4 for RGBA) */
338
+ components: number
339
+ }
340
+
341
+ /**
342
+ * Detect whether an NgffImage has a channel (`"c"`) dimension.
343
+ *
344
+ * @param ngffImage - The NgffImage to inspect
345
+ * @returns Channel information, or `null` if no `"c"` dimension exists
346
+ */
347
+ export function getChannelInfo(ngffImage: NgffImage): ChannelInfo | null {
348
+ const channelAxis = ngffImage.dims.indexOf("c")
349
+ if (channelAxis === -1) return null
350
+ const components = ngffImage.data.shape[channelAxis]
351
+ return { channelAxis, components }
352
+ }
353
+
354
+ /**
355
+ * Check whether an NgffImage represents an RGB or RGBA image.
356
+ *
357
+ * An image is considered RGB/RGBA when it has a `"c"` dimension with
358
+ * 3 or 4 components. Any dtype is supported — non-uint8 data is
359
+ * normalized to uint8 at load time (see {@link needsRGBNormalization}).
360
+ *
361
+ * @param ngffImage - The NgffImage to inspect
362
+ * @returns `true` if the image is RGB (3 components) or RGBA (4 components)
363
+ */
364
+ export function isRGBImage(ngffImage: NgffImage): boolean {
365
+ const info = getChannelInfo(ngffImage)
366
+ if (!info) return false
367
+ return info.components === 3 || info.components === 4
368
+ }
369
+
370
+ /**
371
+ * Check whether a multi-component RGB/RGBA image needs normalization
372
+ * to uint8 before NiiVue can render it.
373
+ *
374
+ * NiiVue only supports `DT_RGB24` (3×uint8) and `DT_RGBA32` (4×uint8)
375
+ * color rendering. Non-uint8 multi-component images must be normalized
376
+ * to uint8 using OMERO window metadata (or computed min/max).
377
+ *
378
+ * @param ngffImage - The NgffImage to inspect
379
+ * @param dtype - The parsed zarr dtype
380
+ * @returns `true` if the image is RGB/RGBA with a non-uint8 dtype
381
+ */
382
+ export function needsRGBNormalization(
383
+ ngffImage: NgffImage,
384
+ dtype: ZarrDtype,
385
+ ): boolean {
386
+ return isRGBImage(ngffImage) && dtype !== "uint8"
387
+ }
388
+
389
+ /**
390
+ * Get the NIfTI data type code for a multi-component image.
391
+ *
392
+ * Returns `RGB24` for 3 components and `RGBA32` for 4 components,
393
+ * regardless of the source dtype. Non-uint8 data is normalized to
394
+ * uint8 at load time before being stored in the NVImage buffer.
395
+ *
396
+ * @param channelInfo - Channel information from `getChannelInfo()`
397
+ * @returns The NIfTI data type code (`RGB24` or `RGBA32`)
398
+ * @throws If the component count is not 3 or 4
399
+ */
400
+ export function getRGBNiftiDataType(
401
+ channelInfo: ChannelInfo,
402
+ ): NiftiDataTypeCode {
403
+ if (channelInfo.components === 3) return NiftiDataType.RGB24
404
+ if (channelInfo.components === 4) return NiftiDataType.RGBA32
405
+ throw new Error(
406
+ `Unsupported multi-component image: ` +
407
+ `components=${channelInfo.components}. ` +
408
+ `Only 3 (RGB) or 4 (RGBA) components are supported.`,
409
+ )
410
+ }
411
+
318
412
  /**
319
413
  * Map zarr dtype to typed array constructor.
320
414
  */
@@ -135,23 +135,24 @@ export function normalizedToWorld(
135
135
  const shape = ngffImage.data.shape
136
136
  const dims = ngffImage.dims
137
137
 
138
- // Find z, y, x indices in dims
139
- const zIdx = dims.indexOf("z")
140
138
  const yIdx = dims.indexOf("y")
141
139
  const xIdx = dims.indexOf("x")
142
140
 
143
- let dimX: number, dimY: number, dimZ: number
144
- if (zIdx === -1 || yIdx === -1 || xIdx === -1) {
145
- const n = shape.length
146
- dimZ = shape[n - 3] || 1
147
- dimY = shape[n - 2] || 1
148
- dimX = shape[n - 1] || 1
149
- } else {
150
- dimZ = shape[zIdx]
151
- dimY = shape[yIdx]
152
- dimX = shape[xIdx]
141
+ if (yIdx === -1 || xIdx === -1) {
142
+ const missingAxes = [xIdx === -1 ? "x" : null, yIdx === -1 ? "y" : null]
143
+ .filter((axis): axis is string => axis !== null)
144
+ .join(", ")
145
+ throw new Error(
146
+ `NgffImage is missing required spatial dimension(s): ${missingAxes}`,
147
+ )
153
148
  }
154
149
 
150
+ // Look up spatial dimensions by name; default z to 1 for 2D images
151
+ const zIdx = dims.indexOf("z")
152
+ const dimZ = zIdx !== -1 ? shape[zIdx] : 1
153
+ const dimY = shape[yIdx]
154
+ const dimX = shape[xIdx]
155
+
155
156
  // Convert normalized to pixel
156
157
  const pixelCoord: [number, number, number] = [
157
158
  normalizedCoord[2] * dimZ, // z
@@ -176,23 +177,24 @@ export function worldToNormalized(
176
177
  const shape = ngffImage.data.shape
177
178
  const dims = ngffImage.dims
178
179
 
179
- // Find z, y, x indices in dims
180
- const zIdx = dims.indexOf("z")
181
180
  const yIdx = dims.indexOf("y")
182
181
  const xIdx = dims.indexOf("x")
183
182
 
184
- let dimX: number, dimY: number, dimZ: number
185
- if (zIdx === -1 || yIdx === -1 || xIdx === -1) {
186
- const n = shape.length
187
- dimZ = shape[n - 3] || 1
188
- dimY = shape[n - 2] || 1
189
- dimX = shape[n - 1] || 1
190
- } else {
191
- dimZ = shape[zIdx]
192
- dimY = shape[yIdx]
193
- dimX = shape[xIdx]
183
+ if (yIdx === -1 || xIdx === -1) {
184
+ const missingAxes = [xIdx === -1 ? "x" : null, yIdx === -1 ? "y" : null]
185
+ .filter((axis): axis is string => axis !== null)
186
+ .join(", ")
187
+ throw new Error(
188
+ `NgffImage is missing required spatial dimension(s): ${missingAxes}`,
189
+ )
194
190
  }
195
191
 
192
+ // Look up spatial dimensions by name; default z to 1 for 2D images
193
+ const zIdx = dims.indexOf("z")
194
+ const dimZ = zIdx !== -1 ? shape[zIdx] : 1
195
+ const dimY = shape[yIdx]
196
+ const dimX = shape[xIdx]
197
+
196
198
  // Convert world to pixel
197
199
  const pixelCoord = worldToPixel(worldCoord, ngffImage)
198
200