diff --git a/README.md b/README.md index 1fa86f55..d489df35 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,9 @@ specification but may be suitable for other applications. Specifies the version of the [OME-Zarr specification](https://ngff.openmicroscopy.org/specifications/index.html) that should be used while writing Zarr. Current supported values are 0.4 and 0.5. +If `--ngff-version 0.5` is used, and the output file name ends with `.ozx`, then a single Zip file +will be written in accordance with [RFC-9](https://ngff.openmicroscopy.org/rfc/9/index.html). + #### --pyramid-name Specifies a subdirectory of the output directory where Zarr data should be written. diff --git a/src/main/java/com/glencoesoftware/bioformats2raw/Converter.java b/src/main/java/com/glencoesoftware/bioformats2raw/Converter.java index d506ee1f..5698ca8c 100644 --- a/src/main/java/com/glencoesoftware/bioformats2raw/Converter.java +++ b/src/main/java/com/glencoesoftware/bioformats2raw/Converter.java @@ -98,7 +98,9 @@ import dev.zarr.zarrjava.core.Attributes; import dev.zarr.zarrjava.core.Group; import dev.zarr.zarrjava.core.chunkkeyencoding.Separator; +import dev.zarr.zarrjava.store.BufferedZipStore; import dev.zarr.zarrjava.store.FilesystemStore; +import dev.zarr.zarrjava.store.Store; import dev.zarr.zarrjava.store.StoreHandle; import dev.zarr.zarrjava.utils.IndexingUtils; import dev.zarr.zarrjava.utils.Utils; @@ -120,6 +122,8 @@ public class Converter implements Callable { */ private static final String METADATA_FILE = "METADATA.ome.xml"; + private static final String ZIP_EXTENSION = ".ozx"; + /** * Minimum size of the largest XY dimension in the smallest resolution, * when calculating the number of resolutions to generate. @@ -175,7 +179,7 @@ public class Converter implements Callable { private volatile SupportedVersions ngffVersion = SupportedVersions.NGFF_04; private volatile boolean v3 = false; - private volatile FilesystemStore store = null; + private volatile Store store = null; private volatile int maxWorkers; private volatile int maxCachedTiles; @@ -1505,6 +1509,7 @@ public void convert() EnumerationException, ZarrException { checkOutputPaths(); + initializeStore(); Cache tileCache = CacheBuilder.newBuilder() .maximumSize(maxCachedTiles) @@ -1634,13 +1639,9 @@ public void convert() } String xml = service.getOMEXML(meta); - // write the original OME-XML to a file - Path metadataPath = getRootPath().resolve("OME"); - if (!Files.exists(metadataPath)) { - Files.createDirectories(metadataPath); - } - Path omexmlFile = metadataPath.resolve(METADATA_FILE); - Files.write(omexmlFile, xml.getBytes(Constants.ENCODING)); + // write the original OME-XML to the store + store.set(new String[] {"OME", METADATA_FILE}, + ByteBuffer.wrap(xml.getBytes(Constants.ENCODING))); } } catch (ServiceException se) { @@ -1702,6 +1703,9 @@ public void convert() LOGGER.error("Exception while closing reader", e); } }); + if (store != null && store instanceof BufferedZipStore) { + ((BufferedZipStore) store).close(); + } } // delete the memo file if it was saved and it's not explicitly kept @@ -1713,11 +1717,19 @@ public void convert() } } - private void writeZarrMetadata() throws IOException, ZarrException { + private void initializeStore() throws IOException, ZarrException { if (store == null) { - store = new FilesystemStore(getRootPath()); + Path rootPath = getRootPath(); + if (rootPath.toString().endsWith(ZIP_EXTENSION) && getV3()) { + store = new BufferedZipStore(rootPath, true); + } + else { + store = new FilesystemStore(rootPath); + } } + } + private void writeZarrMetadata() throws IOException, ZarrException { // fileset level metadata if (!noRootGroup) { Attributes attributes = new Attributes(); diff --git a/src/test/java/com/glencoesoftware/bioformats2raw/test/ZarrV3Test.java b/src/test/java/com/glencoesoftware/bioformats2raw/test/ZarrV3Test.java index 1976847d..2015bd6f 100644 --- a/src/test/java/com/glencoesoftware/bioformats2raw/test/ZarrV3Test.java +++ b/src/test/java/com/glencoesoftware/bioformats2raw/test/ZarrV3Test.java @@ -17,6 +17,7 @@ import com.scalableminds.bloscjava.Blosc; import dev.zarr.zarrjava.core.Attributes; +import dev.zarr.zarrjava.store.ReadOnlyZipStore; import dev.zarr.zarrjava.v3.Array; import dev.zarr.zarrjava.v3.ArrayMetadata; import dev.zarr.zarrjava.v3.DataType; @@ -118,6 +119,69 @@ public void testDefault() throws Exception { } } + /** + * Test basic v3 .ozx conversion. + */ + @Test + public void testDefaultZip() throws Exception { + input = fake(); + output = output.resolve("zipstore.ozx"); + assertTool("--ngff-version", getNGFFVersion()); + + assertFalse(Files.exists(output.resolve("zarr.json"))); + assertFalse( + Files.exists(output.resolve("OME").resolve("METADATA.ome.xml"))); + assertTrue(Files.exists(output)); + + ReadOnlyZipStore zipStore = new ReadOnlyZipStore(output); + + Group rootGroup = Group.open(zipStore.resolve()); + Attributes attrs = rootGroup.metadata().attributes; + Attributes omeAttrs = attrs.getAttributes("ome"); + assertEquals(getNGFFVersion(), omeAttrs.get("version")); + assertEquals(3, omeAttrs.get("bioformats2raw.layout")); + + Array array = Array.open(zipStore.resolve("0", "0")); + assertArrayEquals(new long[] {1, 1, 1, 512, 512}, array.metadata().shape); + + rootGroup = Group.open(zipStore.resolve("0")); + attrs = rootGroup.metadata().attributes; + omeAttrs = attrs.getAttributes("ome"); + assertEquals("0.5", omeAttrs.get("version")); + + List> multiscales = + (List>) omeAttrs.get("multiscales"); + assertEquals(1, multiscales.size()); + Map multiscale = multiscales.get(0); + checkMultiscale(multiscale, "image"); + + List> datasets = + (List>) multiscale.get("datasets"); + assertTrue(datasets.size() > 0); + assertEquals("0", datasets.get(0).get("path")); + + List> axes = + (List>) multiscale.get("axes"); + checkAxes(axes, "TCZYX", null); + + for (int r=0; r dataset = datasets.get(r); + List> transforms = + (List>) dataset.get("coordinateTransformations"); + assertEquals(1, transforms.size()); + Map scale = transforms.get(0); + assertEquals("scale", scale.get("type")); + List axisValues = (List) scale.get("scale"); + + assertEquals(5, axisValues.size()); + double factor = Math.pow(2, r); + // X and Y are the only dimensions that are downsampled, + // so the TCZ physical scales remain the same across all resolutions + assertEquals(axisValues, Arrays.asList(new Double[] { + 1.0, 1.0, 1.0, factor, factor})); + } + } + /** * Test HCS v3 conversion. */ @@ -231,8 +295,8 @@ public void testNoOMEOption() throws Exception { input = fake(); assertTool("--no-ome-meta-export", "--ngff-version", getNGFFVersion()); - assertTrue( - !Files.exists(output.resolve("OME").resolve("METADATA.ome.xml"))); + assertFalse( + Files.exists(output.resolve("OME").resolve("METADATA.ome.xml"))); } /**