Repository: InsightSoftwareConsortium/ITKIOOMEZarrNGFF (language: python)

Issue:
Add zip store support



Discussion / hints:
Ref:

https://github.com/InsightSoftwareConsortium/itk-wasm/blob/990082b1b0ebcbbe4467bb0222f64661b539dccb/CMakeLists.txt#L101-L147

Needs:

- In memory zip creation (no file io)

Fix patch:
diff --git a/include/itkOMEZarrNGFFImageIO.h b/include/itkOMEZarrNGFFImageIO.h
index e001e6f..0071ac4 100644
--- a/include/itkOMEZarrNGFFImageIO.h
+++ b/include/itkOMEZarrNGFFImageIO.h
@@ -70,6 +70,16 @@ class IOOMEZarrNGFF_EXPORT OMEZarrNGFFImageIO : public ImageIOBase
     return false;
   }
 
+  /** Special in-memory zip interface. An address needs to be provided in
+  * the "file name", using pattern address.memory, where address is a
+  * decimal representation of BufferInfo's address.
+  * Sample filename: "12341234.memory". */
+  using BufferInfo = struct
+  {
+    char * pointer;
+    size_t size;
+  };
+
   /*-------- This part of the interfaces deals with reading data. ----- */
 
   /** Determine the file type. Returns true if this ImageIO can read the
@@ -126,7 +136,7 @@ class IOOMEZarrNGFF_EXPORT OMEZarrNGFFImageIO : public ImageIOBase
 
   /** Read a single array and set relevant metadata. */
   void
-  ReadArrayMetadata(std::string path);
+  ReadArrayMetadata(std::string path, std::string driver);
 
   /** Sets the requested dimension, and initializes spatial metadata to identity. */
   void
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index c193ebe..ea53171 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -19,8 +19,8 @@ set(CMAKE_FOLDER TensorStore)
 include(FetchContent)
 FetchContent_Declare(
   tensorstore
-  GIT_REPOSITORY https://github.com/google/tensorstore
-  GIT_TAG        b040186346f8543f865e4205d9045b42514c9f92
+  GIT_REPOSITORY https://github.com/InsightSoftwareConsortium/tensorstore
+  GIT_TAG        master
 )
 FetchContent_MakeAvailable(tensorstore)
 unset(CMAKE_FOLDER)
diff --git a/src/itkOMEZarrNGFFImageIO.cxx b/src/itkOMEZarrNGFFImageIO.cxx
index 8cb9f64..72f68d3 100644
--- a/src/itkOMEZarrNGFFImageIO.cxx
+++ b/src/itkOMEZarrNGFFImageIO.cxx
@@ -29,7 +29,7 @@
 
 namespace itk
 {
-static tensorstore::Context tsContext = tensorstore::Context::Default();
+thread_local tensorstore::Context tsContext = tensorstore::Context::Default();
 
 OMEZarrNGFFImageIO::OMEZarrNGFFImageIO()
 {
@@ -37,11 +37,13 @@ OMEZarrNGFFImageIO::OMEZarrNGFFImageIO()
   this->AddSupportedWriteExtension(".zr2");
   this->AddSupportedWriteExtension(".zr3");
   this->AddSupportedWriteExtension(".zip");
+  this->AddSupportedWriteExtension(".memory");
 
   this->AddSupportedReadExtension(".zarr");
   this->AddSupportedReadExtension(".zr2");
   this->AddSupportedReadExtension(".zr3");
   this->AddSupportedReadExtension(".zip");
+  this->AddSupportedWriteExtension(".memory");
 
   this->Self::SetCompressor("");
   this->Self::SetMaximumCompressionLevel(9);
@@ -167,13 +169,28 @@ itkToTensorstoreComponentType(const IOComponentEnum itkComponentType)
   }
 }
 
+// Returns TensorStore KvStore driver name appropriate for this path.
+// Options are file, zip. TODO: http, gcs (GoogleCouldStorage), etc.
+std::string
+getKVstoreDriver(std::string path)
+{
+  if (path.size() < 4)
+  {
+    return "file";
+  }
+  if (path.substr(path.size() - 4) == ".zip" || path.substr(path.size() - 7) == ".memory")
+  {
+    return "zip";
+  }
+  return "file";
+}
 
 // JSON file path, e.g. "C:/Dev/ITKIOOMEZarrNGFF/v0.4/cyx.ome.zarr/.zgroup"
 void
-writeJson(nlohmann::json json, std::string path)
+writeJson(nlohmann::json json, std::string path, std::string driver)
 {
   auto attrs_store = tensorstore::Open<nlohmann::json, 0>(
-                       { { "driver", "json" }, { "kvstore", { { "driver", "file" }, { "path", path } } } },
+                       { { "driver", "json" }, { "kvstore", { { "driver", driver }, { "path", path } } } },
                        tsContext,
                        tensorstore::OpenMode::create | tensorstore::OpenMode::delete_existing,
                        tensorstore::ReadWriteMode::read_write)
@@ -191,11 +208,11 @@ writeJson(nlohmann::json json, std::string path)
 
 // JSON file path, e.g. "C:/Dev/ITKIOOMEZarrNGFF/v0.4/cyx.ome.zarr/.zattrs"
 bool
-jsonRead(std::string path, nlohmann::json & result)
+jsonRead(std::string path, nlohmann::json & result, std::string driver)
 {
   // Reading JSON via TensorStore allows it to be in the cloud
   auto attrs_store = tensorstore::Open<nlohmann::json, 0>(
-                       { { "driver", "json" }, { "kvstore", { { "driver", "file" }, { "path", path } } } })
+                       { { "driver", "json" }, { "kvstore", { { "driver", driver }, { "path", path } } } }, tsContext)
                        .result()
                        .value();
 
@@ -224,8 +241,9 @@ OMEZarrNGFFImageIO::CanReadFile(const char * filename)
 {
   try
   {
+    std::string    driver = getKVstoreDriver(filename);
     nlohmann::json json;
-    if (!jsonRead(std::string(filename) + "/.zgroup", json))
+    if (!jsonRead(std::string(filename) + "/.zgroup", json, driver))
     {
       return false;
     }
@@ -233,7 +251,7 @@ OMEZarrNGFFImageIO::CanReadFile(const char * filename)
     {
       return false; // unsupported zarr format
     }
-    if (!jsonRead(std::string(filename) + "/.zattrs", json))
+    if (!jsonRead(std::string(filename) + "/.zattrs", json, driver))
     {
       return false;
     }
@@ -265,10 +283,10 @@ OMEZarrNGFFImageIO::CanReadFile(const char * filename)
 thread_local tensorstore::TensorStore<> store; // initialized by ReadImageInformation/ReadArrayMetadata
 
 void
-OMEZarrNGFFImageIO::ReadArrayMetadata(std::string path)
+OMEZarrNGFFImageIO::ReadArrayMetadata(std::string path, std::string driver)
 {
   auto openFuture =
-    tensorstore::Open({ { "driver", "zarr" }, { "kvstore", { { "driver", "file" }, { "path", path } } } },
+    tensorstore::Open({ { "driver", "zarr" }, { "kvstore", { { "driver", driver }, { "path", path } } } },
                       tsContext,
                       tensorstore::OpenMode::open,
                       tensorstore::RecheckCached{ false },
@@ -345,10 +363,11 @@ void
 OMEZarrNGFFImageIO::ReadImageInformation()
 {
   nlohmann::json json;
+  std::string    driver = getKVstoreDriver(this->GetFileName());
 
-  bool status = jsonRead(std::string(this->GetFileName()) + "/.zgroup", json);
+  bool status = jsonRead(std::string(this->GetFileName()) + "/.zgroup", json, driver);
   assert(json.at("zarr_format").get<int>() == 2); // only v2 for now
-  status = jsonRead(std::string(this->GetFileName()) + "/.zattrs", json);
+  status = jsonRead(std::string(this->GetFileName()) + "/.zattrs", json, driver);
   json = json.at("multiscales")[0]; // multiscales must be present in OME-NGFF
   auto version = json.at("version").get<std::string>();
   if (version == "0.4" || version == "0.3" || version == "0.2" || version == "0.1")
@@ -400,7 +419,7 @@ OMEZarrNGFFImageIO::ReadImageInformation()
 
   // TODO: parse stuff from "metadata" object into metadata dictionary
 
-  ReadArrayMetadata(std::string(this->GetFileName()) + "/" + path);
+  ReadArrayMetadata(std::string(this->GetFileName()) + "/" + path, driver);
 }
 
 // We call tensorstoreToITKComponentType for each type.
@@ -454,9 +473,11 @@ OMEZarrNGFFImageIO::CanWriteFile(const char * name)
 void
 OMEZarrNGFFImageIO::WriteImageInformation()
 {
+  std::string driver = getKVstoreDriver(this->GetFileName());
+
   nlohmann::json group;
   group["zarr_format"] = 2;
-  writeJson(group, std::string(this->GetFileName()) + "/.zgroup");
+  writeJson(group, std::string(this->GetFileName()) + "/.zgroup", driver);
 
   unsigned dim = this->GetNumberOfDimensions();
 
@@ -490,7 +511,7 @@ OMEZarrNGFFImageIO::WriteImageInformation()
 
   nlohmann::json zattrs;
   zattrs["multiscales"] = multiscales;
-  writeJson(zattrs, std::string(this->GetFileName()) + "/.zattrs");
+  writeJson(zattrs, std::string(this->GetFileName()) + "/.zattrs", driver);
 }
 
 
@@ -522,7 +543,7 @@ OMEZarrNGFFImageIO::WriteImageInformation()
     auto openFuture = tensorstore::Open(                                                              \
       {                                                                                               \
         { "driver", "zarr" },                                                                         \
-        { "kvstore", { { "driver", "file" }, { "path", this->m_FileName + "/" + path } } },           \
+        { "kvstore", { { "driver", driver }, { "path", this->m_FileName + "/" + path } } },           \
         { "metadata",                                                                                 \
           {                                                                                           \
             { "compressor", { { "id", "blosc" } } },                                                  \
@@ -545,6 +566,7 @@ OMEZarrNGFFImageIO::WriteImageInformation()
 void
 OMEZarrNGFFImageIO::Write(const void * buffer)
 {
+  tsContext = tensorstore::Context::Default(); // start with clean zip handles
   this->WriteImageInformation();
 
   if (itkToTensorstoreComponentType(this->GetComponentType()) == tensorstore::dtype_v<void>)
@@ -575,6 +597,8 @@ OMEZarrNGFFImageIO::Write(const void * buffer)
     dtype = "<";
   }
 
+  std::string driver = getKVstoreDriver(this->GetFileName());
+
   if (false) // start with a plain "if"
   {}         // so element statements can all be "else if"
   ELEMENT_WRITE(int8_t)
@@ -591,6 +615,9 @@ OMEZarrNGFFImageIO::Write(const void * buffer)
   {
     itkExceptionMacro("Unsupported component type: " << GetComponentTypeAsString(this->GetComponentType()));
   }
+
+  // Create a new context to close the open zip handles
+  tsContext = tensorstore::Context::Default();
 }
 
 } // end namespace itk


Test patch:
diff --git a/.github/workflows/build-test-package.yml b/.github/workflows/build-test-package.yml
index 8ec8c53..1bf0296 100644
--- a/.github/workflows/build-test-package.yml
+++ b/.github/workflows/build-test-package.yml
@@ -5,7 +5,7 @@ on: [push,pull_request]
 env:
   itk-git-tag: "v5.3.0"
   itk-wheel-tag: "v5.3.0"
-  ITKPythonPackage-git-tag: "f91e8681dfe226eb1226b97dfed9bafff2e9b289"
+  ITKPythonPackage-git-tag: "ee05fd4fa5feedc32afbed32b84b9c9eb0518036"
 
 jobs:
   build-test-cxx:
@@ -36,10 +36,10 @@ jobs:
       run: |
         sudo xcode-select -s "/Applications/Xcode_13.2.1.app"
 
-    - name: Set up Python 3.8
+    - name: Set up Python 3.9
       uses: actions/setup-python@v4
       with:
-        python-version: "3.8"
+        python-version: "3.9"
 
     - uses: shogo82148/actions-setup-perl@v1
     - uses: ilammy/setup-nasm@v1
@@ -110,6 +110,12 @@ jobs:
           "note: prefix with the address-of operator to silence this warning"
           "warning C4722: 'riegeli::internal::UnreachableStream::~UnreachableStream': destructor never returns, potential memory leak"
           "warning C4722: 'riegeli::internal::CheckFailed::~CheckFailed': destructor never returns, potential memory leak"
+          "_deps[/\\].+-src[/\\]"  # disable all warnings from external projects
+          # the following warnings escape the above filter
+          "warning: Import validate/validate.proto is unused"
+          "warning:.+__builtin___memcpy_chk"
+          "warning:.+__builtin_memchr"
+          "note: source object allocated here"
           )
         set(dashboard_no_clean 1)
         set(ENV{CC} ${{ matrix.c-compiler }})
@@ -140,12 +146,12 @@ jobs:
         ctest --output-on-failure -j 2 -V -S dashboard.cmake
       shell: cmd
 
-  build-linux-python-packages:
+  build-linux-py:
     runs-on: ubuntu-22.04
     strategy:
       max-parallel: 2
       matrix:
-        python-version: ["37", "38", "39", "310", "311"]
+        python-version: ["38", "39", "310", "311"]
 
     steps:
     - uses: actions/checkout@v3
@@ -170,10 +176,12 @@ jobs:
         name: LinuxWheel${{ matrix.python-version }}
         path: dist/*.whl
 
-  build-macos-python-packages:
+  build-macos-py:
     runs-on: macos-12
     strategy:
       max-parallel: 2
+      matrix:
+        python-version: ["3.8", "3.9", "3.10", "3.11"]
 
     steps:
     - uses: actions/checkout@v3
@@ -197,9 +205,31 @@ jobs:
       run: |
         export ITK_PACKAGE_VERSION=${{ env.itk-wheel-tag }}
         export ITKPYTHONPACKAGE_TAG=${{ env.ITKPythonPackage-git-tag }}
-        export MACOSX_DEPLOYMENT_TARGET=10.14
+        export MACOSX_DEPLOYMENT_TARGET=10.15
         pwd
-        ./macpython-download-cache-and-build-module-wheels.sh
+        ./macpython-download-cache-and-build-module-wheels.sh "${{ matrix.python-version }}"
+
+    - name: Set up Python 3.10
+      uses: actions/setup-python@v4
+      with:
+        python-version: "3.10"
+
+    - name: Validate build output
+      shell: bash
+      run: |
+        python -m pip install twine
+        ls dist/
+
+        WHEEL_PATTERN="dist/itk_*macosx*.whl"
+        EXPECTED_WHEEL_COUNT=1
+
+        WHEEL_COUNT=`(ls ${WHEEL_PATTERN} | wc -l)`
+        if (( ${WHEEL_COUNT} != ${EXPECTED_WHEEL_COUNT} )); then
+          echo "Expected ${EXPECTED_WHEEL_COUNT} wheels but found ${WHEEL_COUNT}"
+          exit 1
+        fi
+
+        python -m twine check ${WHEEL_PATTERN}
 
     - name: Publish Python package as GitHub Artifact
       uses: actions/upload-artifact@v3
@@ -207,12 +237,12 @@ jobs:
         name: MacOSWheels
         path: dist/*.whl
 
-  build-windows-python-packages:
+  build-windows-py:
     runs-on: windows-2022
     strategy:
       max-parallel: 2
       matrix:
-        python-version-minor: ["7", "8", "9", "10", "11"]
+        python-version-minor: ["8", "9", "10", "11"]
 
     steps:
     - name: Get specific version of CMake, Ninja
@@ -282,9 +312,9 @@ jobs:
 
   publish-python-packages-to-pypi:
     needs:
-      - build-linux-python-packages
-      - build-macos-python-packages
-      - build-windows-python-packages
+      - build-linux-py
+      - build-macos-py
+      - build-windows-py
     runs-on: ubuntu-22.04
 
     steps:
diff --git a/test/Baseline/cthead1.zarr.zip.sha512 b/test/Baseline/cthead1.zarr.zip.sha512
new file mode 100644
index 0000000..d3728ef
--- /dev/null
+++ b/test/Baseline/cthead1.zarr.zip.sha512
@@ -0,0 +1,1 @@
+431c4f06def790a74e996d40dcffaa5c84cfaadc25601b3f9884729bc571a2177410b78ca6d2b1663130bc9a887f683fddd93f1b6c45a3774950e29a2fc9e697
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 5f8d4d0..6435669 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -2,11 +2,12 @@ itk_module_test()
 
 set(IOOMEZarrNGFFTests
   itkOMEZarrNGFFImageIOTest.cxx
+  itkOMEZarrNGFFInMemoryTest.cxx
   )
 
 CreateTestDriver(IOOMEZarrNGFF "${IOOMEZarrNGFF-Test_LIBRARIES}" "${IOOMEZarrNGFFTests}")
 
-# itk_add_test(NAME itkOMEZarrNGFFImageIOReadConvertTest
+# itk_add_test(NAME IOOMEZarrNGFF_SmallSpectra_nrrd
 #   COMMAND IOOMEZarrNGFFTestDriver
 #     --compare
 #       DATA{Baseline/SmallSpectra.nrrd}
@@ -16,7 +17,7 @@ CreateTestDriver(IOOMEZarrNGFF "${IOOMEZarrNGFF-Test_LIBRARIES}" "${IOOMEZarrNGF
 #       ${ITK_TEST_OUTPUT_DIR}/SmallSpectra.nrrd
 #   )
 # 
-# itk_add_test(NAME itkOMEZarrNGFFImageIOWriteConvertTest
+# itk_add_test(NAME IOOMEZarrNGFF_SmallSpectra_zip
 #   COMMAND IOOMEZarrNGFFTestDriver
 #     --compare
 #       DATA{Input/SmallSpectra.zip}
@@ -25,10 +26,9 @@ CreateTestDriver(IOOMEZarrNGFF "${IOOMEZarrNGFF-Test_LIBRARIES}" "${IOOMEZarrNGF
 #       DATA{Baseline/SmallSpectra.nrrd}
 #       ${ITK_TEST_OUTPUT_DIR}/SmallSpectra.zip
 #   )
-# set_tests_properties(itkOMEZarrNGFFImageIOWriteConvertTest PROPERTIES DEPENDS "itkOMEZarrNGFFImageIOReadConvertTest")
 
 # 2D test with identity metadata
-itk_add_test(NAME itkOMEZarrNGFFImageIOReadConvertTestCT1
+itk_add_test(NAME IOOMEZarrNGFF_cthead1_mha
   COMMAND IOOMEZarrNGFFTestDriver
     --compare
       DATA{Input/cthead1.mha}
@@ -39,7 +39,7 @@ itk_add_test(NAME itkOMEZarrNGFFImageIOReadConvertTestCT1
   )
 
 # 3D test image with non-identity spacing
-itk_add_test(NAME itkOMEZarrNGFFImageIOReadConvertTestDzZ_T1seg
+itk_add_test(NAME IOOMEZarrNGFF_DzZ_T1seg
   COMMAND IOOMEZarrNGFFTestDriver
     --compare
       DATA{Input/DzZ_Seeds.seg.nrrd}
@@ -50,7 +50,7 @@ itk_add_test(NAME itkOMEZarrNGFFImageIOReadConvertTestDzZ_T1seg
   )
 
 # 3D test image with non-identity origin and spacing
-itk_add_test(NAME itkOMEZarrNGFFImageIOReadConvertTest_OriginSpacing
+itk_add_test(NAME IOOMEZarrNGFF_OriginSpacing
   COMMAND IOOMEZarrNGFFTestDriver
     --compare
       DATA{Input/PV6.0_FLASH_FLOAT32.mha}
@@ -60,14 +60,32 @@ itk_add_test(NAME itkOMEZarrNGFFImageIOReadConvertTest_OriginSpacing
       ${ITK_TEST_OUTPUT_DIR}/PV6.0_FLASH_FLOAT32.zarr
   )
 
+itk_add_test(NAME IOOMEZarrNGFF_cthead1_zipWrite
+  COMMAND IOOMEZarrNGFFTestDriver
+    --compare
+      DATA{Input/cthead1.mha}
+      ${ITK_TEST_OUTPUT_DIR}/cthead1.zarr.zip
+    itkOMEZarrNGFFImageIOTest
+      DATA{Input/cthead1.mha}
+      ${ITK_TEST_OUTPUT_DIR}/cthead1.zarr.zip
+  )
 
-# itk_add_test(NAME itkOMEZarrNGFFImageIOWriteConvertTestCT1
-#   COMMAND IOOMEZarrNGFFTestDriver
-#     --compare
-#       DATA{Baseline/cthead1.zip}
-#       ${ITK_TEST_OUTPUT_DIR}/cthead1.mha
-#     itkOMEZarrNGFFImageIOTest
-#       DATA{Baseline/cthead1.zip}
-#       ${ITK_TEST_OUTPUT_DIR}/cthead1.mha
-#   )
-# set_tests_properties(itkOMEZarrNGFFImageIOWriteConvertTestCT1 PROPERTIES DEPENDS "itkOMEZarrNGFFImageIOReadConvertTestCT1")
+itk_add_test(NAME IOOMEZarrNGFF_cthead1_zipRead
+  COMMAND IOOMEZarrNGFFTestDriver
+    --compare
+      DATA{Baseline/cthead1.zarr.zip}
+      ${ITK_TEST_OUTPUT_DIR}/cthead1.mha
+    itkOMEZarrNGFFImageIOTest
+      DATA{Baseline/cthead1.zarr.zip}
+      ${ITK_TEST_OUTPUT_DIR}/cthead1.mha
+  )
+
+itk_add_test(NAME IOOMEZarrNGFF_inMemory_zip
+  COMMAND IOOMEZarrNGFFTestDriver
+    --compare
+      DATA{Baseline/cthead1.zarr.zip}
+      ${ITK_TEST_OUTPUT_DIR}/memory.zip
+    itkOMEZarrNGFFInMemoryTest
+      DATA{Baseline/cthead1.zarr.zip}
+      ${ITK_TEST_OUTPUT_DIR}/memory.zip
+  )
diff --git a/test/itkOMEZarrNGFFImageIOTest.cxx b/test/itkOMEZarrNGFFImageIOTest.cxx
index b0a69be..01a402f 100644
--- a/test/itkOMEZarrNGFFImageIOTest.cxx
+++ b/test/itkOMEZarrNGFFImageIOTest.cxx
@@ -24,7 +24,8 @@
 #include "itkTestingMacros.h"
 #include "itkImageIOBase.h"
 
-
+namespace
+{
 template <typename PixelType, unsigned Dimension>
 int
 doTest(const char * inputFileName, const char * outputFileName)
@@ -71,6 +72,7 @@ doTest(const char * inputFileName, const char * outputFileName)
   std::cout << "Test finished" << std::endl;
   return EXIT_SUCCESS;
 }
+} // namespace
 
 template <typename PixelType>
 int
diff --git a/test/itkOMEZarrNGFFInMemoryTest.cxx b/test/itkOMEZarrNGFFInMemoryTest.cxx
new file mode 100644
index 0000000..0c96b93
--- /dev/null
+++ b/test/itkOMEZarrNGFFInMemoryTest.cxx
@@ -0,0 +1,166 @@
+/*=========================================================================
+ *
+ *  Copyright NumFOCUS
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *         https://www.apache.org/licenses/LICENSE-2.0.txt
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *=========================================================================*/
+
+#include <fstream>
+#include <string>
+#include "itkImageFileReader.h"
+#include "itkImageFileWriter.h"
+#include "itkOMEZarrNGFFImageIO.h"
+#include "itkOMEZarrNGFFImageIOFactory.h"
+#include "itkTestingMacros.h"
+#include "itkImageIOBase.h"
+#include "itkTestingComparisonImageFilter.h"
+
+
+namespace
+{
+template <typename PixelType, unsigned Dimension>
+int
+doTest(const char * inputFileName, const char * outputFileName)
+{
+  using ImageType = itk::Image<PixelType, Dimension>;
+
+  using ReaderType = itk::ImageFileReader<ImageType>;
+  typename ReaderType::Pointer reader = ReaderType::New();
+  reader->SetFileName(inputFileName);
+
+  itk::OMEZarrNGFFImageIO::Pointer zarrIO = itk::OMEZarrNGFFImageIO::New();
+  reader->SetImageIO(zarrIO);
+  ITK_TRY_EXPECT_NO_EXCEPTION(reader->Update());
+  typename ImageType::Pointer image = reader->GetOutput();
+  image->DisconnectPipeline(); // this is our baseline image
+
+  // Adapted from https://stackoverflow.com/a/18816228/276168
+  std::ifstream   file(inputFileName, std::ios::binary | std::ios::ate);
+  std::streamsize size = file.tellg();
+  file.seekg(0, std::ios::beg);
+  std::vector<char> buffer(size);
+  if (!file.read(buffer.data(), size))
+  {
+    itkGenericExceptionMacro(<< "Could not read the input file directly: " << inputFileName);
+  }
+
+  itk::OMEZarrNGFFImageIO::BufferInfo bufferInfo{ buffer.data(), buffer.size() };
+
+  size_t      bufferInfoAddress = reinterpret_cast<size_t>(&bufferInfo);
+  std::string memAddress = std::to_string(bufferInfoAddress) + ".memory";
+
+  reader->SetFileName(memAddress);
+  ITK_TRY_EXPECT_NO_EXCEPTION(reader->Update());
+  typename ImageType::Pointer memImage = reader->GetOutput();
+
+  using CompareType = itk::Testing::ComparisonImageFilter<ImageType, ImageType>;
+  typename CompareType::Pointer comparer = CompareType::New();
+  comparer->SetValidInput(image);
+  comparer->SetTestInput(memImage);
+  comparer->Update();
+  if (comparer->GetNumberOfPixelsWithDifferences() > 0)
+  {
+    itkGenericExceptionMacro("The image read through memory is different from the one read through file");
+  }
+  buffer = std::vector<char>(); // deallocate it
+
+  bufferInfo = itk::OMEZarrNGFFImageIO::BufferInfo{};
+  using WriterType = itk::ImageFileWriter<ImageType>;
+  typename WriterType::Pointer writer = WriterType::New();
+  writer->SetInput(image);
+  writer->SetFileName(memAddress);
+  writer->SetImageIO(zarrIO);
+  ITK_TRY_EXPECT_NO_EXCEPTION(writer->Update());
+
+  std::ofstream oFile(outputFileName, std::ios::binary);
+  oFile.write(bufferInfo.pointer, bufferInfo.size);
+  free(bufferInfo.pointer);
+
+  std::cout << "Test finished" << std::endl;
+  return EXIT_SUCCESS;
+}
+
+template <typename PixelType>
+int
+doTest(const char * inputFileName, const char * outputFileName, unsigned dimension)
+{
+  switch (dimension)
+  {
+    case 2:
+      return doTest<PixelType, 2>(inputFileName, outputFileName);
+    case 3:
+      return doTest<PixelType, 3>(inputFileName, outputFileName);
+    case 4:
+      return doTest<PixelType, 4>(inputFileName, outputFileName);
+    case 5:
+      return doTest<PixelType, 5>(inputFileName, outputFileName);
+    default:
+      itkGenericExceptionMacro("Unsupported image dimension: " << dimension);
+      break;
+  }
+}
+} // namespace
+
+int
+itkOMEZarrNGFFInMemoryTest(int argc, char * argv[])
+{
+  if (argc < 3)
+  {
+    std::cerr << "Missing parameters." << std::endl;
+    std::cerr << "Usage: " << std::endl;
+    std::cerr << itkNameOfTestExecutableMacro(argv) << " Input.zip Output.zip" << std::endl;
+    return EXIT_FAILURE;
+  }
+  const char * inputFileName = argv[1];
+  const char * outputFileName = argv[2];
+
+  itk::OMEZarrNGFFImageIOFactory::RegisterOneFactory();
+
+  using ImageType = itk::Image<unsigned char, 3>;
+  auto imageReader = itk::ImageFileReader<ImageType>::New();
+  imageReader->SetFileName(inputFileName);
+  imageReader->SetImageIO(itk::OMEZarrNGFFImageIO::New()); // explicitly request zarr IO
+  imageReader->UpdateOutputInformation();
+
+  unsigned dim = imageReader->GetImageIO()->GetNumberOfDimensions();
+  auto     componentType = imageReader->GetImageIO()->GetComponentType();
+
+  switch (componentType)
+  {
+    case itk::ImageIOBase::IOComponentEnum::UCHAR:
+      return doTest<unsigned char>(inputFileName, outputFileName, dim);
+      break;
+    case itk::ImageIOBase::IOComponentEnum::CHAR:
+      return doTest<char>(inputFileName, outputFileName, dim);
+      break;
+    case itk::ImageIOBase::IOComponentEnum::USHORT:
+      return doTest<unsigned short>(inputFileName, outputFileName, dim);
+      break;
+    case itk::ImageIOBase::IOComponentEnum::SHORT:
+      return doTest<short>(inputFileName, outputFileName, dim);
+      break;
+    case itk::ImageIOBase::IOComponentEnum::FLOAT:
+      return doTest<float>(inputFileName, outputFileName, dim);
+      break;
+    case itk::ImageIOBase::IOComponentEnum::DOUBLE:
+      return doTest<double>(inputFileName, outputFileName, dim);
+      break;
+    default:
+      std::cerr << "Unsupported input image pixel component type: ";
+      std::cerr << itk::ImageIOBase::GetComponentTypeAsString(componentType);
+      std::cerr << std::endl;
+      return EXIT_FAILURE;
+      break;
+  }
+}