diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f497ef31..f149c542 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,8 @@ jobs: steps: - uses: actions/checkout@v5 + with: + submodules: true - name: Set up JDK uses: actions/setup-java@v4 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..37c054ac --- /dev/null +++ b/.gitmodules @@ -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 diff --git a/pom.xml b/pom.xml index a1f6a47a..7c7e4c74 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ dev.zarr zarr-java - 0.1.0 + 0.1.1-SNAPSHOT zarr-java diff --git a/src/main/java/dev/zarr/zarrjava/core/codec/core/ZstdCodec.java b/src/main/java/dev/zarr/zarrjava/core/codec/core/ZstdCodec.java new file mode 100644 index 00000000..6d8f6590 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/core/codec/core/ZstdCodec.java @@ -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); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/ome/MultiscaleImage.java new file mode 100644 index 00000000..8ad8d6e1 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/MultiscaleImage.java @@ -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 getAxisNames() throws ZarrException { + MultiscalesEntry entry = getMultiscaleNode(0); + List 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 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 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 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> channels; + @Nullable + public final Map rdefs; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public OmeroMetadata( + @Nullable @JsonProperty("channels") List> channels, + @Nullable @JsonProperty("rdefs") Map rdefs + ) { + this.channels = channels; + this.rdefs = rdefs; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/PlateMetadata.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/PlateMetadata.java new file mode 100644 index 00000000..17ec1dda --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/PlateMetadata.java @@ -0,0 +1,44 @@ +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 HCS plate metadata. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class PlateMetadata { + + public final List columns; + public final List rows; + public final List wells; + @Nullable + public final List acquisitions; + @Nullable + public final Integer field_count; + @Nullable + public final String name; + @Nullable + public final String version; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public PlateMetadata( + @JsonProperty(value = "columns", required = true) List columns, + @JsonProperty(value = "rows", required = true) List rows, + @JsonProperty(value = "wells", required = true) List wells, + @Nullable @JsonProperty("acquisitions") List acquisitions, + @Nullable @JsonProperty("field_count") Integer field_count, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("version") String version + ) { + this.columns = columns; + this.rows = rows; + this.wells = wells; + this.acquisitions = acquisitions; + this.field_count = field_count; + this.name = name; + this.version = version; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/WellImage.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/WellImage.java new file mode 100644 index 00000000..7bd9fe31 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/WellImage.java @@ -0,0 +1,25 @@ +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; + +/** A reference to an image within a well. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class WellImage { + + public final String path; + @Nullable + public final Integer acquisition; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public WellImage( + @JsonProperty(value = "path", required = true) String path, + @Nullable @JsonProperty("acquisition") Integer acquisition + ) { + this.path = path; + this.acquisition = acquisition; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/WellMetadata.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/WellMetadata.java new file mode 100644 index 00000000..fec4a899 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/WellMetadata.java @@ -0,0 +1,30 @@ +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 HCS well metadata. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class WellMetadata { + + public final List images; + @Nullable + public final String version; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public WellMetadata( + @JsonProperty(value = "images", required = true) List images, + @Nullable @JsonProperty("version") String version + ) { + this.images = images; + this.version = version; + } + + public WellMetadata(List images) { + this(images, null); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/WellRef.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/WellRef.java new file mode 100644 index 00000000..19cc713f --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/WellRef.java @@ -0,0 +1,25 @@ +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 reference to a well within a plate, identified by path and row/column indices. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class WellRef { + + public final String path; + public final int rowIndex; + public final int columnIndex; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public WellRef( + @JsonProperty(value = "path", required = true) String path, + @JsonProperty(value = "rowIndex", required = true) int rowIndex, + @JsonProperty(value = "columnIndex", required = true) int columnIndex + ) { + this.path = path; + this.rowIndex = rowIndex; + this.columnIndex = columnIndex; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_4/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/ome/v0_4/MultiscaleImage.java new file mode 100644 index 00000000..9ba70dfc --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_4/MultiscaleImage.java @@ -0,0 +1,147 @@ +package dev.zarr.zarrjava.ome.v0_4; + +import com.fasterxml.jackson.core.type.TypeReference; +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.core.Attributes; +import dev.zarr.zarrjava.ome.OmeV2Group; +import dev.zarr.zarrjava.ome.MultiscalesMetadataImage; +import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; +import dev.zarr.zarrjava.ome.metadata.Dataset; +import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.ome.metadata.OmeroMetadata; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v2.Array; +import dev.zarr.zarrjava.v2.Group; +import dev.zarr.zarrjava.v2.GroupMetadata; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * OME-Zarr v0.4 multiscale image backed by a Zarr v2 group. + */ +public final class MultiscaleImage extends OmeV2Group implements MultiscalesMetadataImage { + + private List multiscales; + @Nullable + private OmeroMetadata omeroMetadata; + @Nullable + private Integer bioformats2rawLayout; + + private MultiscaleImage( + @Nonnull StoreHandle storeHandle, + @Nonnull GroupMetadata groupMetadata, + @Nonnull List multiscales, + @Nullable OmeroMetadata omeroMetadata, + @Nullable Integer bioformats2rawLayout + ) { + super(storeHandle, groupMetadata); + this.multiscales = multiscales; + this.omeroMetadata = omeroMetadata; + this.bioformats2rawLayout = bioformats2rawLayout; + } + + /** + * Opens an existing OME-Zarr v0.4 multiscale image at the given store handle. + */ + public static MultiscaleImage openMultiscaleImage(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { + Group group = Group.open(storeHandle); + Attributes attributes = group.metadata.attributes; + List multiscales = readTypedAttribute( + attributes, storeHandle, "multiscales", new TypeReference>() {}); + OmeroMetadata omeroMetadata = attributes.containsKey("omero") + ? readAttribute(attributes, storeHandle, "omero", OmeroMetadata.class) + : null; + Integer bioformats2rawLayout = null; + if (attributes.containsKey("bioformats2raw.layout")) { + Object raw = attributes.get("bioformats2raw.layout"); + if (raw instanceof Number) { + bioformats2rawLayout = ((Number) raw).intValue(); + } + } + return new MultiscaleImage(storeHandle, group.metadata, multiscales, omeroMetadata, bioformats2rawLayout); + } + + /** + * Creates a new OME-Zarr v0.4 multiscale image at the given store handle. + */ + public static MultiscaleImage create( + @Nonnull StoreHandle storeHandle, + @Nonnull MultiscalesEntry multiscalesEntry + ) throws IOException, ZarrException { + List multiscales = Collections.singletonList(multiscalesEntry); + Group group = Group.create(storeHandle, buildAttributes("multiscales", multiscales)); + return new MultiscaleImage(storeHandle, group.metadata, multiscales, null, null); + } + + @Override + public dev.zarr.zarrjava.store.StoreHandle getStoreHandle() { + return this.storeHandle; + } + + @Nullable + public OmeroMetadata getOmeroMetadata() { + return omeroMetadata; + } + + public void setOmeroMetadata(@Nullable OmeroMetadata omeroMetadata) throws IOException, ZarrException { + this.omeroMetadata = omeroMetadata; + persistAttributes(); + } + + @Nullable + public Integer getBioformats2rawLayout() { + return bioformats2rawLayout; + } + + @Override + public MultiscalesEntry getMultiscalesEntry(int i) throws ZarrException { + return multiscales.get(i); + } + + @Override + public dev.zarr.zarrjava.core.Array openScaleLevel(int i) throws IOException, ZarrException { + String path = getMultiscalesEntry(0).datasets.get(i).path; + return Array.open(storeHandle.resolve(path)); + } + + @Override + public int getScaleLevelCount() throws ZarrException { + return getMultiscalesEntry(0).datasets.size(); + } + + @Override + public void createScaleLevel( + String path, + dev.zarr.zarrjava.core.ArrayMetadata arrayMetadata, + List coordinateTransformations + ) throws IOException, ZarrException { + if (!(arrayMetadata instanceof dev.zarr.zarrjava.v2.ArrayMetadata)) { + throw new ZarrException("Expected v2.ArrayMetadata for OME-Zarr v0.4, got " + arrayMetadata.getClass()); + } + Array.create(storeHandle.resolve(path), (dev.zarr.zarrjava.v2.ArrayMetadata) arrayMetadata); + + MultiscalesEntry current = multiscales.get(0); + MultiscalesEntry updated = current.withDataset(new Dataset(path, coordinateTransformations)); + List updatedList = new ArrayList<>(multiscales); + updatedList.set(0, updated); + multiscales = updatedList; + + persistAttributes(); + } + + private void persistAttributes() throws IOException, ZarrException { + Attributes newAttributes = buildAttributes("multiscales", multiscales); + if (omeroMetadata != null) { + newAttributes.put("omero", serialize(omeroMetadata)); + } + if (bioformats2rawLayout != null) { + newAttributes.put("bioformats2raw.layout", bioformats2rawLayout); + } + setAttributes(newAttributes); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_4/Plate.java b/src/main/java/dev/zarr/zarrjava/ome/v0_4/Plate.java new file mode 100644 index 00000000..69bcf80b --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_4/Plate.java @@ -0,0 +1,64 @@ +package dev.zarr.zarrjava.ome.v0_4; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.ome.OmeV2Group; +import dev.zarr.zarrjava.ome.metadata.PlateMetadata; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v2.Group; +import dev.zarr.zarrjava.v2.GroupMetadata; + +import javax.annotation.Nonnull; +import java.io.IOException; + +/** + * OME-Zarr v0.4 HCS plate backed by a Zarr v2 group. + */ +public final class Plate extends OmeV2Group implements dev.zarr.zarrjava.ome.Plate { + + private PlateMetadata plateMetadata; + + private Plate( + @Nonnull StoreHandle storeHandle, + @Nonnull GroupMetadata groupMetadata, + @Nonnull PlateMetadata plateMetadata + ) { + super(storeHandle, groupMetadata); + this.plateMetadata = plateMetadata; + } + + /** + * Opens an existing OME-Zarr v0.4 plate at the given store handle. + */ + public static Plate openPlate(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { + Group group = Group.open(storeHandle); + PlateMetadata plateMetadata = readAttribute( + group.metadata.attributes, storeHandle, "plate", PlateMetadata.class); + return new Plate(storeHandle, group.metadata, plateMetadata); + } + + /** + * Creates a new OME-Zarr v0.4 plate at the given store handle. + */ + public static Plate createPlate( + @Nonnull StoreHandle storeHandle, + @Nonnull PlateMetadata plateMetadata + ) throws IOException, ZarrException { + Group group = Group.create(storeHandle, buildAttributes("plate", plateMetadata)); + return new Plate(storeHandle, group.metadata, plateMetadata); + } + + @Override + public PlateMetadata getPlateMetadata() throws ZarrException { + return plateMetadata; + } + + @Override + public dev.zarr.zarrjava.ome.Well openWell(String rowColPath) throws IOException, ZarrException { + return Well.openWell(storeHandle.resolve(rowColPath)); + } + + @Override + public StoreHandle getStoreHandle() { + return this.storeHandle; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_4/Well.java b/src/main/java/dev/zarr/zarrjava/ome/v0_4/Well.java new file mode 100644 index 00000000..309b7650 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_4/Well.java @@ -0,0 +1,65 @@ +package dev.zarr.zarrjava.ome.v0_4; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.ome.MultiscaleImage; +import dev.zarr.zarrjava.ome.OmeV2Group; +import dev.zarr.zarrjava.ome.metadata.WellMetadata; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v2.Group; +import dev.zarr.zarrjava.v2.GroupMetadata; + +import javax.annotation.Nonnull; +import java.io.IOException; + +/** + * OME-Zarr v0.4 HCS well backed by a Zarr v2 group. + */ +public final class Well extends OmeV2Group implements dev.zarr.zarrjava.ome.Well { + + private WellMetadata wellMetadata; + + private Well( + @Nonnull StoreHandle storeHandle, + @Nonnull GroupMetadata groupMetadata, + @Nonnull WellMetadata wellMetadata + ) { + super(storeHandle, groupMetadata); + this.wellMetadata = wellMetadata; + } + + /** + * Opens an existing OME-Zarr v0.4 well at the given store handle. + */ + public static Well openWell(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { + Group group = Group.open(storeHandle); + WellMetadata wellMetadata = readAttribute( + group.metadata.attributes, storeHandle, "well", WellMetadata.class); + return new Well(storeHandle, group.metadata, wellMetadata); + } + + /** + * Creates a new OME-Zarr v0.4 well at the given store handle. + */ + public static Well createWell( + @Nonnull StoreHandle storeHandle, + @Nonnull WellMetadata wellMetadata + ) throws IOException, ZarrException { + Group group = Group.create(storeHandle, buildAttributes("well", wellMetadata)); + return new Well(storeHandle, group.metadata, wellMetadata); + } + + @Override + public WellMetadata getWellMetadata() throws ZarrException { + return wellMetadata; + } + + @Override + public MultiscaleImage openImage(String path) throws IOException, ZarrException { + return MultiscaleImage.open(storeHandle.resolve(path)); + } + + @Override + public StoreHandle getStoreHandle() { + return this.storeHandle; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_5/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/ome/v0_5/MultiscaleImage.java new file mode 100644 index 00000000..a6aa82d9 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_5/MultiscaleImage.java @@ -0,0 +1,110 @@ +package dev.zarr.zarrjava.ome.v0_5; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.ome.OmeV3Group; +import dev.zarr.zarrjava.ome.MultiscalesMetadataImage; +import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; +import dev.zarr.zarrjava.ome.metadata.Dataset; +import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.ome.metadata.OmeMetadata; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v3.Array; +import dev.zarr.zarrjava.v3.Group; +import dev.zarr.zarrjava.v3.GroupMetadata; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +/** + * OME-Zarr v0.5 multiscale image backed by a Zarr v3 group. + */ +public final class MultiscaleImage extends OmeV3Group implements MultiscalesMetadataImage { + + private OmeMetadata omeMetadata; + + private MultiscaleImage( + @Nonnull StoreHandle storeHandle, + @Nonnull GroupMetadata groupMetadata, + @Nonnull OmeMetadata omeMetadata + ) throws IOException { + super(storeHandle, groupMetadata); + this.omeMetadata = omeMetadata; + } + + /** + * Opens an existing OME-Zarr v0.5 multiscale image at the given store handle. + */ + public static MultiscaleImage openMultiscaleImage(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { + Group group = Group.open(storeHandle); + OmeMetadata omeMetadata = readOmeAttribute(group.metadata.attributes, storeHandle, OmeMetadata.class); + if (!omeMetadata.version.startsWith("0.5")) { + throw new ZarrException( + "Expected OME-Zarr version '0.5', got '" + omeMetadata.version + "' at " + storeHandle); + } + return new MultiscaleImage(storeHandle, group.metadata, omeMetadata); + } + + /** + * Creates a new OME-Zarr v0.5 multiscale image at the given store handle. + */ + public static MultiscaleImage create( + @Nonnull StoreHandle storeHandle, + @Nonnull MultiscalesEntry multiscalesEntry + ) throws IOException, ZarrException { + OmeMetadata omeMetadata = new OmeMetadata("0.5", Collections.singletonList(multiscalesEntry)); + Group group = Group.create(storeHandle, omeAttributes(omeMetadata)); + return new MultiscaleImage(storeHandle, group.metadata, omeMetadata); + } + + @Override + public dev.zarr.zarrjava.store.StoreHandle getStoreHandle() { + return this.storeHandle; + } + + @Override + public MultiscalesEntry getMultiscalesEntry(int i) throws ZarrException { + return omeMetadata.multiscales.get(i); + } + + @javax.annotation.Nullable + public dev.zarr.zarrjava.ome.metadata.OmeroMetadata getOmeroMetadata() { + return omeMetadata.omero; + } + + @javax.annotation.Nullable + public Integer getBioformats2rawLayout() { + return omeMetadata.bioformats2rawLayout; + } + + @Override + public dev.zarr.zarrjava.core.Array openScaleLevel(int i) throws IOException, ZarrException { + String path = getMultiscalesEntry(0).datasets.get(i).path; + return Array.open(storeHandle.resolve(path)); + } + + @Override + public int getScaleLevelCount() throws ZarrException { + return getMultiscalesEntry(0).datasets.size(); + } + + @Override + public void createScaleLevel( + String path, + dev.zarr.zarrjava.core.ArrayMetadata arrayMetadata, + List coordinateTransformations + ) throws IOException, ZarrException { + if (!(arrayMetadata instanceof dev.zarr.zarrjava.v3.ArrayMetadata)) { + throw new ZarrException("Expected v3.ArrayMetadata for OME-Zarr v0.5, got " + arrayMetadata.getClass()); + } + Array.create(storeHandle.resolve(path), (dev.zarr.zarrjava.v3.ArrayMetadata) arrayMetadata); + + MultiscalesEntry current = omeMetadata.multiscales.get(0); + MultiscalesEntry updated = current.withDataset(new Dataset(path, coordinateTransformations)); + List updatedList = new java.util.ArrayList<>(omeMetadata.multiscales); + updatedList.set(0, updated); + omeMetadata = new OmeMetadata(omeMetadata.version, updatedList); + setAttributes(omeAttributes(omeMetadata)); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_5/Plate.java b/src/main/java/dev/zarr/zarrjava/ome/v0_5/Plate.java new file mode 100644 index 00000000..c7b803e9 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_5/Plate.java @@ -0,0 +1,69 @@ +package dev.zarr.zarrjava.ome.v0_5; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.ome.OmeV3Group; +import dev.zarr.zarrjava.ome.metadata.OmeMetadata; +import dev.zarr.zarrjava.ome.metadata.PlateMetadata; +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; + +/** + * OME-Zarr v0.5 HCS plate backed by a Zarr v3 group. + */ +public final class Plate extends OmeV3Group implements dev.zarr.zarrjava.ome.Plate { + + private OmeMetadata omeMetadata; + + private Plate( + @Nonnull StoreHandle storeHandle, + @Nonnull GroupMetadata groupMetadata, + @Nonnull OmeMetadata omeMetadata + ) throws IOException { + super(storeHandle, groupMetadata); + this.omeMetadata = omeMetadata; + } + + /** + * Opens an existing OME-Zarr v0.5 plate at the given store handle. + */ + public static Plate openPlate(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { + Group group = Group.open(storeHandle); + OmeMetadata omeMetadata = readOmeAttribute( + group.metadata.attributes, storeHandle, OmeMetadata.class); + if (omeMetadata.plate == null) { + throw new ZarrException("No 'plate' found in ome metadata at " + storeHandle); + } + return new Plate(storeHandle, group.metadata, omeMetadata); + } + + /** + * Creates a new OME-Zarr v0.5 plate at the given store handle. + */ + public static Plate createPlate( + @Nonnull StoreHandle storeHandle, + @Nonnull PlateMetadata plateMetadata + ) throws IOException, ZarrException { + OmeMetadata omeMetadata = new OmeMetadata("0.5", null, null, null, plateMetadata, null); + Group group = Group.create(storeHandle, omeAttributes(omeMetadata)); + return new Plate(storeHandle, group.metadata, omeMetadata); + } + + @Override + public PlateMetadata getPlateMetadata() throws ZarrException { + return omeMetadata.plate; + } + + @Override + public dev.zarr.zarrjava.ome.Well openWell(String rowColPath) throws IOException, ZarrException { + return Well.openWell(storeHandle.resolve(rowColPath)); + } + + @Override + public StoreHandle getStoreHandle() { + return this.storeHandle; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_5/Well.java b/src/main/java/dev/zarr/zarrjava/ome/v0_5/Well.java new file mode 100644 index 00000000..349bf4aa --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_5/Well.java @@ -0,0 +1,70 @@ +package dev.zarr.zarrjava.ome.v0_5; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.ome.MultiscaleImage; +import dev.zarr.zarrjava.ome.OmeV3Group; +import dev.zarr.zarrjava.ome.metadata.OmeMetadata; +import dev.zarr.zarrjava.ome.metadata.WellMetadata; +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; + +/** + * OME-Zarr v0.5 HCS well backed by a Zarr v3 group. + */ +public final class Well extends OmeV3Group implements dev.zarr.zarrjava.ome.Well { + + private OmeMetadata omeMetadata; + + private Well( + @Nonnull StoreHandle storeHandle, + @Nonnull GroupMetadata groupMetadata, + @Nonnull OmeMetadata omeMetadata + ) throws IOException { + super(storeHandle, groupMetadata); + this.omeMetadata = omeMetadata; + } + + /** + * Opens an existing OME-Zarr v0.5 well at the given store handle. + */ + public static Well openWell(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { + Group group = Group.open(storeHandle); + OmeMetadata omeMetadata = readOmeAttribute( + group.metadata.attributes, storeHandle, OmeMetadata.class); + if (omeMetadata.well == null) { + throw new ZarrException("No 'well' found in ome metadata at " + storeHandle); + } + return new Well(storeHandle, group.metadata, omeMetadata); + } + + /** + * Creates a new OME-Zarr v0.5 well at the given store handle. + */ + public static Well createWell( + @Nonnull StoreHandle storeHandle, + @Nonnull WellMetadata wellMetadata + ) throws IOException, ZarrException { + OmeMetadata omeMetadata = new OmeMetadata("0.5", null, null, null, null, wellMetadata); + Group group = Group.create(storeHandle, omeAttributes(omeMetadata)); + return new Well(storeHandle, group.metadata, omeMetadata); + } + + @Override + public WellMetadata getWellMetadata() throws ZarrException { + return omeMetadata.well; + } + + @Override + public MultiscaleImage openImage(String path) throws IOException, ZarrException { + return MultiscaleImage.open(storeHandle.resolve(path)); + } + + @Override + public StoreHandle getStoreHandle() { + return this.storeHandle; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_6/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/ome/v0_6/MultiscaleImage.java new file mode 100644 index 00000000..5c3aeb7a --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_6/MultiscaleImage.java @@ -0,0 +1,130 @@ +package dev.zarr.zarrjava.ome.v0_6; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.ome.MultiscalesMetadataImage; +import dev.zarr.zarrjava.ome.OmeV3Group; +import dev.zarr.zarrjava.ome.metadata.Axis; +import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; +import dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateSystem; +import dev.zarr.zarrjava.ome.v0_6.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.ome.v0_6.metadata.OmeMetadata; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v3.Array; +import dev.zarr.zarrjava.v3.Group; +import dev.zarr.zarrjava.v3.GroupMetadata; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * OME-Zarr v0.6 (RFC-5) multiscale image backed by a Zarr v3 group. + */ +public final class MultiscaleImage extends OmeV3Group implements MultiscalesMetadataImage { + + private OmeMetadata omeMetadata; + + private MultiscaleImage( + @Nonnull StoreHandle storeHandle, + @Nonnull GroupMetadata groupMetadata, + @Nonnull OmeMetadata omeMetadata + ) throws IOException { + super(storeHandle, groupMetadata); + this.omeMetadata = omeMetadata; + } + + /** + * Opens an existing OME-Zarr v0.6 multiscale image at the given store handle. + */ + public static MultiscaleImage openMultiscaleImage(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { + Group group = Group.open(storeHandle); + OmeMetadata omeMetadata = readOmeAttribute( + group.metadata.attributes, storeHandle, OmeMetadata.class); + if (!omeMetadata.version.startsWith("0.6")) { + throw new ZarrException( + "Expected OME-Zarr version '0.6', got '" + omeMetadata.version + "' at " + storeHandle); + } + return new MultiscaleImage(storeHandle, group.metadata, omeMetadata); + } + + /** + * Creates a new OME-Zarr v0.6 multiscale image at the given store handle. + */ + public static MultiscaleImage create( + @Nonnull StoreHandle storeHandle, + @Nonnull MultiscalesEntry multiscalesEntry + ) throws IOException, ZarrException { + OmeMetadata omeMetadata = new OmeMetadata("0.6", Collections.singletonList(multiscalesEntry)); + Group group = Group.create(storeHandle, omeAttributes(omeMetadata)); + return new MultiscaleImage(storeHandle, group.metadata, omeMetadata); + } + + @Override + public StoreHandle getStoreHandle() { + return this.storeHandle; + } + + @Override + public MultiscalesEntry getMultiscalesEntry(int i) throws ZarrException { + return omeMetadata.multiscales.get(i); + } + + @Override + public dev.zarr.zarrjava.core.Array openScaleLevel(int i) throws IOException, ZarrException { + String path = getMultiscalesEntry(0).datasets.get(i).path; + return Array.open(storeHandle.resolve(path)); + } + + @Override + public int getScaleLevelCount() throws ZarrException { + return getMultiscalesEntry(0).datasets.size(); + } + + @Override + public void createScaleLevel( + String path, + dev.zarr.zarrjava.core.ArrayMetadata arrayMetadata, + List coordinateTransformations + ) throws IOException, ZarrException { + if (!(arrayMetadata instanceof dev.zarr.zarrjava.v3.ArrayMetadata)) { + throw new ZarrException("Expected v3.ArrayMetadata for OME-Zarr v0.6, got " + arrayMetadata.getClass()); + } + Array.create(storeHandle.resolve(path), (dev.zarr.zarrjava.v3.ArrayMetadata) arrayMetadata); + + // Convert ome.metadata.CoordinateTransformation to v0.6 CoordinateTransformation + List v06Transforms = new ArrayList<>(); + for (CoordinateTransformation ct : coordinateTransformations) { + v06Transforms.add(new dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateTransformation( + ct.type, null, null, null, ct.scale, ct.translation, ct.path, null, null, null, null)); + } + + MultiscalesEntry current = omeMetadata.multiscales.get(0); + MultiscalesEntry updated = current.withDataset(new dev.zarr.zarrjava.ome.v0_6.metadata.Dataset(path, v06Transforms)); + List updatedList = new ArrayList<>(omeMetadata.multiscales); + updatedList.set(0, updated); + omeMetadata = new OmeMetadata(omeMetadata.version, updatedList); + setAttributes(omeAttributes(omeMetadata)); + } + + @Override + public dev.zarr.zarrjava.ome.metadata.MultiscalesEntry getMultiscaleNode(int i) throws ZarrException { + MultiscalesEntry entry = getMultiscalesEntry(i); + List mappedDatasets = new ArrayList<>(); + for (dev.zarr.zarrjava.ome.v0_6.metadata.Dataset ds : entry.datasets) { + List mapped = new ArrayList<>(); + for (dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateTransformation ct : ds.coordinateTransformations) { + mapped.add(new CoordinateTransformation(ct.type, ct.scale, ct.translation, ct.path)); + } + mappedDatasets.add(new dev.zarr.zarrjava.ome.metadata.Dataset(ds.path, mapped)); + } + List axes = entry.axes; + if ((axes == null || axes.isEmpty()) && entry.coordinateSystems != null && !entry.coordinateSystems.isEmpty()) { + axes = entry.coordinateSystems.get(0).axes; + } + return new dev.zarr.zarrjava.ome.metadata.MultiscalesEntry( + axes != null ? axes : Collections.emptyList(), + mappedDatasets, null, entry.name, null, null, null); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/CoordinateSystem.java b/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/CoordinateSystem.java new file mode 100644 index 00000000..2b022601 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/CoordinateSystem.java @@ -0,0 +1,24 @@ +package dev.zarr.zarrjava.ome.v0_6.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import dev.zarr.zarrjava.ome.metadata.Axis; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class CoordinateSystem { + + public final String name; + public final List axes; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public CoordinateSystem( + @JsonProperty(value = "name", required = true) String name, + @JsonProperty(value = "axes", required = true) List axes + ) { + this.name = name; + this.axes = axes; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/CoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/CoordinateTransformation.java new file mode 100644 index 00000000..a2341613 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/CoordinateTransformation.java @@ -0,0 +1,67 @@ +package dev.zarr.zarrjava.ome.v0_6.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 String input; + @Nullable public final String output; + @Nullable public final String name; + @Nullable public final List scale; + @Nullable public final List translation; + @Nullable public final String path; + @Nullable public final List transformations; + @Nullable public final List mapAxis; + @Nullable public final List affine; + @Nullable public final CoordinateTransformation transformation; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public CoordinateTransformation( + @JsonProperty(value = "type", required = true) String type, + @Nullable @JsonProperty("input") String input, + @Nullable @JsonProperty("output") String output, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("scale") List scale, + @Nullable @JsonProperty("translation") List translation, + @Nullable @JsonProperty("path") String path, + @Nullable @JsonProperty("transformations") List transformations, + @Nullable @JsonProperty("mapAxis") List mapAxis, + @Nullable @JsonProperty("affine") List affine, + @Nullable @JsonProperty("transformation") CoordinateTransformation transformation + ) { + this.type = type; + this.input = input; + this.output = output; + this.name = name; + this.scale = scale; + this.translation = translation; + this.path = path; + this.transformations = transformations; + this.mapAxis = mapAxis; + this.affine = affine; + this.transformation = transformation; + } + + public static CoordinateTransformation scale(List scale, String input, String output) { + return new CoordinateTransformation("scale", input, output, null, scale, null, null, null, null, null, null); + } + + public static CoordinateTransformation translation(List translation, String input, String output) { + return new CoordinateTransformation("translation", input, output, null, null, translation, null, null, null, null, null); + } + + public static CoordinateTransformation identity(String input, String output) { + return new CoordinateTransformation("identity", input, output, null, null, null, null, null, null, null, null); + } + + public static CoordinateTransformation sequence(List transformations, String input, String output) { + return new CoordinateTransformation("sequence", input, output, null, null, null, null, transformations, null, null, null); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/Dataset.java b/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/Dataset.java new file mode 100644 index 00000000..5d1b4823 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/Dataset.java @@ -0,0 +1,25 @@ +package dev.zarr.zarrjava.ome.v0_6.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +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/v0_6/metadata/MultiscalesEntry.java b/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/MultiscalesEntry.java new file mode 100644 index 00000000..b2e30afd --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/MultiscalesEntry.java @@ -0,0 +1,50 @@ +package dev.zarr.zarrjava.ome.v0_6.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import dev.zarr.zarrjava.ome.metadata.Axis; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class MultiscalesEntry { + + @Nullable public final List axes; + public final List datasets; + @Nullable public final List coordinateSystems; + @Nullable public final String name; + @Nullable public final String type; + @Nullable public final Map metadata; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public MultiscalesEntry( + @Nullable @JsonProperty("axes") List axes, + @JsonProperty(value = "datasets", required = true) List datasets, + @Nullable @JsonProperty("coordinateSystems") List coordinateSystems, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("type") String type, + @Nullable @JsonProperty("metadata") Map metadata + ) { + this.axes = axes; + this.datasets = datasets; + this.coordinateSystems = coordinateSystems; + this.name = name; + this.type = type; + this.metadata = metadata; + } + + public MultiscalesEntry(List datasets, List coordinateSystems, String name) { + this(null, datasets, coordinateSystems, name, 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, coordinateSystems, name, type, metadata); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/OmeMetadata.java b/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/OmeMetadata.java new file mode 100644 index 00000000..5fdbcab2 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/OmeMetadata.java @@ -0,0 +1,25 @@ +package dev.zarr.zarrjava.ome.v0_6.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 v0.6 metadata stored under {@code attributes["ome"]}. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class OmeMetadata { + + public final String version; + @Nullable public final List multiscales; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public OmeMetadata( + @JsonProperty(value = "version", required = true) String version, + @Nullable @JsonProperty("multiscales") List multiscales + ) { + this.version = version; + this.multiscales = multiscales; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v1_0/Collection.java b/src/main/java/dev/zarr/zarrjava/ome/v1_0/Collection.java new file mode 100644 index 00000000..a5723ef6 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v1_0/Collection.java @@ -0,0 +1,83 @@ +package dev.zarr.zarrjava.ome.v1_0; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.ome.OmeV3Group; +import dev.zarr.zarrjava.ome.v1_0.metadata.CollectionMetadata; +import dev.zarr.zarrjava.ome.v1_0.metadata.OmeMetadata; +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; + +/** + * OME-Zarr v1.0 (RFC-8) collection backed by a Zarr v3 group. + */ +public final class Collection extends OmeV3Group { + + private OmeMetadata omeMetadata; + + private Collection( + @Nonnull StoreHandle storeHandle, + @Nonnull GroupMetadata groupMetadata, + @Nonnull OmeMetadata omeMetadata + ) throws IOException { + super(storeHandle, groupMetadata); + this.omeMetadata = omeMetadata; + } + + /** + * Opens an existing OME-Zarr v1.0 collection at the given store handle. + */ + public static Collection openCollection(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { + Group group = Group.open(storeHandle); + OmeMetadata omeMetadata = readOmeAttribute( + group.metadata.attributes, storeHandle, OmeMetadata.class); + if (omeMetadata.collection == null) { + throw new ZarrException("v1.0 store at " + storeHandle + " has no 'collection' — is it a MultiscaleImage?"); + } + return new Collection(storeHandle, group.metadata, omeMetadata); + } + + /** + * Creates a new OME-Zarr v1.0 collection at the given store handle. + */ + public static Collection createCollection( + @Nonnull StoreHandle storeHandle, + @Nonnull CollectionMetadata collectionMetadata + ) throws IOException, ZarrException { + OmeMetadata omeMetadata = new OmeMetadata("1.0-dev", collectionMetadata); + Group group = Group.create(storeHandle, omeAttributes(omeMetadata)); + return new Collection(storeHandle, group.metadata, omeMetadata); + } + + /** + * Returns the v1.0 collection metadata. + */ + public CollectionMetadata getCollectionMetadata() { + return omeMetadata.collection; + } + + public StoreHandle getStoreHandle() { + return this.storeHandle; + } + + /** + * Opens the child node at the given path, returning either a {@link MultiscaleImage} or a + * {@link Collection} depending on the metadata present. + */ + public Object openNode(String path) throws IOException, ZarrException { + StoreHandle child = storeHandle.resolve(path); + Group group = Group.open(child); + OmeMetadata childOme = readOmeAttribute( + group.metadata.attributes, child, OmeMetadata.class); + if (childOme.multiscale != null) { + return MultiscaleImage.openMultiscaleImage(child); + } + if (childOme.collection != null) { + return Collection.openCollection(child); + } + throw new ZarrException("Child node at " + child + " has neither 'multiscale' nor 'collection' in ome metadata"); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v1_0/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/ome/v1_0/MultiscaleImage.java new file mode 100644 index 00000000..b191398c --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v1_0/MultiscaleImage.java @@ -0,0 +1,121 @@ +package dev.zarr.zarrjava.ome.v1_0; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.ome.OmeV3Group; +import dev.zarr.zarrjava.ome.metadata.Axis; +import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; +import dev.zarr.zarrjava.ome.metadata.Dataset; +import dev.zarr.zarrjava.ome.v1_0.metadata.Level; +import dev.zarr.zarrjava.ome.v1_0.metadata.MultiscaleMetadata; +import dev.zarr.zarrjava.ome.v1_0.metadata.OmeMetadata; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v3.Array; +import dev.zarr.zarrjava.v3.Group; +import dev.zarr.zarrjava.v3.GroupMetadata; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * OME-Zarr v1.0 (RFC-8) multiscale image backed by a Zarr v3 group. + */ +public final class MultiscaleImage extends OmeV3Group implements dev.zarr.zarrjava.ome.MultiscaleImage { + + private OmeMetadata omeMetadata; + + private MultiscaleImage( + @Nonnull StoreHandle storeHandle, + @Nonnull GroupMetadata groupMetadata, + @Nonnull OmeMetadata omeMetadata + ) throws IOException { + super(storeHandle, groupMetadata); + this.omeMetadata = omeMetadata; + } + + /** + * Opens an existing OME-Zarr v1.0 multiscale image at the given store handle. + */ + public static MultiscaleImage openMultiscaleImage(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { + Group group = Group.open(storeHandle); + OmeMetadata omeMetadata = readOmeAttribute( + group.metadata.attributes, storeHandle, OmeMetadata.class); + if (omeMetadata.multiscale == null) { + throw new ZarrException("v1.0 store at " + storeHandle + " has no 'multiscale' — is it a Collection?"); + } + return new MultiscaleImage(storeHandle, group.metadata, omeMetadata); + } + + /** + * Creates a new OME-Zarr v1.0 multiscale image at the given store handle. + */ + public static MultiscaleImage create( + @Nonnull StoreHandle storeHandle, + @Nonnull MultiscaleMetadata multiscaleMetadata + ) throws IOException, ZarrException { + OmeMetadata omeMetadata = new OmeMetadata("1.0-dev", multiscaleMetadata); + Group group = Group.create(storeHandle, omeAttributes(omeMetadata)); + return new MultiscaleImage(storeHandle, group.metadata, omeMetadata); + } + + /** + * Returns the v1.0-specific multiscale metadata. + */ + public MultiscaleMetadata getMultiscaleMetadata() { + return omeMetadata.multiscale; + } + + @Override + public StoreHandle getStoreHandle() { + return this.storeHandle; + } + + @Override + public dev.zarr.zarrjava.ome.metadata.MultiscalesEntry getMultiscaleNode(int i) throws ZarrException { + if (i != 0) { + throw new ZarrException("v1.0 has a single multiscale per group; index must be 0, got " + i); + } + MultiscaleMetadata m = omeMetadata.multiscale; + List datasets = new ArrayList<>(); + for (Level level : m.levels) { + List mapped = new ArrayList<>(); + for (dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateTransformation ct : level.coordinateTransformations) { + mapped.add(new CoordinateTransformation(ct.type, ct.scale, ct.translation, ct.path)); + } + datasets.add(new Dataset(level.path, mapped)); + } + List axes = m.axes; + if ((axes == null || axes.isEmpty()) && m.coordinateSystems != null && !m.coordinateSystems.isEmpty()) { + axes = m.coordinateSystems.get(0).axes; + } + return new dev.zarr.zarrjava.ome.metadata.MultiscalesEntry( + axes != null ? axes : Collections.emptyList(), + datasets, null, m.name, null, null, null); + } + + @Override + public dev.zarr.zarrjava.core.Array openScaleLevel(int i) throws IOException, ZarrException { + return Array.open(storeHandle.resolve(omeMetadata.multiscale.levels.get(i).path)); + } + + @Override + public int getScaleLevelCount() throws ZarrException { + return omeMetadata.multiscale.levels.size(); + } + + /** + * Creates an array at the given path and appends a {@link Level} to this multiscale's metadata. + */ + public void createLevel( + String path, + dev.zarr.zarrjava.v3.ArrayMetadata arrayMetadata, + List coordinateTransformations + ) throws IOException, ZarrException { + Array.create(storeHandle.resolve(path), arrayMetadata); + MultiscaleMetadata updated = omeMetadata.multiscale.withLevel(new Level(path, coordinateTransformations)); + omeMetadata = new OmeMetadata(omeMetadata.version, updated); + setAttributes(omeAttributes(omeMetadata)); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/CollectionMetadata.java b/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/CollectionMetadata.java new file mode 100644 index 00000000..5b7bbba2 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/CollectionMetadata.java @@ -0,0 +1,25 @@ +package dev.zarr.zarrjava.ome.v1_0.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 v1.0 collection metadata stored under {@code attributes["ome"]["collection"]}. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class CollectionMetadata { + + @Nullable public final String name; + public final List nodes; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public CollectionMetadata( + @Nullable @JsonProperty("name") String name, + @JsonProperty(value = "nodes", required = true) List nodes + ) { + this.name = name; + this.nodes = nodes; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/Level.java b/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/Level.java new file mode 100644 index 00000000..3cb7f2e9 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/Level.java @@ -0,0 +1,26 @@ +package dev.zarr.zarrjava.ome.v1_0.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateTransformation; + +import java.util.List; + +/** A single resolution level within a v1.0 multiscale image (replaces v0.6 Dataset). */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class Level { + + public final String path; + public final List coordinateTransformations; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Level( + @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/v1_0/metadata/MultiscaleMetadata.java b/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/MultiscaleMetadata.java new file mode 100644 index 00000000..ede1bfeb --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/MultiscaleMetadata.java @@ -0,0 +1,45 @@ +package dev.zarr.zarrjava.ome.v1_0.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import dev.zarr.zarrjava.ome.metadata.Axis; +import dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateSystem; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; + +/** OME-Zarr v1.0 multiscale metadata stored under {@code attributes["ome"]["multiscale"]} (singular). */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class MultiscaleMetadata { + + @Nullable public final String name; + public final List levels; + @Nullable public final List coordinateSystems; + @Nullable public final List axes; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public MultiscaleMetadata( + @Nullable @JsonProperty("name") String name, + @JsonProperty(value = "levels", required = true) List levels, + @Nullable @JsonProperty("coordinateSystems") List coordinateSystems, + @Nullable @JsonProperty("axes") List axes + ) { + this.name = name; + this.levels = levels; + this.coordinateSystems = coordinateSystems; + this.axes = axes; + } + + public MultiscaleMetadata(String name, List levels, List coordinateSystems) { + this(name, levels, coordinateSystems, null); + } + + /** Returns a new MultiscaleMetadata with the given level appended. */ + public MultiscaleMetadata withLevel(Level level) { + List updated = new ArrayList<>(this.levels); + updated.add(level); + return new MultiscaleMetadata(name, updated, coordinateSystems, axes); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/NodeRef.java b/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/NodeRef.java new file mode 100644 index 00000000..17209b85 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/NodeRef.java @@ -0,0 +1,31 @@ +package dev.zarr.zarrjava.ome.v1_0.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; + +/** A reference to a child node within a v1.0 collection. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class NodeRef { + + public final String type; // "multiscale" | "collection" + public final String path; + @Nullable public final String name; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public NodeRef( + @JsonProperty(value = "type", required = true) String type, + @JsonProperty(value = "path", required = true) String path, + @Nullable @JsonProperty("name") String name + ) { + this.type = type; + this.path = path; + this.name = name; + } + + public NodeRef(String type, String path) { + this(type, path, null); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/OmeMetadata.java b/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/OmeMetadata.java new file mode 100644 index 00000000..f601521b --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/OmeMetadata.java @@ -0,0 +1,37 @@ +package dev.zarr.zarrjava.ome.v1_0.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; + +/** OME-Zarr v1.0 top-level wrapper stored under {@code attributes["ome"]}. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class OmeMetadata { + + public final String version; + /** Present when this node is a multiscale image. */ + @Nullable public final MultiscaleMetadata multiscale; + /** Present when this node is a collection. */ + @Nullable public final CollectionMetadata collection; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public OmeMetadata( + @JsonProperty(value = "version", required = true) String version, + @Nullable @JsonProperty("multiscale") MultiscaleMetadata multiscale, + @Nullable @JsonProperty("collection") CollectionMetadata collection + ) { + this.version = version; + this.multiscale = multiscale; + this.collection = collection; + } + + public OmeMetadata(String version, MultiscaleMetadata multiscale) { + this(version, multiscale, null); + } + + public OmeMetadata(String version, CollectionMetadata collection) { + this(version, null, collection); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/v2/codec/CodecRegistry.java b/src/main/java/dev/zarr/zarrjava/v2/codec/CodecRegistry.java index 2a1a9fa5..f0cb7fab 100644 --- a/src/main/java/dev/zarr/zarrjava/v2/codec/CodecRegistry.java +++ b/src/main/java/dev/zarr/zarrjava/v2/codec/CodecRegistry.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.jsontype.NamedType; import dev.zarr.zarrjava.v2.codec.core.BloscCodec; import dev.zarr.zarrjava.v2.codec.core.ZlibCodec; +import dev.zarr.zarrjava.v2.codec.core.ZstdCodec; import java.util.HashMap; import java.util.Map; @@ -14,6 +15,7 @@ public class CodecRegistry { static { addType("blosc", BloscCodec.class); addType("zlib", ZlibCodec.class); + addType("zstd", ZstdCodec.class); } public static void addType(String name, Class codecClass) { diff --git a/src/main/java/dev/zarr/zarrjava/v2/codec/core/ZstdCodec.java b/src/main/java/dev/zarr/zarrjava/v2/codec/core/ZstdCodec.java new file mode 100644 index 00000000..a6e89b40 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/v2/codec/core/ZstdCodec.java @@ -0,0 +1,39 @@ +package dev.zarr.zarrjava.v2.codec.core; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.core.ArrayMetadata; +import dev.zarr.zarrjava.v2.codec.Codec; + +import java.nio.ByteBuffer; + +public class ZstdCodec extends dev.zarr.zarrjava.core.codec.core.ZstdCodec implements Codec { + + @JsonIgnore + public final String id = "zstd"; + public final int level; + public final boolean checksum; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public ZstdCodec( + @JsonProperty(value = "level", defaultValue = "0") int level, + @JsonProperty(value = "checksum", defaultValue = "false") boolean checksum) throws ZarrException { + if (level < -131072 || level > 22) { + throw new ZarrException("'level' needs to be between -131072 and 22."); + } + this.level = level; + this.checksum = checksum; + } + + @Override + public ByteBuffer encode(ByteBuffer chunkBytes) throws ZarrException { + return encodeInternal(this.level, this.checksum, chunkBytes); + } + + @Override + public Codec evolveFromCoreArrayMetadata(ArrayMetadata.CoreArrayMetadata arrayMetadata) { + return this; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/v3/codec/core/ZstdCodec.java b/src/main/java/dev/zarr/zarrjava/v3/codec/core/ZstdCodec.java index 7d4b3365..eca58f73 100644 --- a/src/main/java/dev/zarr/zarrjava/v3/codec/core/ZstdCodec.java +++ b/src/main/java/dev/zarr/zarrjava/v3/codec/core/ZstdCodec.java @@ -3,18 +3,14 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -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 dev.zarr.zarrjava.v3.ArrayMetadata; import dev.zarr.zarrjava.v3.codec.Codec; import javax.annotation.Nonnull; import java.nio.ByteBuffer; -public class ZstdCodec extends BytesBytesCodec implements Codec { +public class ZstdCodec extends dev.zarr.zarrjava.core.codec.core.ZstdCodec implements Codec { @JsonIgnore public final String name = "zstd"; @@ -27,29 +23,9 @@ public ZstdCodec( this.configuration = configuration; } - @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 size"); - } - - byte[] decompressed = Zstd.decompress(compressedArray, (int) originalSize); - return ByteBuffer.wrap(decompressed); - } - @Override public ByteBuffer encode(ByteBuffer chunkBytes) throws ZarrException { - byte[] arr = Utils.toArray(chunkBytes); - byte[] compressed; - try (ZstdCompressCtx ctx = new ZstdCompressCtx()) { - ctx.setLevel(configuration.level); - ctx.setChecksum(configuration.checksum); - compressed = ctx.compress(arr); - } - return ByteBuffer.wrap(compressed); + return encodeInternal(configuration.level, configuration.checksum, chunkBytes); } @Override @@ -75,5 +51,3 @@ public Configuration(@JsonProperty(value = "level", defaultValue = "5") int leve } } } - - diff --git a/src/test/java/dev/zarr/zarrjava/ZarrPythonTests.java b/src/test/java/dev/zarr/zarrjava/ZarrPythonTests.java index 8b685a0d..4030a306 100644 --- a/src/test/java/dev/zarr/zarrjava/ZarrPythonTests.java +++ b/src/test/java/dev/zarr/zarrjava/ZarrPythonTests.java @@ -106,7 +106,9 @@ static Stream compressorAndDataTypeProviderV2() { new Object[]{"blosc", "lz4_shuffle_6", dev.zarr.zarrjava.v2.DataType.INT32}, new Object[]{"blosc", "lz4hc_bitshuffle_3", dev.zarr.zarrjava.v2.DataType.INT32}, new Object[]{"blosc", "zlib_shuffle_5", dev.zarr.zarrjava.v2.DataType.INT32}, - new Object[]{"blosc", "zstd_bitshuffle_9", dev.zarr.zarrjava.v2.DataType.INT32} + new Object[]{"blosc", "zstd_bitshuffle_9", dev.zarr.zarrjava.v2.DataType.INT32}, + new Object[]{"zstd", "0_true", dev.zarr.zarrjava.v2.DataType.INT32}, + new Object[]{"zstd", "5_false", dev.zarr.zarrjava.v2.DataType.INT32} ); return Stream.concat(datatypeTests, bloscTests); diff --git a/src/test/java/dev/zarr/zarrjava/ome/OmeZarrBaseTest.java b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrBaseTest.java new file mode 100644 index 00000000..e06ef1ff --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrBaseTest.java @@ -0,0 +1,87 @@ +package dev.zarr.zarrjava.ome; + +import dev.zarr.zarrjava.ZarrTest; +import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.store.FilesystemStore; +import dev.zarr.zarrjava.store.StoreHandle; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Abstract base for OME-Zarr multiscale image tests. + * + *

Exercises the unified {@link MultiscaleImage} interface contract that all versions + * (v0.4, v0.5, v0.6) must satisfy. Version-specific tests live in the concrete subclasses. + */ +public abstract class OmeZarrBaseTest extends ZarrTest { + + /** Returns the store handle for a representative multiscale image of this version. */ + abstract StoreHandle imageStoreHandle() throws Exception; + + /** Expected concrete implementation class. */ + abstract Class expectedConcreteClass(); + + /** Expected number of scale levels in the test image. */ + abstract int expectedScaleLevelCount(); + + /** Expected shape of scale level 0. */ + abstract long[] expectedLevel0Shape(); + + /** Expected axis names (from the unified interface). */ + abstract List expectedAxisNames(); + + // ── helpers ────────────────────────────────────────────────────────────── + + protected StoreHandle storeHandle(Path path) throws Exception { + return new FilesystemStore(path).resolve(); + } + + // ── unified interface contract tests ───────────────────────────────────── + + @Test + void open_returnsCorrectConcreteType() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + assertInstanceOf(expectedConcreteClass(), image); + } + + @Test + void getMultiscaleNode_hasExpectedAxes() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + MultiscalesEntry entry = image.getMultiscaleNode(0); + assertNotNull(entry); + assertEquals(expectedAxisNames().size(), entry.axes.size()); + for (int i = 0; i < expectedAxisNames().size(); i++) { + assertEquals(expectedAxisNames().get(i), entry.axes.get(i).name); + } + } + + @Test + void getMultiscaleNode_hasExpectedLevelCount() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + MultiscalesEntry entry = image.getMultiscaleNode(0); + assertEquals(expectedScaleLevelCount(), entry.datasets.size()); + } + + @Test + void getAxisNames_returnsExpected() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + assertEquals(expectedAxisNames(), image.getAxisNames()); + } + + @Test + void getScaleLevelCount_returnsExpected() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + assertEquals(expectedScaleLevelCount(), image.getScaleLevelCount()); + } + + @Test + void openScaleLevel_level0HasExpectedShape() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + dev.zarr.zarrjava.core.Array array = image.openScaleLevel(0); + assertArrayEquals(expectedLevel0Shape(), array.metadata().shape); + } +} diff --git a/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV04Test.java b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV04Test.java new file mode 100644 index 00000000..defda730 --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV04Test.java @@ -0,0 +1,182 @@ +package dev.zarr.zarrjava.ome; + +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.ome.metadata.OmeroMetadata; +import dev.zarr.zarrjava.ome.metadata.PlateMetadata; +import dev.zarr.zarrjava.ome.metadata.NamedEntry; +import dev.zarr.zarrjava.ome.metadata.WellRef; +import dev.zarr.zarrjava.store.StoreHandle; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class OmeZarrV04Test extends OmeZarrBaseTest { + + @Override + StoreHandle imageStoreHandle() throws Exception { + return storeHandle(TESTDATA.resolve("ome/v0.4")); + } + + @Override + Class expectedConcreteClass() { + return dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.class; + } + + @Override + int expectedScaleLevelCount() { return 2; } + + @Override + long[] expectedLevel0Shape() { return new long[]{1, 2, 8, 16, 16}; } + + @Override + List expectedAxisNames() { + return Arrays.asList("t", "c", "z", "y", "x"); + } + + // ── typed metadata ─────────────────────────────────────────────────────── + + @Test + void typedEntry_hasVersion() throws Exception { + MultiscalesMetadataImage image = (MultiscalesMetadataImage) MultiscaleImage.open(imageStoreHandle()); + MultiscalesEntry entry = (MultiscalesEntry) image.getMultiscalesEntry(0); + assertEquals("0.4", entry.version); + } + + @Test + void typedEntry_level0ScaleValues() throws Exception { + MultiscalesMetadataImage image = (MultiscalesMetadataImage) MultiscaleImage.open(imageStoreHandle()); + MultiscalesEntry entry = (MultiscalesEntry) image.getMultiscalesEntry(0); + List expected = Arrays.asList(1.0, 1.0, 0.5, 0.5, 0.5); + assertEquals(expected, entry.datasets.get(0).coordinateTransformations.get(0).scale); + } + + // ── omero + bioformats2raw ─────────────────────────────────────────────── + + @Test + void omero_channels() throws Exception { + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage image = + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.openMultiscaleImage(imageStoreHandle()); + OmeroMetadata omero = image.getOmeroMetadata(); + assertNotNull(omero); + assertEquals(2, omero.channels.size()); + assertEquals("DAPI", omero.channels.get(0).get("label")); + assertEquals("color", omero.rdefs.get("model")); + } + + @Test + void bioformats2rawLayout_value() throws Exception { + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage image = + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.openMultiscaleImage(imageStoreHandle()); + assertEquals(Integer.valueOf(3), image.getBioformats2rawLayout()); + } + + // ── labels ─────────────────────────────────────────────────────────────── + + @Test + void labels_list() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + assertEquals(Collections.singletonList("nuclei"), image.getLabels()); + } + + @Test + void labels_openLabel() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + MultiscaleImage nuclei = image.openLabel("nuclei"); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.class, nuclei); + assertEquals(Arrays.asList("z", "y", "x"), nuclei.getAxisNames()); + } + + // ── HCS ────────────────────────────────────────────────────────────────── + + @Test + void hcs_plate() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.4_hcs"))); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.Plate.class, plate); + PlateMetadata meta = plate.getPlateMetadata(); + assertEquals(2, meta.columns.size()); + assertEquals("A", meta.rows.get(0).name); + assertEquals("A/1", meta.wells.get(0).path); + } + + @Test + void hcs_wellViaPlate() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.4_hcs"))); + Well well = plate.openWell("A/1"); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.Well.class, well); + assertEquals(1, well.getWellMetadata().images.size()); + assertEquals("0", well.getWellMetadata().images.get(0).path); + } + + @Test + void hcs_fullNavigation() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.4_hcs"))); + MultiscaleImage fov = plate.openWell("A/1").openImage("0"); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.class, fov); + assertEquals(Arrays.asList("t", "c", "z", "y", "x"), fov.getAxisNames()); + } + + // ── write round-trips ──────────────────────────────────────────────────── + + @Test + void write_createAndReopen() throws Exception { + List axes = Arrays.asList( + new Axis("z", "space", "micrometer"), + new Axis("y", "space", "micrometer")); + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v04_create")); + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage created = + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.create(handle, new MultiscalesEntry(axes, Collections.emptyList())); + created.createScaleLevel("0", + new dev.zarr.zarrjava.v2.ArrayMetadata(2, new long[]{16, 16}, new int[]{16, 16}, + dev.zarr.zarrjava.v2.DataType.FLOAT32, 0, dev.zarr.zarrjava.v2.Order.C, null, null, null), + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(0.5, 0.5)))); + + MultiscaleImage reopened = MultiscaleImage.open(handle); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.class, reopened); + assertEquals(Arrays.asList("z", "y"), reopened.getAxisNames()); + assertEquals(1, reopened.getScaleLevelCount()); + } + + @Test + void write_omeroRoundTrip() throws Exception { + List axes = Arrays.asList(new Axis("z", "space", "micrometer"), new Axis("y", "space", "micrometer")); + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v04_omero")); + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage created = + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.create(handle, new MultiscalesEntry(axes, Collections.emptyList())); + created.createScaleLevel("0", + new dev.zarr.zarrjava.v2.ArrayMetadata(2, new long[]{16, 16}, new int[]{16, 16}, + dev.zarr.zarrjava.v2.DataType.FLOAT32, 0, dev.zarr.zarrjava.v2.Order.C, null, null, null), + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(1.0, 1.0)))); + + java.util.Map ch = new java.util.HashMap(); + ch.put("label", "DAPI"); + java.util.Map rd = new java.util.HashMap(); + rd.put("model", "color"); + created.setOmeroMetadata(new OmeroMetadata(Collections.singletonList(ch), rd)); + + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage reopened = + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.openMultiscaleImage(handle); + OmeroMetadata got = reopened.getOmeroMetadata(); + assertNotNull(got); + assertEquals("DAPI", got.channels.get(0).get("label")); + } + + @Test + void write_plateRoundTrip() throws Exception { + PlateMetadata meta = new PlateMetadata( + Arrays.asList(new NamedEntry("1"), new NamedEntry("2")), + Arrays.asList(new NamedEntry("A"), new NamedEntry("B")), + Collections.singletonList(new WellRef("A/1", 0, 0)), + null, null, null, null); + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v04_plate")); + dev.zarr.zarrjava.ome.v0_4.Plate.createPlate(handle, meta); + Plate reopened = Plate.open(handle); + assertEquals(2, reopened.getPlateMetadata().columns.size()); + assertEquals("A/1", reopened.getPlateMetadata().wells.get(0).path); + } +} diff --git a/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV05Test.java b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV05Test.java new file mode 100644 index 00000000..46638070 --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV05Test.java @@ -0,0 +1,223 @@ +package dev.zarr.zarrjava.ome; + +import dev.zarr.zarrjava.core.Attributes; +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.ome.metadata.NamedEntry; +import dev.zarr.zarrjava.ome.metadata.OmeroMetadata; +import dev.zarr.zarrjava.ome.metadata.PlateMetadata; +import dev.zarr.zarrjava.ome.metadata.WellImage; +import dev.zarr.zarrjava.ome.metadata.WellMetadata; +import dev.zarr.zarrjava.ome.metadata.WellRef; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v3.Array; +import dev.zarr.zarrjava.v3.DataType; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class OmeZarrV05Test extends OmeZarrBaseTest { + + @Override + StoreHandle imageStoreHandle() throws Exception { + return storeHandle(TESTDATA.resolve("ome/v0.5")); + } + + @Override + Class expectedConcreteClass() { + return dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class; + } + + @Override + int expectedScaleLevelCount() { return 2; } + + @Override + long[] expectedLevel0Shape() { return new long[]{1, 2, 8, 16, 16}; } + + @Override + List expectedAxisNames() { + return Arrays.asList("t", "c", "z", "y", "x"); + } + + // ── typed metadata ─────────────────────────────────────────────────────── + + @Test + void typedEntry_noVersion() throws Exception { + MultiscalesMetadataImage image = (MultiscalesMetadataImage) MultiscaleImage.open(imageStoreHandle()); + MultiscalesEntry entry = (MultiscalesEntry) image.getMultiscalesEntry(0); + assertEquals("test_image", entry.name); + assertNull(entry.version); + } + + @Test + void typedEntry_level0ScaleValues() throws Exception { + MultiscalesMetadataImage image = (MultiscalesMetadataImage) MultiscaleImage.open(imageStoreHandle()); + MultiscalesEntry entry = (MultiscalesEntry) image.getMultiscalesEntry(0); + List expected = Arrays.asList(1.0, 1.0, 0.5, 0.5, 0.5); + assertEquals(expected, entry.datasets.get(0).coordinateTransformations.get(0).scale); + } + + @Test + void openScaleLevel_level1HasExpectedShape() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + dev.zarr.zarrjava.core.Array level1 = image.openScaleLevel(1); + assertArrayEquals(new long[]{1, 2, 4, 8, 8}, level1.metadata().shape); + } + + // ── omero + bioformats2raw ─────────────────────────────────────────────── + + @Test + void omero_channels() throws Exception { + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage image = + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.openMultiscaleImage(imageStoreHandle()); + OmeroMetadata omero = image.getOmeroMetadata(); + assertNotNull(omero); + assertEquals(2, omero.channels.size()); + assertEquals("DAPI", omero.channels.get(0).get("label")); + assertEquals("GFP", omero.channels.get(1).get("label")); + assertEquals("color", omero.rdefs.get("model")); + } + + @Test + void bioformats2rawLayout_value() throws Exception { + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage image = + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.openMultiscaleImage(imageStoreHandle()); + assertEquals(Integer.valueOf(3), image.getBioformats2rawLayout()); + } + + // ── labels ─────────────────────────────────────────────────────────────── + + @Test + void labels_list() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + assertEquals(Collections.singletonList("nuclei"), image.getLabels()); + } + + @Test + void labels_openLabel() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + MultiscaleImage nuclei = image.openLabel("nuclei"); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class, nuclei); + assertEquals(Arrays.asList("z", "y", "x"), nuclei.getAxisNames()); + } + + // ── HCS ────────────────────────────────────────────────────────────────── + + @Test + void hcs_plate() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.5_hcs"))); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.Plate.class, plate); + PlateMetadata meta = plate.getPlateMetadata(); + assertEquals(2, meta.columns.size()); + assertEquals(2, meta.rows.size()); + assertEquals("A", meta.rows.get(0).name); + assertEquals("1", meta.columns.get(0).name); + assertEquals("A/1", meta.wells.get(0).path); + } + + @Test + void hcs_wellViaPlate() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.5_hcs"))); + Well well = plate.openWell("A/1"); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.Well.class, well); + assertEquals(1, well.getWellMetadata().images.size()); + assertEquals("0", well.getWellMetadata().images.get(0).path); + assertEquals(Integer.valueOf(0), well.getWellMetadata().images.get(0).acquisition); + } + + @Test + void hcs_fullNavigation() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.5_hcs"))); + MultiscaleImage fov = plate.openWell("A/1").openImage("0"); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class, fov); + assertEquals(Arrays.asList("t", "c", "z", "y", "x"), fov.getAxisNames()); + } + + // ── write round-trips ──────────────────────────────────────────────────── + + @Test + void write_createAndReopen() throws Exception { + List axes = Arrays.asList( + new Axis("z", "space", "micrometer"), + new Axis("y", "space", "micrometer")); + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v05_create")); + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage created = + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.create(handle, new MultiscalesEntry(axes, Collections.emptyList())); + created.createScaleLevel("0", + Array.metadataBuilder().withShape(16, 16).withChunkShape(16, 16).withDataType(DataType.FLOAT32).build(), + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(0.5, 0.5)))); + + MultiscaleImage reopened = MultiscaleImage.open(handle); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class, reopened); + assertEquals(Arrays.asList("z", "y"), reopened.getAxisNames()); + assertEquals(1, reopened.getScaleLevelCount()); + assertEquals("0", reopened.getMultiscaleNode(0).datasets.get(0).path); + } + + @Test + void write_labelsRoundTrip() throws Exception { + List axes = Arrays.asList( + new Axis("z", "space", "micrometer"), + new Axis("y", "space", "micrometer")); + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v05_labels")); + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.create(handle, new MultiscalesEntry(axes, Collections.emptyList())); + + Attributes labelsAttrs = new Attributes(); + labelsAttrs.put("labels", Arrays.asList("nuclei")); + dev.zarr.zarrjava.v3.Group.create(handle.resolve("labels"), labelsAttrs); + + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage nuclei = dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.create( + handle.resolve("labels").resolve("nuclei"), new MultiscalesEntry(axes, Collections.emptyList())); + nuclei.createScaleLevel("0", + Array.metadataBuilder().withShape(16, 16).withChunkShape(16, 16).withDataType(DataType.UINT8).build(), + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(1.0, 1.0)))); + + MultiscaleImage reopened = MultiscaleImage.open(handle); + assertEquals(Collections.singletonList("nuclei"), reopened.getLabels()); + assertEquals(Arrays.asList("z", "y"), reopened.openLabel("nuclei").getAxisNames()); + } + + @Test + void write_plateRoundTrip() throws Exception { + PlateMetadata meta = new PlateMetadata( + Arrays.asList(new NamedEntry("1"), new NamedEntry("2")), + Arrays.asList(new NamedEntry("A"), new NamedEntry("B")), + Collections.singletonList(new WellRef("A/1", 0, 0)), + null, null, null, null); + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v05_plate")); + dev.zarr.zarrjava.ome.v0_5.Plate.createPlate(handle, meta); + Plate reopened = Plate.open(handle); + assertEquals(2, reopened.getPlateMetadata().columns.size()); + assertEquals("A", reopened.getPlateMetadata().rows.get(0).name); + assertEquals("A/1", reopened.getPlateMetadata().wells.get(0).path); + } + + @Test + void write_hcsFullIntegration() throws Exception { + StoreHandle plateHandle = storeHandle(TESTOUTPUT.resolve("ome_v05_hcs_full")); + dev.zarr.zarrjava.ome.v0_5.Plate.createPlate(plateHandle, new PlateMetadata( + Collections.singletonList(new NamedEntry("1")), + Collections.singletonList(new NamedEntry("A")), + Collections.singletonList(new WellRef("A/1", 0, 0)), + null, null, null, null)); + dev.zarr.zarrjava.ome.v0_5.Well.createWell( + plateHandle.resolve("A/1"), + new WellMetadata(Collections.singletonList(new WellImage("0", null)))); + + List axes = Arrays.asList(new Axis("z", "space", "micrometer"), new Axis("y", "space", "micrometer")); + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage fov = dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.create( + plateHandle.resolve("A/1").resolve("0"), new MultiscalesEntry(axes, Collections.emptyList())); + fov.createScaleLevel("0", + Array.metadataBuilder().withShape(16, 16).withChunkShape(16, 16).withDataType(DataType.FLOAT32).build(), + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(1.0, 1.0)))); + + MultiscaleImage image = Plate.open(plateHandle).openWell("A/1").openImage("0"); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class, image); + assertEquals(Arrays.asList("z", "y"), image.getAxisNames()); + } +} diff --git a/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV06Test.java b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV06Test.java new file mode 100644 index 00000000..a02952cf --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV06Test.java @@ -0,0 +1,127 @@ +package dev.zarr.zarrjava.ome; + +import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateSystem; +import dev.zarr.zarrjava.store.StoreHandle; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class OmeZarrV06Test extends OmeZarrBaseTest { + + private static final java.nio.file.Path V06_2D = + TESTDATA.resolve("ome/v0.6/examples/2d/basic/scale_multiscale.zarr"); + private static final java.nio.file.Path V06_3D = + TESTDATA.resolve("ome/v0.6/examples/3d/basic/scale_multiscale.zarr"); + + @Override + StoreHandle imageStoreHandle() throws Exception { + return storeHandle(V06_2D); + } + + @Override + Class expectedConcreteClass() { + return dev.zarr.zarrjava.ome.v0_6.MultiscaleImage.class; + } + + @Override + int expectedScaleLevelCount() { return 3; } + + @Override + long[] expectedLevel0Shape() { return new long[]{576, 720}; } + + @Override + List expectedAxisNames() { + return Arrays.asList("y", "x"); + } + + // ── v0.6-specific: coordinate systems ──────────────────────────────────── + + @Test + @SuppressWarnings("unchecked") + void coordinateSystems_presentInEntry() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + dev.zarr.zarrjava.ome.v0_6.MultiscaleImage v06Image = + (dev.zarr.zarrjava.ome.v0_6.MultiscaleImage) image; + dev.zarr.zarrjava.ome.v0_6.metadata.MultiscalesEntry entry = v06Image.getMultiscalesEntry(0); + + assertNotNull(entry.coordinateSystems); + assertEquals(1, entry.coordinateSystems.size()); + + CoordinateSystem cs = entry.coordinateSystems.get(0); + assertEquals("physical", cs.name); + assertNotNull(cs.axes); + assertEquals(2, cs.axes.size()); + assertEquals("y", cs.axes.get(0).name); + assertEquals("x", cs.axes.get(1).name); + } + + @Test + @SuppressWarnings("unchecked") + void datasets_pathsAndTransformations() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + dev.zarr.zarrjava.ome.v0_6.MultiscaleImage v06Image = + (dev.zarr.zarrjava.ome.v0_6.MultiscaleImage) image; + dev.zarr.zarrjava.ome.v0_6.metadata.MultiscalesEntry entry = v06Image.getMultiscalesEntry(0); + + assertNotNull(entry.datasets); + assertEquals(3, entry.datasets.size()); + assertEquals("s0", entry.datasets.get(0).path); + assertEquals("s1", entry.datasets.get(1).path); + assertEquals("s2", entry.datasets.get(2).path); + + dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateTransformation ct = + entry.datasets.get(0).coordinateTransformations.get(0); + assertEquals("scale", ct.type); + assertEquals("s0", ct.input); + assertEquals("physical", ct.output); + assertNotNull(ct.scale); + assertEquals(2, ct.scale.size()); + assertEquals(6.0, ct.scale.get(0), 1e-9); + assertEquals(4.0, ct.scale.get(1), 1e-9); + } + + @Test + void unifiedInterface_nodesAndPaths() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + MultiscalesEntry entry = image.getMultiscaleNode(0); + + assertEquals("multiscales", entry.name); + assertEquals(3, entry.datasets.size()); + assertEquals("s0", entry.datasets.get(0).path); + assertEquals("scale", entry.datasets.get(0).coordinateTransformations.get(0).type); + } + + // ── 3D example ─────────────────────────────────────────────────────────── + + @Test + @SuppressWarnings("unchecked") + void read3d_axesFromCoordinateSystems() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(V06_3D)); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_6.MultiscaleImage.class, image); + + dev.zarr.zarrjava.ome.v0_6.MultiscaleImage v06Image = + (dev.zarr.zarrjava.ome.v0_6.MultiscaleImage) image; + dev.zarr.zarrjava.ome.v0_6.metadata.MultiscalesEntry entry = v06Image.getMultiscalesEntry(0); + + assertEquals(3, entry.datasets.size()); + assertNotNull(entry.coordinateSystems); + assertFalse(entry.coordinateSystems.isEmpty()); + + List axes = entry.coordinateSystems.get(0).axes; + assertEquals(3, axes.size()); + assertEquals("z", axes.get(0).name); + assertEquals("y", axes.get(1).name); + assertEquals("x", axes.get(2).name); + } + + @Test + void read3d_unifiedAxisNames() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(V06_3D)); + List axisNames = image.getAxisNames(); + assertEquals(Arrays.asList("z", "y", "x"), axisNames); + } +} diff --git a/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV10Test.java b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV10Test.java new file mode 100644 index 00000000..4296dc8e --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV10Test.java @@ -0,0 +1,120 @@ +package dev.zarr.zarrjava.ome; + +import dev.zarr.zarrjava.ZarrTest; +import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateSystem; +import dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateTransformation; +import dev.zarr.zarrjava.ome.v1_0.Collection; +import dev.zarr.zarrjava.ome.v1_0.metadata.CollectionMetadata; +import dev.zarr.zarrjava.ome.v1_0.metadata.Level; +import dev.zarr.zarrjava.ome.v1_0.metadata.MultiscaleMetadata; +import dev.zarr.zarrjava.ome.v1_0.metadata.NodeRef; +import dev.zarr.zarrjava.store.FilesystemStore; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v3.ArrayMetadata; +import dev.zarr.zarrjava.v3.DataType; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for v1.0 (RFC-8). Standalone — Collection does not share the unified + * MultiscaleImage base class contract (different metadata model). + */ +public class OmeZarrV10Test extends ZarrTest { + + private StoreHandle storeHandle(java.nio.file.Path path) throws Exception { + return new FilesystemStore(path).resolve(); + } + + @Test + void readMultiscaleImage_concreteType() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v1.0/image"))); + assertInstanceOf(dev.zarr.zarrjava.ome.v1_0.MultiscaleImage.class, image); + } + + @Test + void readMultiscaleImage_unifiedInterface() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v1.0/image"))); + MultiscalesEntry entry = image.getMultiscaleNode(0); + + assertEquals("test_image", entry.name); + assertNotNull(entry.axes); + assertFalse(entry.axes.isEmpty()); + assertFalse(entry.datasets.isEmpty()); + assertEquals("s0", entry.datasets.get(0).path); + } + + @Test + void readMultiscaleImage_openScaleLevel() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v1.0/image"))); + dev.zarr.zarrjava.core.Array array = image.openScaleLevel(0); + assertArrayEquals(new long[]{8, 16, 16}, array.metadata().shape); + } + + @Test + void readCollection_metadata() throws Exception { + Collection collection = Collection.openCollection(storeHandle(TESTDATA.resolve("ome/v1.0"))); + CollectionMetadata meta = collection.getCollectionMetadata(); + assertNotNull(meta); + assertEquals("test_collection", meta.name); + assertEquals(1, meta.nodes.size()); + assertEquals("multiscale", meta.nodes.get(0).type); + assertEquals("image", meta.nodes.get(0).path); + } + + @Test + void readCollection_openNodeReturnsMultiscaleImage() throws Exception { + Collection collection = Collection.openCollection(storeHandle(TESTDATA.resolve("ome/v1.0"))); + Object node = collection.openNode("image"); + assertInstanceOf(dev.zarr.zarrjava.ome.v1_0.MultiscaleImage.class, node); + } + + @Test + void write_roundTrip() throws Exception { + StoreHandle collectionHandle = new FilesystemStore(TESTOUTPUT.resolve("v10_collection")).resolve(); + StoreHandle imageHandle = collectionHandle.resolve("image"); + + CoordinateSystem cs = new CoordinateSystem("physical", + Arrays.asList( + new dev.zarr.zarrjava.ome.metadata.Axis("z", "space", "micrometer", null, null), + new dev.zarr.zarrjava.ome.metadata.Axis("y", "space", "micrometer", null, null), + new dev.zarr.zarrjava.ome.metadata.Axis("x", "space", "micrometer", null, null))); + + MultiscaleMetadata msm = new MultiscaleMetadata( + "written_image", + Collections.emptyList(), + Collections.singletonList(cs)); + dev.zarr.zarrjava.ome.v1_0.MultiscaleImage image = + dev.zarr.zarrjava.ome.v1_0.MultiscaleImage.create(imageHandle, msm); + + ArrayMetadata arrayMetadata = dev.zarr.zarrjava.v3.Array.metadataBuilder() + .withShape(4, 8, 8) + .withChunkShape(4, 8, 8) + .withDataType(DataType.FLOAT32) + .build(); + image.createLevel("s0", arrayMetadata, + Collections.singletonList( + CoordinateTransformation.scale(Arrays.asList(1.0, 1.0, 1.0), "s0", "physical"))); + + CollectionMetadata cm = new CollectionMetadata( + "written_collection", + Collections.singletonList(new NodeRef("multiscale", "image"))); + Collection.createCollection(collectionHandle, cm); + + Collection readCollection = Collection.openCollection(collectionHandle); + assertEquals("written_collection", readCollection.getCollectionMetadata().name); + assertEquals(1, readCollection.getCollectionMetadata().nodes.size()); + + Object node = readCollection.openNode("image"); + assertInstanceOf(dev.zarr.zarrjava.ome.v1_0.MultiscaleImage.class, node); + dev.zarr.zarrjava.ome.v1_0.MultiscaleImage readImage = + (dev.zarr.zarrjava.ome.v1_0.MultiscaleImage) node; + assertEquals("written_image", readImage.getMultiscaleMetadata().name); + assertEquals(1, readImage.getScaleLevelCount()); + assertEquals("s0", readImage.getMultiscaleMetadata().levels.get(0).path); + } +} diff --git a/src/test/python-scripts/parse_codecs.py b/src/test/python-scripts/parse_codecs.py index edc4fd10..b3502660 100644 --- a/src/test/python-scripts/parse_codecs.py +++ b/src/test/python-scripts/parse_codecs.py @@ -48,6 +48,9 @@ def parse_codecs_zarr_python(codec_string: str, param_string: str, zarr_version: codecs=[BytesCodec(endian="little")]),)) elif codec_string == "crc32c" and zarr_version == 3: compressor = Crc32cCodec() + elif codec_string == "zstd" and zarr_version == 2: + level, checksum = param_string.split("_") + compressor = numcodecs.Zstd(level=int(level), checksum=checksum == 'true') else: raise ValueError(f"Invalid codec: {codec_string}, zarr_version: {zarr_version}") diff --git a/testdata/ome/v0.4/.zattrs b/testdata/ome/v0.4/.zattrs new file mode 100644 index 00000000..552fc1aa --- /dev/null +++ b/testdata/ome/v0.4/.zattrs @@ -0,0 +1,95 @@ +{ + "multiscales": [ + { + "version": "0.4", + "name": "test_image", + "axes": [ + { + "name": "t", + "type": "time", + "unit": "millisecond" + }, + { + "name": "c", + "type": "channel" + }, + { + "name": "z", + "type": "space", + "unit": "micrometer" + }, + { + "name": "y", + "type": "space", + "unit": "micrometer" + }, + { + "name": "x", + "type": "space", + "unit": "micrometer" + } + ], + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 0.5, + 0.5, + 0.5 + ] + } + ] + }, + { + "path": "1", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + ] + } + ], + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + ], + "type": "gaussian" + } + ], + "omero": { + "channels": [ + { + "label": "DAPI", + "color": "0000FF" + }, + { + "label": "GFP", + "color": "00FF00" + } + ], + "rdefs": { + "model": "color" + } + }, + "bioformats2raw.layout": 3 +} \ No newline at end of file diff --git a/testdata/ome/v0.4/.zgroup b/testdata/ome/v0.4/.zgroup new file mode 100644 index 00000000..cab13da6 --- /dev/null +++ b/testdata/ome/v0.4/.zgroup @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file diff --git a/testdata/ome/v0.4/0/.zarray b/testdata/ome/v0.4/0/.zarray new file mode 100644 index 00000000..ce9240fe --- /dev/null +++ b/testdata/ome/v0.4/0/.zarray @@ -0,0 +1,29 @@ +{ + "shape": [ + 1, + 2, + 8, + 16, + 16 + ], + "chunks": [ + 1, + 1, + 8, + 16, + 16 + ], + "dtype": "