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.
+ *
+ * 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);
+ }
+}
diff --git a/src/main/java/dev/zarr/zarrjava/ome/MultiscalesMetadataImage.java b/src/main/java/dev/zarr/zarrjava/ome/MultiscalesMetadataImage.java
new file mode 100644
index 00000000..d804f813
--- /dev/null
+++ b/src/main/java/dev/zarr/zarrjava/ome/MultiscalesMetadataImage.java
@@ -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 the concrete multiscales entry type (may be {@link MultiscalesEntry} or a version-specific subtype)
+ */
+public interface MultiscalesMetadataImage 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 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;
+ }
+}
diff --git a/src/main/java/dev/zarr/zarrjava/ome/OmeV2Group.java b/src/main/java/dev/zarr/zarrjava/ome/OmeV2Group.java
new file mode 100644
index 00000000..d95706a2
--- /dev/null
+++ b/src/main/java/dev/zarr/zarrjava/ome/OmeV2Group.java
@@ -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.
+ *
+ * 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 readAttribute(
+ Attributes attributes, StoreHandle storeHandle, String key, Class 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}). */
+ protected static T readTypedAttribute(
+ Attributes attributes, StoreHandle storeHandle, String key, TypeReference 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: }}, 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);
+ }
+}
diff --git a/src/main/java/dev/zarr/zarrjava/ome/OmeV3Group.java b/src/main/java/dev/zarr/zarrjava/ome/OmeV3Group.java
new file mode 100644
index 00000000..17cabf93
--- /dev/null
+++ b/src/main/java/dev/zarr/zarrjava/ome/OmeV3Group.java
@@ -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.
+ *
+ * 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 readOmeAttribute(
+ Attributes attributes, StoreHandle storeHandle, Class 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": }}, 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;
+ }
+}
diff --git a/src/main/java/dev/zarr/zarrjava/ome/Plate.java b/src/main/java/dev/zarr/zarrjava/ome/Plate.java
new file mode 100644
index 00000000..ea88d2dd
--- /dev/null
+++ b/src/main/java/dev/zarr/zarrjava/ome/Plate.java
@@ -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);
+ }
+}
diff --git a/src/main/java/dev/zarr/zarrjava/ome/Well.java b/src/main/java/dev/zarr/zarrjava/ome/Well.java
new file mode 100644
index 00000000..e39b859b
--- /dev/null
+++ b/src/main/java/dev/zarr/zarrjava/ome/Well.java
@@ -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.WellMetadata;
+import dev.zarr.zarrjava.store.StoreHandle;
+import dev.zarr.zarrjava.utils.Utils;
+
+import java.io.IOException;
+
+/**
+ * Unified interface for reading OME-Zarr HCS wells across Zarr format versions.
+ */
+public interface Well {
+
+ /**
+ * Returns the well metadata.
+ */
+ WellMetadata getWellMetadata() throws ZarrException;
+
+ /**
+ * Opens the image at the given path within this well (e.g. {@code "0"}).
+ */
+ MultiscaleImage openImage(String path) throws IOException, ZarrException;
+
+ /**
+ * Returns the store handle for this well node.
+ */
+ StoreHandle getStoreHandle();
+
+ /**
+ * Opens an OME-Zarr well at the given store handle, auto-detecting the Zarr version.
+ */
+ static Well open(StoreHandle storeHandle) throws IOException, ZarrException {
+ // Try v0.5: zarr.json with "ome" -> "well"
+ 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("well")) {
+ return dev.zarr.zarrjava.ome.v0_5.Well.openWell(storeHandle);
+ }
+ }
+
+ // Try v0.4: .zattrs with "well"
+ 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("well")) {
+ return dev.zarr.zarrjava.ome.v0_4.Well.openWell(storeHandle);
+ }
+ }
+
+ throw new ZarrException("No OME-Zarr well metadata found at " + storeHandle);
+ }
+}
diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/Acquisition.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/Acquisition.java
new file mode 100644
index 00000000..42f8de42
--- /dev/null
+++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/Acquisition.java
@@ -0,0 +1,41 @@
+package dev.zarr.zarrjava.ome.metadata;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import javax.annotation.Nullable;
+
+/** An HCS acquisition entry within a plate. */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public final class Acquisition {
+
+ public final int id;
+ @Nullable
+ public final String name;
+ @Nullable
+ public final Integer maximumfieldcount;
+ @Nullable
+ public final String description;
+ @Nullable
+ public final Long starttime;
+ @Nullable
+ public final Long endtime;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public Acquisition(
+ @JsonProperty(value = "id", required = true) int id,
+ @Nullable @JsonProperty("name") String name,
+ @Nullable @JsonProperty("maximumfieldcount") Integer maximumfieldcount,
+ @Nullable @JsonProperty("description") String description,
+ @Nullable @JsonProperty("starttime") Long starttime,
+ @Nullable @JsonProperty("endtime") Long endtime
+ ) {
+ this.id = id;
+ this.name = name;
+ this.maximumfieldcount = maximumfieldcount;
+ this.description = description;
+ this.starttime = starttime;
+ this.endtime = endtime;
+ }
+}
diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/Axis.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/Axis.java
new file mode 100644
index 00000000..895ea972
--- /dev/null
+++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/Axis.java
@@ -0,0 +1,41 @@
+package dev.zarr.zarrjava.ome.metadata;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import javax.annotation.Nullable;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public final class Axis {
+
+ public final String name;
+ @Nullable
+ public final String type;
+ @Nullable
+ public final String unit;
+ @Nullable
+ public final Boolean discrete;
+ @Nullable
+ @JsonProperty("long_name")
+ public final String longName;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public Axis(
+ @JsonProperty(value = "name", required = true) String name,
+ @Nullable @JsonProperty("type") String type,
+ @Nullable @JsonProperty("unit") String unit,
+ @Nullable @JsonProperty("discrete") Boolean discrete,
+ @Nullable @JsonProperty("long_name") String longName
+ ) {
+ this.name = name;
+ this.type = type;
+ this.unit = unit;
+ this.discrete = discrete;
+ this.longName = longName;
+ }
+
+ public Axis(String name, @Nullable String type, @Nullable String unit) {
+ this(name, type, unit, null, null);
+ }
+}
diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/CoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/CoordinateTransformation.java
new file mode 100644
index 00000000..72755ddf
--- /dev/null
+++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/CoordinateTransformation.java
@@ -0,0 +1,45 @@
+package dev.zarr.zarrjava.ome.metadata;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import javax.annotation.Nullable;
+import java.util.List;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public final class CoordinateTransformation {
+
+ public final String type;
+ @Nullable
+ public final List scale;
+ @Nullable
+ public final List translation;
+ @Nullable
+ public final String path;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public CoordinateTransformation(
+ @JsonProperty(value = "type", required = true) String type,
+ @Nullable @JsonProperty("scale") List scale,
+ @Nullable @JsonProperty("translation") List translation,
+ @Nullable @JsonProperty("path") String path
+ ) {
+ this.type = type;
+ this.scale = scale;
+ this.translation = translation;
+ this.path = path;
+ }
+
+ public static CoordinateTransformation scale(List scale) {
+ return new CoordinateTransformation("scale", scale, null, null);
+ }
+
+ public static CoordinateTransformation translation(List translation) {
+ return new CoordinateTransformation("translation", null, translation, null);
+ }
+
+ public static CoordinateTransformation identity() {
+ return new CoordinateTransformation("identity", null, null, null);
+ }
+}
diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/Dataset.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/Dataset.java
new file mode 100644
index 00000000..71117687
--- /dev/null
+++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/Dataset.java
@@ -0,0 +1,23 @@
+package dev.zarr.zarrjava.ome.metadata;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+public final class Dataset {
+
+ public final String path;
+ @JsonProperty("coordinateTransformations")
+ public final List coordinateTransformations;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public Dataset(
+ @JsonProperty(value = "path", required = true) String path,
+ @JsonProperty(value = "coordinateTransformations", required = true)
+ List coordinateTransformations
+ ) {
+ this.path = path;
+ this.coordinateTransformations = coordinateTransformations;
+ }
+}
diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/MultiscalesEntry.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/MultiscalesEntry.java
new file mode 100644
index 00000000..669d3d79
--- /dev/null
+++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/MultiscalesEntry.java
@@ -0,0 +1,60 @@
+package dev.zarr.zarrjava.ome.metadata;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import javax.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public final class MultiscalesEntry {
+
+ public final List axes;
+ public final List datasets;
+ @Nullable
+ @JsonProperty("coordinateTransformations")
+ public final List coordinateTransformations;
+ @Nullable
+ public final String name;
+ @Nullable
+ public final String type;
+ @Nullable
+ public final Map metadata;
+ @Nullable
+ public final String version;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public MultiscalesEntry(
+ @JsonProperty(value = "axes", required = true) List axes,
+ @JsonProperty(value = "datasets", required = true) List datasets,
+ @Nullable @JsonProperty("coordinateTransformations") List coordinateTransformations,
+ @Nullable @JsonProperty("name") String name,
+ @Nullable @JsonProperty("type") String type,
+ @Nullable @JsonProperty("metadata") Map metadata,
+ @Nullable @JsonProperty("version") String version
+ ) {
+ this.axes = axes;
+ this.datasets = datasets;
+ this.coordinateTransformations = coordinateTransformations;
+ this.name = name;
+ this.type = type;
+ this.metadata = metadata;
+ this.version = version;
+ }
+
+ public MultiscalesEntry(List axes, List datasets) {
+ this(axes, datasets, null, null, null, null, null);
+ }
+
+ /** Returns a new MultiscalesEntry with the given dataset appended. */
+ public MultiscalesEntry withDataset(Dataset dataset) {
+ List updated = new ArrayList<>(this.datasets);
+ updated.add(dataset);
+ return new MultiscalesEntry(axes, updated, coordinateTransformations, name, type, metadata, version);
+ }
+}
diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/NamedEntry.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/NamedEntry.java
new file mode 100644
index 00000000..ea3e45de
--- /dev/null
+++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/NamedEntry.java
@@ -0,0 +1,19 @@
+package dev.zarr.zarrjava.ome.metadata;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/** A named entry used for plate rows/columns. */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public final class NamedEntry {
+
+ public final String name;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public NamedEntry(
+ @JsonProperty(value = "name", required = true) String name
+ ) {
+ this.name = name;
+ }
+}
diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/OmeMetadata.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/OmeMetadata.java
new file mode 100644
index 00000000..c324bf4e
--- /dev/null
+++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/OmeMetadata.java
@@ -0,0 +1,48 @@
+package dev.zarr.zarrjava.ome.metadata;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import javax.annotation.Nullable;
+import java.util.List;
+
+/** OME-Zarr metadata stored under {@code attributes["ome"]} (v0.5). */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public final class OmeMetadata {
+
+ public final String version;
+ @Nullable
+ public final List multiscales;
+ @Nullable
+ public final OmeroMetadata omero;
+ @Nullable
+ @JsonProperty("bioformats2raw.layout")
+ public final Integer bioformats2rawLayout;
+ @Nullable
+ public final PlateMetadata plate;
+ @Nullable
+ public final WellMetadata well;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public OmeMetadata(
+ @JsonProperty(value = "version", required = true) String version,
+ @Nullable @JsonProperty("multiscales") List multiscales,
+ @Nullable @JsonProperty("omero") OmeroMetadata omero,
+ @Nullable @JsonProperty("bioformats2raw.layout") Integer bioformats2rawLayout,
+ @Nullable @JsonProperty("plate") PlateMetadata plate,
+ @Nullable @JsonProperty("well") WellMetadata well
+ ) {
+ this.version = version;
+ this.multiscales = multiscales;
+ this.omero = omero;
+ this.bioformats2rawLayout = bioformats2rawLayout;
+ this.plate = plate;
+ this.well = well;
+ }
+
+ /** Convenience constructor for multiscale images (omero/layout/plate/well all null). */
+ public OmeMetadata(String version, List multiscales) {
+ this(version, multiscales, null, null, null, null);
+ }
+}
diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/OmeroMetadata.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/OmeroMetadata.java
new file mode 100644
index 00000000..5b27b0c2
--- /dev/null
+++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/OmeroMetadata.java
@@ -0,0 +1,28 @@
+package dev.zarr.zarrjava.ome.metadata;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import javax.annotation.Nullable;
+import java.util.List;
+import java.util.Map;
+
+/** Omero display metadata stored in OME-Zarr attributes. */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public final class OmeroMetadata {
+
+ @Nullable
+ public final List