diff --git a/modules/zarr/README.md b/modules/zarr/README.md index dfa24d65e1..df86b348be 100644 --- a/modules/zarr/README.md +++ b/modules/zarr/README.md @@ -1,5 +1,3 @@ -<<<<<<< HEAD:modules/textures/README.md - # @loaders.gl/zarr [loaders.gl](https://loaders.gl/docs) is a collection of framework-independent 3D and geospatial parsers and encoders. diff --git a/modules/zarr/package.json b/modules/zarr/package.json index 6278f83287..2958a41e74 100644 --- a/modules/zarr/package.json +++ b/modules/zarr/package.json @@ -15,7 +15,7 @@ "loader", "zarr" ], - "types": "src/index.ts", + "types": "src/index.d.ts", "main": "dist/es5/index.js", "module": "dist/esm/index.js", "sideEffects": false, @@ -30,11 +30,8 @@ "build-bundle": "webpack --display=minimal --config ../../scripts/webpack/bundle.js" }, "dependencies": { - "@loaders.gl/core": "3.0.0-alpha.18", "@loaders.gl/worker-utils": "3.0.0-alpha.18", - "@luma.gl/constants": "^8.3.0", - "fast-xml-parser": "^3.16.0", - "zarr": "^0.4.0" + "zarr": "^0.5.0" }, "devDependencies": { "@loaders.gl/core": "3.0.0-alpha.18" diff --git a/modules/zarr/src/constants.ts b/modules/zarr/src/constants.ts deleted file mode 100644 index 06ce9b85ca..0000000000 --- a/modules/zarr/src/constants.ts +++ /dev/null @@ -1,74 +0,0 @@ -import GL from '@luma.gl/constants'; -import type {TypedArray} from 'zarr'; - -/** - * @deprecated We plan to remove `DTYPE_VALUES` as a part of Viv's public API as it - * leaks internal implementation details. If this is something your project relies - * on, please open an issue for further discussion. - * - * More info can be found here: https://github.com/hms-dbmi/viv/pull/372#discussion_r571707517 - */ -export const DTYPE_VALUES = { - Uint8: { - format: GL.R8UI, - dataFormat: GL.RED_INTEGER, - type: GL.UNSIGNED_BYTE, - max: 2 ** 8 - 1, - sampler: 'usampler2D' - }, - Uint16: { - format: GL.R16UI, - dataFormat: GL.RED_INTEGER, - type: GL.UNSIGNED_SHORT, - max: 2 ** 16 - 1, - sampler: 'usampler2D' - }, - Uint32: { - format: GL.R32UI, - dataFormat: GL.RED_INTEGER, - type: GL.UNSIGNED_INT, - max: 2 ** 32 - 1, - sampler: 'usampler2D' - }, - Float32: { - format: GL.R32F, - dataFormat: GL.RED, - type: GL.FLOAT, - // Not sure what to do about this one - a good use case for channel stats, I suppose: - // https://en.wikipedia.org/wiki/Single-precision_floating-point_format. - max: 3.4 * 10 ** 38, - sampler: 'sampler2D' - }, - Int8: { - format: GL.R8I, - dataFormat: GL.RED_INTEGER, - type: GL.BYTE, - max: 2 ** (8 - 1) - 1, - sampler: 'isampler2D' - }, - Int16: { - format: GL.R16I, - dataFormat: GL.RED_INTEGER, - type: GL.SHORT, - max: 2 ** (16 - 1) - 1, - sampler: 'isampler2D' - }, - Int32: { - format: GL.R32I, - dataFormat: GL.RED_INTEGER, - type: GL.INT, - max: 2 ** (32 - 1) - 1, - sampler: 'isampler2D' - }, - // Cast Float64 as 32 bit float point so it can be rendered. - Float64: { - format: GL.R32F, - dataFormat: GL.RED, - type: GL.FLOAT, - // Not sure what to do about this one - a good use case for channel stats, I suppose: - // https://en.wikipedia.org/wiki/Single-precision_floating-point_format. - max: 3.4 * 10 ** 38, - sampler: 'sampler2D', - cast: (data: TypedArray) => new Float32Array(data) - } -} as const; diff --git a/modules/zarr/src/index.ts b/modules/zarr/src/index.ts index b73ea718b9..42afca69a8 100644 --- a/modules/zarr/src/index.ts +++ b/modules/zarr/src/index.ts @@ -1,4 +1,2 @@ export {loadZarr} from './lib/load-zarr'; -export {loadBioformatsZarr} from './lib/load-bioformats-zarr'; - export {default as ZarrPixelSource} from './lib/zarr-pixel-source'; diff --git a/modules/zarr/src/lib/load-bioformats-zarr-helper.ts b/modules/zarr/src/lib/load-bioformats-zarr-helper.ts deleted file mode 100644 index cf6ccdd24d..0000000000 --- a/modules/zarr/src/lib/load-bioformats-zarr-helper.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type {ZarrArray} from 'zarr'; - -import {fromString} from './utils/omexml'; -import {guessBioformatsLabels, loadMultiscales, guessTileSize} from './utils/zarr-utils'; -import ZarrPixelSource from './zarr-pixel-source'; - -export async function loadBioformatsZarrHelper( - root: ZarrArray['store'], - xmlSource: string | File | Response -) { - // If 'File' or 'Response', read as text. - if (typeof xmlSource !== 'string') { - xmlSource = await xmlSource.text(); - } - - // Get metadata and multiscale data for _first_ image. - const imgMeta = fromString(xmlSource)[0]; - const {data} = await loadMultiscales(root, '0'); - - const labels = guessBioformatsLabels(data[0], imgMeta); - const tileSize = guessTileSize(data[0]); - const pyramid = data.map((arr) => new ZarrPixelSource(arr, labels, tileSize)); - - return { - data: pyramid, - metadata: imgMeta - }; -} diff --git a/modules/zarr/src/lib/load-bioformats-zarr.ts b/modules/zarr/src/lib/load-bioformats-zarr.ts deleted file mode 100644 index 8cecc93d87..0000000000 --- a/modules/zarr/src/lib/load-bioformats-zarr.ts +++ /dev/null @@ -1,79 +0,0 @@ -import {HTTPStore} from 'zarr'; -import {FileStore} from './utils/storage'; -import {getRootPrefix} from './utils/zarr-utils'; - -import {loadBioformatsZarrHelper} from './load-bioformats-zarr-helper'; -import {loadZarr} from './load-zarr'; - -interface ZarrOptions { - fetchOptions: RequestInit; -} - -/** - * Opens root directory generated via `bioformats2raw --file_type=zarr`. Uses OME-XML metadata, - * and assumes first image. This function is the zarr-equivalent to using loadOmeTiff. - * - * @param {string} source url - * @param {{ fetchOptions: (undefined | RequestInit) }} options - * @return {Promise<{ data: ZarrPixelSource[], metadata: ImageMeta }>} data source and associated OMEXML metadata. - */ -export async function loadBioformatsZarr( - source: string | (File & {path: string})[], - options: Partial = {} -) { - const METADATA = 'METADATA.ome.xml'; - const ZARR_DIR = 'data.zarr'; - - if (typeof source === 'string') { - const url = source.endsWith('/') ? source.slice(0, -1) : source; - const store = new HTTPStore(`${url}/${ZARR_DIR}`, options); - const xmlSource = await fetch(`${url}/${METADATA}`, options.fetchOptions); - return loadBioformatsZarrHelper(store, xmlSource); - } - - /* - * You can't randomly access files from a directory by path name - * without the Native File System API, so we need to get objects for _all_ - * the files right away for Zarr. This is unfortunate because we need to iterate - * over all File objects and create an in-memory index. - * - * fMap is simple key-value mapping from 'some/file/path' -> File - */ - const fMap: Map = new Map(); - - let xmlFile: File | undefined; - for (const file of source) { - if (file.name === METADATA) { - xmlFile = file; - } else { - fMap.set(file.path, file); - } - } - - if (!xmlFile) { - throw Error('No OME-XML metadata found for store.'); - } - - const store = new FileStore(fMap, getRootPrefix(source, ZARR_DIR)); - return loadBioformatsZarrHelper(store, xmlFile); -} - -/** - * Opens root of multiscale OME-Zarr via URL. - * - * @param {string} source url - * @param {{ fetchOptions: (undefined | RequestInit) }} options - * @return {Promise<{ data: ZarrPixelSource[], metadata: RootAttrs }>} data source and associated OME-Zarr metadata. - */ -export async function loadOmeZarr( - source: string, - options: Partial = {} -) { - const store = new HTTPStore(source, options); - - if (options?.type !== 'multiscales') { - throw Error('Only multiscale OME-Zarr is supported.'); - } - - return loadZarr(store); -} diff --git a/modules/zarr/src/lib/load-zarr.ts b/modules/zarr/src/lib/load-zarr.ts index f9ced5b443..9bd18194b8 100644 --- a/modules/zarr/src/lib/load-zarr.ts +++ b/modules/zarr/src/lib/load-zarr.ts @@ -1,44 +1,24 @@ -import type {ZarrArray} from 'zarr'; -import {loadMultiscales, guessTileSize} from './utils/zarr-utils'; +import type {Store} from 'zarr/types/storage/types'; +import {loadMultiscales, guessTileSize, guessLabels, normalizeStore, validLabels} from './utils'; import ZarrPixelSource from './zarr-pixel-source'; -import type {Labels} from '../types'; -interface Channel { - active: boolean; - color: string; - label: string; - window: { - min?: number; - max?: number; - start: number; - end: number; - }; +interface ZarrOptions { + labels?: string[]; } -interface Omero { - channels: Channel[]; - rdefs: { - defaultT?: number; - defaultZ?: number; - model: string; - }; - name?: string; -} +export async function loadZarr(root: string | Store, options: ZarrOptions = {}) { + const store = normalizeStore(root); + const {data, rootAttrs} = await loadMultiscales(store); + const tileSize = guessTileSize(data[0]); -interface Multiscale { - datasets: {path: string}[]; - version?: string; -} + // If no labels are provided, inspect the root attributes for the store. + // For now, we only infer labels for OME-Zarr. + const labels = options.labels ?? guessLabels(rootAttrs); -export interface RootAttrs { - omero: Omero; - multiscales: Multiscale[]; -} + if (!validLabels(labels, data[0].shape)) { + throw new Error('Invalid labels for Zarr array dimensions.'); + } -export async function loadZarr(store: ZarrArray['store']) { - const {data, rootAttrs} = await loadMultiscales(store); - const labels = ['t', 'c', 'z', 'y', 'x'] as Labels<['t', 'c', 'z']>; - const tileSize = guessTileSize(data[0]); const pyramid = data.map((arr) => new ZarrPixelSource(arr, labels, tileSize)); return { data: pyramid, diff --git a/modules/zarr/src/lib/utils.ts b/modules/zarr/src/lib/utils.ts new file mode 100644 index 0000000000..c7beba0d64 --- /dev/null +++ b/modules/zarr/src/lib/utils.ts @@ -0,0 +1,149 @@ +import {openGroup, HTTPStore} from 'zarr'; +import type {ZarrArray} from 'zarr'; +import type {Store} from 'zarr/types/storage/types'; + +import type {PixelSource, RootAttrs, Labels} from '../types'; + +export function normalizeStore(source: string | Store): Store { + if (typeof source === 'string') { + return new HTTPStore(source); + } + return source; +} + +export async function loadMultiscales(store: Store, path = '') { + const grp = await openGroup(store, path); + const rootAttrs = (await grp.attrs.asObject()) as RootAttrs; + + // Root of Zarr store must implement multiscales extension. + // https://github.com/zarr-developers/zarr-specs/issues/50 + if (!Array.isArray(rootAttrs.multiscales)) { + throw new Error('Cannot find Zarr multiscales metadata.'); + } + + const {datasets} = rootAttrs.multiscales[0]; + const promises = datasets.map((d) => grp.getItem(d.path)) as Promise[]; + + return { + data: await Promise.all(promises), + rootAttrs + }; +} + +/* + * Creates an ES6 map of 'label' -> index + * > const labels = ['a', 'b', 'c', 'd']; + * > const dims = getDims(labels); + * > dims('a') === 0; + * > dims('b') === 1; + * > dims('c') === 2; + * > dims('hi!'); // throws + */ +export function getDims(labels: S[]) { + const lookup = new Map(labels.map((name, i) => [name, i])); + if (lookup.size !== labels.length) { + throw Error('Labels must be unique, found duplicated label.'); + } + return (name: S) => { + const index = lookup.get(name); + if (index === undefined) { + throw Error('Invalid dimension.'); + } + return index; + }; +} + +function prevPowerOf2(x: number) { + return 2 ** Math.floor(Math.log2(x)); +} + +/* + * Helper method to determine whether pixel data is interleaved or not. + * > isInterleaved([1, 24, 24]) === false; + * > isInterleaved([1, 24, 24, 3]) === true; + */ +export function isInterleaved(shape: number[]) { + const lastDimSize = shape[shape.length - 1]; + return lastDimSize === 3 || lastDimSize === 4; +} + +export function guessTileSize(arr: ZarrArray) { + const interleaved = isInterleaved(arr.shape); + const [yChunk, xChunk] = arr.chunks.slice(interleaved ? -3 : -2); + const size = Math.min(yChunk, xChunk); + // deck.gl requirement for power-of-two tile size. + return prevPowerOf2(size); +} + +export function guessLabels(rootAttrs: RootAttrs) { + if ('omero' in rootAttrs) { + return ['t', 'c', 'z', 'y', 'x'] as Labels<['t', 'c', 'z']>; + } + throw new Error( + 'Could not infer dimension labels for Zarr source. Must provide dimension labels.' + ); +} + +/* + * The 'indexer' for a Zarr-based source translates + * a 'selection' to an array of indices that align to + * the labeled dimensions. + * + * > const labels = ['a', 'b', 'y', 'x']; + * > const indexer = getIndexer(labels); + * > console.log(indexer({ a: 10, b: 20 })); + * > // [10, 20, 0, 0] + */ +export function getIndexer(labels: T[]) { + const size = labels.length; + const dims = getDims(labels); + return (sel: {[K in T]: number} | number[]) => { + if (Array.isArray(sel)) { + return [...sel]; + } + const selection: number[] = Array(size).fill(0); + for (const [key, value] of Object.entries(sel)) { + selection[dims(key as T)] = value as number; + } + return selection; + }; +} + +export function getImageSize( + source: PixelSource +): {height: number; width: number} { + const interleaved = isInterleaved(source.shape); + // 2D image data in Zarr are represented as (..., rows, columns [, bands]) + // If an image is interleaved (RGB/A), we need to ignore the last dimension (bands) + // to get the height and weight of the image. + const [height, width] = source.shape.slice(interleaved ? -3 : -2); + return {height, width}; +} + +/** + * Preserves (double) slashes earlier in the path, so this works better + * for URLs. From https://stackoverflow.com/a/46427607 + * @param args parts of a path or URL to join. + */ +export function joinUrlParts(...args: string[]) { + return args + .map((part, i) => { + if (i === 0) return part.trim().replace(/[/]*$/g, ''); + return part.trim().replace(/(^[/]*|[/]*$)/g, ''); + }) + .filter((x) => x.length) + .join('/'); +} + +export function validLabels(labels: string[], shape: number[]): labels is Labels { + if (labels.length !== shape.length) { + throw new Error('Labels do not match Zarr array shape.'); + } + const n = shape.length; + if (isInterleaved(shape)) { + // last three dimensions are [row, column, bands] + return labels[n - 3] === 'y' && labels[n - 2] === 'x' && labels[n - 1] === '_c'; + } + // last two dimensions are [row, column] + return labels[n - 2] === 'y' && labels[n - 1] === 'x'; +} diff --git a/modules/zarr/src/lib/utils/fetch-file-store.ts b/modules/zarr/src/lib/utils/fetch-file-store.ts deleted file mode 100644 index 0ff09345e5..0000000000 --- a/modules/zarr/src/lib/utils/fetch-file-store.ts +++ /dev/null @@ -1,56 +0,0 @@ -import {fetchFile} from '@loaders.gl/core'; -import path from 'path'; -import {KeyError} from 'zarr'; -import type {AsyncStore} from 'zarr/types/storage/types'; - -// Node system error -interface SystemError extends Error { - code: string; -} - -function isSystemError(err: unknown): err is SystemError { - return err instanceof Error && 'code' in err; -} - -/* - * Minimal store implementation to read zarr from file system in Node. - */ -export class FileSystemStore implements AsyncStore { - constructor(public root: string) {} - - async getItem(key: string): Promise { - const filepath = path.join(this.root, key); - try { - const response = await fetchFile(filepath); - if (!response.ok) { - throw new KeyError(key); - } - const value = await response.arrayBuffer(); - return value; - } catch (err) { - // Zarr requires a special exception to be thrown in case of missing chunks - if (isSystemError(err) && err.code === 'ENOENT') { - throw new KeyError(key); - } - throw err; - } - } - - containsItem(key: string): Promise { - return this.getItem(key) - .then(() => true) - .catch(() => false); - } - - keys(): Promise { - return Promise.resolve([]); - } - - setItem(): never { - throw new Error('setItem not implemented.'); - } - - deleteItem(): never { - throw new Error('deleteItem not implemented.'); - } -} diff --git a/modules/zarr/src/lib/utils/indexer.ts b/modules/zarr/src/lib/utils/indexer.ts deleted file mode 100644 index fdb6e7eafc..0000000000 --- a/modules/zarr/src/lib/utils/indexer.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {getDims} from './utils'; - -/* - * The 'indexer' for a Zarr-based source translates - * a 'selection' to an array of indices that align to - * the labeled dimensions. - * - * > const labels = ['a', 'b', 'y', 'x']; - * > const indexer = getIndexer(labels); - * > console.log(indexer({ a: 10, b: 20 })); - * > // [10, 20, 0, 0] - */ -export function getIndexer(labels: T[]) { - const size = labels.length; - const dims = getDims(labels); - return (sel: {[K in T]: number} | number[]) => { - if (Array.isArray(sel)) { - return [...sel]; - } - const selection: number[] = Array(size).fill(0); - for (const [key, value] of Object.entries(sel)) { - selection[dims(key as T)] = value as number; - } - return selection; - }; -} diff --git a/modules/zarr/src/lib/utils/omexml.ts b/modules/zarr/src/lib/utils/omexml.ts deleted file mode 100644 index 1b3e48b9c2..0000000000 --- a/modules/zarr/src/lib/utils/omexml.ts +++ /dev/null @@ -1,179 +0,0 @@ -import parser from 'fast-xml-parser'; -import {ensureArray, intToRgba} from './utils'; - -// WARNING: Changes to the parser options _will_ effect the types in types/omexml.d.ts. -const PARSER_OPTIONS = { - // Nests attributes withtout prefix under 'attr' key for each node - attributeNamePrefix: '', - attrNodeName: 'attr', - - // Parses numbers for both attributes and nodes - parseNodeValue: true, - parseAttributeValue: true, - - // Forces attributes to be parsed - ignoreAttributes: false -}; - -const parse = (str: string): Root => parser.parse(str, PARSER_OPTIONS); - -export function fromString(str: string) { - const res = parse(str); - if (!res.OME) { - throw Error('Failed to parse OME-XML metadata.'); - } - return ensureArray(res.OME.Image).map((img) => { - const Channels = ensureArray(img.Pixels.Channel).map((c) => { - if ('Color' in c.attr) { - return {...c.attr, Color: intToRgba(c.attr.Color)}; - } - return {...c.attr}; - }); - const {AquisitionDate = '', Description = ''} = img; - const image = { - ...img.attr, - AquisitionDate, - Description, - Pixels: { - ...img.Pixels.attr, - Channels - } - }; - return { - ...image, - format() { - const {Pixels} = image; - - const sizes = (['X', 'Y', 'Z'] as const) - .map((name) => { - const size = Pixels[`PhysicalSize${name}` as const]; - const unit = Pixels[`PhysicalSize${name}Unit` as const]; - return size && unit ? `${size} ${unit}` : '-'; - }) - .join(' x '); - - return { - 'Acquisition Date': image.AquisitionDate, - 'Dimensions (XY)': `${Pixels.SizeX} x ${Pixels.SizeY}`, - 'Pixels Type': Pixels.Type, - 'Pixels Size (XYZ)': sizes, - 'Z-sections/Timepoints': `${Pixels.SizeZ} x ${Pixels.SizeT}`, - Channels: Pixels.SizeC - }; - } - }; - }); -} - -export type OMEXML = ReturnType; -export type DimensionOrder = 'XYZCT' | 'XYZTC' | 'XYCTZ' | 'XYCZT' | 'XYTCZ' | 'XYTZC'; - -// Structure of node is determined by the PARSER_OPTIONS. -type Node = T & {attr: A}; -type Attrs = {[K in Fields]: T}; - -type OMEAttrs = Attrs<'xmlns' | 'xmlns:xsi' | 'xsi:schemaLocation'>; -type OME = Node<{Insturment: Insturment; Image: Image | Image[]}, OMEAttrs>; - -type Insturment = Node< - {Objective: Node<{}, Attrs<'ID' | 'Model' | 'NominalMagnification'>>}, - Attrs<'ID'> ->; - -interface ImageNodes { - AquisitionDate?: string; - Description?: string; - Pixels: Pixels; - InstrumentRef: Node<{}, {ID: string}>; - ObjectiveSettings: Node<{}, {ID: string}>; -} -type Image = Node>; - -type PixelType = - | 'int8' - | 'int16' - | 'int32' - | 'uint8' - | 'uint16' - | 'uint32' - | 'float' - | 'bit' - | 'double' - | 'complex' - | 'double-complex'; - -export type UnitsLength = - | 'Ym' - | 'Zm' - | 'Em' - | 'Pm' - | 'Tm' - | 'Gm' - | 'Mm' - | 'km' - | 'hm' - | 'dam' - | 'm' - | 'dm' - | 'cm' - | 'mm' - | 'µm' - | 'nm' - | 'pm' - | 'fm' - | 'am' - | 'zm' - | 'ym' - | 'Å' - | 'thou' - | 'li' - | 'in' - | 'ft' - | 'yd' - | 'mi' - | 'ua' - | 'ly' - | 'pc' - | 'pt' - | 'pixel' - | 'reference frame'; - -type PhysicalSize = `PhysicalSize${Name}`; -type PhysicalSizeUnit = `PhysicalSize${Name}Unit`; -type Size = `Size${Names}`; - -type PixelAttrs = Attrs< - PhysicalSize<'X' | 'Y' | 'Z'> | 'SignificantBits' | Size<'T' | 'C' | 'Z' | 'Y' | 'X'>, - number -> & - Attrs, UnitsLength> & - Attrs<'BigEndian' | 'Interleaved', boolean> & { - ID: string; - DimensionOrder: DimensionOrder; - Type: PixelType; - }; - -type Pixels = Node< - { - Channel: Channel | Channel[]; - TiffData: Node<{}, Attrs<'IFD' | 'PlaneCount'>>; - }, - PixelAttrs ->; - -type ChannelAttrs = - | { - ID: string; - SamplesPerPixel: number; - Name?: string; - } - | { - ID: string; - SamplesPerPixel: number; - Name?: string; - Color: number; - }; - -type Channel = Node<{}, ChannelAttrs>; - -type Root = {OME: OME}; diff --git a/modules/zarr/src/lib/utils/storage.ts b/modules/zarr/src/lib/utils/storage.ts deleted file mode 100644 index eee8b52478..0000000000 --- a/modules/zarr/src/lib/utils/storage.ts +++ /dev/null @@ -1,61 +0,0 @@ -import {KeyError} from 'zarr'; -import type {AsyncStore} from 'zarr/types/storage/types'; - -/** - * Preserves (double) slashes earlier in the path, so this works better - * for URLs. From https://stackoverflow.com/a/46427607/4178400 - * @param args parts of a path or URL to join. - */ -function joinUrlParts(...args: string[]) { - return args - .map((part, i) => { - if (i === 0) return part.trim().replace(/[/]*$/g, ''); - return part.trim().replace(/(^[/]*|[/]*$)/g, ''); - }) - .filter((x) => x.length) - .join('/'); -} - -class ReadOnlyStore { - async keys() { - return []; - } - - async deleteItem() { - return false; - } - - async setItem() { - console.warn('Cannot write to read-only store.'); // eslint-disable-line no-console - return false; - } -} - -export class FileStore extends ReadOnlyStore implements AsyncStore { - private _map: Map; - private _rootPrefix: string; - - constructor(fileMap: Map, rootPrefix = '') { - super(); - this._map = fileMap; - this._rootPrefix = rootPrefix; - } - - private _key(key: string) { - return joinUrlParts(this._rootPrefix, key); - } - - async getItem(key: string) { - const file = this._map.get(this._key(key)); - if (!file) { - throw new KeyError(key); - } - const buffer = await file.arrayBuffer(); - return buffer; - } - - async containsItem(key: string) { - const path = this._key(key); - return this._map.has(path); - } -} diff --git a/modules/zarr/src/lib/utils/utils.ts b/modules/zarr/src/lib/utils/utils.ts deleted file mode 100644 index 8930b1ea61..0000000000 --- a/modules/zarr/src/lib/utils/utils.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type {OMEXML} from './omexml'; -import type {Labels, PixelSource} from '../../types'; - -export function ensureArray(x: T | T[]) { - return Array.isArray(x) ? x : [x]; -} - -/* - * Converts 32-bit integer color representation to RGBA tuple. - * Used to serialize colors from OME-XML metadata. - * - * > console.log(intToRgba(100100)); - * > // [0, 1, 135, 4] - */ -export function intToRgba(int: number) { - if (!Number.isInteger(int)) { - throw Error('Not an integer.'); - } - - // Write number to int32 representation (4 bytes). - const buffer = new ArrayBuffer(4); - const view = new DataView(buffer); - view.setInt32(0, int, false); // offset === 0, littleEndian === false - - // Take u8 view and extract number for each byte (1 byte for R/G/B/A). - const bytes = new Uint8Array(buffer); - return Array.from(bytes) as [number, number, number, number]; -} - -/* - * Helper method to determine whether pixel data is interleaved or not. - * > isInterleaved([1, 24, 24]) === false; - * > isInterleaved([1, 24, 24, 3]) === true; - */ -export function isInterleaved(shape: number[]) { - const lastDimSize = shape[shape.length - 1]; - return lastDimSize === 3 || lastDimSize === 4; -} - -/* - * Creates typed labels from DimensionOrder. - * > imgMeta.Pixels.DimensionOrder === 'XYCZT' - * > getLabels(imgMeta.Pixels) === ['t', 'z', 'c', 'y', 'x'] - */ -// eslint-disable-next-line -type Sel = Dim extends `${infer Z}${infer X}${infer A}${infer B}${infer C}` - ? [C, B, A] - : 'error'; -export function getLabels(dimOrder: OMEXML[0]['Pixels']['DimensionOrder']) { - return dimOrder.toLowerCase().split('').reverse() as Labels>>; -} - -/* - * Creates an ES6 map of 'label' -> index - * > const labels = ['a', 'b', 'c', 'd']; - * > const dims = getDims(labels); - * > dims('a') === 0; - * > dims('b') === 1; - * > dims('c') === 2; - * > dims('hi!'); // throws - */ -export function getDims(labels: S[]) { - const lookup = new Map(labels.map((name, i) => [name, i])); - if (lookup.size !== labels.length) { - throw Error('Labels must be unique, found duplicated label.'); - } - return (name: S) => { - const index = lookup.get(name); - if (index === undefined) { - throw Error('Invalid dimension.'); - } - return index; - }; -} - -export function getImageSize(source: PixelSource) { - const interleaved = isInterleaved(source.shape); - const [height, width] = source.shape.slice(interleaved ? -3 : -2); - return {height, width}; -} - -export const SIGNAL_ABORTED = '__vivSignalAborted'; diff --git a/modules/zarr/src/lib/utils/zarr-utils.ts b/modules/zarr/src/lib/utils/zarr-utils.ts deleted file mode 100644 index 6e1d3b5a03..0000000000 --- a/modules/zarr/src/lib/utils/zarr-utils.ts +++ /dev/null @@ -1,97 +0,0 @@ -import {openGroup} from 'zarr'; -import type {ZarrArray} from 'zarr'; -import type {OMEXML} from './omexml'; -import {getLabels, isInterleaved} from './utils'; - -// TODO "Circular dependency" -import type {RootAttrs} from '../load-zarr'; - -/* - * Returns true if data shape is that expected for OME-Zarr. - */ -function isOmeZarr(dataShape: number[], Pixels: OMEXML[0]['Pixels']) { - const {SizeT, SizeC, SizeZ, SizeY, SizeX} = Pixels; - // OME-Zarr dim order is always ['t', 'c', 'z', 'y', 'x'] - const omeZarrShape = [SizeT, SizeC, SizeZ, SizeY, SizeX]; - return dataShape.every((size, i) => omeZarrShape[i] === size); -} - -/* - * Specifying different dimension orders form the METADATA.ome.xml is - * possible and necessary for creating an OME-Zarr precursor. - * - * e.g. `bioformats2raw --file_type=zarr --dimension-order='XYZCT'` - * - * This is fragile code, and will only be executed if someone - * tries to specify different dimension orders. - */ -export function guessBioformatsLabels({shape}: ZarrArray, {Pixels}: OMEXML[0]) { - if (isOmeZarr(shape, Pixels)) { - // It's an OME-Zarr Image, - return getLabels('XYZCT'); - } - - // Guess labels derived from OME-XML - const labels = getLabels(Pixels.DimensionOrder); - labels.forEach((lower, i) => { - const label = lower.toUpperCase(); - const xmlSize = (Pixels as any)[`Size${label}`] as number; - if (!xmlSize) { - throw Error(`Dimension ${label} is invalid for OME-XML.`); - } - if (shape[i] !== xmlSize) { - throw Error('Dimension mismatch between zarr source and OME-XML.'); - } - }); - - return labels; -} - -/* - * Looks for the first file with root path and returns the full path prefix. - * - * > const files = [ - * > { path: '/some/long/path/to/data.zarr/.zattrs' }, - * > { path: '/some/long/path/to/data.zarr/.zgroup' }, - * > { path: '/some/long/path/to/data.zarr/0/.zarray' }, - * > { path: '/some/long/path/to/data.zarr/0/0.0' }, - * > ]; - * > getRootPrefix(files, 'data.zarr') === '/some/long/path/to/data.zarr' - */ -export function getRootPrefix(files: {path: string}[], rootName: string) { - const first = files.find((f) => f.path.indexOf(rootName) > 0); - if (!first) { - throw Error('Could not find root in store.'); - } - const prefixLength = first.path.indexOf(rootName) + rootName.length; - return first.path.slice(0, prefixLength); -} - -export async function loadMultiscales(store: ZarrArray['store'], path = '') { - const grp = await openGroup(store, path); - const rootAttrs = (await grp.attrs.asObject()) as RootAttrs; - - let paths = ['0']; - if ('multiscales' in rootAttrs) { - const {datasets} = rootAttrs.multiscales[0]; - paths = datasets.map((d) => d.path); - } - - const data = paths.map(() => grp.getItem(path)); - return { - data: (await Promise.all(data)) as ZarrArray[], - rootAttrs - }; -} - -function prevPowerOf2(x: number) { - return 2 ** Math.floor(Math.log2(x)); -} - -export function guessTileSize(arr: ZarrArray) { - const interleaved = isInterleaved(arr.shape); - const [yChunk, xChunk] = arr.chunks.slice(interleaved ? -3 : -2); - const size = Math.min(yChunk, xChunk); - // deck.gl requirement for power-of-two tile size. - return prevPowerOf2(size); -} diff --git a/modules/zarr/src/lib/zarr-pixel-source.ts b/modules/zarr/src/lib/zarr-pixel-source.ts index bb439aad7a..ea4bcdff03 100644 --- a/modules/zarr/src/lib/zarr-pixel-source.ts +++ b/modules/zarr/src/lib/zarr-pixel-source.ts @@ -1,10 +1,8 @@ import {BoundsCheckError, slice} from 'zarr'; -import {getImageSize, isInterleaved} from './utils/utils'; -import {getIndexer} from './utils/indexer'; - import type {ZarrArray} from 'zarr'; import type {RawArray} from 'zarr/types/rawArray'; +import {getImageSize, isInterleaved, getIndexer} from './utils'; import type { PixelSource, Labels, @@ -14,7 +12,7 @@ import type { TileSelection } from '../types'; -const DTYPE_LOOKUP = { +export const DTYPE_LOOKUP = { u1: 'Uint8', u2: 'Uint16', u4: 'Uint32', diff --git a/modules/zarr/src/types.ts b/modules/zarr/src/types.ts index 8900432984..d5fe0f6a44 100644 --- a/modules/zarr/src/types.ts +++ b/modules/zarr/src/types.ts @@ -1,8 +1,44 @@ -import type {DTYPE_VALUES} from './constants'; - -export type SupportedDtype = keyof typeof DTYPE_VALUES; +import {DTYPE_LOOKUP} from './lib/zarr-pixel-source'; +export type SupportedDtype = typeof DTYPE_LOOKUP[keyof typeof DTYPE_LOOKUP]; export type SupportedTypedArray = InstanceType; +interface Multiscale { + datasets: {path: string}[]; + version?: string; +} + +interface Channel { + active: boolean; + color: string; + label: string; + window: { + min?: number; + max?: number; + start: number; + end: number; + }; +} + +interface Omero { + channels: Channel[]; + rdefs: { + defaultT?: number; + defaultZ?: number; + model: string; + }; + name?: string; +} + +interface MultiscaleAttrs { + multiscales: Multiscale[]; +} + +interface OmeAttrs extends MultiscaleAttrs { + omero: Omero; +} + +export type RootAttrs = MultiscaleAttrs | OmeAttrs; + export interface PixelData { data: SupportedTypedArray; width: number; diff --git a/modules/zarr/test/bioformats-zarr.spec.js b/modules/zarr/test/bioformats-zarr.spec.js deleted file mode 100644 index eadb5fcd95..0000000000 --- a/modules/zarr/test/bioformats-zarr.spec.js +++ /dev/null @@ -1,76 +0,0 @@ -import {test} from 'tape-promise/tape'; -import {isBrowser, fetchFile} from '@loaders.gl/core'; -import {FileSystemStore} from '@loaders.gl/zarr/lib/utils/fetch-file-store'; -import {loadBioformatsZarr} from '@loaders.gl/zarr'; - -const TEST_DATA_URL = '@loaders.gl/zarr/test/data/bioformats-zarr'; -const store = new FileSystemStore(`${TEST_DATA_URL}/data.zarr`); - -async function getMeta() { - const response = await fetchFile(`${TEST_DATA_URL}/METADATA.ome.xml`); - const meta = await response.text(); - return meta; -} - -test('Creates correct ZarrPixelSource.', async (t) => { - if (isBrowser) { - t.end(); - return; - } - - // @ts-ignore - const {data} = await loadBioformatsZarr(store, await getMeta()); - t.equal(data.length, 2, 'Image should have two levels.'); - const [base] = data; - t.deepEqual(base.labels, ['t', 'c', 'z', 'y', 'x'], 'should have DimensionOrder "XYZCT".'); - t.deepEqual(base.shape, [1, 3, 1, 167, 439], 'shape should match dimensions.'); - t.end(); -}); - -test('Get raster data.', async (t) => { - if (isBrowser) { - t.end(); - return; - } - - // @ts-ignore - const {data} = await loadBioformatsZarr(store, await getMeta()); - const [base] = data; - - for (let c = 0; c < 3; c += 1) { - const selection = {c, z: 0, t: 0}; - const pixelData = await base.getRaster({selection}); // eslint-disable-line no-await-in-loop - t.equal(pixelData.width, 439); - t.equal(pixelData.height, 167); - t.equal(pixelData.data.length, 439 * 167); - t.equal(pixelData.data.constructor.name, 'Int8Array'); - } - - try { - await base.getRaster({selection: {c: 3, z: 0, t: 0}}); - } catch (e) { - t.ok(e instanceof Error, 'index should be out of bounds.'); - } - t.end(); -}); - -test('Correct OME-XML.', async (t) => { - if (isBrowser) { - t.end(); - return; - } - - // @ts-ignore - const {metadata} = await loadBioformatsZarr(store, await getMeta()); - const {Name, Pixels} = metadata; - t.equal(Name, 'multi-channel.ome.tif', `Name should be 'multi-channel.ome.tif'.`); - t.equal(Pixels.SizeC, 3, 'Should have three channels.'); - t.equal(Pixels.SizeT, 1, 'Should have one time index.'); - t.equal(Pixels.SizeX, 439, 'Should have SizeX of 429.'); - t.equal(Pixels.SizeY, 167, 'Should have SizeY of 167.'); - t.equal(Pixels.SizeZ, 1, 'Should have one z index.'); - t.equal(Pixels.Type, 'int8', 'Should be int8 pixel type.'); - t.equal(Pixels.Channels.length, 3); - t.equal(Pixels.Channels[0].SamplesPerPixel, 1); - t.end(); -}); diff --git a/modules/zarr/test/data/bioformats-zarr/METADATA.ome.xml b/modules/zarr/test/data/bioformats-zarr/METADATA.ome.xml deleted file mode 100644 index aa80a01bf2..0000000000 --- a/modules/zarr/test/data/bioformats-zarr/METADATA.ome.xml +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/modules/zarr/test/data/bioformats-zarr/data.zarr/.zattrs b/modules/zarr/test/data/bioformats-zarr/data.zarr/.zattrs deleted file mode 100644 index f438219bd8..0000000000 --- a/modules/zarr/test/data/bioformats-zarr/data.zarr/.zattrs +++ /dev/null @@ -1 +0,0 @@ -{"bioformats2raw.layout":1} \ No newline at end of file diff --git a/modules/zarr/test/data/bioformats-zarr/data.zarr/0/.zattrs b/modules/zarr/test/data/multiscale.zarr/.zattrs similarity index 100% rename from modules/zarr/test/data/bioformats-zarr/data.zarr/0/.zattrs rename to modules/zarr/test/data/multiscale.zarr/.zattrs diff --git a/modules/zarr/test/data/bioformats-zarr/data.zarr/.zgroup b/modules/zarr/test/data/multiscale.zarr/.zgroup similarity index 100% rename from modules/zarr/test/data/bioformats-zarr/data.zarr/.zgroup rename to modules/zarr/test/data/multiscale.zarr/.zgroup diff --git a/modules/zarr/test/data/bioformats-zarr/data.zarr/0/0/.zarray b/modules/zarr/test/data/multiscale.zarr/0/.zarray similarity index 100% rename from modules/zarr/test/data/bioformats-zarr/data.zarr/0/0/.zarray rename to modules/zarr/test/data/multiscale.zarr/0/.zarray diff --git a/modules/zarr/test/data/bioformats-zarr/data.zarr/0/0/0.0.0.0.0 b/modules/zarr/test/data/multiscale.zarr/0/0.0.0.0.0 similarity index 100% rename from modules/zarr/test/data/bioformats-zarr/data.zarr/0/0/0.0.0.0.0 rename to modules/zarr/test/data/multiscale.zarr/0/0.0.0.0.0 diff --git a/modules/zarr/test/data/bioformats-zarr/data.zarr/0/0/0.1.0.0.0 b/modules/zarr/test/data/multiscale.zarr/0/0.1.0.0.0 similarity index 100% rename from modules/zarr/test/data/bioformats-zarr/data.zarr/0/0/0.1.0.0.0 rename to modules/zarr/test/data/multiscale.zarr/0/0.1.0.0.0 diff --git a/modules/zarr/test/data/bioformats-zarr/data.zarr/0/0/0.2.0.0.0 b/modules/zarr/test/data/multiscale.zarr/0/0.2.0.0.0 similarity index 100% rename from modules/zarr/test/data/bioformats-zarr/data.zarr/0/0/0.2.0.0.0 rename to modules/zarr/test/data/multiscale.zarr/0/0.2.0.0.0 diff --git a/modules/zarr/test/data/bioformats-zarr/data.zarr/0/1/.zarray b/modules/zarr/test/data/multiscale.zarr/1/.zarray similarity index 100% rename from modules/zarr/test/data/bioformats-zarr/data.zarr/0/1/.zarray rename to modules/zarr/test/data/multiscale.zarr/1/.zarray diff --git a/modules/zarr/test/data/bioformats-zarr/data.zarr/0/1/0.0.0.0.0 b/modules/zarr/test/data/multiscale.zarr/1/0.0.0.0.0 similarity index 100% rename from modules/zarr/test/data/bioformats-zarr/data.zarr/0/1/0.0.0.0.0 rename to modules/zarr/test/data/multiscale.zarr/1/0.0.0.0.0 diff --git a/modules/zarr/test/data/bioformats-zarr/data.zarr/0/1/0.1.0.0.0 b/modules/zarr/test/data/multiscale.zarr/1/0.1.0.0.0 similarity index 100% rename from modules/zarr/test/data/bioformats-zarr/data.zarr/0/1/0.1.0.0.0 rename to modules/zarr/test/data/multiscale.zarr/1/0.1.0.0.0 diff --git a/modules/zarr/test/data/bioformats-zarr/data.zarr/0/1/0.2.0.0.0 b/modules/zarr/test/data/multiscale.zarr/1/0.2.0.0.0 similarity index 100% rename from modules/zarr/test/data/bioformats-zarr/data.zarr/0/1/0.2.0.0.0 rename to modules/zarr/test/data/multiscale.zarr/1/0.2.0.0.0 diff --git a/modules/zarr/test/data/ome.zarr/.zattrs b/modules/zarr/test/data/ome.zarr/.zattrs new file mode 100644 index 0000000000..5e9ca6aa7e --- /dev/null +++ b/modules/zarr/test/data/ome.zarr/.zattrs @@ -0,0 +1,65 @@ +{ + "multiscales": [ + { + "datasets": [ + { + "path": "0" + }, + { + "path": "1" + } + ], + "version": "0.1" + } + ], + "omero": { + "channels": [ + { + "active": true, + "color": "00FF00", + "family": "linear", + "inverted": false, + "label": "channel0", + "window": { + "end": 100, + "max": 127, + "min": 0, + "start": 0 + } + }, + { + "active": true, + "color": "FF0000", + "family": "linear", + "inverted": false, + "label": "channel1", + "window": { + "end": 50, + "max": 127, + "min": 0, + "start": 0 + } + }, + { + "active": true, + "color": "0000FF", + "family": "linear", + "inverted": false, + "label": "channel1", + "window": { + "end": 25, + "max": 127, + "min": 0, + "start": 0 + } + } + ], + "name": "ome-zarr example", + "rdefs": { + "defaultT": 0, + "defaultZ": 0, + "model": "color" + }, + "version": "0.1" + } +} diff --git a/modules/zarr/test/data/bioformats-zarr/data.zarr/0/.zgroup b/modules/zarr/test/data/ome.zarr/.zgroup similarity index 100% rename from modules/zarr/test/data/bioformats-zarr/data.zarr/0/.zgroup rename to modules/zarr/test/data/ome.zarr/.zgroup diff --git a/modules/zarr/test/data/ome.zarr/0/.zarray b/modules/zarr/test/data/ome.zarr/0/.zarray new file mode 100644 index 0000000000..e5f525cc82 --- /dev/null +++ b/modules/zarr/test/data/ome.zarr/0/.zarray @@ -0,0 +1 @@ +{"shape":[1,3,1,167,439],"chunks":[1,1,1,167,439],"fill_value":"0","dtype":"|i1","filters":[],"zarr_format":2,"compressor":{"id":"blosc","cname":"lz4","clevel":5,"shuffle":1,"blocksize":0},"order":"C"} \ No newline at end of file diff --git a/modules/zarr/test/data/ome.zarr/0/0.0.0.0.0 b/modules/zarr/test/data/ome.zarr/0/0.0.0.0.0 new file mode 100644 index 0000000000..0811a2409a Binary files /dev/null and b/modules/zarr/test/data/ome.zarr/0/0.0.0.0.0 differ diff --git a/modules/zarr/test/data/ome.zarr/0/0.1.0.0.0 b/modules/zarr/test/data/ome.zarr/0/0.1.0.0.0 new file mode 100644 index 0000000000..cfa49b120c Binary files /dev/null and b/modules/zarr/test/data/ome.zarr/0/0.1.0.0.0 differ diff --git a/modules/zarr/test/data/ome.zarr/0/0.2.0.0.0 b/modules/zarr/test/data/ome.zarr/0/0.2.0.0.0 new file mode 100644 index 0000000000..cce0cda9b2 Binary files /dev/null and b/modules/zarr/test/data/ome.zarr/0/0.2.0.0.0 differ diff --git a/modules/zarr/test/data/ome.zarr/1/.zarray b/modules/zarr/test/data/ome.zarr/1/.zarray new file mode 100644 index 0000000000..edfd21616b --- /dev/null +++ b/modules/zarr/test/data/ome.zarr/1/.zarray @@ -0,0 +1 @@ +{"shape":[1,3,1,83,219],"chunks":[1,1,1,83,219],"fill_value":"0","dtype":"|i1","filters":[],"zarr_format":2,"compressor":{"id":"blosc","cname":"lz4","clevel":5,"shuffle":1,"blocksize":0},"order":"C"} \ No newline at end of file diff --git a/modules/zarr/test/data/ome.zarr/1/0.0.0.0.0 b/modules/zarr/test/data/ome.zarr/1/0.0.0.0.0 new file mode 100644 index 0000000000..6efe8c9d2f Binary files /dev/null and b/modules/zarr/test/data/ome.zarr/1/0.0.0.0.0 differ diff --git a/modules/zarr/test/data/ome.zarr/1/0.1.0.0.0 b/modules/zarr/test/data/ome.zarr/1/0.1.0.0.0 new file mode 100644 index 0000000000..86d3c0b443 Binary files /dev/null and b/modules/zarr/test/data/ome.zarr/1/0.1.0.0.0 differ diff --git a/modules/zarr/test/data/ome.zarr/1/0.2.0.0.0 b/modules/zarr/test/data/ome.zarr/1/0.2.0.0.0 new file mode 100644 index 0000000000..6990d2a773 Binary files /dev/null and b/modules/zarr/test/data/ome.zarr/1/0.2.0.0.0 differ diff --git a/modules/zarr/test/index.js b/modules/zarr/test/index.js index ddbbde9705..0af5bdec70 100644 --- a/modules/zarr/test/index.js +++ b/modules/zarr/test/index.js @@ -1,4 +1,2 @@ import './utils.spec'; - -// import './bioformats-zarr.spec'; -// import './zarr-lib.spec'; +import './pixel-source.spec'; diff --git a/modules/zarr/test/pixel-source.spec.js b/modules/zarr/test/pixel-source.spec.js new file mode 100644 index 0000000000..43bdc280cb --- /dev/null +++ b/modules/zarr/test/pixel-source.spec.js @@ -0,0 +1,98 @@ +import {test} from 'tape-promise/tape'; +import {loadZarr} from '@loaders.gl/zarr'; +import {resolvePath, isBrowser} from '@loaders.gl/core'; + +const CONTENT_BASE = resolvePath('@loaders.gl/zarr/test/data'); +const OME_FIXTURE = `${CONTENT_BASE}/ome.zarr`; +const FIXTURE = `${CONTENT_BASE}/multiscale.zarr`; +const LABELS = ['foo', 'bar', 'baz', 'y', 'x']; + +// TODO: Fix browser tests! +// Requests for fixtures in the browser return a 404, so these tests fail. +// ref: https://github.com/visgl/loaders.gl/pull/1462#discussion_r653063007 + +test('Creates correct ZarrPixelSource.', async (t) => { + if (isBrowser) { + t.end(); + return; + } + t.plan(3); + try { + const {data} = await loadZarr(FIXTURE, {labels: LABELS}); + t.equal(data.length, 2, 'Image should have two levels.'); + const [base] = data; + t.deepEqual(base.labels, ['foo', 'bar', 'baz', 'y', 'x']); + t.deepEqual(base.shape, [1, 3, 1, 167, 439], 'shape should match dimensions.'); + } catch (e) { + t.fail(e); + } +}); + +test('Creates correct OME ZarrPixelSource.', async (t) => { + if (isBrowser) { + t.end(); + return; + } + t.plan(3); + try { + const {data} = await loadZarr(OME_FIXTURE); + t.equal(data.length, 2, 'Image should have two levels.'); + const [base] = data; + t.deepEqual(base.labels, ['t', 'c', 'z', 'y', 'x'], 'should have DimensionOrder "XYZCT".'); + t.deepEqual(base.shape, [1, 3, 1, 167, 439], 'shape should match dimensions.'); + } catch (e) { + t.fail(e); + } +}); + +test('Get raster data.', async (t) => { + if (isBrowser) { + t.end(); + return; + } + t.plan(13); + try { + const {data} = await loadZarr(FIXTURE, {labels: LABELS}); + const [base] = data; + + for (let i = 0; i < 3; i += 1) { + const selection = {bar: i, foo: 0, baz: 0}; + const pixelData = await base.getRaster({selection}); + t.equal(pixelData.width, 439); + t.equal(pixelData.height, 167); + t.equal(pixelData.data.length, 439 * 167); + t.equal(pixelData.data.constructor.name, 'Int8Array'); + } + + try { + await base.getRaster({selection: {c: 3, z: 0, t: 0}}); + } catch (e) { + t.ok(e instanceof Error, 'index should be out of bounds.'); + } + } catch (e) { + t.fail(e); + } +}); + +test('Invalid labels.', async (t) => { + if (isBrowser) { + t.end(); + return; + } + t.plan(3); + try { + await loadZarr(FIXTURE, {labels: ['a', 'b', 'y', 'x']}); + } catch (e) { + t.ok(e instanceof Error, 'labels should correspond to array shape.'); + } + try { + await loadZarr(FIXTURE, {labels: ['a', 'b', 'c', 'y', 'w']}); + } catch (e) { + t.ok(e instanceof Error, 'labels should end with y and x.'); + } + try { + await loadZarr(FIXTURE, {labels: ['a', 'b', 'y', 'x', '_c']}); + } catch (e) { + t.ok(e instanceof Error, 'labels should end with y and x.'); + } +}); diff --git a/modules/zarr/test/utils.spec.js b/modules/zarr/test/utils.spec.js index 493d78c3c3..a995f13697 100644 --- a/modules/zarr/test/utils.spec.js +++ b/modules/zarr/test/utils.spec.js @@ -1,12 +1,6 @@ import test from 'tape-promise/tape'; -import {intToRgba, isInterleaved} from '@loaders.gl/zarr/lib/utils/utils'; - -test('Convert int to RGBA color', (t) => { - t.plan(2); - t.deepEqual(intToRgba(0), [0, 0, 0, 0]); - t.deepEqual(intToRgba(100100), [0, 1, 135, 4]); -}); +import {isInterleaved, getIndexer} from '@loaders.gl/zarr/lib/utils'; test('isInterleaved', (t) => { t.plan(4); @@ -15,3 +9,13 @@ test('isInterleaved', (t) => { t.ok(!isInterleaved([1, 2, 400, 400])); t.ok(!isInterleaved([1, 3, 4, 4000000])); }); + +test('Indexer creation and usage.', (t) => { + t.plan(4); + const labels = ['a', 'b', 'y', 'x']; + const indexer = getIndexer(labels); + t.deepEqual(indexer({a: 10, b: 20}), [10, 20, 0, 0], 'should allow named indexing.'); + t.deepEqual(indexer([10, 20, 0, 0]), [10, 20, 0, 0], 'allows array like indexing.'); + t.throws(() => indexer({c: 0, b: 0}), 'should throw with invalid dim name.'); + t.throws(() => getIndexer(['a', 'b', 'c', 'b', 'y', 'x']), 'no duplicated labels names.'); +}); diff --git a/modules/zarr/test/zarr-lib.spec.js b/modules/zarr/test/zarr-lib.spec.js deleted file mode 100644 index 82d9e471c1..0000000000 --- a/modules/zarr/test/zarr-lib.spec.js +++ /dev/null @@ -1,28 +0,0 @@ -import {test} from 'tape-promise/tape'; -import {isBrowser} from '@loaders.gl/core'; -import {FileSystemStore} from '@loaders.gl/zarr/lib/utils/fetch-file-store'; -import {loadMultiscales} from '@loaders.gl/zarr/lib/utils/zarr-utils'; -import {getIndexer} from '@loaders.gl/zarr/lib/utils/indexer'; - -const FIXTURE = '@loaders.gl/zarr/test/data/bioformats-zarr'; - -test('Loads zarr-multiscales', async (t) => { - if (isBrowser) { - t.end(); - return; - } - const store = new FileSystemStore(`${FIXTURE}/data.zarr`); - const {data} = await loadMultiscales(store, '0'); - t.equal(data.length, 2, 'Should have two multiscale images.'); - t.end(); -}); - -test('Indexer creation and usage.', async (t) => { - const labels = ['a', 'b', 'y', 'x']; - const indexer = getIndexer(labels); - t.deepEqual(indexer({a: 10, b: 20}), [10, 20, 0, 0], 'should allow named indexing.'); - t.deepEqual(indexer([10, 20, 0, 0]), [10, 20, 0, 0], 'allows array like indexing.'); - t.throws(() => indexer({c: 0, b: 0}), 'should throw with invalid dim name.'); - t.throws(() => getIndexer(['a', 'b', 'c', 'b', 'y', 'x']), 'no duplicated labels names.'); - t.end(); -}); diff --git a/yarn.lock b/yarn.lock index 2ce16f19e0..f748a608c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12472,10 +12472,10 @@ yauzl@^2.10.0: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" -zarr@^0.4.0: - version "0.4.2" - resolved "https://registry.yarnpkg.com/zarr/-/zarr-0.4.2.tgz#4b505ff99c4bc3a3d0738e57f6f6abb2a2353670" - integrity sha512-zyC1DaVURXqSEP6O0R8XOYa83RZkCpEHLUFOCsYn1a5n1j6ojwzwoUgeMOtHuNfuJwUnb8dGK0g3gTGGs87QiQ== +zarr@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/zarr/-/zarr-0.5.0.tgz#a12f6286370e3c72da9b92d5dfabf9457792eac7" + integrity sha512-8A6nV4VR6Y927lUuTwtj6Qd4MwWGBxBVcS7mnZMkYJGPjXv48SOiGr5lC0xIWp6GtX+tQMCgzEloT5rwUsv0Dw== dependencies: numcodecs "^0.2.0" p-queue "6.2.0"