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/USERGUIDE-OME-ZARR.md b/USERGUIDE-OME-ZARR.md new file mode 100644 index 00000000..313945af --- /dev/null +++ b/USERGUIDE-OME-ZARR.md @@ -0,0 +1,151 @@ +# OME-Zarr Guide for zarr-java + +## Scope and supported versions + +`dev.zarr.zarrjava.experimental.ome` supports: + +- v0.4 (Zarr v2 layout) +- v0.5 (Zarr v3 layout) +- v0.6 / RFC-5 + +## Primary entry points + +Use these static open methods: + +- `MultiscaleImage.open(StoreHandle)` for multiscale images (auto-detects v0.4/v0.5/v0.6 image nodes) +- `Plate.open(StoreHandle)` for HCS plates (v0.4/v0.5) +- `Well.open(StoreHandle)` for HCS wells (v0.4/v0.5) + +### StoreHandle and stores + +OME-Zarr APIs are store-agnostic: pass any `StoreHandle` (filesystem, S3, HTTP, ZIP, memory) to `open(...)`. +See storage backend setup in [`USERGUIDE.md#storage-backends`](USERGUIDE.md#storage-backends). + +```java +StoreHandle s3 = new S3Store(client, "idr", "zarr/v0.5/idr0083").resolve("9822152.zarr"); +MultiscaleImage image = MultiscaleImage.open(s3); +``` + +## Essential methods + +### MultiscaleImage + +Metadata: + +- `getMultiscaleNode(int i)` → normalized `ome.metadata.MultiscalesEntry` +- `getAxisNames()` → axis names from multiscale `0` +- `getScaleLevelCount()` → number of datasets/levels in multiscale `0` +- `getLabels()` / `openLabel(String)` → labels subgroup helpers + +Array access: + +- `openScaleLevel(int i)` → `dev.zarr.zarrjava.core.Array` +- then call `read()` or `read(offset, shape)` on that array +- typical viewer flow: read axes + scale count first, then select a level by `i` + +### Plate and Well (HCS) + +Metadata: + +- `Plate.getPlateMetadata()` +- `Well.getWellMetadata()` + +Navigation: + +- `Plate.openWell(String rowColPath)` (for example `"A/1"`) +- `Well.openImage(String path)` (for example `"0"`) + +## Version-specific typed metadata + +If you need the raw version-specific metadata model instead of normalized `MultiscalesEntry`: + +- Cast to `MultiscalesMetadataImage` and call `getMultiscalesEntry(i)` + + +## v0.6 Scene metadata + +Scene roots (groups with `ome.scene`) are supported via `dev.zarr.zarrjava.experimental.ome.v0_6.Scene`: + +- `Scene.openScene(StoreHandle)` / `Scene.open(StoreHandle)` +- `Scene.createScene(StoreHandle, SceneMetadata)` / `Scene.create(...)` +- `listImageNodes()` and `openImageNode(String)` for sibling multiscale images +- `getCoordinateTransformationGraph()` for lightweight metadata graph inspection + +Notes: +- Parsing is permissive and explicit (no strict full-spec validation). +- Scene-level references (`input`/`output`) are resolved against scene-root coordinate systems and child image coordinate systems for graph inspection. +- Path-based transform assets can be normalized with `Scene.normalizeCoordinateTransformPath(...)` and grouped under `coordinateTransformations/` via `createCoordinateTransformationsGroup()`. + +## Read example + +```java +import dev.zarr.zarrjava.experimental.ome.MultiscaleImage; +import dev.zarr.zarrjava.experimental.ome.Plate; +import dev.zarr.zarrjava.experimental.ome.Well; +import dev.zarr.zarrjava.store.FilesystemStore; +import dev.zarr.zarrjava.store.StoreHandle; + +StoreHandle imageHandle = new FilesystemStore("/data/ome/image.zarr").resolve(); +MultiscaleImage image = MultiscaleImage.open(imageHandle); + +int scaleCount = image.getScaleLevelCount(); +java.util.List axisNames = image.getAxisNames(); +dev.zarr.zarrjava.experimental.ome.metadata.MultiscalesEntry entry0 = image.getMultiscaleNode(0); + +dev.zarr.zarrjava.core.Array s0 = image.openScaleLevel(0); +ucar.ma2.Array full = s0.read(); +ucar.ma2.Array subset = s0.read(new long[]{0, 0, 0, 0, 0}, new long[]{1, 1, 4, 8, 8}); + +java.util.List labels = image.getLabels(); +if (!labels.isEmpty()) { + MultiscaleImage label = image.openLabel(labels.get(0)); +} + +StoreHandle plateHandle = new FilesystemStore("/data/ome/plate.zarr").resolve(); +Plate plate = Plate.open(plateHandle); +Well well = plate.openWell("A/1"); +MultiscaleImage wellImage = well.openImage("0"); +``` + +## Write example + +Creation is version-specific, but the pattern is the same: create node with version metadata, then append levels/datasets with scale transforms. For example, for v0.5: + +```java +import dev.zarr.zarrjava.experimental.ome.metadata.Axis; +import dev.zarr.zarrjava.experimental.ome.metadata.CoordinateTransformation; +import dev.zarr.zarrjava.experimental.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.store.FilesystemStore; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v3.Array; +import dev.zarr.zarrjava.v3.DataType; + +import java.util.Arrays; +import java.util.Collections; + +StoreHandle out = new FilesystemStore("/tmp/ome_v05.zarr").resolve(); +MultiscalesEntry ms = new MultiscalesEntry( + Arrays.asList(new Axis("y", "space", "micrometer"), new Axis("x", "space", "micrometer")), + Collections.emptyList()); +); +dev.zarr.zarrjava.experimental.ome.v0_5.MultiscaleImage written = dev.zarr.zarrjava.experimental.ome.v0_5.MultiscaleImage.create(out, ms); + +written.createScaleLevel( + "s0", + Array.metadataBuilder().withShape(1024, 1024).withChunkShape(256, 256).withDataType(DataType.UINT16).build(), + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(1.0, 1.0))) +); +written.createScaleLevel( + "s1", + Array.metadataBuilder().withShape(512, 512).withChunkShape(256, 256).withDataType(DataType.UINT16).build(), + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(2.0, 2.0))) +); +``` + +## Write entry points by version + +- `ome.v0_4.MultiscaleImage.create(...)` +- `ome.v0_5.MultiscaleImage.create(...)` +- `ome.v0_6.MultiscaleImage.create(...)` + +Use the corresponding metadata classes for each version package. diff --git a/USERGUIDE.md b/USERGUIDE.md index 1c32edf9..65bf9944 100644 --- a/USERGUIDE.md +++ b/USERGUIDE.md @@ -11,8 +11,9 @@ 7. [Storage Backends](#storage-backends) 8. [Compression and Codecs](#compression-and-codecs) 9. [Advanced Topics](#advanced-topics) -10. [Examples](#examples) -11. [Troubleshooting](#troubleshooting) +10. [OME-Zarr](#ome-zarr-v04-v05-v06) +11. [Examples](#examples) +12. [Troubleshooting](#troubleshooting) --- ## Introduction zarr-java is a Java implementation of the [Zarr specification](https://zarr.dev/) for chunked, compressed, N-dimensional arrays. It supports both Zarr version 2 and version 3 formats, providing a unified API for working with large scientific datasets. @@ -653,7 +654,6 @@ try { - `"No Zarr array found at the specified location"` - Check path and ensure `.zarray` (v2) or `zarr.json` (v3) exists - `"Requested data is outside of the array's domain"` - Verify that `offset + shape <= array.shape` - `"Failed to read from store"` - Check network connectivity, file permissions, or storage availability ---- ### Best Practices 1. **Chunk sizes for Best Performance**: @@ -677,7 +677,14 @@ try { // For balanced 3D access .withChunkShape(100, 100, 100) // Balanced for all dimensions ``` - + +## OME-Zarr (v0.4, v0.5, v0.6) + +For a focused OME-Zarr API guide (metadata access, array access, version behavior, and concise examples), +see: + +- [`USERGUIDE-OME-ZARR.md`](USERGUIDE-OME-ZARR.md) + ## Examples ### Complete Example: Creating a 3D Dataset ```java 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/experimental/ome/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/MultiscaleImage.java new file mode 100644 index 00000000..3911360f --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/MultiscaleImage.java @@ -0,0 +1,137 @@ +package dev.zarr.zarrjava.experimental.ome; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.core.Node; +import dev.zarr.zarrjava.experimental.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.utils.Utils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Unified interface for reading OME-Zarr multiscale images across Zarr format versions. + */ +public interface MultiscaleImage { + + /** + * Returns the store handle for this multiscale image node. + */ + StoreHandle getStoreHandle(); + + /** + * Returns a {@link MultiscalesEntry} view of multiscale {@code i}, normalized to the shared + * metadata type. All axis and dataset information is accessible from the returned entry. + */ + MultiscalesEntry getMultiscaleNode(int i) throws ZarrException; + + /** + * Opens the scale level array at index {@code i} within the first multiscale entry. + */ + dev.zarr.zarrjava.core.Array openScaleLevel(int i) throws IOException, ZarrException; + + /** + * Returns the number of scale levels in the first multiscale entry. + */ + int getScaleLevelCount() throws ZarrException; + + /** + * Returns the axis names of the first multiscale entry. + */ + default List getAxisNames() throws ZarrException { + MultiscalesEntry entry = getMultiscaleNode(0); + List names = new ArrayList<>(); + for (dev.zarr.zarrjava.experimental.ome.metadata.Axis axis : entry.axes) { + names.add(axis.name); + } + return names; + } + + /** + * Returns all label names from the {@code labels/} sub-group, or an empty list if none exist. + */ + default List 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.6 (zarr.json with version "0.6"), then v0.5 (zarr.json with "ome" key), then v0.4 (.zattrs with "multiscales" key). + */ + static MultiscaleImage open(StoreHandle storeHandle) throws IOException, ZarrException { + // Try version >= 0.5: zarr.json with "ome" key + StoreHandle zarrJson = storeHandle.resolve(Node.ZARR_JSON); + if (zarrJson.exists()) { + com.fasterxml.jackson.databind.ObjectMapper mapper = OmeObjectMappers.makeV3Mapper(); + byte[] bytes = Utils.toArray(zarrJson.readNonNull()); + com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes); + com.fasterxml.jackson.databind.JsonNode attrs = root.get("attributes"); + if (attrs != null && attrs.has("ome")) { + com.fasterxml.jackson.databind.JsonNode omeNode = attrs.get("ome"); + String version = omeNode.has("version") ? omeNode.get("version").asText() : ""; + if (version.startsWith("0.6")) { + return dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage.openMultiscaleImage(storeHandle); + } + return dev.zarr.zarrjava.experimental.ome.v0_5.MultiscaleImage.openMultiscaleImage(storeHandle); + } + } + + // Try v0.4: .zattrs with "multiscales" key + StoreHandle zattrs = storeHandle.resolve(Node.ZATTRS); + if (zattrs.exists()) { + com.fasterxml.jackson.databind.ObjectMapper mapper = OmeObjectMappers.makeV2Mapper(); + byte[] bytes = Utils.toArray(zattrs.readNonNull()); + com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes); + if (root.has("multiscales")) { + return dev.zarr.zarrjava.experimental.ome.v0_4.MultiscaleImage.openMultiscaleImage(storeHandle); + } + } + + throw new ZarrException("No OME-Zarr multiscale metadata found at " + storeHandle); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/MultiscalesMetadataImage.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/MultiscalesMetadataImage.java new file mode 100644 index 00000000..eddc1a58 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/MultiscalesMetadataImage.java @@ -0,0 +1,48 @@ +package dev.zarr.zarrjava.experimental.ome; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.experimental.ome.metadata.transform.CoordinateTransformation; +import dev.zarr.zarrjava.experimental.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) 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/experimental/ome/OmeObjectMappers.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/OmeObjectMappers.java new file mode 100644 index 00000000..9f1c7c3f --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/OmeObjectMappers.java @@ -0,0 +1,57 @@ +package dev.zarr.zarrjava.experimental.ome; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler; + +import java.io.IOException; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +final class OmeObjectMappers { + private OmeObjectMappers() { + } + + static ObjectMapper makeV2Mapper() { + ObjectMapper mapper = dev.zarr.zarrjava.v2.Node.makeObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true); + mapper.addHandler(new UnknownOmePropertyWarningHandler()); + return mapper; + } + + static ObjectMapper makeV3Mapper() { + ObjectMapper mapper = dev.zarr.zarrjava.v3.Node.makeObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true); + mapper.addHandler(new UnknownOmePropertyWarningHandler()); + return mapper; + } + + private static final class UnknownOmePropertyWarningHandler extends DeserializationProblemHandler { + private static final Logger LOGGER = Logger.getLogger(UnknownOmePropertyWarningHandler.class.getName()); + private static final Set WARNED_FIELDS = ConcurrentHashMap.newKeySet(); + + @Override + public boolean handleUnknownProperty( + DeserializationContext ctxt, + JsonParser p, + JsonDeserializer deserializer, + Object beanOrClass, + String propertyName + ) throws IOException { + String target = (beanOrClass instanceof Class) + ? ((Class) beanOrClass).getName() + : beanOrClass.getClass().getName(); + String key = target + "#" + propertyName; + if (WARNED_FIELDS.add(key)) { + LOGGER.warning( + "Ignoring unknown OME metadata field '" + propertyName + "' for " + target); + } + p.skipChildren(); + return true; + } + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/OmeV2Group.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/OmeV2Group.java new file mode 100644 index 00000000..a65dbcd0 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/OmeV2Group.java @@ -0,0 +1,62 @@ +package dev.zarr.zarrjava.experimental.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 OmeObjectMappers.makeV2Mapper().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 OmeObjectMappers.makeV2Mapper().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/experimental/ome/OmeV3Group.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/OmeV3Group.java new file mode 100644 index 00000000..dcafd3d3 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/OmeV3Group.java @@ -0,0 +1,48 @@ +package dev.zarr.zarrjava.experimental.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 OmeObjectMappers.makeV3Mapper().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/experimental/ome/Plate.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/Plate.java new file mode 100644 index 00000000..c8ac2777 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/Plate.java @@ -0,0 +1,65 @@ +package dev.zarr.zarrjava.experimental.ome; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.core.Node; +import dev.zarr.zarrjava.experimental.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 version >= 0.5: zarr.json with "ome" -> "plate" + StoreHandle zarrJson = storeHandle.resolve(Node.ZARR_JSON); + if (zarrJson.exists()) { + com.fasterxml.jackson.databind.ObjectMapper mapper = OmeObjectMappers.makeV3Mapper(); + byte[] bytes = Utils.toArray(zarrJson.readNonNull()); + com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes); + com.fasterxml.jackson.databind.JsonNode attrs = root.get("attributes"); + if (attrs != null && attrs.has("ome") && attrs.get("ome").has("plate")) { + com.fasterxml.jackson.databind.JsonNode omeNode = attrs.get("ome"); + String version = omeNode.has("version") ? omeNode.get("version").asText() : ""; + if (version.startsWith("0.6")) { + return dev.zarr.zarrjava.experimental.ome.v0_6.Plate.openPlate(storeHandle); + } + return dev.zarr.zarrjava.experimental.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 = OmeObjectMappers.makeV2Mapper(); + byte[] bytes = Utils.toArray(zattrs.readNonNull()); + com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes); + if (root.has("plate")) { + return dev.zarr.zarrjava.experimental.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/experimental/ome/Well.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/Well.java new file mode 100644 index 00000000..be4f5366 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/Well.java @@ -0,0 +1,65 @@ +package dev.zarr.zarrjava.experimental.ome; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.core.Node; +import dev.zarr.zarrjava.experimental.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 version >= 0.5: zarr.json with "ome" -> "well" + StoreHandle zarrJson = storeHandle.resolve(Node.ZARR_JSON); + if (zarrJson.exists()) { + com.fasterxml.jackson.databind.ObjectMapper mapper = OmeObjectMappers.makeV3Mapper(); + byte[] bytes = Utils.toArray(zarrJson.readNonNull()); + com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes); + com.fasterxml.jackson.databind.JsonNode attrs = root.get("attributes"); + if (attrs != null && attrs.has("ome") && attrs.get("ome").has("well")) { + com.fasterxml.jackson.databind.JsonNode omeNode = attrs.get("ome"); + String version = omeNode.has("version") ? omeNode.get("version").asText() : ""; + if (version.startsWith("0.6")) { + return dev.zarr.zarrjava.experimental.ome.v0_6.Well.openWell(storeHandle); + } + return dev.zarr.zarrjava.experimental.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 = OmeObjectMappers.makeV2Mapper(); + byte[] bytes = Utils.toArray(zattrs.readNonNull()); + com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes); + if (root.has("well")) { + return dev.zarr.zarrjava.experimental.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/experimental/ome/metadata/Acquisition.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/Acquisition.java new file mode 100644 index 00000000..d7ae004b --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/Acquisition.java @@ -0,0 +1,41 @@ +package dev.zarr.zarrjava.experimental.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/experimental/ome/metadata/Axis.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/Axis.java new file mode 100644 index 00000000..0b24c9d6 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/Axis.java @@ -0,0 +1,41 @@ +package dev.zarr.zarrjava.experimental.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/experimental/ome/metadata/Dataset.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/Dataset.java new file mode 100644 index 00000000..8d928dcc --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/Dataset.java @@ -0,0 +1,24 @@ +package dev.zarr.zarrjava.experimental.ome.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import dev.zarr.zarrjava.experimental.ome.metadata.transform.CoordinateTransformation; + +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/experimental/ome/metadata/MultiscalesEntry.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/MultiscalesEntry.java new file mode 100644 index 00000000..5223eb56 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/MultiscalesEntry.java @@ -0,0 +1,59 @@ +package dev.zarr.zarrjava.experimental.ome.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import dev.zarr.zarrjava.experimental.ome.metadata.transform.CoordinateTransformation; + +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 { + + 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/experimental/ome/metadata/NamedEntry.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/NamedEntry.java new file mode 100644 index 00000000..24b94b28 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/NamedEntry.java @@ -0,0 +1,19 @@ +package dev.zarr.zarrjava.experimental.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/experimental/ome/metadata/OmeMetadata.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/OmeMetadata.java new file mode 100644 index 00000000..4e8920a3 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/OmeMetadata.java @@ -0,0 +1,48 @@ +package dev.zarr.zarrjava.experimental.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/experimental/ome/metadata/OmeroChannel.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/OmeroChannel.java new file mode 100644 index 00000000..4a82e675 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/OmeroChannel.java @@ -0,0 +1,38 @@ +package dev.zarr.zarrjava.experimental.ome.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; + +/** OMERO channel rendering metadata. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class OmeroChannel { + @Nullable public final Boolean active; + @Nullable public final Double coefficient; + @Nullable public final String color; + @Nullable public final String family; + @Nullable public final Boolean inverted; + @Nullable public final String label; + @Nullable public final OmeroWindow window; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public OmeroChannel( + @Nullable @JsonProperty("active") Boolean active, + @Nullable @JsonProperty("coefficient") Double coefficient, + @Nullable @JsonProperty("color") String color, + @Nullable @JsonProperty("family") String family, + @Nullable @JsonProperty("inverted") Boolean inverted, + @Nullable @JsonProperty("label") String label, + @Nullable @JsonProperty("window") OmeroWindow window + ) { + this.active = active; + this.coefficient = coefficient; + this.color = color; + this.family = family; + this.inverted = inverted; + this.label = label; + this.window = window; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/OmeroMetadata.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/OmeroMetadata.java new file mode 100644 index 00000000..21cb4e98 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/OmeroMetadata.java @@ -0,0 +1,91 @@ +package dev.zarr.zarrjava.experimental.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.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +/** Omero display metadata stored in OME-Zarr attributes. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class OmeroMetadata { + private static final Logger LOGGER = Logger.getLogger(OmeroMetadata.class.getName()); + private static final Set WARNED = ConcurrentHashMap.newKeySet(); + + @Nullable + public final Integer id; + @Nullable + public final String version; + @Nullable + public final String name; + @Nullable + public final List channels; + @Nullable + public final OmeroRdefs rdefs; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public OmeroMetadata( + @Nullable @JsonProperty("id") Integer id, + @Nullable @JsonProperty("version") String version, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("channels") List channels, + @Nullable @JsonProperty("rdefs") OmeroRdefs rdefs + ) { + this.id = id; + this.version = version; + this.name = name; + this.channels = channels; + this.rdefs = rdefs; + warnIfRequiredFieldsMissing(); + } + + public OmeroMetadata( + @Nullable List channels, + @Nullable OmeroRdefs rdefs + ) { + this(null, null, null, channels, rdefs); + } + + private void warnIfRequiredFieldsMissing() { + if (channels == null) { + warnOnce("missing-channels", "OMERO metadata is present but missing required field 'channels'."); + return; + } + for (int i = 0; i < channels.size(); i++) { + OmeroChannel channel = channels.get(i); + if (channel == null) { + warnOnce("channel-null", "OMERO metadata channel[" + i + "] is null; required fields 'color' and 'window' are missing."); + continue; + } + if (channel.color == null || channel.color.isEmpty()) { + warnOnce("channel-missing-color", "OMERO metadata channel[" + i + "] is missing required field 'color'."); + } + if (channel.window == null) { + warnOnce("channel-missing-window", "OMERO metadata channel[" + i + "] is missing required field 'window'."); + continue; + } + if (channel.window.min == null) { + warnOnce("window-missing-min", "OMERO metadata channel[" + i + "].window is missing required field 'min'."); + } + if (channel.window.max == null) { + warnOnce("window-missing-max", "OMERO metadata channel[" + i + "].window is missing required field 'max'."); + } + if (channel.window.start == null) { + warnOnce("window-missing-start", "OMERO metadata channel[" + i + "].window is missing required field 'start'."); + } + if (channel.window.end == null) { + warnOnce("window-missing-end", "OMERO metadata channel[" + i + "].window is missing required field 'end'."); + } + } + } + + private static void warnOnce(String key, String message) { + if (WARNED.add(key)) { + LOGGER.warning(message); + } + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/OmeroRdefs.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/OmeroRdefs.java new file mode 100644 index 00000000..e83c0b22 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/OmeroRdefs.java @@ -0,0 +1,26 @@ +package dev.zarr.zarrjava.experimental.ome.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; + +/** OMERO rendering defaults metadata. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class OmeroRdefs { + @Nullable @JsonProperty("defaultT") public final Integer defaultT; + @Nullable @JsonProperty("defaultZ") public final Integer defaultZ; + @Nullable public final String model; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public OmeroRdefs( + @Nullable @JsonProperty("defaultT") Integer defaultT, + @Nullable @JsonProperty("defaultZ") Integer defaultZ, + @Nullable @JsonProperty("model") String model + ) { + this.defaultT = defaultT; + this.defaultZ = defaultZ; + this.model = model; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/OmeroWindow.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/OmeroWindow.java new file mode 100644 index 00000000..db10f943 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/OmeroWindow.java @@ -0,0 +1,29 @@ +package dev.zarr.zarrjava.experimental.ome.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; + +/** OMERO channel window metadata. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class OmeroWindow { + @Nullable public final Double min; + @Nullable public final Double max; + @Nullable public final Double start; + @Nullable public final Double end; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public OmeroWindow( + @Nullable @JsonProperty("min") Double min, + @Nullable @JsonProperty("max") Double max, + @Nullable @JsonProperty("start") Double start, + @Nullable @JsonProperty("end") Double end + ) { + this.min = min; + this.max = max; + this.start = start; + this.end = end; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/PlateMetadata.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/PlateMetadata.java new file mode 100644 index 00000000..123db718 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/PlateMetadata.java @@ -0,0 +1,44 @@ +package dev.zarr.zarrjava.experimental.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/experimental/ome/metadata/WellImage.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/WellImage.java new file mode 100644 index 00000000..1f1a2eba --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/WellImage.java @@ -0,0 +1,25 @@ +package dev.zarr.zarrjava.experimental.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/experimental/ome/metadata/WellMetadata.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/WellMetadata.java new file mode 100644 index 00000000..661d955c --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/WellMetadata.java @@ -0,0 +1,30 @@ +package dev.zarr.zarrjava.experimental.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/experimental/ome/metadata/WellRef.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/WellRef.java new file mode 100644 index 00000000..f34a92c4 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/WellRef.java @@ -0,0 +1,25 @@ +package dev.zarr.zarrjava.experimental.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/experimental/ome/metadata/transform/CoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/transform/CoordinateTransformation.java new file mode 100644 index 00000000..8406a2e7 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/transform/CoordinateTransformation.java @@ -0,0 +1,45 @@ +package dev.zarr.zarrjava.experimental.ome.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type", + visible = true, + defaultImpl = GenericCoordinateTransformation.class) +@JsonSubTypes({ + @JsonSubTypes.Type(value = ScaleCoordinateTransformation.class, name = "scale"), + @JsonSubTypes.Type(value = TranslationCoordinateTransformation.class, name = "translation"), + @JsonSubTypes.Type(value = IdentityCoordinateTransformation.class, name = "identity") +}) +public abstract class CoordinateTransformation { + + public final String type; + + protected CoordinateTransformation( + @JsonProperty(value = "type", required = true) String type + ) { + this.type = type; + } + + public static CoordinateTransformation scale(List scale) { + return new ScaleCoordinateTransformation(scale, null); + } + + public static CoordinateTransformation translation(List translation) { + return new TranslationCoordinateTransformation(translation, null); + } + + public static CoordinateTransformation identity() { + return new IdentityCoordinateTransformation(null); + } + +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/transform/GenericCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/transform/GenericCoordinateTransformation.java new file mode 100644 index 00000000..a83461e9 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/transform/GenericCoordinateTransformation.java @@ -0,0 +1,31 @@ +package dev.zarr.zarrjava.experimental.ome.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Forward-compatibility fallback for transform types not yet modeled explicitly. + * + *

Readers can deserialize and retain metadata for unknown/extension transform types + * without failing hard. + */ +public final class GenericCoordinateTransformation extends CoordinateTransformation { + public final Map raw = new LinkedHashMap<>(); + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public GenericCoordinateTransformation( + @JsonProperty(value = "type", required = true) String type + ) { + super(type); + } + + @JsonAnySetter + public void capture(String key, Object value) { + raw.put(key, value); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/transform/IdentityCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/transform/IdentityCoordinateTransformation.java new file mode 100644 index 00000000..ca4d0947 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/transform/IdentityCoordinateTransformation.java @@ -0,0 +1,16 @@ +package dev.zarr.zarrjava.experimental.ome.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; + +public final class IdentityCoordinateTransformation extends CoordinateTransformation { + @Nullable public final String path; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public IdentityCoordinateTransformation(@Nullable @JsonProperty("path") String path) { + super("identity"); + this.path = path; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/transform/ScaleCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/transform/ScaleCoordinateTransformation.java new file mode 100644 index 00000000..de61caf5 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/transform/ScaleCoordinateTransformation.java @@ -0,0 +1,22 @@ +package dev.zarr.zarrjava.experimental.ome.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.List; + +public final class ScaleCoordinateTransformation extends CoordinateTransformation { + @Nullable public final List scale; + @Nullable public final String path; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public ScaleCoordinateTransformation( + @Nullable @JsonProperty("scale") List scale, + @Nullable @JsonProperty("path") String path + ) { + super("scale"); + this.scale = scale; + this.path = path; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/transform/TranslationCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/transform/TranslationCoordinateTransformation.java new file mode 100644 index 00000000..dd9a52bd --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/transform/TranslationCoordinateTransformation.java @@ -0,0 +1,22 @@ +package dev.zarr.zarrjava.experimental.ome.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.List; + +public final class TranslationCoordinateTransformation extends CoordinateTransformation { + @Nullable public final List translation; + @Nullable public final String path; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public TranslationCoordinateTransformation( + @Nullable @JsonProperty("translation") List translation, + @Nullable @JsonProperty("path") String path + ) { + super("translation"); + this.translation = translation; + this.path = path; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_4/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_4/MultiscaleImage.java new file mode 100644 index 00000000..ae6095c6 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_4/MultiscaleImage.java @@ -0,0 +1,147 @@ +package dev.zarr.zarrjava.experimental.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.experimental.ome.OmeV2Group; +import dev.zarr.zarrjava.experimental.ome.MultiscalesMetadataImage; +import dev.zarr.zarrjava.experimental.ome.metadata.transform.CoordinateTransformation; +import dev.zarr.zarrjava.experimental.ome.metadata.Dataset; +import dev.zarr.zarrjava.experimental.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.experimental.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/experimental/ome/v0_4/Plate.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_4/Plate.java new file mode 100644 index 00000000..c6f4c49d --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_4/Plate.java @@ -0,0 +1,64 @@ +package dev.zarr.zarrjava.experimental.ome.v0_4; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.experimental.ome.OmeV2Group; +import dev.zarr.zarrjava.experimental.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.experimental.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.experimental.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/experimental/ome/v0_4/Well.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_4/Well.java new file mode 100644 index 00000000..98381efb --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_4/Well.java @@ -0,0 +1,65 @@ +package dev.zarr.zarrjava.experimental.ome.v0_4; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.experimental.ome.MultiscaleImage; +import dev.zarr.zarrjava.experimental.ome.OmeV2Group; +import dev.zarr.zarrjava.experimental.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.experimental.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/experimental/ome/v0_5/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_5/MultiscaleImage.java new file mode 100644 index 00000000..9623cf5b --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_5/MultiscaleImage.java @@ -0,0 +1,110 @@ +package dev.zarr.zarrjava.experimental.ome.v0_5; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.experimental.ome.OmeV3Group; +import dev.zarr.zarrjava.experimental.ome.MultiscalesMetadataImage; +import dev.zarr.zarrjava.experimental.ome.metadata.transform.CoordinateTransformation; +import dev.zarr.zarrjava.experimental.ome.metadata.Dataset; +import dev.zarr.zarrjava.experimental.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.experimental.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.experimental.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/experimental/ome/v0_5/Plate.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_5/Plate.java new file mode 100644 index 00000000..b9f09e19 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_5/Plate.java @@ -0,0 +1,69 @@ +package dev.zarr.zarrjava.experimental.ome.v0_5; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.experimental.ome.OmeV3Group; +import dev.zarr.zarrjava.experimental.ome.metadata.OmeMetadata; +import dev.zarr.zarrjava.experimental.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.experimental.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.experimental.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/experimental/ome/v0_5/Well.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_5/Well.java new file mode 100644 index 00000000..790aaf4b --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_5/Well.java @@ -0,0 +1,70 @@ +package dev.zarr.zarrjava.experimental.ome.v0_5; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.experimental.ome.MultiscaleImage; +import dev.zarr.zarrjava.experimental.ome.OmeV3Group; +import dev.zarr.zarrjava.experimental.ome.metadata.OmeMetadata; +import dev.zarr.zarrjava.experimental.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.experimental.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/experimental/ome/v0_6/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/MultiscaleImage.java new file mode 100644 index 00000000..9aaa96ea --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/MultiscaleImage.java @@ -0,0 +1,495 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.experimental.ome.MultiscalesMetadataImage; +import dev.zarr.zarrjava.experimental.ome.OmeV3Group; +import dev.zarr.zarrjava.experimental.ome.metadata.Axis; +import dev.zarr.zarrjava.experimental.ome.metadata.transform.CoordinateTransformation; +import dev.zarr.zarrjava.experimental.ome.metadata.transform.IdentityCoordinateTransformation; +import dev.zarr.zarrjava.experimental.ome.metadata.transform.ScaleCoordinateTransformation; +import dev.zarr.zarrjava.experimental.ome.metadata.transform.TranslationCoordinateTransformation; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.CoordinateSystem; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.GenericCoordinateTransformation; +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.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 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); + } + if (omeMetadata.multiscales == null || omeMetadata.multiscales.isEmpty()) { + if (omeMetadata.scene != null) { + throw new ZarrException( + "OME-Zarr v0.6 scene metadata found at " + storeHandle + + "; use dev.zarr.zarrjava.experimental.ome.v0_6.Scene.open(...)"); + } + throw new ZarrException("No 'multiscales' found in ome metadata 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); + } + + @javax.annotation.Nullable + public dev.zarr.zarrjava.experimental.ome.metadata.OmeroMetadata getOmeroMetadata() { + return omeMetadata.omero; + } + + @javax.annotation.Nullable + public Integer getBioformats2rawLayout() { + return omeMetadata.bioformats2rawLayout; + } + + OmeMetadata getRawOmeMetadata() { + return omeMetadata; + } + + @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) { + String type = ct.type; + List scale = null; + List translation = null; + String rawPath = null; + if (ct instanceof dev.zarr.zarrjava.experimental.ome.metadata.transform.ScaleCoordinateTransformation) { + dev.zarr.zarrjava.experimental.ome.metadata.transform.ScaleCoordinateTransformation t = + (dev.zarr.zarrjava.experimental.ome.metadata.transform.ScaleCoordinateTransformation) ct; + scale = t.scale; + rawPath = t.path; + } else if (ct instanceof dev.zarr.zarrjava.experimental.ome.metadata.transform.TranslationCoordinateTransformation) { + dev.zarr.zarrjava.experimental.ome.metadata.transform.TranslationCoordinateTransformation t = + (dev.zarr.zarrjava.experimental.ome.metadata.transform.TranslationCoordinateTransformation) ct; + translation = t.translation; + rawPath = t.path; + } else if (ct instanceof dev.zarr.zarrjava.experimental.ome.metadata.transform.IdentityCoordinateTransformation) { + dev.zarr.zarrjava.experimental.ome.metadata.transform.IdentityCoordinateTransformation t = + (dev.zarr.zarrjava.experimental.ome.metadata.transform.IdentityCoordinateTransformation) ct; + rawPath = t.path; + } else if (ct instanceof dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation) { + Map raw = ((dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation) ct).raw; + Object s = raw.get("scale"); + Object tr = raw.get("translation"); + Object p = raw.get("path"); + if (s instanceof List) scale = castDoubleList((List) s); + if (tr instanceof List) translation = castDoubleList((List) tr); + if (p instanceof String) rawPath = (String) p; + + if ("sequence".equals(type)) { + v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.SequenceCoordinateTransformation( + null, null, null, castV06TransformList(raw.get("transformations")))); + continue; + } + if ("mapAxis".equals(type)) { + v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.MapAxisCoordinateTransformation( + null, null, null, castIntList(raw.get("mapAxis")), castV06Transform(raw.get("transformation")))); + continue; + } + if ("affine".equals(type)) { + v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation( + null, null, null, castMatrix(raw.get("affine")), rawPath)); + continue; + } + if ("rotation".equals(type)) { + v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.RotationCoordinateTransformation( + null, null, null, castMatrix(raw.get("rotation")), rawPath)); + continue; + } + if ("displacements".equals(type)) { + v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.DisplacementsCoordinateTransformation( + null, null, null, rawPath)); + continue; + } + if ("coordinates".equals(type)) { + v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinatesCoordinateTransformation( + null, null, null, rawPath)); + continue; + } + if ("bijection".equals(type)) { + v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.BijectionCoordinateTransformation( + null, null, null, castV06Transform(raw.get("forward")), castV06Transform(raw.get("inverse")))); + continue; + } + if ("byDimension".equals(type)) { + v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ByDimensionCoordinateTransformation( + null, null, null, castByDimensionTransformList(raw.get("transformations")))); + continue; + } + } + if ("scale".equals(type)) { + v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ScaleCoordinateTransformation( + null, null, null, scale, rawPath)); + } else if ("translation".equals(type)) { + v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.TranslationCoordinateTransformation( + null, null, null, translation, rawPath)); + } else if ("identity".equals(type)) { + v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.IdentityCoordinateTransformation( + null, null, null, rawPath)); + } else { + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.GenericCoordinateTransformation generic = + new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.GenericCoordinateTransformation(type, null, null, null); + if (scale != null) generic.raw.put("scale", scale); + if (translation != null) generic.raw.put("translation", translation); + if (rawPath != null) generic.raw.put("path", rawPath); + v06Transforms.add(generic); + } + } + + MultiscalesEntry current = omeMetadata.multiscales.get(0); + MultiscalesEntry updated = current.withDataset(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.Dataset(path, v06Transforms)); + List updatedList = new ArrayList<>(omeMetadata.multiscales); + updatedList.set(0, updated); + omeMetadata = new OmeMetadata( + omeMetadata.version, + updatedList, + omeMetadata.omero, + omeMetadata.bioformats2rawLayout, + omeMetadata.scene, + omeMetadata.plate, + omeMetadata.well); + setAttributes(omeAttributes(omeMetadata)); + } + + @Override + public dev.zarr.zarrjava.experimental.ome.metadata.MultiscalesEntry getMultiscaleNode(int i) throws ZarrException { + MultiscalesEntry entry = getMultiscalesEntry(i); + List mappedDatasets = new ArrayList<>(); + for (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.Dataset ds : entry.datasets) { + List mapped = new ArrayList<>(); + for (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation ct : ds.coordinateTransformations) { + mapped.add(mapTransform(ct)); + } + mappedDatasets.add(new dev.zarr.zarrjava.experimental.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.experimental.ome.metadata.MultiscalesEntry( + axes != null ? axes : Collections.emptyList(), + mappedDatasets, null, entry.name, null, null, null); + } + + private static CoordinateTransformation mapTransform( + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation ct) { + if (ct instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ScaleCoordinateTransformation) { + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ScaleCoordinateTransformation t = + (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ScaleCoordinateTransformation) ct; + return new ScaleCoordinateTransformation(t.scale, t.path); + } + if (ct instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.TranslationCoordinateTransformation) { + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.TranslationCoordinateTransformation t = + (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.TranslationCoordinateTransformation) ct; + return new TranslationCoordinateTransformation(t.translation, t.path); + } + if (ct instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.IdentityCoordinateTransformation) { + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.IdentityCoordinateTransformation t = + (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.IdentityCoordinateTransformation) ct; + return new IdentityCoordinateTransformation(t.path); + } + if (ct instanceof GenericCoordinateTransformation) { + GenericCoordinateTransformation t = (GenericCoordinateTransformation) ct; + dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation generic = + new dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation(ct.type); + for (Map.Entry entry : t.raw.entrySet()) { + generic.raw.put(entry.getKey(), convertRawValue(entry.getValue())); + } + return generic; + } + if (ct instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.SequenceCoordinateTransformation) { + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.SequenceCoordinateTransformation t = + (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.SequenceCoordinateTransformation) ct; + dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation generic = + new dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation("sequence"); + generic.raw.put("transformations", mapTransformList(t.transformations)); + return generic; + } + if (ct instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.MapAxisCoordinateTransformation) { + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.MapAxisCoordinateTransformation t = + (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.MapAxisCoordinateTransformation) ct; + dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation generic = + new dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation("mapAxis"); + generic.raw.put("mapAxis", t.mapAxis); + if (t.transformation != null) { + generic.raw.put("transformation", mapTransform(t.transformation)); + } + return generic; + } + if (ct instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation) { + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation t = + (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation) ct; + dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation generic = + new dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation("affine"); + generic.raw.put("affine", t.affine); + if (t.path != null) { + generic.raw.put("path", t.path); + } + return generic; + } + if (ct instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.RotationCoordinateTransformation) { + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.RotationCoordinateTransformation t = + (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.RotationCoordinateTransformation) ct; + dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation generic = + new dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation("rotation"); + generic.raw.put("rotation", t.rotation); + if (t.path != null) { + generic.raw.put("path", t.path); + } + return generic; + } + if (ct instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.DisplacementsCoordinateTransformation) { + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.DisplacementsCoordinateTransformation t = + (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.DisplacementsCoordinateTransformation) ct; + dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation generic = + new dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation("displacements"); + generic.raw.put("path", t.path); + return generic; + } + if (ct instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinatesCoordinateTransformation) { + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinatesCoordinateTransformation t = + (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinatesCoordinateTransformation) ct; + dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation generic = + new dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation("coordinates"); + generic.raw.put("path", t.path); + return generic; + } + if (ct instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.BijectionCoordinateTransformation) { + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.BijectionCoordinateTransformation t = + (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.BijectionCoordinateTransformation) ct; + dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation generic = + new dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation("bijection"); + if (t.forward != null) { + generic.raw.put("forward", mapTransform(t.forward)); + } + if (t.inverse != null) { + generic.raw.put("inverse", mapTransform(t.inverse)); + } + return generic; + } + if (ct instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ByDimensionCoordinateTransformation) { + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ByDimensionCoordinateTransformation t = + (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ByDimensionCoordinateTransformation) ct; + dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation generic = + new dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation("byDimension"); + if (t.transformations != null) { + List> transformed = new ArrayList<>(); + for (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ByDimensionCoordinateTransformation.ByDimensionTransformation item : t.transformations) { + Map map = new LinkedHashMap<>(); + map.put("input_axes", item.inputAxes); + map.put("output_axes", item.outputAxes); + map.put("transformation", item.transformation != null ? mapTransform(item.transformation) : null); + transformed.add(map); + } + generic.raw.put("transformations", transformed); + } + return generic; + } + return new dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation(ct.type); + } + + private static Object convertRawValue(Object value) { + if (value instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation) { + return mapTransform((dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation) value); + } + if (value instanceof List) { + List list = (List) value; + List converted = new ArrayList<>(list.size()); + for (Object item : list) { + converted.add(convertRawValue(item)); + } + return converted; + } + if (value instanceof Map) { + Map map = (Map) value; + Map converted = new LinkedHashMap<>(); + for (Map.Entry entry : map.entrySet()) { + converted.put(String.valueOf(entry.getKey()), convertRawValue(entry.getValue())); + } + return converted; + } + return value; + } + + private static List mapTransformList( + List transforms) { + if (transforms == null) { + return null; + } + List out = new ArrayList<>(transforms.size()); + for (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation transform : transforms) { + out.add(mapTransform(transform)); + } + return out; + } + + private static List castDoubleList(List values) { + List out = new ArrayList<>(); + for (Object value : values) { + if (value instanceof Number) { + out.add(((Number) value).doubleValue()); + } + } + return out; + } + + private static List castIntList(Object raw) { + if (!(raw instanceof List)) { + return null; + } + List out = new ArrayList<>(); + for (Object value : (List) raw) { + if (value instanceof Number) { + out.add(((Number) value).intValue()); + } + } + return out; + } + + private static List> castMatrix(Object raw) { + if (!(raw instanceof List)) { + return null; + } + List> out = new ArrayList<>(); + for (Object row : (List) raw) { + if (row instanceof List) { + out.add(castDoubleList((List) row)); + } + } + return out; + } + + private static dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation castV06Transform(Object raw) { + if (raw instanceof dev.zarr.zarrjava.experimental.ome.metadata.transform.CoordinateTransformation) { + dev.zarr.zarrjava.experimental.ome.metadata.transform.CoordinateTransformation core = + (dev.zarr.zarrjava.experimental.ome.metadata.transform.CoordinateTransformation) raw; + if (core instanceof dev.zarr.zarrjava.experimental.ome.metadata.transform.ScaleCoordinateTransformation) { + dev.zarr.zarrjava.experimental.ome.metadata.transform.ScaleCoordinateTransformation t = + (dev.zarr.zarrjava.experimental.ome.metadata.transform.ScaleCoordinateTransformation) core; + return new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ScaleCoordinateTransformation( + null, null, null, t.scale, t.path); + } + if (core instanceof dev.zarr.zarrjava.experimental.ome.metadata.transform.TranslationCoordinateTransformation) { + dev.zarr.zarrjava.experimental.ome.metadata.transform.TranslationCoordinateTransformation t = + (dev.zarr.zarrjava.experimental.ome.metadata.transform.TranslationCoordinateTransformation) core; + return new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.TranslationCoordinateTransformation( + null, null, null, t.translation, t.path); + } + if (core instanceof dev.zarr.zarrjava.experimental.ome.metadata.transform.IdentityCoordinateTransformation) { + dev.zarr.zarrjava.experimental.ome.metadata.transform.IdentityCoordinateTransformation t = + (dev.zarr.zarrjava.experimental.ome.metadata.transform.IdentityCoordinateTransformation) core; + return new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.IdentityCoordinateTransformation( + null, null, null, t.path); + } + } + return null; + } + + private static List castV06TransformList(Object raw) { + if (!(raw instanceof List)) { + return null; + } + List out = new ArrayList<>(); + for (Object item : (List) raw) { + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation cast = castV06Transform(item); + if (cast != null) { + out.add(cast); + } + } + return out; + } + + private static List castByDimensionTransformList(Object raw) { + if (!(raw instanceof List)) { + return null; + } + List out = + new ArrayList<>(); + for (Object item : (List) raw) { + if (!(item instanceof Map)) { + continue; + } + Map map = (Map) item; + out.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ByDimensionCoordinateTransformation.ByDimensionTransformation( + castIntList(map.get("input_axes")), + castIntList(map.get("output_axes")), + castV06Transform(map.get("transformation")))); + } + return out; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/Plate.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/Plate.java new file mode 100644 index 00000000..d270f9a1 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/Plate.java @@ -0,0 +1,73 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.experimental.ome.OmeV3Group; +import dev.zarr.zarrjava.experimental.ome.metadata.PlateMetadata; +import dev.zarr.zarrjava.experimental.ome.v0_6.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 v0.6 HCS plate backed by a Zarr v3 group. + */ +public final class Plate extends OmeV3Group implements dev.zarr.zarrjava.experimental.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.6 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.version.startsWith("0.6")) { + throw new ZarrException( + "Expected OME-Zarr version '0.6', got '" + omeMetadata.version + "' at " + storeHandle); + } + 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.6 plate at the given store handle. + */ + public static Plate createPlate( + @Nonnull StoreHandle storeHandle, + @Nonnull PlateMetadata plateMetadata + ) throws IOException, ZarrException { + OmeMetadata omeMetadata = new OmeMetadata("0.6", null, 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.experimental.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/experimental/ome/v0_6/Scene.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/Scene.java new file mode 100644 index 00000000..41b14cd7 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/Scene.java @@ -0,0 +1,282 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.experimental.ome.OmeV3Group; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.SceneMetadata; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ByDimensionCoordinateTransformation; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.BijectionCoordinateTransformation; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.GenericCoordinateTransformation; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.MapAxisCoordinateTransformation; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.SequenceCoordinateTransformation; +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; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** OME-Zarr v0.6 scene root group (scene metadata). */ +public final class Scene extends OmeV3Group { + + private final dev.zarr.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata omeMetadata; + private final Map imageNodes; + + private Scene( + @Nonnull StoreHandle storeHandle, + @Nonnull GroupMetadata groupMetadata, + @Nonnull dev.zarr.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata omeMetadata, + @Nonnull Map imageNodes + ) throws IOException { + super(storeHandle, groupMetadata); + this.omeMetadata = omeMetadata; + this.imageNodes = Collections.unmodifiableMap(new LinkedHashMap<>(imageNodes)); + } + + public static Scene open(@Nonnull StoreHandle storeHandle) throws IOException { + try { + return openScene(storeHandle); + } catch (ZarrException e) { + throw new IOException(e.getMessage(), e); + } + } + + public static Scene openScene(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { + Group group = Group.open(storeHandle); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata omeMetadata = readOmeAttribute( + group.metadata.attributes, storeHandle, dev.zarr.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata.class); + if (!omeMetadata.version.startsWith("0.6")) { + throw new ZarrException( + "Expected OME-Zarr version '0.6', got '" + omeMetadata.version + "' at " + storeHandle); + } + if (omeMetadata.scene == null) { + throw new ZarrException("No 'scene' found in ome metadata at " + storeHandle); + } + + Map discovered = new LinkedHashMap<>(); + for (String child : asList(storeHandle.listChildren())) { + try { + MultiscaleImage image = MultiscaleImage.openMultiscaleImage(storeHandle.resolve(child)); + if (image.getRawOmeMetadata().multiscales != null && !image.getRawOmeMetadata().multiscales.isEmpty()) { + discovered.put(child, image); + } + } catch (Exception ignored) { + // child is not a v0.6 multiscale image, ignore + } + } + return new Scene(storeHandle, group.metadata, omeMetadata, discovered); + } + + public static Scene createScene( + @Nonnull StoreHandle storeHandle, + @Nonnull SceneMetadata sceneMetadata + ) throws IOException, ZarrException { + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata omeMetadata = + new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata("0.6", null, null, sceneMetadata); + Group group = Group.create(storeHandle, omeAttributes(omeMetadata)); + return new Scene(storeHandle, group.metadata, omeMetadata, new LinkedHashMap()); + } + + public static Scene create( + @Nonnull StoreHandle storeHandle, + @Nonnull SceneMetadata sceneMetadata + ) throws IOException, ZarrException { + return createScene(storeHandle, sceneMetadata); + } + + public SceneMetadata getSceneMetadata() { + return omeMetadata.scene; + } + + public List listImageNodes() { + return new ArrayList<>(imageNodes.keySet()); + } + + public MultiscaleImage openImageNode(String path) throws IOException, ZarrException { + if (path == null || path.isEmpty()) { + throw new ZarrException("Image node path must be non-empty"); + } + MultiscaleImage discovered = imageNodes.get(path); + if (discovered != null) { + return discovered; + } + MultiscaleImage opened = MultiscaleImage.openMultiscaleImage(storeHandle.resolve(path)); + if (opened.getRawOmeMetadata().multiscales == null || opened.getRawOmeMetadata().multiscales.isEmpty()) { + throw new ZarrException("No multiscales metadata found at image node path '" + path + "'"); + } + return opened; + } + + public SceneTransformationGraph getCoordinateTransformationGraph() { + List nodes = new ArrayList<>(); + List edges = new ArrayList<>(); + List warnings = new ArrayList<>(); + + SceneReferenceResolver resolver = new SceneReferenceResolver(getSceneMetadata(), imageNodes); + for (SceneReferenceResolver.ResolvedCoordinateSystem resolved : resolver.list()) { + List axisNames = new ArrayList<>(); + if (resolved.coordinateSystem.axes != null) { + for (dev.zarr.zarrjava.experimental.ome.metadata.Axis axis : resolved.coordinateSystem.axes) { + axisNames.add(axis.name); + } + } + nodes.add(new SceneTransformationGraph.Node( + resolved.id, + resolved.groupPath, + resolved.coordinateSystem.name, + axisNames)); + } + + if (omeMetadata.scene != null && omeMetadata.scene.coordinateTransformations != null) { + for (CoordinateTransformation transformation : omeMetadata.scene.coordinateTransformations) { + addTransformationEdges(transformation, resolver, edges, warnings, null); + } + } + + return new SceneTransformationGraph(nodes, edges, warnings); + } + + public Group createCoordinateTransformationsGroup() throws IOException, ZarrException { + return createGroup("coordinateTransformations"); + } + + public static String normalizeCoordinateTransformPath(String path) { + if (path == null) { + return null; + } + String normalized = path.trim().replace('\\', '/'); + while (normalized.startsWith("./")) { + normalized = normalized.substring(2); + } + while (normalized.startsWith("/")) { + normalized = normalized.substring(1); + } + return normalized; + } + + private static void addTransformationEdges( + CoordinateTransformation transformation, + SceneReferenceResolver resolver, + List edges, + List warnings, + String inheritedName + ) { + if (transformation == null) { + return; + } + String edgeName = transformation.name != null ? transformation.name : inheritedName; + SceneReferenceResolver.ResolvedCoordinateSystem input = resolver.resolve(transformation.input); + SceneReferenceResolver.ResolvedCoordinateSystem output = resolver.resolve(transformation.output); + + if (transformation.input != null && input == null) { + warnings.add("Unresolved scene input coordinate system: " + transformation.input); + } + if (transformation.output != null && output == null) { + warnings.add("Unresolved scene output coordinate system: " + transformation.output); + } + + edges.add(new SceneTransformationGraph.Edge( + edgeName, + transformation.type, + input != null ? input.id : null, + output != null ? output.id : null, + normalizeCoordinateTransformPath(extractPath(transformation)))); + + if (transformation instanceof SequenceCoordinateTransformation) { + SequenceCoordinateTransformation seq = (SequenceCoordinateTransformation) transformation; + if (seq.transformations != null) for (CoordinateTransformation nested : seq.transformations) { + addTransformationEdges(nested, resolver, edges, warnings, edgeName); + } + } + if (transformation instanceof ByDimensionCoordinateTransformation) { + ByDimensionCoordinateTransformation byDim = (ByDimensionCoordinateTransformation) transformation; + if (byDim.transformations != null) for (ByDimensionCoordinateTransformation.ByDimensionTransformation item : byDim.transformations) { + addTransformationEdges(item.transformation, resolver, edges, warnings, edgeName); + } + } + if (transformation instanceof MapAxisCoordinateTransformation) { + MapAxisCoordinateTransformation mapAxis = (MapAxisCoordinateTransformation) transformation; + if (mapAxis.transformation != null) { + addTransformationEdges(mapAxis.transformation, resolver, edges, warnings, edgeName); + } + } + if (transformation instanceof BijectionCoordinateTransformation) { + BijectionCoordinateTransformation b = (BijectionCoordinateTransformation) transformation; + if (b.forward != null) { + addTransformationEdges(b.forward, resolver, edges, warnings, edgeName); + } + if (b.inverse != null) { + addTransformationEdges(b.inverse, resolver, edges, warnings, edgeName); + } + } + if (transformation instanceof GenericCoordinateTransformation) { + GenericCoordinateTransformation generic = (GenericCoordinateTransformation) transformation; + Object nested = generic.raw.get("transformations"); + if (nested instanceof List) { + for (Object item : (List) nested) { + if (item instanceof CoordinateTransformation) { + addTransformationEdges((CoordinateTransformation) item, resolver, edges, warnings, edgeName); + } else if (item instanceof ByDimensionCoordinateTransformation.ByDimensionTransformation) { + addTransformationEdges( + ((ByDimensionCoordinateTransformation.ByDimensionTransformation) item).transformation, + resolver, edges, warnings, edgeName); + } + } + } + Object inner = generic.raw.get("transformation"); + if (inner instanceof CoordinateTransformation) { + addTransformationEdges((CoordinateTransformation) inner, resolver, edges, warnings, edgeName); + } + Object forward = generic.raw.get("forward"); + if (forward instanceof CoordinateTransformation) { + addTransformationEdges((CoordinateTransformation) forward, resolver, edges, warnings, edgeName); + } + Object inverse = generic.raw.get("inverse"); + if (inverse instanceof CoordinateTransformation) { + addTransformationEdges((CoordinateTransformation) inverse, resolver, edges, warnings, edgeName); + } + } + } + + private static String extractPath(CoordinateTransformation transformation) { + if (transformation instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ScaleCoordinateTransformation) { + return ((dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ScaleCoordinateTransformation) transformation).path; + } + if (transformation instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.TranslationCoordinateTransformation) { + return ((dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.TranslationCoordinateTransformation) transformation).path; + } + if (transformation instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.IdentityCoordinateTransformation) { + return ((dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.IdentityCoordinateTransformation) transformation).path; + } + if (transformation instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation) { + return ((dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation) transformation).path; + } + if (transformation instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.RotationCoordinateTransformation) { + return ((dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.RotationCoordinateTransformation) transformation).path; + } + if (transformation instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.DisplacementsCoordinateTransformation) { + return ((dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.DisplacementsCoordinateTransformation) transformation).path; + } + if (transformation instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinatesCoordinateTransformation) { + return ((dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinatesCoordinateTransformation) transformation).path; + } + if (transformation instanceof GenericCoordinateTransformation) { + Object p = ((GenericCoordinateTransformation) transformation).raw.get("path"); + return p instanceof String ? (String) p : null; + } + return null; + } + + private static List asList(java.util.stream.Stream stream) { + try { + return stream.collect(java.util.stream.Collectors.toList()); + } finally { + stream.close(); + } + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/SceneReferenceResolver.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/SceneReferenceResolver.java new file mode 100644 index 00000000..88ea7497 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/SceneReferenceResolver.java @@ -0,0 +1,78 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6; + +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.CoordinateSystem; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.SceneMetadata; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +final class SceneReferenceResolver { + + private final Map index = new LinkedHashMap<>(); + + SceneReferenceResolver(SceneMetadata sceneMetadata, Map imageNodes) { + if (sceneMetadata != null && sceneMetadata.coordinateSystems != null) { + for (CoordinateSystem coordinateSystem : sceneMetadata.coordinateSystems) { + addCoordinateSystem(".", coordinateSystem); + } + } + for (Map.Entry entry : imageNodes.entrySet()) { + String path = entry.getKey(); + MultiscaleImage image = entry.getValue(); + try { + int count = image.getRawOmeMetadata().multiscales == null ? 0 : image.getRawOmeMetadata().multiscales.size(); + for (int i = 0; i < count; i++) { + MultiscalesEntry multiscale = image.getMultiscalesEntry(i); + if (multiscale.coordinateSystems == null) { + continue; + } + for (CoordinateSystem coordinateSystem : multiscale.coordinateSystems) { + addCoordinateSystem(path, coordinateSystem); + } + } + } catch (Exception ignored) { + // keep permissive parsing/open behavior + } + } + } + + @Nullable + ResolvedCoordinateSystem resolve(@Nullable String reference) { + if (reference == null || reference.isEmpty()) { + return null; + } + if (!reference.contains("#")) { + return null; + } + return index.get(reference); + } + + List list() { + return new ArrayList<>(index.values()); + } + + private void addCoordinateSystem(String groupPath, CoordinateSystem coordinateSystem) { + if (coordinateSystem == null || coordinateSystem.name == null) { + return; + } + String canonicalPath = groupPath == null || groupPath.isEmpty() ? "." : groupPath; + String id = canonicalPath + "#" + coordinateSystem.name; + index.put(id, new ResolvedCoordinateSystem(id, canonicalPath, coordinateSystem)); + } + + static final class ResolvedCoordinateSystem { + final String id; + final String groupPath; + final CoordinateSystem coordinateSystem; + + ResolvedCoordinateSystem(String id, String groupPath, CoordinateSystem coordinateSystem) { + this.id = id; + this.groupPath = groupPath; + this.coordinateSystem = coordinateSystem; + } + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/SceneTransformationGraph.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/SceneTransformationGraph.java new file mode 100644 index 00000000..5b5bcde0 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/SceneTransformationGraph.java @@ -0,0 +1,50 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** Lightweight scene transformation graph view for tooling/debugging. */ +public final class SceneTransformationGraph { + public final List nodes; + public final List edges; + public final List warnings; + + SceneTransformationGraph(List nodes, List edges, List warnings) { + this.nodes = Collections.unmodifiableList(new ArrayList<>(nodes)); + this.edges = Collections.unmodifiableList(new ArrayList<>(edges)); + this.warnings = Collections.unmodifiableList(new ArrayList<>(warnings)); + } + + public static final class Node { + public final String id; + public final String groupPath; + public final String coordinateSystemName; + public final List axisNames; + + public Node(String id, String groupPath, String coordinateSystemName, List axisNames) { + this.id = id; + this.groupPath = groupPath; + this.coordinateSystemName = coordinateSystemName; + this.axisNames = axisNames == null + ? Collections.emptyList() + : Collections.unmodifiableList(new ArrayList<>(axisNames)); + } + } + + public static final class Edge { + public final String name; + public final String type; + public final String inputNodeId; + public final String outputNodeId; + public final String path; + + public Edge(String name, String type, String inputNodeId, String outputNodeId, String path) { + this.name = name; + this.type = type; + this.inputNodeId = inputNodeId; + this.outputNodeId = outputNodeId; + this.path = path; + } + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/Well.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/Well.java new file mode 100644 index 00000000..e2b0da1d --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/Well.java @@ -0,0 +1,74 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.experimental.ome.MultiscaleImage; +import dev.zarr.zarrjava.experimental.ome.OmeV3Group; +import dev.zarr.zarrjava.experimental.ome.metadata.WellMetadata; +import dev.zarr.zarrjava.experimental.ome.v0_6.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 v0.6 HCS well backed by a Zarr v3 group. + */ +public final class Well extends OmeV3Group implements dev.zarr.zarrjava.experimental.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.6 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.version.startsWith("0.6")) { + throw new ZarrException( + "Expected OME-Zarr version '0.6', got '" + omeMetadata.version + "' at " + storeHandle); + } + 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.6 well at the given store handle. + */ + public static Well createWell( + @Nonnull StoreHandle storeHandle, + @Nonnull WellMetadata wellMetadata + ) throws IOException, ZarrException { + OmeMetadata omeMetadata = new OmeMetadata("0.6", null, 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/experimental/ome/v0_6/metadata/CoordinateSystem.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/CoordinateSystem.java new file mode 100644 index 00000000..454f675a --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/CoordinateSystem.java @@ -0,0 +1,24 @@ +package dev.zarr.zarrjava.experimental.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.experimental.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/experimental/ome/v0_6/metadata/Dataset.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/Dataset.java new file mode 100644 index 00000000..f08e39db --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/Dataset.java @@ -0,0 +1,26 @@ +package dev.zarr.zarrjava.experimental.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.experimental.ome.v0_6.metadata.transform.CoordinateTransformation; + +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/experimental/ome/v0_6/metadata/MultiscalesEntry.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/MultiscalesEntry.java new file mode 100644 index 00000000..b62f51a1 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/MultiscalesEntry.java @@ -0,0 +1,54 @@ +package dev.zarr.zarrjava.experimental.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.experimental.ome.metadata.Axis; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation; + +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 coordinateTransformations; + @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("coordinateTransformations") List coordinateTransformations, + @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.coordinateTransformations = coordinateTransformations; + this.coordinateSystems = coordinateSystems; + this.name = name; + this.type = type; + this.metadata = metadata; + } + + public MultiscalesEntry(List datasets, List coordinateSystems, String name) { + this(null, datasets, null, 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, coordinateTransformations, coordinateSystems, name, type, metadata); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/OmeMetadata.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/OmeMetadata.java new file mode 100644 index 00000000..0040ae91 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/OmeMetadata.java @@ -0,0 +1,66 @@ +package dev.zarr.zarrjava.experimental.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.experimental.ome.metadata.OmeroMetadata; +import dev.zarr.zarrjava.experimental.ome.metadata.PlateMetadata; +import dev.zarr.zarrjava.experimental.ome.metadata.WellMetadata; + +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; + @Nullable public final OmeroMetadata omero; + @Nullable + @JsonProperty("bioformats2raw.layout") + public final Integer bioformats2rawLayout; + @Nullable public final SceneMetadata scene; + @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("scene") SceneMetadata scene, + @Nullable @JsonProperty("plate") PlateMetadata plate, + @Nullable @JsonProperty("well") WellMetadata well + ) { + this.version = version; + this.multiscales = multiscales; + this.omero = omero; + this.bioformats2rawLayout = bioformats2rawLayout; + this.scene = scene; + this.plate = plate; + this.well = well; + } + + public OmeMetadata(String version, @Nullable List multiscales) { + this(version, multiscales, null, null, null, null, null); + } + + public OmeMetadata( + String version, + @Nullable List multiscales, + @Nullable OmeroMetadata omero + ) { + this(version, multiscales, omero, null, null, null, null); + } + + public OmeMetadata( + String version, + @Nullable List multiscales, + @Nullable OmeroMetadata omero, + @Nullable SceneMetadata scene + ) { + this(version, multiscales, omero, null, scene, null, null); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/SceneMetadata.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/SceneMetadata.java new file mode 100644 index 00000000..c739269f --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/SceneMetadata.java @@ -0,0 +1,30 @@ +package dev.zarr.zarrjava.experimental.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.experimental.ome.v0_6.metadata.transform.CoordinateTransformation; + +import javax.annotation.Nullable; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class SceneMetadata { + + @Nullable @JsonProperty("coordinateTransformations") + public final List coordinateTransformations; + + @Nullable @JsonProperty("coordinateSystems") + public final List coordinateSystems; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public SceneMetadata( + @Nullable @JsonProperty("coordinateTransformations") + List coordinateTransformations, + @Nullable @JsonProperty("coordinateSystems") + List coordinateSystems + ) { + this.coordinateTransformations = coordinateTransformations; + this.coordinateSystems = coordinateSystems; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/AffineCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/AffineCoordinateTransformation.java new file mode 100644 index 00000000..15960278 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/AffineCoordinateTransformation.java @@ -0,0 +1,25 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.List; + +public final class AffineCoordinateTransformation extends CoordinateTransformation { + @Nullable public final List> affine; + @Nullable public final String path; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public AffineCoordinateTransformation( + @Nullable @JsonProperty("input") String input, + @Nullable @JsonProperty("output") String output, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("affine") List> affine, + @Nullable @JsonProperty("path") String path + ) { + super("affine", input, output, name); + this.affine = affine; + this.path = path; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/BijectionCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/BijectionCoordinateTransformation.java new file mode 100644 index 00000000..9617a335 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/BijectionCoordinateTransformation.java @@ -0,0 +1,24 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; + +public final class BijectionCoordinateTransformation extends CoordinateTransformation { + @Nullable public final CoordinateTransformation forward; + @Nullable public final CoordinateTransformation inverse; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public BijectionCoordinateTransformation( + @Nullable @JsonProperty("input") String input, + @Nullable @JsonProperty("output") String output, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("forward") CoordinateTransformation forward, + @Nullable @JsonProperty("inverse") CoordinateTransformation inverse + ) { + super("bijection", input, output, name); + this.forward = forward; + this.inverse = inverse; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ByDimensionCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ByDimensionCoordinateTransformation.java new file mode 100644 index 00000000..bf238d77 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ByDimensionCoordinateTransformation.java @@ -0,0 +1,39 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.List; + +public final class ByDimensionCoordinateTransformation extends CoordinateTransformation { + @Nullable public final List transformations; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public ByDimensionCoordinateTransformation( + @Nullable @JsonProperty("input") String input, + @Nullable @JsonProperty("output") String output, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("transformations") List transformations + ) { + super("byDimension", input, output, name); + this.transformations = transformations; + } + + public static final class ByDimensionTransformation { + @Nullable public final List inputAxes; + @Nullable public final List outputAxes; + @Nullable public final CoordinateTransformation transformation; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public ByDimensionTransformation( + @Nullable @JsonProperty("input_axes") List inputAxes, + @Nullable @JsonProperty("output_axes") List outputAxes, + @Nullable @JsonProperty("transformation") CoordinateTransformation transformation + ) { + this.inputAxes = inputAxes; + this.outputAxes = outputAxes; + this.transformation = transformation; + } + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinateTransformation.java new file mode 100644 index 00000000..549ab10a --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinateTransformation.java @@ -0,0 +1,139 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type", + visible = true, + defaultImpl = GenericCoordinateTransformation.class) +@JsonSubTypes({ + @JsonSubTypes.Type(value = ScaleCoordinateTransformation.class, name = "scale"), + @JsonSubTypes.Type(value = TranslationCoordinateTransformation.class, name = "translation"), + @JsonSubTypes.Type(value = IdentityCoordinateTransformation.class, name = "identity"), + @JsonSubTypes.Type(value = SequenceCoordinateTransformation.class, name = "sequence"), + @JsonSubTypes.Type(value = MapAxisCoordinateTransformation.class, name = "mapAxis"), + @JsonSubTypes.Type(value = AffineCoordinateTransformation.class, name = "affine"), + @JsonSubTypes.Type(value = RotationCoordinateTransformation.class, name = "rotation"), + @JsonSubTypes.Type(value = DisplacementsCoordinateTransformation.class, name = "displacements"), + @JsonSubTypes.Type(value = CoordinatesCoordinateTransformation.class, name = "coordinates"), + @JsonSubTypes.Type(value = BijectionCoordinateTransformation.class, name = "bijection"), + @JsonSubTypes.Type(value = ByDimensionCoordinateTransformation.class, name = "byDimension") +}) +public abstract class CoordinateTransformation { + + public final String type; + @Nullable + @JsonDeserialize(using = CoordinateSystemRefDeserializer.class) + @JsonSerialize(using = CoordinateSystemRefSerializer.class) + public final String input; + @Nullable + @JsonDeserialize(using = CoordinateSystemRefDeserializer.class) + @JsonSerialize(using = CoordinateSystemRefSerializer.class) + public final String output; + @Nullable public final String name; + + protected CoordinateTransformation( + @JsonProperty(value = "type", required = true) String type, + @Nullable @JsonProperty("input") String input, + @Nullable @JsonProperty("output") String output, + @Nullable @JsonProperty("name") String name + ) { + this.type = type; + this.input = input; + this.output = output; + this.name = name; + } + + public static CoordinateTransformation scale(List scale, String input, String output) { + return new ScaleCoordinateTransformation(input, output, null, scale, null); + } + + public static CoordinateTransformation translation(List translation, String input, String output) { + return new TranslationCoordinateTransformation(input, output, null, translation, null); + } + + public static CoordinateTransformation identity(String input, String output) { + return new IdentityCoordinateTransformation(input, output, null, null); + } + + public static CoordinateTransformation sequence(List transformations, String input, String output) { + return new SequenceCoordinateTransformation(input, output, null, transformations); + } + + public static final class CoordinateSystemRefDeserializer extends JsonDeserializer { + @Override + public String deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (parser.currentToken() == JsonToken.VALUE_STRING) { + return parser.getValueAsString(); + } + if (parser.currentToken() == JsonToken.START_OBJECT) { + String path = null; + String name = null; + while (parser.nextToken() != JsonToken.END_OBJECT) { + String field = parser.getCurrentName(); + parser.nextToken(); + if ("path".equals(field)) { + path = parser.getValueAsString(); + } else if ("name".equals(field)) { + name = parser.getValueAsString(); + } else { + parser.skipChildren(); + } + } + if (name == null || name.isEmpty()) { + return null; + } + String prefix = (path == null || path.isEmpty()) ? "." : path; + return prefix + "#" + name; + } + if (parser.currentToken() == JsonToken.VALUE_NULL) { + return null; + } + return parser.getValueAsString(); + } + } + + public static final class CoordinateSystemRefSerializer extends JsonSerializer { + @Override + public void serialize(String value, JsonGenerator generator, SerializerProvider serializers) throws IOException { + if (value == null) { + generator.writeNull(); + return; + } + int split = value.indexOf('#'); + if (split <= 0 || split >= value.length() - 1) { + generator.writeString(value); + return; + } + String path = value.substring(0, split); + String name = value.substring(split + 1); + generator.writeStartObject(); + if (!".".equals(path)) { + generator.writeStringField("path", path); + } + generator.writeStringField("name", name); + generator.writeEndObject(); + } + } + +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinatesCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinatesCoordinateTransformation.java new file mode 100644 index 00000000..2fadb80e --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinatesCoordinateTransformation.java @@ -0,0 +1,21 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; + +public final class CoordinatesCoordinateTransformation extends CoordinateTransformation { + @Nullable public final String path; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public CoordinatesCoordinateTransformation( + @Nullable @JsonProperty("input") String input, + @Nullable @JsonProperty("output") String output, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("path") String path + ) { + super("coordinates", input, output, name); + this.path = path; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/DisplacementsCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/DisplacementsCoordinateTransformation.java new file mode 100644 index 00000000..e19addcd --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/DisplacementsCoordinateTransformation.java @@ -0,0 +1,21 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; + +public final class DisplacementsCoordinateTransformation extends CoordinateTransformation { + @Nullable public final String path; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public DisplacementsCoordinateTransformation( + @Nullable @JsonProperty("input") String input, + @Nullable @JsonProperty("output") String output, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("path") String path + ) { + super("displacements", input, output, name); + this.path = path; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/GenericCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/GenericCoordinateTransformation.java new file mode 100644 index 00000000..998ec732 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/GenericCoordinateTransformation.java @@ -0,0 +1,34 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Forward-compatibility fallback for transform types not yet modeled explicitly. + * + *

Used to preserve unknown/extension transform metadata during deserialization + * rather than rejecting the entire OME payload. + */ +public final class GenericCoordinateTransformation extends CoordinateTransformation { + public final Map raw = new LinkedHashMap<>(); + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public GenericCoordinateTransformation( + @JsonProperty(value = "type", required = true) String type, + @Nullable @JsonProperty("input") String input, + @Nullable @JsonProperty("output") String output, + @Nullable @JsonProperty("name") String name + ) { + super(type, input, output, name); + } + + @JsonAnySetter + public void capture(String key, Object value) { + raw.put(key, value); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/IdentityCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/IdentityCoordinateTransformation.java new file mode 100644 index 00000000..66e11b89 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/IdentityCoordinateTransformation.java @@ -0,0 +1,21 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; + +public final class IdentityCoordinateTransformation extends CoordinateTransformation { + @Nullable public final String path; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public IdentityCoordinateTransformation( + @Nullable @JsonProperty("input") String input, + @Nullable @JsonProperty("output") String output, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("path") String path + ) { + super("identity", input, output, name); + this.path = path; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/MapAxisCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/MapAxisCoordinateTransformation.java new file mode 100644 index 00000000..23835f3d --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/MapAxisCoordinateTransformation.java @@ -0,0 +1,25 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.List; + +public final class MapAxisCoordinateTransformation extends CoordinateTransformation { + @Nullable public final List mapAxis; + @Nullable public final CoordinateTransformation transformation; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public MapAxisCoordinateTransformation( + @Nullable @JsonProperty("input") String input, + @Nullable @JsonProperty("output") String output, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("mapAxis") List mapAxis, + @Nullable @JsonProperty("transformation") CoordinateTransformation transformation + ) { + super("mapAxis", input, output, name); + this.mapAxis = mapAxis; + this.transformation = transformation; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/RotationCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/RotationCoordinateTransformation.java new file mode 100644 index 00000000..8093edab --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/RotationCoordinateTransformation.java @@ -0,0 +1,25 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.List; + +public final class RotationCoordinateTransformation extends CoordinateTransformation { + @Nullable public final List> rotation; + @Nullable public final String path; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public RotationCoordinateTransformation( + @Nullable @JsonProperty("input") String input, + @Nullable @JsonProperty("output") String output, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("rotation") List> rotation, + @Nullable @JsonProperty("path") String path + ) { + super("rotation", input, output, name); + this.rotation = rotation; + this.path = path; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ScaleCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ScaleCoordinateTransformation.java new file mode 100644 index 00000000..65df4aa2 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ScaleCoordinateTransformation.java @@ -0,0 +1,25 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.List; + +public final class ScaleCoordinateTransformation extends CoordinateTransformation { + @Nullable public final List scale; + @Nullable public final String path; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public ScaleCoordinateTransformation( + @Nullable @JsonProperty("input") String input, + @Nullable @JsonProperty("output") String output, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("scale") List scale, + @Nullable @JsonProperty("path") String path + ) { + super("scale", input, output, name); + this.scale = scale; + this.path = path; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/SequenceCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/SequenceCoordinateTransformation.java new file mode 100644 index 00000000..188f16a4 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/SequenceCoordinateTransformation.java @@ -0,0 +1,22 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.List; + +public final class SequenceCoordinateTransformation extends CoordinateTransformation { + @Nullable public final List transformations; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public SequenceCoordinateTransformation( + @Nullable @JsonProperty("input") String input, + @Nullable @JsonProperty("output") String output, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("transformations") List transformations + ) { + super("sequence", input, output, name); + this.transformations = transformations; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/TranslationCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/TranslationCoordinateTransformation.java new file mode 100644 index 00000000..306ff8db --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/TranslationCoordinateTransformation.java @@ -0,0 +1,25 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.List; + +public final class TranslationCoordinateTransformation extends CoordinateTransformation { + @Nullable public final List translation; + @Nullable public final String path; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public TranslationCoordinateTransformation( + @Nullable @JsonProperty("input") String input, + @Nullable @JsonProperty("output") String output, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("translation") List translation, + @Nullable @JsonProperty("path") String path + ) { + super("translation", input, output, name); + this.translation = translation; + this.path = path; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/v2/ArrayMetadataBuilder.java b/src/main/java/dev/zarr/zarrjava/v2/ArrayMetadataBuilder.java index 9a26f93b..c35acd3c 100644 --- a/src/main/java/dev/zarr/zarrjava/v2/ArrayMetadataBuilder.java +++ b/src/main/java/dev/zarr/zarrjava/v2/ArrayMetadataBuilder.java @@ -8,6 +8,7 @@ import dev.zarr.zarrjava.v2.codec.Codec; import dev.zarr.zarrjava.v2.codec.core.BloscCodec; import dev.zarr.zarrjava.v2.codec.core.ZlibCodec; +import dev.zarr.zarrjava.v2.codec.core.ZstdCodec; public class ArrayMetadataBuilder { long[] shape = null; @@ -129,6 +130,23 @@ public ArrayMetadataBuilder withZlibCompressor() { return withZlibCompressor(5); } + public ArrayMetadataBuilder withZstdCompressor(int level, boolean checksum) { + try { + this.compressor = new ZstdCodec(level, checksum); + } catch (ZarrException e) { + throw new RuntimeException(e); + } + return this; + } + + public ArrayMetadataBuilder withZstdCompressor(int level) { + return withZstdCompressor(level, ZstdCodec.DEFAULT_CHECKSUM); + } + + public ArrayMetadataBuilder withZstdCompressor() { + return withZstdCompressor(ZstdCodec.DEFAULT_LEVEL); + } + public ArrayMetadataBuilder putAttribute(String key, Object value) { this.attributes.put(key, value); return this; 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..34d3b057 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/v2/codec/core/ZstdCodec.java @@ -0,0 +1,41 @@ +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 { + + public static final int DEFAULT_LEVEL = 0; + public static final boolean DEFAULT_CHECKSUM = false; + @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 = "" + DEFAULT_LEVEL) int level, + @JsonProperty(value = "checksum", defaultValue = "" + DEFAULT_CHECKSUM) 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..c5e797d2 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); @@ -242,6 +244,9 @@ public void testWriteV2(String compressor, String compressorParam, dev.zarr.zarr case "zlib": builder = builder.withZlibCompressor(Integer.parseInt(compressorParam)); break; + case "zstd": + builder = builder.withZstdCompressor(Integer.parseInt(compressorParam.split("_")[0]), Boolean.parseBoolean(compressorParam.split("_")[1])); + break; default: throw new IllegalArgumentException("Invalid compressor: " + compressor); } diff --git a/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeObjectMappersTest.java b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeObjectMappersTest.java new file mode 100644 index 00000000..b25369ae --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeObjectMappersTest.java @@ -0,0 +1,375 @@ +package dev.zarr.zarrjava.experimental.ome; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.zarr.zarrjava.experimental.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.experimental.ome.metadata.OmeMetadata; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class OmeObjectMappersTest { + + private static final String WARNING_LOGGER_NAME = + "dev.zarr.zarrjava.experimental.ome.OmeObjectMappers$UnknownOmePropertyWarningHandler"; + + @Test + void v3MapperWarnsAndContinuesOnUnknownOmeFields() { + String unknownTop = "unknown_top_" + UUID.randomUUID(); + String unknownAxis = "unknown_axis_" + UUID.randomUUID(); + + Map axis = new HashMap<>(); + axis.put("name", "x"); + axis.put("type", "space"); + axis.put(unknownAxis, 7); + + Map transform = new HashMap<>(); + transform.put("type", "scale"); + transform.put("scale", Arrays.asList(1.0, 1.0)); + + Map dataset = new HashMap<>(); + dataset.put("path", "0"); + dataset.put("coordinateTransformations", Arrays.asList(transform)); + + Map multiscale = new HashMap<>(); + multiscale.put("axes", Arrays.asList(axis)); + multiscale.put("datasets", Arrays.asList(dataset)); + + Map omeRaw = new HashMap<>(); + omeRaw.put("version", "0.5"); + omeRaw.put("multiscales", Arrays.asList(multiscale)); + omeRaw.put(unknownTop, "surprise"); + + Logger logger = Logger.getLogger(WARNING_LOGGER_NAME); + CapturingHandler handler = new CapturingHandler(); + logger.addHandler(handler); + try { + ObjectMapper mapper = OmeObjectMappers.makeV3Mapper(); + OmeMetadata parsed = mapper.convertValue(omeRaw, OmeMetadata.class); + + assertNotNull(parsed); + assertEquals("0.5", parsed.version); + assertNotNull(parsed.multiscales); + assertEquals(1, parsed.multiscales.size()); + assertEquals("x", parsed.multiscales.get(0).axes.get(0).name); + assertTrue(handler.containsWarningWith(unknownTop)); + assertTrue(handler.containsWarningWith(unknownAxis)); + } finally { + logger.removeHandler(handler); + } + } + + @Test + void v2MapperWarnsAndContinuesOnUnknownFields() { + String unknownEntryField = "unknown_entry_" + UUID.randomUUID(); + + Map axis = new HashMap<>(); + axis.put("name", "x"); + axis.put("type", "space"); + + Map transform = new HashMap<>(); + transform.put("type", "scale"); + transform.put("scale", Arrays.asList(1.0, 1.0)); + + Map dataset = new HashMap<>(); + dataset.put("path", "0"); + dataset.put("coordinateTransformations", Arrays.asList(transform)); + + Map entryRaw = new HashMap<>(); + entryRaw.put("axes", Arrays.asList(axis)); + entryRaw.put("datasets", Arrays.asList(dataset)); + entryRaw.put(unknownEntryField, 123); + + Logger logger = Logger.getLogger(WARNING_LOGGER_NAME); + CapturingHandler handler = new CapturingHandler(); + logger.addHandler(handler); + try { + ObjectMapper mapper = OmeObjectMappers.makeV2Mapper(); + MultiscalesEntry entry = mapper.convertValue(entryRaw, MultiscalesEntry.class); + + assertNotNull(entry); + assertEquals(1, entry.axes.size()); + assertEquals(1, entry.datasets.size()); + assertFalse(entry.datasets.get(0).coordinateTransformations.isEmpty()); + assertTrue(handler.containsWarningWith(unknownEntryField)); + } finally { + logger.removeHandler(handler); + } + } + + @Test + void v3MapperParsesUnknownTransformAsGenericAndPreservesRawFields() { + Map unknownTransform = new HashMap<>(); + unknownTransform.put("type", "vendorWarp"); + unknownTransform.put("strength", 3.5); + unknownTransform.put("axes", Arrays.asList("y", "x")); + Map vendorPayload = new HashMap<>(); + vendorPayload.put("mode", "spline"); + vendorPayload.put("order", 3); + unknownTransform.put("vendor", vendorPayload); + + Map dataset = new HashMap<>(); + dataset.put("path", "0"); + dataset.put("coordinateTransformations", Arrays.asList(unknownTransform)); + + Map axisY = new HashMap<>(); + axisY.put("name", "y"); + axisY.put("type", "space"); + Map axisX = new HashMap<>(); + axisX.put("name", "x"); + axisX.put("type", "space"); + + Map entryRaw = new HashMap<>(); + entryRaw.put("axes", Arrays.asList(axisY, axisX)); + entryRaw.put("datasets", Arrays.asList(dataset)); + + ObjectMapper mapper = OmeObjectMappers.makeV3Mapper(); + MultiscalesEntry entry = mapper.convertValue(entryRaw, MultiscalesEntry.class); + + assertNotNull(entry); + assertEquals(1, entry.datasets.size()); + assertEquals(1, entry.datasets.get(0).coordinateTransformations.size()); + assertTrue( + entry.datasets.get(0).coordinateTransformations.get(0) + instanceof dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation); + + dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation generic = + (dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation) + entry.datasets.get(0).coordinateTransformations.get(0); + assertEquals("vendorWarp", generic.type); + assertEquals(3.5, generic.raw.get("strength")); + assertEquals(Arrays.asList("y", "x"), generic.raw.get("axes")); + assertEquals(vendorPayload, generic.raw.get("vendor")); + } + + @Test + void v2MapperParsesUnknownV06TransformAsGenericAndPreservesRawFields() { + Map unknownTransform = new HashMap<>(); + unknownTransform.put("type", "customNonLinear"); + unknownTransform.put("input", "s0"); + unknownTransform.put("output", "physical"); + unknownTransform.put("name", "custom-stage"); + unknownTransform.put("lut", Arrays.asList(1, 4, 9)); + Map extension = new HashMap<>(); + extension.put("author", "vendor"); + extension.put("version", 2); + unknownTransform.put("extension", extension); + + Map dataset = new HashMap<>(); + dataset.put("path", "s0"); + dataset.put("coordinateTransformations", Arrays.asList(unknownTransform)); + + Map entryRaw = new HashMap<>(); + entryRaw.put("datasets", Arrays.asList(dataset)); + + ObjectMapper mapper = OmeObjectMappers.makeV2Mapper(); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry entry = + mapper.convertValue(entryRaw, dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry.class); + + assertNotNull(entry); + assertEquals(1, entry.datasets.size()); + assertEquals(1, entry.datasets.get(0).coordinateTransformations.size()); + assertTrue( + entry.datasets.get(0).coordinateTransformations.get(0) + instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.GenericCoordinateTransformation); + + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.GenericCoordinateTransformation generic = + (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.GenericCoordinateTransformation) + entry.datasets.get(0).coordinateTransformations.get(0); + assertEquals("customNonLinear", generic.type); + assertEquals("s0", generic.input); + assertEquals("physical", generic.output); + assertEquals("custom-stage", generic.name); + assertEquals(Arrays.asList(1, 4, 9), generic.raw.get("lut")); + assertEquals(extension, generic.raw.get("extension")); + } + + @Test + void v2MapperParsesTypedV06AffineAndByDimensionTransforms() { + Map affine = new HashMap<>(); + affine.put("type", "affine"); + affine.put("input", "s0"); + affine.put("output", "physical"); + affine.put("name", "affine-stage"); + affine.put("affine", Arrays.asList( + Arrays.asList(1.0, 0.0, 3.0), + Arrays.asList(0.0, 1.0, 4.0))); + affine.put("path", "coordinateTransformations/affine"); + + Map childScale = new HashMap<>(); + childScale.put("type", "scale"); + childScale.put("scale", Arrays.asList(2.0)); + + Map byDimItem = new HashMap<>(); + byDimItem.put("input_axes", Arrays.asList(1)); + byDimItem.put("output_axes", Arrays.asList(0)); + byDimItem.put("transformation", childScale); + + Map byDimension = new HashMap<>(); + byDimension.put("type", "byDimension"); + byDimension.put("input", "s0"); + byDimension.put("output", "physical"); + byDimension.put("transformations", Arrays.asList(byDimItem)); + + Map dataset = new HashMap<>(); + dataset.put("path", "s0"); + dataset.put("coordinateTransformations", Arrays.asList(affine, byDimension)); + + Map entryRaw = new HashMap<>(); + entryRaw.put("datasets", Arrays.asList(dataset)); + + ObjectMapper mapper = OmeObjectMappers.makeV2Mapper(); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry entry = + mapper.convertValue(entryRaw, dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry.class); + + assertNotNull(entry); + assertEquals(2, entry.datasets.get(0).coordinateTransformations.size()); + + assertTrue( + entry.datasets.get(0).coordinateTransformations.get(0) + instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation affineParsed = + (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation) + entry.datasets.get(0).coordinateTransformations.get(0); + assertEquals("affine-stage", affineParsed.name); + assertEquals("coordinateTransformations/affine", affineParsed.path); + assertEquals(2, affineParsed.affine.size()); + assertEquals(Arrays.asList(1.0, 0.0, 3.0), affineParsed.affine.get(0)); + + assertTrue( + entry.datasets.get(0).coordinateTransformations.get(1) + instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ByDimensionCoordinateTransformation); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ByDimensionCoordinateTransformation byDimensionParsed = + (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ByDimensionCoordinateTransformation) + entry.datasets.get(0).coordinateTransformations.get(1); + assertNotNull(byDimensionParsed.transformations); + assertEquals(1, byDimensionParsed.transformations.size()); + assertEquals(Arrays.asList(1), byDimensionParsed.transformations.get(0).inputAxes); + assertEquals(Arrays.asList(0), byDimensionParsed.transformations.get(0).outputAxes); + assertTrue(byDimensionParsed.transformations.get(0).transformation + instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ScaleCoordinateTransformation); + } + + @Test + void v3MapperParsesV06SceneMetadataRefsAndNestedTransforms() { + Map sceneTranslation = new HashMap<>(); + sceneTranslation.put("type", "translation"); + sceneTranslation.put("input", new HashMap() {{ + put("path", "imgA"); + put("name", "physical"); + }}); + sceneTranslation.put("output", new HashMap() {{ + put("name", "world"); + }}); + sceneTranslation.put("translation", Arrays.asList(1.0, 2.0)); + + Map byDimInner = new HashMap<>(); + byDimInner.put("type", "identity"); + Map byDimStep = new HashMap<>(); + byDimStep.put("input_axes", Arrays.asList(0)); + byDimStep.put("output_axes", Arrays.asList(0)); + byDimStep.put("transformation", byDimInner); + Map byDim = new HashMap<>(); + byDim.put("type", "byDimension"); + byDim.put("input", new HashMap() {{ + put("path", "imgB"); + put("name", "physical"); + }}); + byDim.put("output", new HashMap() {{ + put("name", "world"); + }}); + byDim.put("transformations", Arrays.asList(byDimStep)); + + Map sequence = new HashMap<>(); + sequence.put("type", "sequence"); + sequence.put("input", new HashMap() {{ + put("path", "imgC"); + put("name", "physical"); + }}); + sequence.put("output", new HashMap() {{ + put("name", "world"); + }}); + sequence.put("transformations", Arrays.asList(sceneTranslation, byDim)); + + Map scene = new HashMap<>(); + scene.put("coordinateTransformations", Arrays.asList(sequence)); + + Map omeRaw = new HashMap<>(); + omeRaw.put("version", "0.6.dev3"); + omeRaw.put("scene", scene); + + ObjectMapper mapper = OmeObjectMappers.makeV3Mapper(); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata parsed = + mapper.convertValue(omeRaw, dev.zarr.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata.class); + + assertNotNull(parsed.scene); + assertEquals(1, parsed.scene.coordinateTransformations.size()); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation parsedSequence = + parsed.scene.coordinateTransformations.get(0); + assertEquals("sequence", parsedSequence.type); + assertTrue(parsedSequence instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.SequenceCoordinateTransformation); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.SequenceCoordinateTransformation seq = + (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.SequenceCoordinateTransformation) parsedSequence; + assertNotNull(seq.transformations); + assertEquals(2, seq.transformations.size()); + + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation parsedTranslation = + seq.transformations.get(0); + assertEquals("translation", parsedTranslation.type); + assertEquals("imgA#physical", parsedTranslation.input); + assertEquals(".#world", parsedTranslation.output); + + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation parsedByDim = + seq.transformations.get(1); + assertEquals("byDimension", parsedByDim.type); + assertTrue(parsedByDim instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ByDimensionCoordinateTransformation); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ByDimensionCoordinateTransformation parsedByDimension = + (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ByDimensionCoordinateTransformation) parsedByDim; + assertNotNull(parsedByDimension.transformations); + assertEquals(1, parsedByDimension.transformations.size()); + assertEquals(Arrays.asList(0), parsedByDimension.transformations.get(0).inputAxes); + assertEquals("identity", parsedByDimension.transformations.get(0).transformation.type); + } + + private static final class CapturingHandler extends Handler { + private final List warnings = new ArrayList<>(); + + @Override + public void publish(LogRecord record) { + if (record.getLevel().intValue() >= Level.WARNING.intValue()) { + warnings.add(record.getMessage()); + } + } + + @Override + public void flush() { + } + + @Override + public void close() { + } + + boolean containsWarningWith(String token) { + for (String warning : warnings) { + if (warning != null && warning.contains(token)) { + return true; + } + } + return false; + } + } +} diff --git a/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrBaseTest.java b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrBaseTest.java new file mode 100644 index 00000000..fcfea4c6 --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrBaseTest.java @@ -0,0 +1,87 @@ +package dev.zarr.zarrjava.experimental.ome; + +import dev.zarr.zarrjava.ZarrTest; +import dev.zarr.zarrjava.experimental.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 openReturnsCorrectConcreteType() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + assertInstanceOf(expectedConcreteClass(), image); + } + + @Test + void getMultiscaleNodeHasExpectedAxes() 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 getMultiscaleNodeHasExpectedLevelCount() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + MultiscalesEntry entry = image.getMultiscaleNode(0); + assertEquals(expectedScaleLevelCount(), entry.datasets.size()); + } + + @Test + void getAxisNamesReturnsExpected() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + assertEquals(expectedAxisNames(), image.getAxisNames()); + } + + @Test + void getScaleLevelCountReturnsExpected() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + assertEquals(expectedScaleLevelCount(), image.getScaleLevelCount()); + } + + @Test + void openScaleLevelLevel0HasExpectedShape() 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/experimental/ome/OmeZarrSceneV06Test.java b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrSceneV06Test.java new file mode 100644 index 00000000..60bc767d --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrSceneV06Test.java @@ -0,0 +1,193 @@ +package dev.zarr.zarrjava.experimental.ome; + +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.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +public class OmeZarrSceneV06Test extends OmeZarrBaseTest { + + private static final Path V06_SCENE_REGISTRATION = + TESTDATA.resolve("ome/v0.6/examples/user_stories/image_registration_3d.zarr"); + private static final Path V06_SCENE_EXAMPLE1 = + TESTDATA.resolve("ome/v0.6_scene/example1_instrument_registration.zarr"); + private static final Path V06_SCENE_EXAMPLE2 = + TESTDATA.resolve("ome/v0.6_scene/example2_multi_instrument_chain.zarr"); + + @Override + StoreHandle imageStoreHandle() throws Exception { + return new FilesystemStore(TESTDATA.resolve("ome/v0.6/examples/2d/basic/scale_multiscale.zarr")).resolve(); + } + + @Override + Class expectedConcreteClass() { + return dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage.class; + } + + @Override + int expectedScaleLevelCount() { + return 3; + } + + @Override + long[] expectedLevel0Shape() { + return new long[]{576, 720}; + } + + @Override + java.util.List expectedAxisNames() { + return Arrays.asList("y", "x"); + } + + @Test + void openSceneAndNavigateImages() throws Exception { + StoreHandle sceneHandle = new FilesystemStore(V06_SCENE_REGISTRATION).resolve(); + dev.zarr.zarrjava.experimental.ome.v0_6.Scene scene = dev.zarr.zarrjava.experimental.ome.v0_6.Scene.openScene(sceneHandle); + + assertNotNull(scene.getSceneMetadata()); + assertNotNull(scene.getSceneMetadata().coordinateTransformations); + assertFalse(scene.getSceneMetadata().coordinateTransformations.isEmpty()); + + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation top = + scene.getSceneMetadata().coordinateTransformations.get(0); + assertEquals("bijection", top.type); + assertEquals("JRC2018F#physical", top.input); + assertEquals("FCWB#physical", top.output); + + assertTrue(scene.listImageNodes().contains("FCWB")); + assertTrue(scene.listImageNodes().contains("JRC2018F")); + + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage image = scene.openImageNode("FCWB"); + assertEquals(2, image.getScaleLevelCount()); + assertEquals(Arrays.asList("z", "y", "x"), image.getAxisNames()); + + dev.zarr.zarrjava.experimental.ome.v0_6.SceneTransformationGraph graph = scene.getCoordinateTransformationGraph(); + assertFalse(graph.nodes.isEmpty()); + assertFalse(graph.edges.isEmpty()); + assertTrue(graph.warnings.isEmpty()); + } + + @Test + void multiscaleOpenOnSceneRootGivesGuidance() throws Exception { + StoreHandle sceneHandle = new FilesystemStore(V06_SCENE_REGISTRATION).resolve(); + Exception ex = assertThrows(dev.zarr.zarrjava.ZarrException.class, () -> MultiscaleImage.open(sceneHandle)); + assertTrue(ex.getMessage().contains("Scene.open")); + } + + @Test + void createAndReopenScene() throws Exception { + Path out = TESTOUTPUT.resolve("ome_v06_scene_create"); + StoreHandle root = new FilesystemStore(out).resolve(); + + dev.zarr.zarrjava.experimental.ome.metadata.Axis y = new dev.zarr.zarrjava.experimental.ome.metadata.Axis("y", "space", "micrometer"); + dev.zarr.zarrjava.experimental.ome.metadata.Axis x = new dev.zarr.zarrjava.experimental.ome.metadata.Axis("x", "space", "micrometer"); + + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.SceneMetadata sceneMetadata = + new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.SceneMetadata( + Collections.singletonList(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.TranslationCoordinateTransformation( + "imageA#physical", ".#world", "imageA to world", Arrays.asList(1.0, 2.0), null)), + Collections.singletonList(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.CoordinateSystem( + "world", Arrays.asList(y, x))) + ); + + dev.zarr.zarrjava.experimental.ome.v0_6.Scene.createScene(root, sceneMetadata); + + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry imageAEntry = + new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry( + null, + Collections.singletonList(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.Dataset( + "s0", + Collections.singletonList( + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation.scale( + Arrays.asList(1.0, 1.0), "s0", "physical")))), + null, + Collections.singletonList(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.CoordinateSystem( + "physical", Arrays.asList(y, x))), + "multiscales", + null, + null); + + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage.create(root.resolve("imageA"), imageAEntry); + dev.zarr.zarrjava.v3.Array.create( + root.resolve("imageA").resolve("s0"), + dev.zarr.zarrjava.v3.Array.metadataBuilder() + .withShape(16, 16) + .withChunkShape(8, 8) + .withDataType(dev.zarr.zarrjava.v3.DataType.UINT16) + .build()); + + dev.zarr.zarrjava.experimental.ome.v0_6.Scene reopened = dev.zarr.zarrjava.experimental.ome.v0_6.Scene.openScene(root); + assertEquals(Collections.singletonList("imageA"), reopened.listImageNodes()); + assertEquals("world", reopened.getSceneMetadata().coordinateSystems.get(0).name); + assertEquals("imageA#physical", reopened.getSceneMetadata().coordinateTransformations.get(0).input); + + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage imageA = reopened.openImageNode("imageA"); + assertEquals(1, imageA.getScaleLevelCount()); + assertArrayEquals(new long[]{16, 16}, imageA.openScaleLevel(0).metadata().shape); + + dev.zarr.zarrjava.experimental.ome.v0_6.SceneTransformationGraph graph = reopened.getCoordinateTransformationGraph(); + assertEquals(2, graph.nodes.size()); + assertEquals(1, graph.edges.size()); + assertEquals("coordinateTransformations/lens", dev.zarr.zarrjava.experimental.ome.v0_6.Scene.normalizeCoordinateTransformPath("./coordinateTransformations/lens")); + + reopened.createCoordinateTransformationsGroup(); + assertTrue(root.resolve("coordinateTransformations").resolve(dev.zarr.zarrjava.core.Node.ZARR_JSON).exists()); + } + + @Test + void openSceneExample1AffinePathBetweenInstruments() throws Exception { + dev.zarr.zarrjava.experimental.ome.v0_6.Scene scene = + dev.zarr.zarrjava.experimental.ome.v0_6.Scene.openScene(new FilesystemStore(V06_SCENE_EXAMPLE1).resolve()); + assertEquals(Arrays.asList("sampleA_instrument1", "sampleA_instrument2"), scene.listImageNodes()); + assertEquals(1, scene.getSceneMetadata().coordinateTransformations.size()); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation ct = + scene.getSceneMetadata().coordinateTransformations.get(0); + assertEquals("affine", ct.type); + assertEquals("sampleA_instrument2#physical_instrument2", ct.input); + assertEquals("sampleA_instrument1#physical_instrument1", ct.output); + assertTrue(ct instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation); + assertEquals("coordinateTransformations/sampleA_instrument2-to-instrument1", + ((dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation) ct).path); + + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage i1 = scene.openImageNode("sampleA_instrument1"); + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage i2 = scene.openImageNode("sampleA_instrument2"); + assertEquals(Arrays.asList("z", "y", "x"), i1.getAxisNames()); + assertEquals(Arrays.asList("z", "y", "x"), i2.getAxisNames()); + + dev.zarr.zarrjava.experimental.ome.v0_6.SceneTransformationGraph graph = scene.getCoordinateTransformationGraph(); + assertEquals(2, graph.nodes.size()); + assertEquals(1, graph.edges.size()); + assertTrue(graph.warnings.isEmpty()); + } + + @Test + void openSceneExample2TwoAffineLinksViaInstrument2() throws Exception { + dev.zarr.zarrjava.experimental.ome.v0_6.Scene scene = + dev.zarr.zarrjava.experimental.ome.v0_6.Scene.openScene(new FilesystemStore(V06_SCENE_EXAMPLE2).resolve()); + assertEquals(Arrays.asList("instrument1", "instrument2", "instrument3"), scene.listImageNodes()); + assertEquals(2, scene.getSceneMetadata().coordinateTransformations.size()); + + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation t0 = + scene.getSceneMetadata().coordinateTransformations.get(0); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation t1 = + scene.getSceneMetadata().coordinateTransformations.get(1); + assertEquals("affine", t0.type); + assertEquals("affine", t1.type); + assertEquals("instrument1#physical", t0.input); + assertEquals("instrument2#physical", t0.output); + assertEquals("instrument3#physical", t1.input); + assertEquals("instrument2#physical", t1.output); + assertTrue(t0 instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation); + assertTrue(((dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation) t0).affine.size() > 0); + + dev.zarr.zarrjava.experimental.ome.v0_6.SceneTransformationGraph graph = scene.getCoordinateTransformationGraph(); + assertEquals(3, graph.nodes.size()); + assertEquals(2, graph.edges.size()); + assertTrue(graph.warnings.isEmpty()); + } +} diff --git a/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrUserGuideExamplesTest.java b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrUserGuideExamplesTest.java new file mode 100644 index 00000000..033422b7 --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrUserGuideExamplesTest.java @@ -0,0 +1,91 @@ +package dev.zarr.zarrjava.experimental.ome; + +import dev.zarr.zarrjava.ZarrTest; +import dev.zarr.zarrjava.experimental.ome.metadata.Axis; +import dev.zarr.zarrjava.experimental.ome.metadata.transform.CoordinateTransformation; +import dev.zarr.zarrjava.experimental.ome.metadata.transform.ScaleCoordinateTransformation; +import dev.zarr.zarrjava.experimental.ome.metadata.Dataset; +import dev.zarr.zarrjava.experimental.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.store.FilesystemStore; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v3.Array; +import dev.zarr.zarrjava.v3.DataType; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class OmeZarrUserGuideExamplesTest extends ZarrTest { + + private StoreHandle storeHandle(java.nio.file.Path path) throws Exception { + return new FilesystemStore(path).resolve(); + } + + @Test + void userGuideReadingExampleWorks() throws Exception { + StoreHandle imageHandle = storeHandle(TESTDATA.resolve("ome/v0.5")); + MultiscaleImage image = MultiscaleImage.open(imageHandle); + + int scaleCount = image.getScaleLevelCount(); + List axisNames = image.getAxisNames(); + dev.zarr.zarrjava.experimental.ome.metadata.MultiscalesEntry entry0 = image.getMultiscaleNode(0); + + dev.zarr.zarrjava.core.Array s0 = image.openScaleLevel(0); + ucar.ma2.Array full = s0.read(); + ucar.ma2.Array subset = s0.read(new long[]{0, 0, 0, 0, 0}, new long[]{1, 1, 4, 8, 8}); + + assertTrue(scaleCount > 0); + assertFalse(axisNames.isEmpty()); + assertNotNull(entry0); + assertArrayEquals(new int[]{1, 2, 8, 16, 16}, full.getShape()); + assertArrayEquals(new int[]{1, 1, 4, 8, 8}, subset.getShape()); + + List labels = image.getLabels(); + assertEquals(Collections.singletonList("nuclei"), labels); + MultiscaleImage labelImage = image.openLabel(labels.get(0)); + assertEquals(Arrays.asList("z", "y", "x"), labelImage.getAxisNames()); + + StoreHandle plateHandle = storeHandle(TESTDATA.resolve("ome/v0.5_hcs")); + Plate plate = Plate.open(plateHandle); + Well well = plate.openWell("A/1"); + MultiscaleImage wellImage = well.openImage("0"); + assertEquals(Arrays.asList("t", "c", "z", "y", "x"), wellImage.getAxisNames()); + } + + @Test + void userGuideWritingExampleWorks() throws Exception { + StoreHandle out = storeHandle(TESTOUTPUT.resolve("ome_userguide_v05.zarr")); + MultiscalesEntry ms = new MultiscalesEntry( + Arrays.asList(new Axis("y", "space", "micrometer"), new Axis("x", "space", "micrometer")), + Collections.emptyList()); + dev.zarr.zarrjava.experimental.ome.v0_5.MultiscaleImage written = dev.zarr.zarrjava.experimental.ome.v0_5.MultiscaleImage.create(out, ms); + + written.createScaleLevel( + "s0", + Array.metadataBuilder() + .withShape(1024, 1024) + .withChunkShape(256, 256) + .withDataType(DataType.UINT16) + .build(), + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(1.0, 1.0)))); + written.createScaleLevel( + "s1", + Array.metadataBuilder() + .withShape(512, 512) + .withChunkShape(256, 256) + .withDataType(DataType.UINT16) + .build(), + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(2.0, 2.0)))); + + MultiscaleImage reopened = MultiscaleImage.open(out); + assertInstanceOf(dev.zarr.zarrjava.experimental.ome.v0_5.MultiscaleImage.class, reopened); + assertEquals(2, reopened.getScaleLevelCount()); + assertEquals("s0", reopened.getMultiscaleNode(0).datasets.get(0).path); + assertEquals("s1", reopened.getMultiscaleNode(0).datasets.get(1).path); + assertEquals(Arrays.asList(2.0, 2.0), + ((ScaleCoordinateTransformation) reopened.getMultiscaleNode(0).datasets.get(1).coordinateTransformations.get(0)).scale); + } +} diff --git a/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV04Test.java b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV04Test.java new file mode 100644 index 00000000..b08a7715 --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV04Test.java @@ -0,0 +1,244 @@ +package dev.zarr.zarrjava.experimental.ome; + +import dev.zarr.zarrjava.experimental.ome.metadata.Axis; +import dev.zarr.zarrjava.experimental.ome.metadata.transform.CoordinateTransformation; +import dev.zarr.zarrjava.experimental.ome.metadata.transform.ScaleCoordinateTransformation; +import dev.zarr.zarrjava.experimental.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.experimental.ome.metadata.OmeroMetadata; +import dev.zarr.zarrjava.experimental.ome.metadata.OmeroChannel; +import dev.zarr.zarrjava.experimental.ome.metadata.OmeroRdefs; +import dev.zarr.zarrjava.experimental.ome.metadata.OmeroWindow; +import dev.zarr.zarrjava.experimental.ome.metadata.PlateMetadata; +import dev.zarr.zarrjava.experimental.ome.metadata.NamedEntry; +import dev.zarr.zarrjava.experimental.ome.metadata.WellRef; +import dev.zarr.zarrjava.store.S3Store; +import dev.zarr.zarrjava.store.StoreHandle; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3Configuration; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class OmeZarrV04Test extends OmeZarrBaseTest { + private static final String UK1_S3_ENDPOINT = "https://uk1s3.embassy.ebi.ac.uk"; + + private static StoreHandle publicIrdV04Store(String key) { + S3Client client = S3Client.builder() + .endpointOverride(URI.create(UK1_S3_ENDPOINT)) + .region(Region.US_EAST_1) + .serviceConfiguration(S3Configuration.builder().pathStyleAccessEnabled(true).build()) + .credentialsProvider(AnonymousCredentialsProvider.create()) + .build(); + return new S3Store(client, "idr", "zarr/v0.4/idr0048A").resolve(key); + } + + @Override + StoreHandle imageStoreHandle() throws Exception { + return storeHandle(TESTDATA.resolve("ome/v0.4")); + } + + @Override + Class expectedConcreteClass() { + return dev.zarr.zarrjava.experimental.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 typedEntryHasVersion() throws Exception { + MultiscalesMetadataImage image = (MultiscalesMetadataImage) MultiscaleImage.open(imageStoreHandle()); + MultiscalesEntry entry = (MultiscalesEntry) image.getMultiscalesEntry(0); + assertEquals("0.4", entry.version); + } + + @Test + void typedEntryLevel0ScaleValues() 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, ((ScaleCoordinateTransformation) entry.datasets.get(0).coordinateTransformations.get(0)).scale); + } + + // ── omero + bioformats2raw ─────────────────────────────────────────────── + + @Test + void omeroChannels() throws Exception { + dev.zarr.zarrjava.experimental.ome.v0_4.MultiscaleImage image = + dev.zarr.zarrjava.experimental.ome.v0_4.MultiscaleImage.openMultiscaleImage(imageStoreHandle()); + OmeroMetadata omero = image.getOmeroMetadata(); + assertNotNull(omero); + assertNull(omero.id); + assertNull(omero.version); + assertNull(omero.name); + assertEquals(2, omero.channels.size()); + assertEquals("DAPI", omero.channels.get(0).label); + assertEquals("color", omero.rdefs.model); + } + + @Test + void bioformats2rawLayoutValue() throws Exception { + dev.zarr.zarrjava.experimental.ome.v0_4.MultiscaleImage image = + dev.zarr.zarrjava.experimental.ome.v0_4.MultiscaleImage.openMultiscaleImage(imageStoreHandle()); + assertEquals(Integer.valueOf(3), image.getBioformats2rawLayout()); + } + + // ── labels ─────────────────────────────────────────────────────────────── + + @Test + void labelsList() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + assertEquals(Collections.singletonList("nuclei"), image.getLabels()); + } + + @Test + void labelsOpenLabel() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + MultiscaleImage nuclei = image.openLabel("nuclei"); + assertInstanceOf(dev.zarr.zarrjava.experimental.ome.v0_4.MultiscaleImage.class, nuclei); + assertEquals(Arrays.asList("z", "y", "x"), nuclei.getAxisNames()); + } + + // ── HCS ────────────────────────────────────────────────────────────────── + + @Test + void hcsPlate() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.4_hcs"))); + assertInstanceOf(dev.zarr.zarrjava.experimental.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 hcsWellViaPlate() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.4_hcs"))); + Well well = plate.openWell("A/1"); + assertInstanceOf(dev.zarr.zarrjava.experimental.ome.v0_4.Well.class, well); + assertEquals(1, well.getWellMetadata().images.size()); + assertEquals("0", well.getWellMetadata().images.get(0).path); + } + + @Test + void hcsFullNavigation() 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.experimental.ome.v0_4.MultiscaleImage.class, fov); + assertEquals(Arrays.asList("t", "c", "z", "y", "x"), fov.getAxisNames()); + } + + // ── write round-trips ──────────────────────────────────────────────────── + + @Test + void writeCreateAndReopen() 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.experimental.ome.v0_4.MultiscaleImage created = + dev.zarr.zarrjava.experimental.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.experimental.ome.v0_4.MultiscaleImage.class, reopened); + assertEquals(Arrays.asList("z", "y"), reopened.getAxisNames()); + assertEquals(1, reopened.getScaleLevelCount()); + } + + @Test + void writeOmeroRoundTrip() 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.experimental.ome.v0_4.MultiscaleImage created = + dev.zarr.zarrjava.experimental.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)))); + + OmeroChannel ch = new OmeroChannel( + true, 1.0, "0000FF", "linear", false, "DAPI", + new OmeroWindow(0.0, 65535.0, 0.0, 1500.0)); + OmeroRdefs rd = new OmeroRdefs(0, 0, "color"); + created.setOmeroMetadata(new OmeroMetadata(1, "0.5", "example.tif", Collections.singletonList(ch), rd)); + + dev.zarr.zarrjava.experimental.ome.v0_4.MultiscaleImage reopened = + dev.zarr.zarrjava.experimental.ome.v0_4.MultiscaleImage.openMultiscaleImage(handle); + OmeroMetadata got = reopened.getOmeroMetadata(); + assertNotNull(got); + assertEquals(Integer.valueOf(1), got.id); + assertEquals("0.5", got.version); + assertEquals("example.tif", got.name); + assertEquals("DAPI", got.channels.get(0).label); + assertEquals("color", got.rdefs.model); + } + + @Test + void writePlateRoundTrip() 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.experimental.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); + } + + @Test + void openS3Idr0048AV04() throws Exception { + StoreHandle handle = publicIrdV04Store("9846152.zarr"); + MultiscaleImage image = MultiscaleImage.open(handle); + assertInstanceOf(dev.zarr.zarrjava.experimental.ome.v0_4.MultiscaleImage.class, image); + assertEquals(9, image.getScaleLevelCount()); + + MultiscalesMetadataImage typed = (MultiscalesMetadataImage) image; + MultiscalesEntry entry = (MultiscalesEntry) typed.getMultiscalesEntry(0); + assertEquals("0.4", entry.version); + assertEquals(Arrays.asList("c", "z", "y", "x"), image.getAxisNames()); + assertEquals(9, entry.datasets.size()); + for (int i = 0; i < entry.datasets.size(); i++) { + dev.zarr.zarrjava.experimental.ome.metadata.Dataset ds = entry.datasets.get(i); + assertEquals(Integer.toString(i), ds.path); + assertEquals("scale", ds.coordinateTransformations.get(0).type); + assertEquals(4, ((ScaleCoordinateTransformation) ds.coordinateTransformations.get(0)).scale.size()); + } + + dev.zarr.zarrjava.core.Array level0 = image.openScaleLevel(0); + long[] shape = level0.metadata().shape; + assertEquals(4, shape.length); + // IDR sample catalog (idr0048A/9846152): X=19120, Y=13350, Z=91, C=3. + assertArrayEquals(new long[]{3, 91, 13350, 19120}, shape); + long[] offset = new long[shape.length]; + long[] readShape = new long[shape.length]; + Arrays.fill(readShape, 1L); + assertEquals(1L, level0.read(offset, readShape).getSize()); + long[] shape1 = image.openScaleLevel(1).metadata().shape; + for (int i = 0; i < shape.length; i++) { + assertTrue(shape1[i] <= shape[i], "expected downsampled-or-equal shape at dim " + i); + } + } +} diff --git a/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV05Test.java b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV05Test.java new file mode 100644 index 00000000..fd820c88 --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV05Test.java @@ -0,0 +1,279 @@ +package dev.zarr.zarrjava.experimental.ome; + +import dev.zarr.zarrjava.core.Attributes; +import dev.zarr.zarrjava.experimental.ome.metadata.Axis; +import dev.zarr.zarrjava.experimental.ome.metadata.transform.CoordinateTransformation; +import dev.zarr.zarrjava.experimental.ome.metadata.transform.ScaleCoordinateTransformation; +import dev.zarr.zarrjava.experimental.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.experimental.ome.metadata.NamedEntry; +import dev.zarr.zarrjava.experimental.ome.metadata.OmeroMetadata; +import dev.zarr.zarrjava.experimental.ome.metadata.PlateMetadata; +import dev.zarr.zarrjava.experimental.ome.metadata.WellImage; +import dev.zarr.zarrjava.experimental.ome.metadata.WellMetadata; +import dev.zarr.zarrjava.experimental.ome.metadata.WellRef; +import dev.zarr.zarrjava.store.S3Store; +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 software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3Configuration; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class OmeZarrV05Test extends OmeZarrBaseTest { + private static final String UK1_S3_ENDPOINT = "https://uk1s3.embassy.ebi.ac.uk"; + + private static StoreHandle publicIrdV05Store(String prefix, String key) { + S3Client client = S3Client.builder() + .endpointOverride(URI.create(UK1_S3_ENDPOINT)) + .region(Region.US_EAST_1) + .serviceConfiguration(S3Configuration.builder().pathStyleAccessEnabled(true).build()) + .credentialsProvider(AnonymousCredentialsProvider.create()) + .build(); + return new S3Store(client, "idr", prefix).resolve(key); + } + + @Override + StoreHandle imageStoreHandle() throws Exception { + return storeHandle(TESTDATA.resolve("ome/v0.5")); + } + + @Override + Class expectedConcreteClass() { + return dev.zarr.zarrjava.experimental.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 typedEntryNoVersion() throws Exception { + MultiscalesMetadataImage image = (MultiscalesMetadataImage) MultiscaleImage.open(imageStoreHandle()); + MultiscalesEntry entry = (MultiscalesEntry) image.getMultiscalesEntry(0); + assertEquals("test_image", entry.name); + assertNull(entry.version); + } + + @Test + void typedEntryLevel0ScaleValues() 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, ((ScaleCoordinateTransformation) entry.datasets.get(0).coordinateTransformations.get(0)).scale); + } + + @Test + void openScaleLevelLevel1HasExpectedShape() 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 omeroChannels() throws Exception { + dev.zarr.zarrjava.experimental.ome.v0_5.MultiscaleImage image = + dev.zarr.zarrjava.experimental.ome.v0_5.MultiscaleImage.openMultiscaleImage(imageStoreHandle()); + OmeroMetadata omero = image.getOmeroMetadata(); + assertNotNull(omero); + assertNull(omero.id); + assertNull(omero.version); + assertNull(omero.name); + assertEquals(2, omero.channels.size()); + assertEquals("DAPI", omero.channels.get(0).label); + assertEquals("GFP", omero.channels.get(1).label); + assertEquals("color", omero.rdefs.model); + } + + @Test + void bioformats2rawLayoutValue() throws Exception { + dev.zarr.zarrjava.experimental.ome.v0_5.MultiscaleImage image = + dev.zarr.zarrjava.experimental.ome.v0_5.MultiscaleImage.openMultiscaleImage(imageStoreHandle()); + assertEquals(Integer.valueOf(3), image.getBioformats2rawLayout()); + } + + // ── labels ─────────────────────────────────────────────────────────────── + + @Test + void labelsList() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + assertEquals(Collections.singletonList("nuclei"), image.getLabels()); + } + + @Test + void labelsOpenLabel() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + MultiscaleImage nuclei = image.openLabel("nuclei"); + assertInstanceOf(dev.zarr.zarrjava.experimental.ome.v0_5.MultiscaleImage.class, nuclei); + assertEquals(Arrays.asList("z", "y", "x"), nuclei.getAxisNames()); + } + + // ── HCS ────────────────────────────────────────────────────────────────── + + @Test + void hcsPlate() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.5_hcs"))); + assertInstanceOf(dev.zarr.zarrjava.experimental.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 hcsWellViaPlate() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.5_hcs"))); + Well well = plate.openWell("A/1"); + assertInstanceOf(dev.zarr.zarrjava.experimental.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 hcsFullNavigation() 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.experimental.ome.v0_5.MultiscaleImage.class, fov); + assertEquals(Arrays.asList("t", "c", "z", "y", "x"), fov.getAxisNames()); + } + + // ── write round-trips ──────────────────────────────────────────────────── + + @Test + void writeCreateAndReopen() 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.experimental.ome.v0_5.MultiscaleImage created = + dev.zarr.zarrjava.experimental.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.experimental.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 writeLabelsRoundTrip() 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.experimental.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.experimental.ome.v0_5.MultiscaleImage nuclei = dev.zarr.zarrjava.experimental.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 writePlateRoundTrip() 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.experimental.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 writeHcsFullIntegration() throws Exception { + StoreHandle plateHandle = storeHandle(TESTOUTPUT.resolve("ome_v05_hcs_full")); + dev.zarr.zarrjava.experimental.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.experimental.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.experimental.ome.v0_5.MultiscaleImage fov = dev.zarr.zarrjava.experimental.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.experimental.ome.v0_5.MultiscaleImage.class, image); + assertEquals(Arrays.asList("z", "y"), image.getAxisNames()); + } + + @Test + void openS3Idr0083V05() throws Exception { + StoreHandle handle = publicIrdV05Store("zarr/v0.5/idr0083", "9822152.zarr"); + MultiscaleImage image = MultiscaleImage.open(handle); + assertInstanceOf(dev.zarr.zarrjava.experimental.ome.v0_5.MultiscaleImage.class, image); + assertEquals(11, image.getScaleLevelCount()); + + MultiscalesMetadataImage typed = (MultiscalesMetadataImage) image; + MultiscalesEntry entry = (MultiscalesEntry) typed.getMultiscalesEntry(0); + assertNotNull(entry); + assertNull(entry.version); // v0.5 keeps version at ome.version, not per multiscales entry + assertEquals(Arrays.asList("t", "c", "z", "y", "x"), image.getAxisNames()); + assertEquals(11, entry.datasets.size()); + for (int i = 0; i < entry.datasets.size(); i++) { + dev.zarr.zarrjava.experimental.ome.metadata.Dataset ds = entry.datasets.get(i); + assertEquals(Integer.toString(i), ds.path); + assertEquals("scale", ds.coordinateTransformations.get(0).type); + assertEquals(5, ((ScaleCoordinateTransformation) ds.coordinateTransformations.get(0)).scale.size()); + } + + dev.zarr.zarrjava.core.Array level0 = image.openScaleLevel(0); + long[] shape = level0.metadata().shape; + assertEquals(5, shape.length); + // IDR sample catalog (idr0083/9822152): X=144384, Y=93184, Z=1, C=1, T=1. + assertArrayEquals(new long[]{1, 1, 1, 93184, 144384}, shape); + long[] offset = new long[shape.length]; + long[] readShape = new long[shape.length]; + Arrays.fill(readShape, 1L); + assertEquals(1L, level0.read(offset, readShape).getSize()); + long[] shape1 = image.openScaleLevel(1).metadata().shape; + for (int i = 0; i < shape.length; i++) { + assertTrue(shape1[i] <= shape[i], "expected downsampled-or-equal shape at dim " + i); + } + } +} diff --git a/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV06Test.java b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV06Test.java new file mode 100644 index 00000000..0e23502d --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV06Test.java @@ -0,0 +1,329 @@ +package dev.zarr.zarrjava.experimental.ome; + +import dev.zarr.zarrjava.experimental.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.experimental.ome.metadata.NamedEntry; +import dev.zarr.zarrjava.experimental.ome.metadata.OmeroChannel; +import dev.zarr.zarrjava.experimental.ome.metadata.OmeroRdefs; +import dev.zarr.zarrjava.experimental.ome.metadata.OmeroWindow; +import dev.zarr.zarrjava.experimental.ome.metadata.PlateMetadata; +import dev.zarr.zarrjava.experimental.ome.metadata.WellImage; +import dev.zarr.zarrjava.experimental.ome.metadata.WellMetadata; +import dev.zarr.zarrjava.experimental.ome.metadata.WellRef; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.CoordinateSystem; +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 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"); + private static final java.nio.file.Path V06_HUMAN_ORGAN_ATLAS_OVERVIEW = + TESTDATA.resolve("ome/v0.6/examples/user_stories/human_organ_atlas.zarr/overview.ome.zarr"); + + @Override + StoreHandle imageStoreHandle() throws Exception { + return storeHandle(V06_2D); + } + + @Override + Class expectedConcreteClass() { + return dev.zarr.zarrjava.experimental.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 coordinateSystemsPresentInEntry() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage v06Image = + (dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage) image; + dev.zarr.zarrjava.experimental.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 datasetsPathsAndTransformations() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage v06Image = + (dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage) image; + dev.zarr.zarrjava.experimental.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.experimental.ome.v0_6.metadata.transform.CoordinateTransformation ct = + entry.datasets.get(0).coordinateTransformations.get(0); + assertEquals("scale", ct.type); + assertEquals("s0", ct.input); + assertEquals("physical", ct.output); + assertInstanceOf(dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ScaleCoordinateTransformation.class, ct); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ScaleCoordinateTransformation scaleCt = + (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ScaleCoordinateTransformation) ct; + assertNotNull(scaleCt.scale); + assertEquals(2, scaleCt.scale.size()); + assertEquals(6.0, scaleCt.scale.get(0), 1e-9); + assertEquals(4.0, scaleCt.scale.get(1), 1e-9); + } + + @Test + void unifiedInterfaceNodesAndPaths() 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 read3dAxesFromCoordinateSystems() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(V06_3D)); + assertInstanceOf(dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage.class, image); + + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage v06Image = + (dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage) image; + dev.zarr.zarrjava.experimental.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 read3dUnifiedAxisNames() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(V06_3D)); + List axisNames = image.getAxisNames(); + assertEquals(Arrays.asList("z", "y", "x"), axisNames); + } + + @Test + void readHumanOrganAtlasOverview() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(V06_HUMAN_ORGAN_ATLAS_OVERVIEW)); + assertInstanceOf(dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage.class, image); + + assertEquals(2, image.getScaleLevelCount()); + assertEquals(Arrays.asList("x", "y", "z"), image.getAxisNames()); + assertEquals(Arrays.asList("0", "1"), Arrays.asList( + image.getMultiscaleNode(0).datasets.get(0).path, + image.getMultiscaleNode(0).datasets.get(1).path)); + + dev.zarr.zarrjava.core.Array level0 = image.openScaleLevel(0); + assertArrayEquals(new long[]{8308, 8308, 9564}, level0.metadata().shape); + + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage typed = (dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage) image; + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry entry = typed.getMultiscalesEntry(0); + assertNotNull(entry.coordinateSystems); + assertEquals(2, entry.coordinateSystems.size()); + assertEquals("physical", entry.coordinateSystems.get(0).name); + assertEquals("anatomical", entry.coordinateSystems.get(1).name); + assertEquals("sequence", entry.datasets.get(0).coordinateTransformations.get(0).type); + } + + @Test + void readV06WithOmeroMetadata() throws Exception { + java.nio.file.Path out = TESTOUTPUT.resolve("ome_v06_with_omero"); + StoreHandle outHandle = storeHandle(out); + java.util.List axes = Arrays.asList( + new dev.zarr.zarrjava.experimental.ome.metadata.Axis("y", "space", "micrometer"), + new dev.zarr.zarrjava.experimental.ome.metadata.Axis("x", "space", "micrometer")); + java.util.List datasets = + java.util.Collections.singletonList(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.Dataset( + "s0", + java.util.Collections.singletonList( + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation.scale( + Arrays.asList(1.0, 1.0), "s0", "physical")))); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry ms = + new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry( + null, + datasets, + null, + java.util.Collections.singletonList(new CoordinateSystem("physical", axes)), + "multiscales", + null, + null); + + OmeroChannel ch = new OmeroChannel( + true, + 1.0, + "0000FF", + "linear", + false, + "LaminB1", + new OmeroWindow(0.0, 65535.0, 0.0, 1500.0)); + OmeroChannel ch2 = new OmeroChannel( + true, + 1.0, + "00FF00", + "linear", + false, + "Actin", + new OmeroWindow(10.0, 4096.0, 50.0, 2000.0)); + dev.zarr.zarrjava.experimental.ome.metadata.OmeroMetadata omero = + new dev.zarr.zarrjava.experimental.ome.metadata.OmeroMetadata( + 1, + "0.5", + "example.tif", + Arrays.asList(ch, ch2), + new OmeroRdefs(0, 0, "color")); + + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata ome = + new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata("0.6", java.util.Collections.singletonList(ms), omero); + dev.zarr.zarrjava.v3.Group.create(outHandle, dev.zarr.zarrjava.experimental.ome.OmeV3Group.omeAttributes(ome)); + dev.zarr.zarrjava.v3.Array.create( + outHandle.resolve("s0"), + dev.zarr.zarrjava.v3.Array.metadataBuilder() + .withShape(16, 16) + .withChunkShape(8, 8) + .withDataType(dev.zarr.zarrjava.v3.DataType.UINT16) + .build()); + + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage read = + (dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage) MultiscaleImage.open(outHandle); + assertNotNull(read.getOmeroMetadata()); + assertEquals(Integer.valueOf(1), read.getOmeroMetadata().id); + assertEquals("0.5", read.getOmeroMetadata().version); + assertEquals("example.tif", read.getOmeroMetadata().name); + assertEquals(2, read.getOmeroMetadata().channels.size()); + assertEquals("LaminB1", read.getOmeroMetadata().channels.get(0).label); + assertEquals("Actin", read.getOmeroMetadata().channels.get(1).label); + assertEquals("color", read.getOmeroMetadata().rdefs.model); + } + + @Test + void readV06WithBioformats2rawLayout() throws Exception { + java.nio.file.Path out = TESTOUTPUT.resolve("ome_v06_with_layout"); + StoreHandle outHandle = storeHandle(out); + + java.util.List axes = Arrays.asList( + new dev.zarr.zarrjava.experimental.ome.metadata.Axis("y", "space", "micrometer"), + new dev.zarr.zarrjava.experimental.ome.metadata.Axis("x", "space", "micrometer")); + java.util.List datasets = + java.util.Collections.singletonList(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.Dataset( + "s0", + java.util.Collections.singletonList( + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation.scale( + Arrays.asList(1.0, 1.0), "s0", "physical")))); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry ms = + new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry( + null, + datasets, + null, + java.util.Collections.singletonList(new CoordinateSystem("physical", axes)), + "multiscales", + null, + null); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata ome = + new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata( + "0.6", + java.util.Collections.singletonList(ms), + null, + 7, + null, + null, + null); + dev.zarr.zarrjava.v3.Group.create(outHandle, dev.zarr.zarrjava.experimental.ome.OmeV3Group.omeAttributes(ome)); + dev.zarr.zarrjava.v3.Array.create( + outHandle.resolve("s0"), + dev.zarr.zarrjava.v3.Array.metadataBuilder() + .withShape(16, 16) + .withChunkShape(8, 8) + .withDataType(dev.zarr.zarrjava.v3.DataType.UINT16) + .build()); + + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage read = + (dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage) MultiscaleImage.open(outHandle); + assertEquals(Integer.valueOf(7), read.getBioformats2rawLayout()); + } + + @Test + void hcsV06PlateWellDispatchAndNavigation() throws Exception { + StoreHandle plateHandle = storeHandle(TESTOUTPUT.resolve("ome_v06_hcs_full")); + dev.zarr.zarrjava.experimental.ome.v0_6.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.experimental.ome.v0_6.Well.createWell( + plateHandle.resolve("A/1"), + new WellMetadata(Collections.singletonList(new WellImage("0", null)))); + + List axes = Arrays.asList( + new dev.zarr.zarrjava.experimental.ome.metadata.Axis("y", "space", "micrometer"), + new dev.zarr.zarrjava.experimental.ome.metadata.Axis("x", "space", "micrometer")); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry ms = + new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry( + null, + Collections.emptyList(), + null, + Collections.singletonList(new CoordinateSystem("physical", axes)), + "multiscales", + null, + null); + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage fov = dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage.create( + plateHandle.resolve("A/1").resolve("0"), ms); + fov.createScaleLevel( + "s0", + dev.zarr.zarrjava.v3.Array.metadataBuilder() + .withShape(16, 16) + .withChunkShape(8, 8) + .withDataType(dev.zarr.zarrjava.v3.DataType.FLOAT32) + .build(), + Collections.singletonList(dev.zarr.zarrjava.experimental.ome.metadata.transform.CoordinateTransformation.scale( + Arrays.asList(1.0, 1.0)))); + + Plate plate = Plate.open(plateHandle); + assertInstanceOf(dev.zarr.zarrjava.experimental.ome.v0_6.Plate.class, plate); + assertEquals("A/1", plate.getPlateMetadata().wells.get(0).path); + Well well = plate.openWell("A/1"); + assertInstanceOf(dev.zarr.zarrjava.experimental.ome.v0_6.Well.class, well); + assertEquals("0", well.getWellMetadata().images.get(0).path); + MultiscaleImage image = well.openImage("0"); + assertInstanceOf(dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage.class, image); + assertEquals(Collections.singletonList("s0"), + Collections.singletonList(image.getMultiscaleNode(0).datasets.get(0).path)); + assertEquals(Arrays.asList("y", "x"), image.getAxisNames()); + } +} 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": "