Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
639b8ef
Add ngff-rfc5 coordinate transformation examples as submodule
brokkoli71 Mar 4, 2026
6b4877e
add ome testdata for v0.5 and v0.4
brokkoli71 Mar 4, 2026
67c0970
add ome v0.4 and v0.5 implementation
brokkoli71 Mar 4, 2026
33f57b7
add ome v0.4 and v0.5 tests
brokkoli71 Mar 4, 2026
6f31a28
add omero, bioformats2raw, labels, hcs
brokkoli71 Mar 5, 2026
0630e56
Merge branch 'main' into ome-zarr
brokkoli71 Mar 5, 2026
f9ee1ed
add tests for omero, bioformats2raw, labels, and hcs
brokkoli71 Mar 5, 2026
215de0f
add ome v1.0 and v0.6
brokkoli71 Mar 5, 2026
c1eb287
reduce code duplication and better unified interface between versions…
brokkoli71 Mar 5, 2026
d8a7c92
allow unknown parameters for MultiscalesEntry
brokkoli71 Mar 9, 2026
9d969b9
add zstd codec for v2
brokkoli71 Mar 9, 2026
2b0b643
version to SNAPSHOT for compability with ome-zarr-fiji-java PR
brokkoli71 Mar 9, 2026
52b3198
update user guide to include OME-Zarr section and create dedicated OM…
brokkoli71 Mar 13, 2026
96e9cb2
Merge branch 'main' into ome-zarr
brokkoli71 Mar 19, 2026
88c158b
rename tests
brokkoli71 Mar 19, 2026
7e5d2ba
add zstd to ZarrPythonTests.testWriteV2
brokkoli71 Mar 19, 2026
c2b6cac
add OmeZarrUserGuideExamplesTest
brokkoli71 Mar 19, 2026
585690f
hardened OME metadata deserialization by ignoring unknown fields
brokkoli71 Mar 19, 2026
71749b4
add test to read ome sample data from s3
brokkoli71 Mar 19, 2026
366478c
add hint to stores in USERGUIDE-OME-ZARR.md
brokkoli71 Mar 19, 2026
94ef037
warn on unknown metadata fields
brokkoli71 Mar 19, 2026
60c63fa
add v0.4zarr3
brokkoli71 Mar 20, 2026
ed0e018
add optional omero fields id, version, name
brokkoli71 Mar 20, 2026
1700490
add omero to v0.6
brokkoli71 Mar 20, 2026
c06d7df
typing of Omero Metadata
brokkoli71 Mar 20, 2026
46b98d0
better representation of transformations
brokkoli71 Mar 20, 2026
fbb8bda
better representation of transformations
brokkoli71 Mar 20, 2026
8f9ca29
test object mappers
brokkoli71 Mar 20, 2026
1b52165
remove ome versions v0.4-zarr and v1.0
brokkoli71 Mar 20, 2026
f65677a
add scene, plate, well, and bioformats2raw to v0.6
brokkoli71 Mar 20, 2026
31f798c
refactor scene to use existing transformations
brokkoli71 Mar 20, 2026
4aa3ba5
test scene
brokkoli71 Mar 20, 2026
ddc72e8
move the namespace of ome to dev.zarr.zarrjava.experimental.ome
brokkoli71 Mar 20, 2026
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
151 changes: 151 additions & 0 deletions USERGUIDE-OME-ZARR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# OME-Zarr Guide for zarr-java

## Scope and supported versions

`dev.zarr.zarrjava.experimental.ome` supports:

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

## Primary entry points

Use these static open methods:

- `MultiscaleImage.open(StoreHandle)` for multiscale images (auto-detects v0.4/v0.5/v0.6 image nodes)
- `Plate.open(StoreHandle)` for HCS plates (v0.4/v0.5)
- `Well.open(StoreHandle)` for HCS wells (v0.4/v0.5)

### StoreHandle and stores

OME-Zarr APIs are store-agnostic: pass any `StoreHandle` (filesystem, S3, HTTP, ZIP, memory) to `open(...)`.
See storage backend setup in [`USERGUIDE.md#storage-backends`](USERGUIDE.md#storage-backends).

```java
StoreHandle s3 = new S3Store(client, "idr", "zarr/v0.5/idr0083").resolve("9822152.zarr");
MultiscaleImage image = MultiscaleImage.open(s3);
```

## Essential methods

### 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

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

- Cast to `MultiscalesMetadataImage<?>` and call `getMultiscalesEntry(i)`


## v0.6 Scene metadata

Scene roots (groups with `ome.scene`) are supported via `dev.zarr.zarrjava.experimental.ome.v0_6.Scene`:

- `Scene.openScene(StoreHandle)` / `Scene.open(StoreHandle)`
- `Scene.createScene(StoreHandle, SceneMetadata)` / `Scene.create(...)`
- `listImageNodes()` and `openImageNode(String)` for sibling multiscale images
- `getCoordinateTransformationGraph()` for lightweight metadata graph inspection

Notes:
- Parsing is permissive and explicit (no strict full-spec validation).
- Scene-level references (`input`/`output`) are resolved against scene-root coordinate systems and child image coordinate systems for graph inspection.
- Path-based transform assets can be normalized with `Scene.normalizeCoordinateTransformPath(...)` and grouped under `coordinateTransformations/` via `createCoordinateTransformationsGroup()`.

## Read example

```java
import dev.zarr.zarrjava.experimental.ome.MultiscaleImage;
import dev.zarr.zarrjava.experimental.ome.Plate;
import dev.zarr.zarrjava.experimental.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);

int scaleCount = image.getScaleLevelCount();
java.util.List<String> axisNames = image.getAxisNames();
dev.zarr.zarrjava.experimental.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 wellImage = 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.experimental.ome.metadata.Axis;
import dev.zarr.zarrjava.experimental.ome.metadata.CoordinateTransformation;
import dev.zarr.zarrjava.experimental.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.<Dataset>emptyList());
);
dev.zarr.zarrjava.experimental.ome.v0_5.MultiscaleImage written = dev.zarr.zarrjava.experimental.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(...)`

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)
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)

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);
}
}
137 changes: 137 additions & 0 deletions src/main/java/dev/zarr/zarrjava/experimental/ome/MultiscaleImage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package dev.zarr.zarrjava.experimental.ome;

import dev.zarr.zarrjava.ZarrException;
import dev.zarr.zarrjava.core.Node;
import dev.zarr.zarrjava.experimental.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.experimental.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.6 (zarr.json with version "0.6"), then v0.5 (zarr.json with "ome" key), 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 = OmeObjectMappers.makeV3Mapper();
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("0.6")) {
return dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage.openMultiscaleImage(storeHandle);
}
return dev.zarr.zarrjava.experimental.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 = OmeObjectMappers.makeV2Mapper();
byte[] bytes = Utils.toArray(zattrs.readNonNull());
com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes);
if (root.has("multiscales")) {
return dev.zarr.zarrjava.experimental.ome.v0_4.MultiscaleImage.openMultiscaleImage(storeHandle);
}
}

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