Skip to content

Commit

Permalink
feat: replace zarr.js with zarrita.js
Browse files Browse the repository at this point in the history
  • Loading branch information
manzt committed Aug 25, 2023
1 parent 6b6c696 commit ba20ee9
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 141 deletions.
67 changes: 59 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"@hms-dbmi/viv": "^0.13.0",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@zarrita/core": "^0.0.3",
"@zarrita/indexing": "^0.0.3",
"@zarrita/storage": "^0.0.2",
"deck.gl": "^8.6.7",
"imjoy-rpc": "^0.2.23",
"jotai": "^1.0.0",
Expand All @@ -13,9 +16,7 @@
"p-map": "^5.5.0",
"quick-lru": "^6.0.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"reference-spec-reader": "^0.1.1",
"zarr": "^0.5.2"
"react-dom": "^17.0.1"
},
"scripts": {
"start": "vite",
Expand Down
2 changes: 0 additions & 2 deletions src/codecs/jpeg2k.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@ export default class Jpeg2k {
static fromConfig(_: Record<string, any>): Jpeg2k {
return new Jpeg2k();
}

encode(_: Uint8Array): never {
throw new Error('encode not implemented');
}

async decode(data: Uint8Array): Promise<Uint8Array> {
const img = new JpxImage();
img.failOnCorruptedImage = true;
Expand Down
33 changes: 2 additions & 31 deletions src/codecs/register.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,2 @@
import { addCodec } from 'zarr';
import type { CodecConstructor } from 'numcodecs';

type CodecModule = { default: CodecConstructor<Record<string, unknown>> };

const cache: Map<string, Promise<CodecModule['default']>> = new Map();

function add(name: string, load: () => Promise<CodecModule>): void {
// Cache import to avoid duplicate loads
const loadAndCache = () => {
if (!cache.has(name)) {
const promise = load()
.then((m) => m.default)
.catch((err) => {
cache.delete(name);
throw err;
});
cache.set(name, promise);
}
return cache.get(name)!;
};
addCodec(name, loadAndCache);
}

// Add dynmaically imported codecs to Zarr.js registry.
add('lz4', () => import('numcodecs/lz4'));
add('gzip', () => import('numcodecs/gzip'));
add('zlib', () => import('numcodecs/zlib'));
add('zstd', () => import('numcodecs/zstd'));
add('blosc', () => import('numcodecs/blosc'));
add('jpeg2k', () => import('./jpeg2k'));
import { registry } from '@zarrita/core';
registry.set('jpeg2k', () => import('./jpeg2k').then((m) => m.default));
54 changes: 36 additions & 18 deletions src/io.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ImageLayer, MultiscaleImageLayer, ZarrPixelSource } from '@hms-dbmi/viv';
import { Group as ZarrGroup, openGroup, ZarrArray } from 'zarr';
import * as zarr from '@zarrita/core';
import type { Readable } from '@zarrita/storage';
import GridLayer from './gridLayer';
import { loadOmeroMultiscales, loadPlate, loadWell } from './ome';
import type { ImageLayerConfig, LayerState, MultichannelConfig, SingleChannelConfig, SourceData } from './state';
Expand All @@ -19,6 +20,7 @@ import {
range,
calcDataRange,
calcConstrastLimits,
createZarrArrayAdapter,
} from './utils';

async function loadSingleChannel(config: SingleChannelConfig, data: ZarrPixelSource<string[]>[]): Promise<SourceData> {
Expand Down Expand Up @@ -83,38 +85,54 @@ async function loadMultiChannel(
};
}

function isOmePlate(attrs: zarr.Attributes): attrs is { plate: Ome.Plate } {
return "plate" in attrs;
}

function isOmeWell(attrs: zarr.Attributes): attrs is { well: Ome.Well } {
return "well" in attrs;
}

function isOmeroMultiscales(attrs: zarr.Attributes): attrs is { omero: Ome.Omero; multiscales: Ome.Multiscale[] } {
return "omero" in attrs && "multiscales" in attrs;
}

function isMultiscales(attrs: zarr.Attributes): attrs is { multiscales: Ome.Multiscale[] } {
return "multiscales" in attrs;
}

export async function createSourceData(config: ImageLayerConfig): Promise<SourceData> {
const node = await open(config.source);
let data: ZarrArray[];
let data: zarr.Array<zarr.DataType, Readable>[];
let axes: Ome.Axis[] | undefined;

if (node instanceof ZarrGroup) {
const attrs = (await node.attrs.asObject()) as Ome.Attrs;
if (node instanceof zarr.Group) {
const attrs = node.attrs;

if ('plate' in attrs) {
if (isOmePlate(attrs)) {
return loadPlate(config, node, attrs.plate);
}

if ('well' in attrs) {
return loadWell(config, node, attrs.well);
if (isOmeWell(attrs)) {
return loadWell(config, node, attrs.well as Ome.Well);
}

if ('omero' in attrs) {
return loadOmeroMultiscales(config, node, attrs);
if (isOmeroMultiscales(attrs)) {
return loadOmeroMultiscales(config, node, attrs as {
omero: Ome.Omero;
multiscales: Ome.Multiscale[];
});
}

if (Object.keys(attrs).length === 0 && node.path) {
// No rootAttrs in this group.
// if url is to a plate/acquisition/ check parent dir for 'plate' zattrs
const parentPath = node.path.slice(0, node.path.lastIndexOf('/'));
const parent = await openGroup(node.store, parentPath);
const parentAttrs = (await parent.attrs.asObject()) as Ome.Attrs;
if ('plate' in parentAttrs) {
return loadPlate(config, parent, parentAttrs.plate);
const parent = await zarr.open(node.resolve('..'), { kind: 'group' });
if ('plate' in parent.attrs) {
return loadPlate(config, parent, parent.attrs.plate as Ome.Plate);
}
}

if (!('multiscales' in attrs)) {
if (!isMultiscales(attrs)) {
throw Error('Group is missing multiscales specification.');
}

Expand All @@ -130,7 +148,7 @@ export async function createSourceData(config: ImageLayerConfig): Promise<Source
const { channel_axis, labels } = getAxisLabelsAndChannelAxis(config, axes, data[0]);

const tileSize = guessTileSize(data[0]);
const loader = data.map((d) => new ZarrPixelSource(d, labels, tileSize));
const loader = data.map((d) => new ZarrPixelSource(createZarrArrayAdapter(d), labels, tileSize));
const [base] = loader;

// If explicit channel axis is provided, try to load as multichannel.
Expand All @@ -152,7 +170,7 @@ type Labels = [...string[], 'y', 'x'];
function getAxisLabelsAndChannelAxis(
config: ImageLayerConfig,
ngffAxes: Ome.Axis[] | undefined,
arr: ZarrArray
arr: zarr.Array<zarr.DataType, Readable>
): { labels: Labels; channel_axis: number } {
// type cast string[] to Labels
const maybeAxisLabels = config.axis_labels as undefined | Labels;
Expand Down
28 changes: 5 additions & 23 deletions src/lru-store.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,21 @@
import type { AsyncStore } from 'zarr/types/storage/types';
import type { AsyncReadable } from '@zarrita/storage';
import QuickLRU from 'quick-lru';

export class LRUCacheStore<S extends AsyncStore<ArrayBuffer>> {
cache: QuickLRU<string, Promise<ArrayBuffer>>;

export class LRUCacheStore<S extends AsyncReadable> {
cache: QuickLRU<string, Promise<Uint8Array | undefined>>;
constructor(public store: S, maxSize: number = 100) {
this.cache = new QuickLRU({ maxSize });
}

getItem(...args: Parameters<S['getItem']>) {
get(...args: Parameters<S['get']>) {
const [key, opts] = args;
if (this.cache.has(key)) {
return this.cache.get(key)!;
}
const value = this.store.getItem(key, opts).catch((err) => {
const value = this.store.get(key, opts).catch((err) => {
this.cache.delete(key);
throw err;
});
this.cache.set(key, value);
return value;
}

async containsItem(key: string) {
return this.cache.has(key) || this.store.containsItem(key);
}

keys() {
return this.store.keys();
}

deleteItem(key: string): never {
throw new Error('deleteItem not implemented');
}

setItem(key: string, value: ArrayBuffer): never {
throw new Error('setItem not implemented');
}
}
Loading

0 comments on commit ba20ee9

Please sign in to comment.