Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ jobs:

steps:
- uses: actions/checkout@v5
with:
submodules: true

- name: Set up JDK
uses: actions/setup-java@v4
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "testdata/ome/v0.6/examples"]
path = testdata/ome/v0.6/examples
url = https://github.com/jo-mueller/ngff-rfc5-coordinate-transformation-examples
138 changes: 138 additions & 0 deletions USERGUIDE-OME-ZARR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# OME-Zarr Guide for zarr-java

## Scope and supported versions

`dev.zarr.zarrjava.ome` supports:

- v0.4 (Zarr v2 layout)
- v0.5 (Zarr v3 layout)
- v0.6 / RFC-5
- v1.0 / RFC-8

## Primary entry points

Use these static open methods:

- `MultiscaleImage.open(StoreHandle)` for multiscale images (auto-detects v0.4/v0.5/v0.6/v1.0 image nodes)
- `Plate.open(StoreHandle)` for HCS plates (v0.4/v0.5)
- `Well.open(StoreHandle)` for HCS wells (v0.4/v0.5)
- `dev.zarr.zarrjava.ome.v1_0.Collection.openCollection(StoreHandle)` for v1.0 collection roots

Important v1.0 behavior:

- `MultiscaleImage.open(...)` throws if the node is a v1.0 collection root (without `ome.multiscale`)
- Open collection roots via `v1_0.Collection.openCollection(...)` and traverse with `openNode(...)`

## Essential methods for visualization tools

### MultiscaleImage

Metadata:

- `getMultiscaleNode(int i)` → normalized `ome.metadata.MultiscalesEntry`
- `getAxisNames()` → axis names from multiscale `0`
- `getScaleLevelCount()` → number of datasets/levels in multiscale `0`
- `getLabels()` / `openLabel(String)` → labels subgroup helpers

Array access:

- `openScaleLevel(int i)` → `dev.zarr.zarrjava.core.Array`
- then call `read()` or `read(offset, shape)` on that array
- typical viewer flow: read axes + scale count first, then select a level by `i`

### Plate and Well (HCS)

Metadata:

- `Plate.getPlateMetadata()`
- `Well.getWellMetadata()`

Navigation:

- `Plate.openWell(String rowColPath)` (for example `"A/1"`)
- `Well.openImage(String path)` (for example `"0"`)

## Version-specific typed metadata (when needed)

If you need the raw version-specific metadata model instead of normalized `MultiscalesEntry`:

- Cast to `MultiscalesMetadataImage<?>` and call `getMultiscalesEntry(i)`
- For v1.0 image-specific metadata, cast to `dev.zarr.zarrjava.ome.v1_0.MultiscaleImage` and use `getMultiscaleMetadata()`
- For v1.0 collections, use `dev.zarr.zarrjava.ome.v1_0.Collection.getCollectionMetadata()`

## Read example

```java
import dev.zarr.zarrjava.ome.MultiscaleImage;
import dev.zarr.zarrjava.ome.Plate;
import dev.zarr.zarrjava.ome.Well;
import dev.zarr.zarrjava.store.FilesystemStore;
import dev.zarr.zarrjava.store.StoreHandle;

StoreHandle imageHandle = new FilesystemStore("/data/ome/image.zarr").resolve();
MultiscaleImage image = MultiscaleImage.open(imageHandle);

// Drive viewer model
int scaleCount = image.getScaleLevelCount();
java.util.List<String> axisNames = image.getAxisNames();
dev.zarr.zarrjava.ome.metadata.MultiscalesEntry entry0 = image.getMultiscaleNode(0);

dev.zarr.zarrjava.core.Array s0 = image.openScaleLevel(0);
ucar.ma2.Array full = s0.read();
ucar.ma2.Array subset = s0.read(new long[]{0, 0, 0, 0, 0}, new long[]{1, 1, 4, 8, 8});

java.util.List<String> labels = image.getLabels();
if (!labels.isEmpty()) {
MultiscaleImage label = image.openLabel(labels.get(0));
}

StoreHandle plateHandle = new FilesystemStore("/data/ome/plate.zarr").resolve();
Plate plate = Plate.open(plateHandle);
Well well = plate.openWell("A/1");
MultiscaleImage fov = well.openImage("0");
```

## Write example

Creation is version-specific, but the pattern is the same: create node with version metadata, then append levels/datasets with scale transforms. For example, for v0.5:

```java
import dev.zarr.zarrjava.ome.metadata.Axis;
import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation;
import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry;
import dev.zarr.zarrjava.store.FilesystemStore;
import dev.zarr.zarrjava.store.StoreHandle;
import dev.zarr.zarrjava.v3.Array;
import dev.zarr.zarrjava.v3.DataType;

import java.util.Arrays;
import java.util.Collections;

StoreHandle out = new FilesystemStore("/tmp/ome_v05.zarr").resolve();
MultiscalesEntry ms = new MultiscalesEntry(
Arrays.asList(new Axis("y", "space", "micrometer"), new Axis("x", "space", "micrometer")),
Collections.emptyList()
);
dev.zarr.zarrjava.ome.v0_5.MultiscaleImage written = dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.create(out, ms);

written.createScaleLevel(
"s0",
Array.metadataBuilder().withShape(1024, 1024).withChunkShape(256, 256).withDataType(DataType.UINT16).build(),
Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(1.0, 1.0)))
);
written.createScaleLevel(
"s1",
Array.metadataBuilder().withShape(512, 512).withChunkShape(256, 256).withDataType(DataType.UINT16).build(),
Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(2.0, 2.0)))
);
```

## Write entry points by version

- `ome.v0_4.MultiscaleImage.create(...)`
- `ome.v0_5.MultiscaleImage.create(...)`
- `ome.v0_6.MultiscaleImage.create(...)`
- `ome.v1_0.MultiscaleImage.create(...)`
- `ome.v1_0.Collection.createCollection(...)`

Use the corresponding metadata classes for each version package.
15 changes: 11 additions & 4 deletions USERGUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
7. [Storage Backends](#storage-backends)
8. [Compression and Codecs](#compression-and-codecs)
9. [Advanced Topics](#advanced-topics)
10. [Examples](#examples)
11. [Troubleshooting](#troubleshooting)
10. [OME-Zarr](#ome-zarr-v04-v05-v06-v10)
11. [Examples](#examples)
12. [Troubleshooting](#troubleshooting)
---
## Introduction
zarr-java is a Java implementation of the [Zarr specification](https://zarr.dev/) for chunked, compressed, N-dimensional arrays. It supports both Zarr version 2 and version 3 formats, providing a unified API for working with large scientific datasets.
Expand Down Expand Up @@ -653,7 +654,6 @@ try {
- `"No Zarr array found at the specified location"` - Check path and ensure `.zarray` (v2) or `zarr.json` (v3) exists
- `"Requested data is outside of the array's domain"` - Verify that `offset + shape <= array.shape`
- `"Failed to read from store"` - Check network connectivity, file permissions, or storage availability
---

### Best Practices
1. **Chunk sizes for Best Performance**:
Expand All @@ -677,7 +677,14 @@ try {
// For balanced 3D access
.withChunkShape(100, 100, 100) // Balanced for all dimensions
```


## OME-Zarr (v0.4, v0.5, v0.6, v1.0)

For a focused OME-Zarr API guide (metadata access, array access, version behavior, and concise examples),
see:

- [`USERGUIDE-OME-ZARR.md`](USERGUIDE-OME-ZARR.md)

## Examples
### Complete Example: Creating a 3D Dataset
```java
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>dev.zarr</groupId>
<artifactId>zarr-java</artifactId>
<version>0.1.0</version>
<version>0.1.1-SNAPSHOT</version>

<name>zarr-java</name>

Expand Down
35 changes: 35 additions & 0 deletions src/main/java/dev/zarr/zarrjava/core/codec/core/ZstdCodec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package dev.zarr.zarrjava.core.codec.core;

import com.github.luben.zstd.Zstd;
import com.github.luben.zstd.ZstdCompressCtx;
import dev.zarr.zarrjava.ZarrException;
import dev.zarr.zarrjava.core.codec.BytesBytesCodec;
import dev.zarr.zarrjava.utils.Utils;

import java.nio.ByteBuffer;

public abstract class ZstdCodec extends BytesBytesCodec {

@Override
public ByteBuffer decode(ByteBuffer compressedBytes) throws ZarrException {
byte[] compressedArray = Utils.toArray(compressedBytes);
long originalSize = Zstd.getFrameContentSize(compressedArray);
if (originalSize < 0) {
throw new ZarrException("Failed to get decompressed zstd size.");
}
byte[] decompressed = Zstd.decompress(compressedArray, (int) originalSize);
return ByteBuffer.wrap(decompressed);
}

protected ByteBuffer encodeInternal(int level, boolean checksum, ByteBuffer chunkBytes)
throws ZarrException {
byte[] arr = Utils.toArray(chunkBytes);
byte[] compressed;
try (ZstdCompressCtx ctx = new ZstdCompressCtx()) {
ctx.setLevel(level);
ctx.setChecksum(checksum);
compressed = ctx.compress(arr);
}
return ByteBuffer.wrap(compressed);
}
}
143 changes: 143 additions & 0 deletions src/main/java/dev/zarr/zarrjava/ome/MultiscaleImage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package dev.zarr.zarrjava.ome;

import dev.zarr.zarrjava.ZarrException;
import dev.zarr.zarrjava.core.Node;
import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry;
import dev.zarr.zarrjava.store.StoreHandle;
import dev.zarr.zarrjava.utils.Utils;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
* Unified interface for reading OME-Zarr multiscale images across Zarr format versions.
*/
public interface MultiscaleImage {

/**
* Returns the store handle for this multiscale image node.
*/
StoreHandle getStoreHandle();

/**
* Returns a {@link MultiscalesEntry} view of multiscale {@code i}, normalized to the shared
* metadata type. All axis and dataset information is accessible from the returned entry.
*/
MultiscalesEntry getMultiscaleNode(int i) throws ZarrException;

/**
* Opens the scale level array at index {@code i} within the first multiscale entry.
*/
dev.zarr.zarrjava.core.Array openScaleLevel(int i) throws IOException, ZarrException;

/**
* Returns the number of scale levels in the first multiscale entry.
*/
int getScaleLevelCount() throws ZarrException;

/**
* Returns the axis names of the first multiscale entry.
*/
default List<String> getAxisNames() throws ZarrException {
MultiscalesEntry entry = getMultiscaleNode(0);
List<String> names = new ArrayList<>();
for (dev.zarr.zarrjava.ome.metadata.Axis axis : entry.axes) {
names.add(axis.name);
}
return names;
}

/**
* Returns all label names from the {@code labels/} sub-group, or an empty list if none exist.
*/
default List<String> getLabels() throws IOException, ZarrException {
StoreHandle labelsHandle = getStoreHandle().resolve("labels");

// Try v0.5: labels/zarr.json with {"attributes": {"labels": [...]}}
StoreHandle zarrJson = labelsHandle.resolve(Node.ZARR_JSON);
if (zarrJson.exists()) {
com.fasterxml.jackson.databind.ObjectMapper mapper = dev.zarr.zarrjava.v3.Node.makeObjectMapper();
byte[] bytes = Utils.toArray(zarrJson.readNonNull());
com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes);
com.fasterxml.jackson.databind.JsonNode attrs = root.get("attributes");
if (attrs != null && attrs.has("labels")) {
com.fasterxml.jackson.databind.JsonNode labelsNode = attrs.get("labels");
List<String> result = new ArrayList<>();
for (com.fasterxml.jackson.databind.JsonNode item : labelsNode) {
result.add(item.asText());
}
return result;
}
}

// Try v0.4: labels/.zattrs with {"labels": [...]}
StoreHandle zattrs = labelsHandle.resolve(Node.ZATTRS);
if (zattrs.exists()) {
com.fasterxml.jackson.databind.ObjectMapper mapper = dev.zarr.zarrjava.v2.Node.makeObjectMapper();
byte[] bytes = Utils.toArray(zattrs.readNonNull());
com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes);
if (root.has("labels")) {
com.fasterxml.jackson.databind.JsonNode labelsNode = root.get("labels");
List<String> result = new ArrayList<>();
for (com.fasterxml.jackson.databind.JsonNode item : labelsNode) {
result.add(item.asText());
}
return result;
}
}

return Collections.emptyList();
}

/**
* Opens the named label image from the {@code labels/} sub-group.
*/
default MultiscaleImage openLabel(String name) throws IOException, ZarrException {
return MultiscaleImage.open(getStoreHandle().resolve("labels").resolve(name));
}

/**
* Opens an OME-Zarr multiscale image at the given store handle, auto-detecting the Zarr version.
*
* <p>Tries v0.5 (zarr.json with "ome" key) first, then v0.4 (.zattrs with "multiscales" key).
*/
static MultiscaleImage open(StoreHandle storeHandle) throws IOException, ZarrException {
// Try version>= 0.5: zarr.json with "ome" key
StoreHandle zarrJson = storeHandle.resolve(Node.ZARR_JSON);
if (zarrJson.exists()) {
com.fasterxml.jackson.databind.ObjectMapper mapper = dev.zarr.zarrjava.v3.Node.makeObjectMapper();
byte[] bytes = Utils.toArray(zarrJson.readNonNull());
com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes);
com.fasterxml.jackson.databind.JsonNode attrs = root.get("attributes");
if (attrs != null && attrs.has("ome")) {
com.fasterxml.jackson.databind.JsonNode omeNode = attrs.get("ome");
String version = omeNode.has("version") ? omeNode.get("version").asText() : "";
if (version.startsWith("1.")) {
if (omeNode.has("multiscale")) {
return dev.zarr.zarrjava.ome.v1_0.MultiscaleImage.openMultiscaleImage(storeHandle);
}
throw new ZarrException("v1.0 store at " + storeHandle + " is a Collection, not a MultiscaleImage. Use v1_0.Collection.openCollection() instead.");
}
if (version.startsWith("0.6")) {
return dev.zarr.zarrjava.ome.v0_6.MultiscaleImage.openMultiscaleImage(storeHandle);
}
return dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.openMultiscaleImage(storeHandle);
}
}

// Try v0.4: .zattrs with "multiscales" key
StoreHandle zattrs = storeHandle.resolve(Node.ZATTRS);
if (zattrs.exists()) {
com.fasterxml.jackson.databind.ObjectMapper mapper = dev.zarr.zarrjava.v2.Node.makeObjectMapper();
byte[] bytes = Utils.toArray(zattrs.readNonNull());
com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes);
if (root.has("multiscales")) {
return dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.openMultiscaleImage(storeHandle);
}
}

throw new ZarrException("No OME-Zarr multiscale metadata found at " + storeHandle);
}
}
Loading
Loading