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

import dev.zarr.zarrjava.ZarrException;
import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation;
import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry;

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

/**
* Extension of {@link MultiscaleImage} that provides typed access to OME-Zarr multiscales metadata
* and supports creating new scale levels.
*
* @param <M> the concrete multiscales entry type (may be {@link MultiscalesEntry} or a version-specific subtype)
*/
public interface MultiscalesMetadataImage<M> extends MultiscaleImage {

/**
* Returns the raw multiscales entry at index {@code i} — the version-specific type.
*/
M getMultiscalesEntry(int i) throws ZarrException;

/**
* Creates a new scale level array at {@code path} with the given metadata and coordinate
* transformations, then registers it in the multiscales metadata.
*/
void createScaleLevel(
String path,
dev.zarr.zarrjava.core.ArrayMetadata arrayMetadata,
List<CoordinateTransformation> coordinateTransformations
) throws IOException, ZarrException;

/**
* Default implementation: casts the version-specific entry to the shared {@link MultiscalesEntry}.
* Versions whose entry type does not extend {@link MultiscalesEntry} (e.g., v0.6, v1.0) must
* override {@link #getMultiscaleNode(int)} directly.
*/
@Override
default MultiscalesEntry getMultiscaleNode(int i) throws ZarrException {
Object entry = getMultiscalesEntry(i);
if (!(entry instanceof MultiscalesEntry)) {
throw new ZarrException(
"getMultiscaleNode() not supported for entry type " + entry.getClass().getName()
+ "; override getMultiscaleNode() in your implementation.");
}
return (MultiscalesEntry) entry;
}
}
62 changes: 62 additions & 0 deletions src/main/java/dev/zarr/zarrjava/ome/OmeV2Group.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package dev.zarr.zarrjava.ome;

import com.fasterxml.jackson.core.type.TypeReference;
import dev.zarr.zarrjava.ZarrException;
import dev.zarr.zarrjava.core.Attributes;
import dev.zarr.zarrjava.store.StoreHandle;
import dev.zarr.zarrjava.v2.Group;
import dev.zarr.zarrjava.v2.GroupMetadata;

import javax.annotation.Nonnull;

/**
* Base class for all OME-Zarr nodes backed by a Zarr v2 group.
*
* <p>Provides {@code protected static} helpers for reading attributes and building
* {@link Attributes} for writing. The actual byte serialization is performed by
* {@link dev.zarr.zarrjava.v2.Node#makeObjectWriter()} inside {@code Group.create()} and
* {@code Group.setAttributes()}.
*/
public abstract class OmeV2Group extends Group {

protected OmeV2Group(@Nonnull StoreHandle storeHandle, @Nonnull GroupMetadata groupMetadata) {
super(storeHandle, groupMetadata);
}

/** Reads and converts a named attribute value from the given v2 group's attributes. */
protected static <T> T readAttribute(
Attributes attributes, StoreHandle storeHandle, String key, Class<T> cls)
throws ZarrException {
if (attributes == null || !attributes.containsKey(key)) {
throw new ZarrException("No '" + key + "' key found in attributes at " + storeHandle);
}
return dev.zarr.zarrjava.v2.Node.makeObjectMapper().convertValue(attributes.get(key), cls);
}

/** Reads and converts a named attribute using a {@link TypeReference} (e.g. for {@code List<T>}). */
protected static <T> T readTypedAttribute(
Attributes attributes, StoreHandle storeHandle, String key, TypeReference<T> typeRef)
throws ZarrException {
if (attributes == null || !attributes.containsKey(key)) {
throw new ZarrException("No '" + key + "' key found in attributes at " + storeHandle);
}
return dev.zarr.zarrjava.v2.Node.makeObjectMapper().convertValue(attributes.get(key), typeRef);
}

/**
* Builds {@link Attributes} containing {@code {key: <serialized value>}}, ready to
* pass to {@code Group.create()} or {@code Group.setAttributes()}.
*/
protected static Attributes buildAttributes(String key, Object value) {
Object serialized = dev.zarr.zarrjava.v2.Node.makeObjectMapper()
.convertValue(value, Object.class);
Attributes attrs = new Attributes();
attrs.put(key, serialized);
return attrs;
}

/** Serializes {@code value} via the v2 mapper to a plain Java object (Map/List/primitive). */
protected static Object serialize(Object value) {
return dev.zarr.zarrjava.v2.Node.makeObjectMapper().convertValue(value, Object.class);
}
}
48 changes: 48 additions & 0 deletions src/main/java/dev/zarr/zarrjava/ome/OmeV3Group.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package dev.zarr.zarrjava.ome;

import com.fasterxml.jackson.core.type.TypeReference;
import dev.zarr.zarrjava.ZarrException;
import dev.zarr.zarrjava.core.Attributes;
import dev.zarr.zarrjava.store.StoreHandle;
import dev.zarr.zarrjava.v3.Group;
import dev.zarr.zarrjava.v3.GroupMetadata;

import javax.annotation.Nonnull;
import java.io.IOException;

/**
* Base class for all OME-Zarr nodes backed by a Zarr v3 group.
*
* <p>Provides {@code protected static} helpers for reading OME attributes and building
* {@link Attributes} for writing. The actual byte serialization is performed by
* {@link dev.zarr.zarrjava.v3.Node#makeObjectWriter()} inside {@code Group.create()} and
* {@code Group.setAttributes()}.
*/
public abstract class OmeV3Group extends Group {

protected OmeV3Group(@Nonnull StoreHandle storeHandle, @Nonnull GroupMetadata groupMetadata)
throws IOException {
super(storeHandle, groupMetadata);
}

/** Reads and converts the {@code "ome"} attribute value from the given group's attributes. */
protected static <T> T readOmeAttribute(
Attributes attributes, StoreHandle storeHandle, Class<T> cls) throws ZarrException {
if (attributes == null || !attributes.containsKey("ome")) {
throw new ZarrException("No 'ome' key found in attributes at " + storeHandle);
}
return dev.zarr.zarrjava.v3.Node.makeObjectMapper().convertValue(attributes.get("ome"), cls);
}

/**
* Builds {@link Attributes} containing {@code {"ome": <serialized omeMetadata>}}, ready to
* pass to {@code Group.create()} or {@code Group.setAttributes()}.
*/
protected static Attributes omeAttributes(Object omeMetadata) {
Object serialized = dev.zarr.zarrjava.v3.Node.makeObjectMapper()
.convertValue(omeMetadata, Object.class);
Attributes attrs = new Attributes();
attrs.put("ome", serialized);
return attrs;
}
}
60 changes: 60 additions & 0 deletions src/main/java/dev/zarr/zarrjava/ome/Plate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package dev.zarr.zarrjava.ome;

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

import java.io.IOException;

/**
* Unified interface for reading OME-Zarr HCS plates across Zarr format versions.
*/
public interface Plate {

/**
* Returns the plate metadata.
*/
PlateMetadata getPlateMetadata() throws ZarrException;

/**
* Opens the well at the given row/column path (e.g. {@code "A/1"}).
*/
Well openWell(String rowColPath) throws IOException, ZarrException;

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

/**
* Opens an OME-Zarr plate at the given store handle, auto-detecting the Zarr version.
*/
static Plate open(StoreHandle storeHandle) throws IOException, ZarrException {
// Try v0.5: zarr.json with "ome" -> "plate"
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") && attrs.get("ome").has("plate")) {
return dev.zarr.zarrjava.ome.v0_5.Plate.openPlate(storeHandle);
}
}

// Try v0.4: .zattrs with "plate"
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("plate")) {
return dev.zarr.zarrjava.ome.v0_4.Plate.openPlate(storeHandle);
}
}

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