Repository: AllenCellModeling/aicspylibczi (language: python)

Issue:
Coordinates Overspecified Exception with certain Mosaic files
I discovered it when I tried opening a tilescan with _aicsimageio_. So, I created a library with any type of possible combinations of dimensions: M, S, T and Z with two channels.

Versions of packages I was using:

python: 3.8.5
aicspylibczi: 2.7.6
aicsimageio: 3.3.3
czifile: 2019.7.2

If I try to open tilescans (all of them with two channels) with only tiles, tiles and time, tiles and z, or tiles, time and z I get an error in line 57 of the file aicspylibczi/CziFile.py: __aicspylibczi.PylibCZI_CDimCoordinatesOverspecifiedException_

However, if I have positions (S) as an additional dimension I don't get this error, but I do get the same behaviour as already specified in the issue here: [AllenCellModeling/aicsimageio/issues/163].

I tried using CziFile in terminal, but also from scripts, starting with AICSImage or CziReader. In contrary, if I use the package czifile everything works smoothly and I can load any of the files.

I can provide the file library I created via E-Mail.

_Originally posted by @dimi-huer in https://github.com/AllenCellModeling/aicspylibczi/issues/62#issuecomment-745613504_


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/Publish.yml b/.github/workflows/Publish.yml
index 834c28d2..6d3a9923 100644
--- a/.github/workflows/Publish.yml
+++ b/.github/workflows/Publish.yml
@@ -10,7 +10,7 @@ jobs:
     strategy:
       matrix:
         python-version: [3.6, 3.7, 3.8, 3.9]
-        os: [windows-latest, macOS-latest]
+        os: [windows-2019, macOS-10.15, macOS-11.0]
 
     steps:
       - uses: actions/checkout@v1
diff --git a/.github/workflows/pythonbuild-master.yml b/.github/workflows/pythonbuild-master.yml
index fa3f111b..00db6b17 100644
--- a/.github/workflows/pythonbuild-master.yml
+++ b/.github/workflows/pythonbuild-master.yml
@@ -13,7 +13,7 @@ jobs:
       max-parallel: 6
       matrix:
         python-version: [3.6, 3.7, 3.8, 3.9]
-        os: [ubuntu-latest, windows-latest, macOS-latest]
+        os: [ubuntu-latest, windows-2019, macOS-10.15, macOS-11.0]
     steps:
       - uses: actions/checkout@v1
         with:
diff --git a/.github/workflows/pythonbuild.yml b/.github/workflows/pythonbuild.yml
index 1e14314f..76bfa010 100644
--- a/.github/workflows/pythonbuild.yml
+++ b/.github/workflows/pythonbuild.yml
@@ -7,7 +7,7 @@ jobs:
       max-parallel: 6
       matrix:
         python-version: [3.6, 3.7, 3.8, 3.9]
-        os: [ubuntu-latest, windows-latest, macOS-latest]
+        os: [ubuntu-latest, windows-2019, macos-10.15]
 
     steps:
       - uses: actions/checkout@v1
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7d1e32cc..a3da7972 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,5 +1,11 @@
 cmake_minimum_required(VERSION 2.8.12)
+
 set(CMAKE_MACOSX_RPATH 1)
+
+IF (APPLE)
+    set(CMAKE_OSX_DEPLOYMENT_TARGET "10.9" CACHE STRING "Minimum OS X deployment version" FORCE)
+ENDIF(APPLE)
+
 project(_aicspylibczi)
 # build problems with library dependencies if I go above C++14
 # namely the dependencies on the Eigen library and they have to update
@@ -20,9 +26,7 @@ ELSE()
     SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread -std=c++14 -fPIC -O0 -g -D_FILE_OFFSET_BITS=64 -fvisibility=hidden")
     SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D__ANSI__ -fPIC -D_FILE_OFFSET_BITS=64")
     add_compile_definitions(LINUXENV)
-    IF (APPLE)
-        set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "Minimum OS X deployment version" FORCE)
-    ENDIF(APPLE)
+
 ENDIF(WIN32)
 
 SET(pybind11_DIR pybind11)
diff --git a/_aicspylibczi/Reader.cpp b/_aicspylibczi/Reader.cpp
index f2e49574..1d44a79e 100644
--- a/_aicspylibczi/Reader.cpp
+++ b/_aicspylibczi/Reader.cpp
@@ -1,469 +1,481 @@
 #include <iterator>
+#include <set>
 #include <thread>
 #include <tuple>
-#include <set>
 #include <utility>
 
-#include "inc_libCZI.h"
-#include "Reader.h"
 #include "ImageFactory.h"
 #include "ImagesContainer.h"
-#include "exceptions.h"
+#include "Reader.h"
 #include "StreamImplLockingRead.h"
 #include "SubblockMetaVec.h"
 #include "Threadpool.h"
+#include "exceptions.h"
+#include "inc_libCZI.h"
 
 namespace pylibczi {
 
-  // this ISteam type needs to be threadsafe like StreamImplLockingRead the examples in libCZI are not threadsafe
-  Reader::Reader(std::shared_ptr<libCZI::IStream> istream_)
-      :m_czireader(new CCZIReader), m_specifyScene(true)
-  {
-      m_czireader->Open(std::move(istream_));
-      m_statistics = m_czireader->GetStatistics();
-      m_pixelType = libCZI::PixelType::Invalid; // get the pixeltype of the first readable subblock
+// this ISteam type needs to be threadsafe like StreamImplLockingRead the examples in libCZI are not threadsafe
+Reader::Reader(std::shared_ptr<libCZI::IStream> istream_)
+  : m_czireader(new CCZIReader)
+  , m_specifyScene(true)
+{
+  m_czireader->Open(std::move(istream_));
+  m_statistics = m_czireader->GetStatistics();
+  m_pixelType = libCZI::PixelType::Invalid; // get the pixeltype of the first readable subblock
 
-      checkSceneShapes();
-  }
+  checkSceneShapes();
+}
 
-  Reader::Reader(const wchar_t* file_name_)
-      :m_czireader(new CCZIReader), m_specifyScene(true)
-  {
-      std::shared_ptr<libCZI::IStream> sp;
-      sp = std::shared_ptr<libCZI::IStream>(new StreamImplLockingRead(file_name_));
-      m_czireader->Open(sp);
-      m_statistics = m_czireader->GetStatistics();
-      // create a reference for finding one or more subblock indices from a CDimCoordinate
-      checkSceneShapes();
-  }
+Reader::Reader(const wchar_t* file_name_)
+  : m_czireader(new CCZIReader)
+  , m_specifyScene(true)
+  , m_pixelType(libCZI::PixelType::Invalid)
+{
+  std::shared_ptr<libCZI::IStream> sp;
+  sp = std::shared_ptr<libCZI::IStream>(new StreamImplLockingRead(file_name_));
+  m_czireader->Open(sp);
+  m_statistics = m_czireader->GetStatistics();
+  // create a reference for finding one or more subblock indices from a CDimCoordinate
+  checkSceneShapes();
+}
 
-  void
-  Reader::checkSceneShapes()
-  {
-      auto dShapes = readDimsRange();
-      m_specifyScene = !consistentShape(dShapes);
-  }
+void
+Reader::checkSceneShapes()
+{
+  auto dShapes = readDimsRange();
+  m_specifyScene = !consistentShape(dShapes);
+}
 
-  std::string
-  Reader::readMeta()
-  {
-      auto mds = m_czireader->ReadMetadataSegment();
-      auto md = mds->CreateMetaFromMetadataSegment();
-      std::string xml = md->GetXml();
-      return xml;
-  }
+std::string
+Reader::readMeta()
+{
+  auto mds = m_czireader->ReadMetadataSegment();
+  auto md = mds->CreateMetaFromMetadataSegment();
+  std::string xml = md->GetXml();
+  return xml;
+}
 
-  bool Reader::isMosaic()
-  {
-      return (m_statistics.maxMindex>0);
-  }
+bool
+Reader::isMosaic() const
+{
+  return (m_statistics.maxMindex > 0);
+}
 
-  /// @brief get_shape_from_fp returns the Dimensions of a ZISRAW/CZI when provided a ICZIReader object
-  /// @param czi: a shared_ptr to an initialized CziReader object
-  /// @return A Python Dictionary as a PyObject*
-  Reader::DimsShape
-  Reader::readDimsRange()
-  {
-      DimsShape ans;
-      int sceneStart(0), sceneSize(0);
-      if (!m_statistics.dimBounds.TryGetInterval(libCZI::DimensionIndex::S, &sceneStart, &sceneSize)) {
-          ans.push_back(sceneShape(-1));
-          return ans;
-      }
-      for (int i = sceneStart; i<sceneStart+sceneSize; i++)
-          ans.push_back(sceneShape(i));
-      if (!m_specifyScene && ans.size()>1) {
-          ans[0][DimIndex::S].second = (*(ans.rbegin()))[DimIndex::S].second;
-          ans.resize(1); // remove the exta channels
-      }
-      return ans;
+/// @brief get_shape_from_fp returns the Dimensions of a ZISRAW/CZI when provided a ICZIReader object
+/// @param czi: a shared_ptr to an initialized CziReader object
+/// @return A Python Dictionary as a PyObject*
+Reader::DimsShape
+Reader::readDimsRange()
+{
+  DimsShape ans;
+  int sceneStart(0), sceneSize(0);
+  if (!m_statistics.dimBounds.TryGetInterval(libCZI::DimensionIndex::S, &sceneStart, &sceneSize)) {
+    ans.push_back(sceneShape(-1));
+    return ans;
   }
-
-  /// @brief get the size of each dimension
-  /// @return vector of the integer sizes for each dimension
-  std::vector<int>
-  Reader::dimSizes()
-  {
-      std::string dString = dimsString();
-      if (m_specifyScene) return std::vector<int>(dString.size(), -1);
-
-      DimIndexRangeMap tbl;
-      m_statistics.dimBounds.EnumValidDimensions([&](libCZI::DimensionIndex di_, int start_, int size_) -> bool {
-          tbl.emplace(dimensionIndexToDimIndex(di_), std::make_pair(start_, size_)); // changed from [start, end) to be [start, end]
-          return true;
-      }); // sorts the letters into ascending order by default { Z, C, T, S }
-      std::vector<int> ans(tbl.size());
-      // use rbegin and rend to change it to ascending order.
-      transform(tbl.rbegin(), tbl.rend(), ans.begin(), [](const auto& pr_) {
-          return pr_.second.second;
-      });
-
-      if (isMosaic()) { ans.push_back(m_statistics.maxMindex+1); } // The M-index is zero based
-
-      libCZI::IntRect sbsize = getSceneYXSize();
-      ans.push_back(sbsize.h);
-      ans.push_back(sbsize.w);
-
-      return ans;
+  for (int i = sceneStart; i < sceneStart + sceneSize; i++)
+    ans.push_back(sceneShape(i));
+  if (!m_specifyScene && ans.size() > 1) {
+    ans[0][DimIndex::S].second = (*(ans.rbegin()))[DimIndex::S].second;
+    ans.resize(1); // remove the exta channels
   }
+  return ans;
+}
 
-  std::tuple<bool, int, int>
-  Reader::scenesStartSize()
-  {
-      bool scenesDefined = false;
-      int sceneStart = 0;
-      int sceneSize = 0;
-      scenesDefined = m_statistics.dimBounds.TryGetInterval(libCZI::DimensionIndex::S, &sceneStart, &sceneSize);
-      return {scenesDefined, sceneStart, sceneSize};
-  }
+/// @brief get the size of each dimension
+/// @return vector of the integer sizes for each dimension
+std::vector<int>
+Reader::dimSizes()
+{
+  std::string dString = dimsString();
+  if (m_specifyScene)
+    return std::vector<int>(dString.size(), -1);
+
+  DimIndexRangeMap tbl;
+  m_statistics.dimBounds.EnumValidDimensions([&](libCZI::DimensionIndex di_, int start_, int size_) -> bool {
+    tbl.emplace(dimensionIndexToDimIndex(di_),
+                std::make_pair(start_, size_)); // changed from [start, end) to be [start, end]
+    return true;
+  }); // sorts the letters into ascending order by default { Z, C, T, S }
+  std::vector<int> ans(tbl.size());
+  // use rbegin and rend to change it to ascending order.
+  transform(tbl.rbegin(), tbl.rend(), ans.begin(), [](const auto& pr_) { return pr_.second.second; });
+
+  if (isMosaic()) {
+    ans.push_back(m_statistics.maxMindex + 1);
+  } // The M-index is zero based
+
+  libCZI::IntRect sbsize = getSceneYXSize();
+  ans.push_back(sbsize.h);
+  ans.push_back(sbsize.w);
+
+  return ans;
+}
 
-  bool
-  Reader::consistentShape(DimsShape& dShape_)
-  {
-      bool regularShape = true;
-      for (int i = 1; regularShape && i<dShape_.size(); i++) {
-          for (auto kVal: dShape_[i]) {
-              if (kVal.first==DimIndex::S) continue;
-              auto found = dShape_[0].find(kVal.first);
-              if (found==dShape_[0].end()) regularShape = false;
-              else regularShape &= (kVal==*found);
-          }
-      }
-      return regularShape;
-  }
+std::tuple<bool, int, int>
+Reader::scenesStartSize() const
+{
+  int sceneStart = 0;
+  int sceneSize = 0;
+  bool scenesDefined = m_statistics.dimBounds.TryGetInterval(libCZI::DimensionIndex::S, &sceneStart, &sceneSize);
+  return { scenesDefined, sceneStart, sceneSize };
+}
 
-  Reader::DimIndexRangeMap
-  Reader::sceneShape(int scene_index_)
-  {
-      bool sceneBool(false);
-      int sceneStart(0), sceneSize(0);
-      tie(sceneBool, sceneStart, sceneSize) = scenesStartSize();
-
-      DimIndexRangeMap tbl;
-
-      if (!isMosaic() && (!sceneBool || sceneSize==1)) {
-          // scenes are not defined so the dimBounds define the shape
-          m_statistics.dimBounds.EnumValidDimensions([&tbl](libCZI::DimensionIndex di_, int start_, int size_) -> bool {
-              tbl.emplace(dimensionIndexToDimIndex(di_),
-                  std::make_pair(start_, size_+start_)); // changed from [start, end) to be [start, end]
-              return true;
-          });
-          auto xySize = getSceneYXSize();
-          tbl.emplace(charToDimIndex('Y'), std::make_pair(0, xySize.h));
-          tbl.emplace(charToDimIndex('X'), std::make_pair(0, xySize.w));
-      }
-      else {
-          if (scene_index_<sceneStart || sceneStart+sceneSize<=scene_index_) {
-              std::stringstream ss;
-              ss << "Scene index " << scene_index_ << " ∉ " << "[" << sceneStart << ", " << sceneStart+sceneSize << ")";
-              throw CDimCoordinatesOverspecifiedException(ss.str().c_str());
-          }
-          libCZI::CDimCoordinate cDim{{libCZI::DimensionIndex::S, scene_index_}};
-          SubblockSortable sceneToFind(&cDim, -1, false);
-          SubblockIndexVec matches = getMatches(sceneToFind);
-
-          // get the condensed set of values
-          std::map<DimIndex, set<int> > definedDims;
-          for (auto x : matches) {
-              x.first.coordinatePtr()->EnumValidDimensions([&definedDims](libCZI::DimensionIndex di_, int val_) -> bool {
-                  definedDims[dimensionIndexToDimIndex(di_)].emplace(val_);
-                  return true;
-              });
-              if (isMosaic()) definedDims[DimIndex::M].emplace(x.first.mIndex());
-          }
-          for (auto x : definedDims)
-            tbl.emplace(
-              x.first,
-              std::make_pair(*x.second.begin(), *x.second.rbegin() + 1));
-
-          auto xySize = getSceneYXSize(scene_index_);
-          tbl.emplace(DimIndex::Y, std::make_pair(0, xySize.h));
-          tbl.emplace(DimIndex::X, std::make_pair(0, xySize.w));
-      }
-      return tbl;
+bool
+Reader::consistentShape(DimsShape& dShape_)
+{
+  bool regularShape = true;
+  for (int i = 1; regularShape && i < dShape_.size(); i++) {
+    for (auto kVal : dShape_[i]) {
+      if (kVal.first == DimIndex::S)
+        continue;
+      auto found = dShape_[0].find(kVal.first);
+      if (found == dShape_[0].end())
+        regularShape = false;
+      else
+        regularShape &= (kVal == *found);
+    }
   }
+  return regularShape;
+}
 
-  std::vector<libCZI::IntRect>
-  Reader::getAllSceneYXSize(int scene_index_, bool get_all_matches_)
-  {
-    std::vector<libCZI::IntRect> result;
-    bool hasScene = m_statistics.dimBounds.IsValid(libCZI::DimensionIndex::S);
-    if (!isMosaic() && hasScene) {
-      int sStart(0), sSize(0);
-      m_statistics.dimBounds.TryGetInterval(
-        libCZI::DimensionIndex::S, &sStart, &sSize);
-      if (scene_index_ >= sStart && (sStart + sSize - 1) >= scene_index_ &&
-          !m_statistics.sceneBoundingBoxes.empty()) {
-        result.emplace_back(
-          m_statistics.sceneBoundingBoxes[scene_index_].boundingBoxLayer0);
-        return result;
-      }
+Reader::DimIndexRangeMap
+Reader::sceneShape(int scene_index_)
+{
+  bool sceneBool(false);
+  int sceneStart(0), sceneSize(0);
+  tie(sceneBool, sceneStart, sceneSize) = scenesStartSize();
+
+  DimIndexRangeMap tbl;
+
+  if ((!sceneBool || sceneSize == 1)) {
+    // scenes are not defined so the dimBounds define the shape
+    m_statistics.dimBounds.EnumValidDimensions([&tbl](libCZI::DimensionIndex di_, int start_, int size_) -> bool {
+      tbl.emplace(dimensionIndexToDimIndex(di_),
+                  std::make_pair(start_, size_ + start_)); // changed from [start, end) to be [start, end]
+      return true;
+    });
+    if (isMosaic()) {
+      tbl.emplace(charToDimIndex('M'), std::make_pair(m_statistics.minMindex, m_statistics.maxMindex + 1));
     }
+    auto xySize = getSceneYXSize();
+    tbl.emplace(charToDimIndex('Y'), std::make_pair(0, xySize.h));
+    tbl.emplace(charToDimIndex('X'), std::make_pair(0, xySize.w));
+  } else {
+    if (scene_index_ < sceneStart || sceneStart + sceneSize <= scene_index_) {
+      std::stringstream ss;
+      ss << "Scene index " << scene_index_ << " ∉ " // x ∉ Y means x is not an element of Y, or x is not in Y
+         << "[" << sceneStart << ", " << sceneStart + sceneSize << ")";
+      throw CDimCoordinatesOverspecifiedException(ss.str());
+    }
+    libCZI::CDimCoordinate cDim{ { libCZI::DimensionIndex::S, scene_index_ } };
+    SubblockSortable sceneToFind(&cDim, -1, false);
+    SubblockIndexVec matches = getMatches(sceneToFind);
 
-    libCZI::CDimCoordinate scene_coord; // default constructor
-    if(hasScene && scene_index_ >= 0)
-      scene_coord = libCZI::CDimCoordinate({{libCZI::DimensionIndex::S, scene_index_}});
-    SubblockSortable subblocksToFind(&scene_coord, -1, false);
-    SubblockIndexVec matches = getMatches(subblocksToFind);
-
-    int embeddedSceneIndex = 0;
+    // get the condensed set of values
+    std::map<DimIndex, set<int>> definedDims;
     for (const auto& x : matches) {
-      if (hasScene) {
-        x.first.coordinatePtr()->TryGetPosition(libCZI::DimensionIndex::S,
-                                                &embeddedSceneIndex);
-        if (embeddedSceneIndex == scene_index_) {
-          int index = x.second;
-          auto subblk = m_czireader->ReadSubBlock(index);
-          auto sbkInfo = subblk->GetSubBlockInfo();
-          result.emplace_back(sbkInfo.logicalRect);
-          if (!get_all_matches_)
-            return result;
-        }
-      }
+      x.first.coordinatePtr()->EnumValidDimensions([&definedDims](libCZI::DimensionIndex di_, int val_) -> bool {
+        definedDims[dimensionIndexToDimIndex(di_)].emplace(val_);
+        return true;
+      });
+      if (isMosaic())
+        definedDims[DimIndex::M].emplace(x.first.mIndex());
     }
-    if (hasScene && !result.empty())
-      return result;
+    for (auto x : definedDims)
+      tbl.emplace(x.first, std::make_pair(*x.second.begin(), *x.second.rbegin() + 1));
 
-    m_czireader->EnumerateSubBlocks(
-      [&result, get_all_matches_](int index,
-                                  const libCZI::SubBlockInfo& info) -> bool {
-        if (!isPyramid0(info))
-          return true;
+    auto xySize = getSceneYXSize(scene_index_);
+    tbl.emplace(DimIndex::Y, std::make_pair(0, xySize.h));
+    tbl.emplace(DimIndex::X, std::make_pair(0, xySize.w));
+  }
+  return tbl;
+}
 
-        result.emplace_back(info.logicalRect);
-        return get_all_matches_;
-      });
-    return result;
+std::vector<libCZI::IntRect>
+Reader::getAllSceneYXSize(int scene_index_, bool get_all_matches_)
+{
+  std::vector<libCZI::IntRect> result;
+  bool hasScene = m_statistics.dimBounds.IsValid(libCZI::DimensionIndex::S);
+  if (!isMosaic() && hasScene) {
+    int sStart(0), sSize(0);
+    m_statistics.dimBounds.TryGetInterval(libCZI::DimensionIndex::S, &sStart, &sSize);
+    if (scene_index_ >= sStart && (sStart + sSize - 1) >= scene_index_ && !m_statistics.sceneBoundingBoxes.empty()) {
+      result.emplace_back(m_statistics.sceneBoundingBoxes[scene_index_].boundingBoxLayer0);
+      return result;
+    }
   }
 
-  libCZI::PixelType
-  Reader::getFirstPixelType(void)
-  {
-      libCZI::PixelType pixelType = libCZI::PixelType::Invalid;
-      m_czireader->EnumerateSubBlocks([&pixelType](int index_, const libCZI::SubBlockInfo& info_) -> bool {
-          if (!isPyramid0(info_)) return true;
-          pixelType = info_.pixelType;
-          return false;
-      });
-      return pixelType;
+  libCZI::CDimCoordinate scene_coord; // default constructor
+  if (hasScene && scene_index_ >= 0)
+    scene_coord = libCZI::CDimCoordinate({ { libCZI::DimensionIndex::S, scene_index_ } });
+  SubblockSortable subblocksToFind(&scene_coord, -1, false);
+  SubblockIndexVec matches = getMatches(subblocksToFind);
+
+  int embeddedSceneIndex = 0;
+  for (const auto& x : matches) {
+    if (hasScene) {
+      x.first.coordinatePtr()->TryGetPosition(libCZI::DimensionIndex::S, &embeddedSceneIndex);
+      if (embeddedSceneIndex == scene_index_) {
+        int index = x.second;
+        auto subblk = m_czireader->ReadSubBlock(index);
+        auto sbkInfo = subblk->GetSubBlockInfo();
+        result.emplace_back(sbkInfo.logicalRect);
+        if (!get_all_matches_)
+          return result;
+      }
+    }
   }
+  if (hasScene && !result.empty())
+    return result;
 
-  /// @brief get the Dimensions in the order they appear in
-  /// @return a string containing the Dimensions for the image data object
-  std::string
-  Reader::dimsString()
-  {
-      std::string ans;
-      m_statistics.dimBounds.EnumValidDimensions([&ans](libCZI::DimensionIndex di_, int start_, int size_) -> bool {
-          ans += Reader::dimToChar(di_);
-          return true;
-      });
+  m_czireader->EnumerateSubBlocks([&result, get_all_matches_](int index, const libCZI::SubBlockInfo& info) -> bool {
+    if (!isPyramid0(info))
+      return true;
 
-      std::sort(ans.begin(), ans.end(), [](const char a_, const char b_) {
-          return libCZI::Utils::CharToDimension(a_)>libCZI::Utils::CharToDimension(b_);
-      });
+    result.emplace_back(info.logicalRect);
+    return get_all_matches_;
+  });
+  return result;
+}
+
+libCZI::PixelType
+Reader::getFirstPixelType()
+{
+  libCZI::PixelType pixelType = libCZI::PixelType::Invalid;
+  m_czireader->EnumerateSubBlocks([&pixelType](int index_, const libCZI::SubBlockInfo& info_) -> bool {
+    if (!isPyramid0(info_))
+      return true;
+    pixelType = info_.pixelType;
+    return false;
+  });
+  return pixelType;
+}
 
-      if (isMosaic()) ans += "M";
+/// @brief get the Dimensions in the order they appear in
+/// @return a string containing the Dimensions for the image data object
+std::string
+Reader::dimsString() const
+{
+  std::string ans;
+  m_statistics.dimBounds.EnumValidDimensions([&ans](libCZI::DimensionIndex di_, int start_, int size_) -> bool {
+    ans += Reader::dimToChar(di_);
+    return true;
+  });
+
+  std::sort(ans.begin(), ans.end(), [](const char a_, const char b_) {
+    return libCZI::Utils::CharToDimension(a_) > libCZI::Utils::CharToDimension(b_);
+  });
+
+  if (isMosaic())
+    ans += "M";
+
+  ans += "YX";
+  return ans;
+}
 
-      ans += "YX";
-      return ans;
+std::pair<ImagesContainerBase::ImagesContainerBasePtr, std::vector<std::pair<char, size_t>>>
+Reader::readSelected(libCZI::CDimCoordinate& plane_coord_, int index_m_, unsigned int cores_)
+{
+  int pos;
+  if (m_specifyScene && !plane_coord_.TryGetPosition(libCZI::DimensionIndex::S, &pos)) {
+    throw ImageAccessUnderspecifiedException(0,
+                                             1,
+                                             "Scenes must be read individually "
+                                             "for this file, scenes have inconsistent YX shapes!");
   }
-
-  std::pair<ImagesContainerBase::ImagesContainerBasePtr, std::vector<std::pair<char, size_t> > >
-  Reader::readSelected(libCZI::CDimCoordinate& plane_coord_, int index_m_, unsigned int cores_)
+  SubblockSortable subblocksToFind(&plane_coord_, index_m_, isMosaic());
+  // SubblockIndexVec is actually a set this is crucial to preserve the image order
+  SubblockIndexVec matches = getMatches(subblocksToFind);
+  m_pixelType = matches.begin()->first.pixelType();
+  size_t bgrScaling = ImageFactory::numberOfChannels(m_pixelType);
+
+  libCZI::IntRect w_by_h = getSceneYXSize();
+  size_t n_of_pixels = matches.size() * w_by_h.w * w_by_h.h * bgrScaling;
+  ImageFactory imageFactory(m_pixelType, n_of_pixels);
+
+  imageFactory.setMosaic(isMosaic());
+  size_t memOffset = 0;
+
+  /*
+   * On windows the python code says there are far more cores than the C++ code. For that reason we have
+   * implemented this in such a way that it rescales to a workable value when necessary.
+   */
+  unsigned int min_cores = 1; // for the case when hardware_concurency fails and returns 0
+  unsigned int number_of_cores = std::max(min_cores, std::min(cores_, std::thread::hardware_concurrency() - 1));
   {
-      int pos;
-      if (m_specifyScene && !plane_coord_.TryGetPosition(libCZI::DimensionIndex::S, &pos)) {
-          throw ImageAccessUnderspecifiedException(0, 1, "Scenes must be read individually "
-                                                         "for this file, scenes have inconsistent YX shapes!");
-      }
-      SubblockSortable subblocksToFind(&plane_coord_, index_m_, isMosaic());
-      // SubblockIndexVec is actually a set this is crucial to preserve the image order
-      SubblockIndexVec matches = getMatches(subblocksToFind);
-      m_pixelType = matches.begin()->first.pixelType();
-      size_t bgrScaling = ImageFactory::numberOfChannels(m_pixelType);
-
-      libCZI::IntRect w_by_h = getSceneYXSize();
-      size_t n_of_pixels = matches.size()*w_by_h.w*w_by_h.h*bgrScaling;
-      ImageFactory imageFactory(m_pixelType, n_of_pixels);
-
-      imageFactory.setMosaic(isMosaic());
-      size_t memOffset = 0;
-
-      /*
-       * On windows the python code says there are far more cores than the C++ code. For that reason we have
-       * implemented this in such a way that it rescales to a workable value when necessary.
-       */
-      unsigned int min_cores = 1; // for the case when hardware_concurency fails and returns 0
-      unsigned int number_of_cores = std::max(min_cores, std::min(cores_, std::thread::hardware_concurrency()-1));
-      {
-          std::vector< std::future<bool> > jobs;
-          Tasks tasks;
-          for_each(matches.begin(), matches.end(), [&](const SubblockIndexVec::value_type match_) {
-              int sb_index = match_.second;
-
-              jobs.push_back(tasks.queue([this, &imageFactory, sb_index, memOffset]()->bool {
-                  auto subblock = m_czireader->ReadSubBlock(sb_index);
-                  const libCZI::SubBlockInfo& info = subblock->GetSubBlockInfo();
-                  if (m_pixelType!=info.pixelType)
-                      throw PixelTypeException(info.pixelType, "Selected subblocks have inconsistent PixelTypes."
-                                                               " You must select subblocks with consistent PixelTypes.");
-                  // the throw above covers a possible edge case which the file has multiple pixel types. If this is
-                  // the case the exception is intentionally sent back to the user to deal with as they will have to
-                  // select subblocks with consistent pixelType. There's no way to know which of the conflicting
-                  // types they wanted.
-
-                  auto bitmap = subblock->CreateBitmap();
-                  libCZI::IntSize size = bitmap->GetSize();
-
-                  imageFactory.constructImage(bitmap, size,
-                      &info.coordinate, info.logicalRect, memOffset, info.mIndex);
-                  return true;
-              }));
-
-              memOffset += bgrScaling*w_by_h.w*w_by_h.h;
-          });
-          tasks.start(number_of_cores);
-          for_each( jobs.begin(), jobs.end(), [](auto &x){ x.get(); });
-      }
+    std::vector<std::future<bool>> jobs;
+    Tasks tasks;
+    for_each(matches.begin(), matches.end(), [&](const SubblockIndexVec::value_type& match_) {
+      int sb_index = match_.second;
+
+      jobs.push_back(tasks.queue([this, &imageFactory, sb_index, memOffset]() -> bool {
+        auto subblock = m_czireader->ReadSubBlock(sb_index);
+        const libCZI::SubBlockInfo& info = subblock->GetSubBlockInfo();
+        if (m_pixelType != info.pixelType)
+          throw PixelTypeException(info.pixelType,
+                                   "Selected subblocks have inconsistent PixelTypes."
+                                   " You must select subblocks with consistent PixelTypes.");
+        // the throw above covers a possible edge case which the file has multiple pixel types. If this is
+        // the case the exception is intentionally sent back to the user to deal with as they will have to
+        // select subblocks with consistent pixelType. There's no way to know which of the conflicting
+        // types they wanted.
+
+        auto bitmap = subblock->CreateBitmap();
+        libCZI::IntSize size = bitmap->GetSize();
+
+        imageFactory.constructImage(bitmap, size, &info.coordinate, info.logicalRect, memOffset, info.mIndex);
+        return true;
+      }));
+
+      memOffset += bgrScaling * w_by_h.w * w_by_h.h;
+    });
+    tasks.start(number_of_cores);
+    for_each(jobs.begin(), jobs.end(), [](auto& x) { x.get(); });
+  }
 
-      if (imageFactory.numberOfImages()==0) {
-          throw pylibczi::CdimSelectionZeroImagesException(plane_coord_, m_statistics.dimBounds, "No pyramid0 selectable subblocks.");
-      }
-      auto charShape = imageFactory.getFixedShape();
-      ImageVector &imageVector = imageFactory.images();
-      imageVector.sort(); // this sort is mostly cosmetic, putting the imgs in memory order.
-      return std::make_pair(imageFactory.transferMemoryContainer(), charShape);
+  if (imageFactory.numberOfImages() == 0) {
+    throw pylibczi::CdimSelectionZeroImagesException(
+      plane_coord_, m_statistics.dimBounds, "No pyramid0 selectable subblocks.");
   }
+  auto charShape = imageFactory.getFixedShape();
+  ImageVector& imageVector = imageFactory.images();
+  imageVector.sort(); // this sort is mostly cosmetic, putting the imgs in memory order.
+  return std::make_pair(imageFactory.transferMemoryContainer(), charShape);
+}
 
-  SubblockMetaVec
-  Reader::readSubblockMeta(libCZI::CDimCoordinate& plane_coord_, int index_m_)
-  {
-      SubblockMetaVec metaSubblocks;
-      metaSubblocks.setMosaic(isMosaic());
-
-      SubblockSortable subBlockToFind(&plane_coord_, index_m_, isMosaic());
-      SubblockIndexVec matches = getMatches(subBlockToFind);
-
-      for_each(matches.begin(), matches.end(), [&](const SubblockIndexVec::value_type& match_) {
-          size_t metaSize = 0;
-          auto subblock = m_czireader->ReadSubBlock(match_.second);
-          auto sharedPtrString = subblock->GetRawData(libCZI::ISubBlock::Metadata, &metaSize);
-          metaSubblocks.emplace_back(match_.first.coordinatePtr(), match_.first.mIndex(),
-              isMosaic(), (char*) (sharedPtrString.get()), metaSize);
-      });
+SubblockMetaVec
+Reader::readSubblockMeta(libCZI::CDimCoordinate& plane_coord_, int index_m_)
+{
+  SubblockMetaVec metaSubblocks;
+  metaSubblocks.setMosaic(isMosaic());
 
-      return metaSubblocks;
-  }
+  SubblockSortable subBlockToFind(&plane_coord_, index_m_, isMosaic());
+  SubblockIndexVec matches = getMatches(subBlockToFind);
 
-  libCZI::IntRect
-  Reader::readSubblockRect(libCZI::CDimCoordinate& plane_coord_, int index_m_)
-  {
-      SubblockSortable subBlockToFind(&plane_coord_, index_m_, isMosaic());
-      SubblockIndexVec matches = getMatches(subBlockToFind);
+  for_each(matches.begin(), matches.end(), [&](const SubblockIndexVec::value_type& match_) {
+    size_t metaSize = 0;
+    auto subblock = m_czireader->ReadSubBlock(match_.second);
+    auto sharedPtrString = subblock->GetRawData(libCZI::ISubBlock::Metadata, &metaSize);
+    metaSubblocks.emplace_back(
+      match_.first.coordinatePtr(), match_.first.mIndex(), isMosaic(), (char*)(sharedPtrString.get()), metaSize);
+  });
 
-      if (matches.empty())
-          throw CDimCoordinatesOverspecifiedException("The specified dimensions and M-index matched nothing.");
+  return metaSubblocks;
+}
 
-      auto subblk = m_czireader->ReadSubBlock(matches.begin()->second);
-      return subblk->GetSubBlockInfo().logicalRect;
-  }
+libCZI::IntRect
+Reader::readSubblockRect(libCZI::CDimCoordinate& plane_coord_, int index_m_)
+{
+  SubblockSortable subBlockToFind(&plane_coord_, index_m_, isMosaic());
+  SubblockIndexVec matches = getMatches(subBlockToFind);
+
+  if (matches.empty())
+    throw CDimCoordinatesOverspecifiedException("The specified dimensions and M-index matched nothing.");
+
+  auto subblk = m_czireader->ReadSubBlock(matches.begin()->second);
+  return subblk->GetSubBlockInfo().logicalRect;
+}
 
 // private methods
 
-  Reader::SubblockIndexVec
-  Reader::getMatches(SubblockSortable& match_)
-  {
-      SubblockIndexVec ans;
-      m_czireader->EnumerateSubBlocks([&](int index_, const libCZI::SubBlockInfo& info_) -> bool {
-          SubblockSortable subInfo(&(info_.coordinate), info_.mIndex, isMosaic(), info_.pixelType);
-          if (isPyramid0(info_) && match_==subInfo) {
-              ans.emplace(std::pair<SubblockSortable, int>(subInfo, index_));
-          }
-          return true; // Enumerate through every subblock
-      });
+Reader::SubblockIndexVec
+Reader::getMatches(SubblockSortable& match_)
+{
+  SubblockIndexVec ans;
+  m_czireader->EnumerateSubBlocks([&](int index_, const libCZI::SubBlockInfo& info_) -> bool {
+    SubblockSortable subInfo(&(info_.coordinate), info_.mIndex, isMosaic(), info_.pixelType);
+    if (isPyramid0(info_) && match_ == subInfo) {
+      ans.emplace(std::pair<SubblockSortable, int>(subInfo, index_));
+    }
+    return true; // Enumerate through every subblock
+  });
+
+  if (ans.empty()) {
+    // check for invalid Dimension specification
+    match_.coordinatePtr()->EnumValidDimensions([&](libCZI::DimensionIndex di_, int value_) {
+      if (!m_statistics.dimBounds.IsValid(di_)) {
+        std::stringstream tmp;
+        tmp << dimToChar(di_) << " Not present in defined file Coordinates!";
+        throw CDimCoordinatesOverspecifiedException(tmp.str());
+      }
 
-      if (ans.empty()) {
-          // check for invalid Dimension specification
-          match_.coordinatePtr()->EnumValidDimensions([&](libCZI::DimensionIndex di_, int value_) {
-              bool keepGoing = true;
-              if (!m_statistics.dimBounds.IsValid(di_)) {
-                  std::stringstream tmp;
-                  tmp << dimToChar(di_) << " Not present in defined file Coordinates!";
-                  throw CDimCoordinatesOverspecifiedException(tmp.str());
-              }
-
-              int start(0), size(0);
-              m_statistics.dimBounds.TryGetInterval(di_, &start, &size);
-              if (value_<start || value_>=(start+size)) {
-                  std::stringstream tmp;
-                  tmp << dimToChar(di_) << " value " << value_ << "invalid, ∉ [" << start << ", "
-                      << start+size << ")" << std::endl;
-                  throw CDimCoordinatesOverspecifiedException(tmp.str());
-              }
-              return true;
-          });
+      int start(0), size(0);
+      m_statistics.dimBounds.TryGetInterval(di_, &start, &size);
+      if (value_ < start || value_ >= (start + size)) {
+        std::stringstream tmp;
+        tmp << dimToChar(di_) << " value " << value_ << "invalid, ∉ [" << start << ", " << start + size << ")"
+            << std::endl;
+        throw CDimCoordinatesOverspecifiedException(tmp.str());
       }
-      return ans;
+      return true;
+    });
   }
+  return ans;
+}
 
-  bool
-  Reader::isValidRegion(const libCZI::IntRect& in_box_, const libCZI::IntRect& czi_box_)
-  {
-      bool ans = true;
-      // check origin is in domain
-      if (in_box_.x<czi_box_.x || czi_box_.x+czi_box_.w<in_box_.x) ans = false;
-      if (in_box_.y<czi_box_.y || czi_box_.y+czi_box_.h<in_box_.y) ans = false;
-
-      // check  (x1, y1) point is in domain
-      int x1 = in_box_.x+in_box_.w;
-      int y1 = in_box_.y+in_box_.h;
-      if (x1<czi_box_.x || czi_box_.x+czi_box_.w<x1) ans = false;
-      if (y1<czi_box_.y || czi_box_.y+czi_box_.h<y1) ans = false;
-
-      if (!ans) throw RegionSelectionException(in_box_, czi_box_, "Requested region not in image!");
-      if (in_box_.w<1 || 1>in_box_.h)
-          throw RegionSelectionException(in_box_, czi_box_, "Requested region must have non-negative width and height!");
-
-      return ans;
-  }
+bool
+Reader::isValidRegion(const libCZI::IntRect& in_box_, const libCZI::IntRect& czi_box_)
+{
+  bool ans = true;
+  // check origin is in domain
+  if (in_box_.x < czi_box_.x || czi_box_.x + czi_box_.w < in_box_.x)
+    ans = false;
+  if (in_box_.y < czi_box_.y || czi_box_.y + czi_box_.h < in_box_.y)
+    ans = false;
+
+  // check  (x1, y1) point is in domain
+  int x1 = in_box_.x + in_box_.w;
+  int y1 = in_box_.y + in_box_.h;
+  if (x1 < czi_box_.x || czi_box_.x + czi_box_.w < x1)
+    ans = false;
+  if (y1 < czi_box_.y || czi_box_.y + czi_box_.h < y1)
+    ans = false;
+
+  if (!ans)
+    throw RegionSelectionException(in_box_, czi_box_, "Requested region not in image!");
+  if (in_box_.w < 1 || 1 > in_box_.h)
+    throw RegionSelectionException(in_box_, czi_box_, "Requested region must have non-negative width and height!");
+
+  return ans;
+}
 
-  ImagesContainerBase::ImagesContainerBasePtr
-  Reader::readMosaic(libCZI::CDimCoordinate plane_coord_, float scale_factor_, libCZI::IntRect im_box_)
-  {
-      // handle the case where the function was called with region=None (default to all)
-      if (im_box_.w==-1 && im_box_.h==-1) im_box_ = m_statistics.boundingBox;
-      isValidRegion(im_box_, m_statistics.boundingBox); // if not throws RegionSelectionException
+ImagesContainerBase::ImagesContainerBasePtr
+Reader::readMosaic(libCZI::CDimCoordinate plane_coord_, float scale_factor_, libCZI::IntRect im_box_)
+{
+  // handle the case where the function was called with region=None (default to all)
+  if (im_box_.w == -1 && im_box_.h == -1)
+    im_box_ = m_statistics.boundingBox;
+  isValidRegion(im_box_, m_statistics.boundingBox); // if not throws RegionSelectionException
 
-      if (plane_coord_.IsValid(libCZI::DimensionIndex::S)) {
-          throw CDimCoordinatesOverspecifiedException("Do not set S when reading mosaic files!");
-      }
+  if (plane_coord_.IsValid(libCZI::DimensionIndex::S)) {
+    throw CDimCoordinatesOverspecifiedException("Do not set S when reading mosaic files!");
+  }
 
-      if (!plane_coord_.IsValid(libCZI::DimensionIndex::C)) {
-          throw CDimCoordinatesUnderspecifiedException("C is not set, to read mosaic files you must specify C.");
-      }
-      SubblockSortable subBlockToFind(&plane_coord_, -1); // just check that the dims match something ignore that it's a mosaic file
-      SubblockIndexVec matches = getMatches(subBlockToFind); // this does the checking
-      m_pixelType = matches.begin()->first.pixelType();
-      size_t bgrScaling = ImageFactory::numberOfChannels(m_pixelType);
-      auto accessor = m_czireader->CreateSingleChannelScalingTileAccessor();
-
-      // multiTile accessor is not compatible with S, it composites the Scenes and the mIndexs together
-      auto multiTileComposite = accessor->Get(
-          im_box_,
-          &plane_coord_,
-          scale_factor_,
-          nullptr);   // use default options
-
-      libCZI::IntSize size = multiTileComposite->GetSize();
-      size_t pixels_in_image = size.h*size.w*bgrScaling;
-      // the original pixels_in_image calculation was done using the file statistics container from libCZI but that
-      // gives an incorrect size for the image which seems like a bug in libCZI
-      // do not use m_statistics.boundingBoxLayer0Only.w*m_statistics.boundingBoxLayer0Only.h*bgrScaling;
-      ImageFactory imageFactory(m_pixelType, pixels_in_image);
-      imageFactory.constructImage(multiTileComposite, size, &plane_coord_, im_box_, 0, -1);
-      // set is mosaic?
-      return imageFactory.transferMemoryContainer();
+  if (!plane_coord_.IsValid(libCZI::DimensionIndex::C)) {
+    throw CDimCoordinatesUnderspecifiedException("C is not set, to read mosaic files you must specify C.");
   }
+  SubblockSortable subBlockToFind(&plane_coord_,
+                                  -1); // just check that the dims match something ignore that it's a mosaic file
+  SubblockIndexVec matches = getMatches(subBlockToFind); // this does the checking
+  m_pixelType = matches.begin()->first.pixelType();
+  size_t bgrScaling = ImageFactory::numberOfChannels(m_pixelType);
+  auto accessor = m_czireader->CreateSingleChannelScalingTileAccessor();
+
+  // multiTile accessor is not compatible with S, it composites the Scenes and the mIndexs together
+  auto multiTileComposite = accessor->Get(im_box_, &plane_coord_, scale_factor_,
+                                          nullptr); // use default options
+
+  libCZI::IntSize size = multiTileComposite->GetSize();
+  size_t pixels_in_image = size.h * size.w * bgrScaling;
+  // the original pixels_in_image calculation was done using the file statistics container from libCZI but that
+  // gives an incorrect size for the image which seems like a bug in libCZI
+  // do not use m_statistics.boundingBoxLayer0Only.w*m_statistics.boundingBoxLayer0Only.h*bgrScaling;
+  ImageFactory imageFactory(m_pixelType, pixels_in_image);
+  imageFactory.constructImage(multiTileComposite, size, &plane_coord_, im_box_, 0, -1);
+  // set is mosaic?
+  return imageFactory.transferMemoryContainer();
+}
 
 }
diff --git a/_aicspylibczi/Reader.h b/_aicspylibczi/Reader.h
index 739b9388..801fe0f7 100644
--- a/_aicspylibczi/Reader.h
+++ b/_aicspylibczi/Reader.h
@@ -2,19 +2,19 @@
 #define _PYLIBCZI_READER_H
 
 #include <cstdio>
+#include <functional>
 #include <iostream>
 #include <typeinfo>
-#include <functional>
 #include <vector>
 
 #include "inc_libCZI.h"
 
 #include "DimIndex.h"
-#include "IndexMap.h"
 #include "Image.h"
-#include "SubblockSortable.h"
-#include "SubblockMetaVec.h"
 #include "ImagesContainer.h"
+#include "IndexMap.h"
+#include "SubblockMetaVec.h"
+#include "SubblockSortable.h"
 
 /*! \mainpage libCZI_c++_extension
  *
@@ -66,265 +66,260 @@
  * \endcode
  */
 
-
 using namespace std;
 
 namespace pylibczi {
 
+/*!
+ * @brief Reader class for ZISRAW / CZI files
+ *
+ * This class opens has the functionality to read mosaic files as well as single and multi-scene files.
+ * It relies on libCZI from Zeiss and our intent is that this code will enable the reading of CZI files
+ * generated by Zeiss systems. If you have a file that causes problems please contact us and share the
+ * file if possible and we will do our best to enable support. Ideally make an issue on the github repo
+ * https://github.com/elhuhdron/pylibczi
+ */
+class Reader
+{
+
+  std::shared_ptr<CCZIReader> m_czireader; // required for cast in libCZI
+  libCZI::SubBlockStatistics m_statistics;
+  libCZI::PixelType m_pixelType;
+  bool m_specifyScene;
+
+public:
+  // was vector
+  using SubblockIndexVec = std::set<std::pair<SubblockSortable, int>>;
+  using DimIndexRangeMap = std::map<DimIndex, std::pair<int, int>>;
+  using Shape = std::vector<std::pair<char, size_t>>;
+  using DimsShape = std::vector<DimIndexRangeMap>;
+
+  /*!
+   * @brief Construct the Reader and load the file statistics (dimensions etc)
+   *
+   * Example:
+   * @code
+   *    fp = std::fopen("my_czi_file.czi", "rb");      // #include <cstdio>
+   *    czi = pylibczi::Reader(fp);
+   * @endcode
+   *
+   * @param f_in_ A C-style FILE pointer to the CZI file
+   */
+  explicit Reader(std::shared_ptr<libCZI::IStream> istream_);
+
+  /*!
+   * @brief A convenience function for testing or use by C++ developers
+   * @param file_name_ a wide character string such as L"my_filename.czi"
+   */
+  explicit Reader(const wchar_t* file_name_);
+
+  /*!
+   * @brief Check if the file is a mosaic file.
+   *
+   * This test is done by checking the maximum mIndex, mosaic files will have mIndex > 0
+   *
+   * @return True if the file is a mosaic file, false if it is not.
+   */
+  bool isMosaic() const;
+
+  /*!
+   * @brief Get the dimensions (shape) of the file.
+   *
+   * Z = The Z-dimension.
+   * C = The C-dimension ("channel").
+   * T = The T-dimension ("time").
+   * R = The R-dimension ("rotation").
+   * S = The S-dimension ("scene").
+   * I = The I-dimension ("illumination").
+   * H = The H-dimension ("phase").
+   * V = The V-dimension ("view").
+   * B = The B-dimension ("block") - its use is deprecated, but it still gets returned 🙃
+   *
+   * The internal structure suggests that a given dimension could start at a value other than zero.
+   * Although I have yet to observe this we return a pair containing both the start and the size for the index.
+   *
+   * @return A vector< map< DimensionIndex, pair<int(start), int(end)> > > which conforms to [start, end) meaning
+   *    for( int i = start; i < end ; i++) is valid. If the shape of the scenes is consistent there will be one
+   *    element in the vector. If the shape is not consistent each Scene will be present in the vector with it's
+   *    explicit dimensions.
+   */
+  Reader::DimsShape readDimsRange();
+
+  /*!
+   * @brief Get the Dimensions in the order that they appear.
+   * @return a string containing the dimensions. Y & X are included for completeness despite not being DimensionIndexes.
+   */
+  std::string dimsString() const;
+
+  /*!
+   * @brief Get the size of the dimensions in the same order as dimsString()
+   * @return a vector containing the dimension sizes. Y & X are included for completeness.
+   *    If the dims are not consistent a vector of -1 values is returned. The used should then use readDimsRange()
+   */
+  std::vector<int> dimSizes();
+
+  /*!
+   * @brief check if the dims are consistent across scenes
+   * @param dShape_ A vector of scene specific dimensions
+   * @return true if the shape is the same across scene false if it is not
+   */
+  static bool consistentShape(DimsShape& dShape_);
+
+  /*!
+   * @brief Enable the user to query the Scene shape directly
+   * @return a tuple, the first index is if S is defined the second is the start index and the third is the number of S
+   * indexs
+   */
+  std::tuple<bool, int, int> scenesStartSize() const;
+
+  /*!
+   * @brief get the shape of the specified Scene
+   * @param scene_index_ the integer index of the Scene
+   * @return A map containing the DimIndex with a pair of integers start, end such that for(int i = start; i < end ;
+   * i++) is valid.
+   */
+  Reader::DimIndexRangeMap sceneShape(int scene_index_);
+
+  /*!
+   * @brief Get the metadata from the CZI file.
+   * @return A string containing the xml metadata from the file. Y & X are included for completeness despite not being
+   * DimensionIndexes.
+   */
+  std::string readMeta();
+
+  /*!
+   * @brief Given a CDimCoordinate, even an empty one, return the planes that match as a numpy ndarray
+   *
+   * The plane coordinate acts as a constraint, in the example below the dims are a set of constraints.
+   * What this means is that every image taken that has Z=8, T=0, C=1 will be returned.
+   * @code
+   *    FILE *fp = std::fopen("mosaic_file.czi", "rb");
+   *    czi = pylibczi::Reader(fp);
+   *    auto dims = CDimCoordinate{ { DimensionIndex::Z,8 } , { DimensionIndex::T,0 }, { DimensionIndex::C,1 } };
+   *    auto img_coords_dims = czi.cziread_selected(dims)
+   * @endcode
+   *
+   * @param plane_coord_ A structure containing the Dimension constraints
+   * @param index_m_ Is only relevant for mosaic files, if you wish to select one frame.
+   */
+  std::pair<ImagesContainerBase::ImagesContainerBasePtr, std::vector<std::pair<char, size_t>>>
+  readSelected(libCZI::CDimCoordinate& plane_coord_, int index_m_ = -1, unsigned int cores_ = 3);
+
+  /*!
+   * @brief provide the subblock metadata in index order consistent with readSelected.
+   * @param plane_coord_ A structure containing the Dimension constraints
+   * @param index_m_ Is only relevant for mosaic files, if you wish to select one frame.
+   * @param cores_ The number of cores to use to process threads
+   * @return a vector of metadata string blocks
+   */
+  SubblockMetaVec readSubblockMeta(libCZI::CDimCoordinate& plane_coord_, int index_m_ = -1);
+
   /*!
-   * @brief Reader class for ZISRAW / CZI files
+   * @brief provide the subblock info logicalRect coordinates
+   * @param plane_coord_ A structure containing the Dimension constraints
+   * @param index_m_ Must be set to select the tile of the mosaic file
+   * @return an IntRect {x, y, w, h}
+   */
+  libCZI::IntRect readSubblockRect(libCZI::CDimCoordinate& plane_coord_, int index_m_ = -1);
+
+  /*!
+   * @brief If the czi file is a mosaic tiled image this function can be used to reconstruct it into an image.
+   * @param plane_coord_ A class constraining the data to an individual plane.
+   * @param scale_factor_ (optional) The native size for mosaic files can be huge the scale factor allows one to get
+   * something back of a more reasonable size. The default is 0.1 meaning 10% of the native image.
+   * @param im_box_ (optional) The {x0, y0, width, height} of a sub-region, the default is the whole image.
+   * @return an ImagesContainerBasePtr containing the raw memory, a list of images, and a list of corresponding
+   * dimensions
+   *
+   * @code
+   *    FILE *fp = std::fopen("mosaicfile.czi", "rb");
+   *    czi = pylibczi::Reader(fp);
+   *    auto dims = czi.read_dims();  // dims = {{DimensionIndex::T, {0, 10}}, {DimensionIndex::C, {0,3}}
+   *    // the dims above mean that T and C need to be constrained to single values
    *
-   * This class opens has the functionality to read mosaic files as well as single and multi-scene files.
-   * It relies on libCZI from Zeiss and our intent is that this code will enable the reading of CZI files
-   * generated by Zeiss systems. If you have a file that causes problems please contact us and share the
-   * file if possible and we will do our best to enable support. Ideally make an issue on the github repo
-   * https://github.com/elhuhdron/pylibczi
+   *    auto c_dims = CDimCoordinate{ { DimensionIndex::T,0 }, { DimensionIndex::C,1 } };
+   *    auto img_p = czi.read_mosaic( c_dims, scaleFactor=0.15 );
+   *    if(img_p->is_type_match<uint16_t>(){ // supported types are uint8_t, uint16_t, and float
+   *        std::shared_ptr<uint16_t> img = img_p->get_derived<uint16_t>(); // cast the image to the memory size it was
+   * acquired as
+   *        // do what you like with the Image<uint16_t>
+   *    }
+   * @endcode
    */
-  class Reader {
-
-      std::shared_ptr<CCZIReader> m_czireader; // required for cast in libCZI
-      libCZI::SubBlockStatistics m_statistics;
-      libCZI::PixelType m_pixelType;
-      bool m_specifyScene;
-
-  public:
-      // was vector
-      using SubblockIndexVec = std::set<std::pair<SubblockSortable, int> >;
-      using DimIndexRangeMap = std::map<DimIndex, std::pair<int, int> >;
-      using Shape = std::vector<std::pair<char, size_t> >;
-      using DimsShape = std::vector<DimIndexRangeMap>;
-
-      /*!
-       * @brief Construct the Reader and load the file statistics (dimensions etc)
-       *
-       * Example:
-       * @code
-       *    fp = std::fopen("my_czi_file.czi", "rb");      // #include <cstdio>
-       *    czi = pylibczi::Reader(fp);
-       * @endcode
-       *
-       * @param f_in_ A C-style FILE pointer to the CZI file
-       */
-      explicit Reader(std::shared_ptr<libCZI::IStream> istream_);
-
-      /*!
-       * @brief A convenience function for testing or use by C++ developers
-       * @param file_name_ a wide character string such as L"my_filename.czi"
-       */
-      explicit Reader(const wchar_t* file_name_);
-
-      /*!
-       * @brief Check if the file is a mosaic file.
-       *
-       * This test is done by checking the maximum mIndex, mosaic files will have mIndex > 0
-       *
-       * @return True if the file is a mosaic file, false if it is not.
-       */
-      bool isMosaic();
-
-      /*!
-       * @brief Get the dimensions (shape) of the file.
-       *
-       * Z = The Z-dimension.
-       * C = The C-dimension ("channel").
-       * T = The T-dimension ("time").
-       * R = The R-dimension ("rotation").
-       * S = The S-dimension ("scene").
-       * I = The I-dimension ("illumination").
-       * H = The H-dimension ("phase").
-       * V = The V-dimension ("view").
-       * B = The B-dimension ("block") - its use is deprecated, but it still gets returned 🙃
-       *
-       * The internal structure suggests that a given dimension could start at a value other than zero.
-       * Although I have yet to observe this we return a pair containing both the start and the size for the index.
-       *
-       * @return A vector< map< DimensionIndex, pair<int(start), int(end)> > > which conforms to [start, end) meaning
-       *    for( int i = start; i < end ; i++) is valid. If the shape of the scenes is consistent there will be one
-       *    element in the vector. If the shape is not consistent each Scene will be present in the vector with it's
-       *    explicit dimensions.
-       */
-      Reader::DimsShape readDimsRange();
-
-      /*!
-       * @brief Get the Dimensions in the order that they appear.
-       * @return a string containing the dimensions. Y & X are included for completeness despite not being DimensionIndexes.
-       */
-      std::string dimsString();
-
-      /*!
-       * @brief Get the size of the dimensions in the same order as dimsString()
-       * @return a vector containing the dimension sizes. Y & X are included for completeness.
-       *    If the dims are not consistent a vector of -1 values is returned. The used should then use readDimsRange()
-       */
-      std::vector<int> dimSizes();
-
-      /*!
-       * @brief check if the dims are consistent across scenes
-       * @param dShape_ A vector of scene specific dimensions
-       * @return true if the shape is the same across scene false if it is not
-       */
-      bool consistentShape(DimsShape& dShape_);
-
-      /*!
-       * @brief Enable the user to query the Scene shape directly
-       * @return a tuple, the first index is if S is defined the second is the start index and the third is the number of S indexs
-       */
-      std::tuple<bool, int, int> scenesStartSize();
-
-      /*!
-       * @brief get the shape of the specified Scene
-       * @param scene_index_ the integer index of the Scene
-       * @return A map containing the DimIndex with a pair of integers start, end such that for(int i = start; i < end ; i++) is valid.
-       */
-      Reader::DimIndexRangeMap sceneShape(int scene_index_);
-
-      /*!
-       * @brief Get the metadata from the CZI file.
-       * @return A string containing the xml metadata from the file. Y & X are included for completeness despite not being DimensionIndexes.
-       */
-      std::string readMeta();
-
-      /*!
-       * @brief Given a CDimCoordinate, even an empty one, return the planes that match as a numpy ndarray
-       *
-       * The plane coordinate acts as a constraint, in the example below the dims are a set of constraints.
-       * What this means is that every image taken that has Z=8, T=0, C=1 will be returned.
-       * @code
-       *    FILE *fp = std::fopen("mosaic_file.czi", "rb");
-       *    czi = pylibczi::Reader(fp);
-       *    auto dims = CDimCoordinate{ { DimensionIndex::Z,8 } , { DimensionIndex::T,0 }, { DimensionIndex::C,1 } };
-       *    auto img_coords_dims = czi.cziread_selected(dims)
-       * @endcode
-       *
-       * @param plane_coord_ A structure containing the Dimension constraints
-       * @param index_m_ Is only relevant for mosaic files, if you wish to select one frame.
-       */
-      std::pair<ImagesContainerBase::ImagesContainerBasePtr, std::vector< std::pair<char, size_t> > >
-      readSelected(libCZI::CDimCoordinate& plane_coord_, int index_m_ = -1, unsigned int cores_=3);
-
-      /*!
-       * @brief provide the subblock metadata in index order consistent with readSelected.
-       * @param plane_coord_ A structure containing the Dimension constraints
-       * @param index_m_ Is only relevant for mosaic files, if you wish to select one frame.
-       * @param cores_ The number of cores to use to process threads
-       * @return a vector of metadata string blocks
-       */
-      SubblockMetaVec readSubblockMeta(libCZI::CDimCoordinate& plane_coord_, int index_m_ = -1);
-
-      /*!
-       * @brief provide the subblock info logicalRect coordinates
-       * @param plane_coord_ A structure containing the Dimension constraints
-       * @param index_m_ Must be set to select the tile of the mosaic file
-       * @return an IntRect {x, y, w, h}
-       */
-      libCZI::IntRect readSubblockRect(libCZI::CDimCoordinate& plane_coord_, int index_m_ = -1);
-
-      /*!
-       * @brief If the czi file is a mosaic tiled image this function can be used to reconstruct it into an image.
-       * @param plane_coord_ A class constraining the data to an individual plane.
-       * @param scale_factor_ (optional) The native size for mosaic files can be huge the scale factor allows one to get something back of a more reasonable size. The default is 0.1 meaning 10% of the native image.
-       * @param im_box_ (optional) The {x0, y0, width, height} of a sub-region, the default is the whole image.
-       * @return an ImagesContainerBasePtr containing the raw memory, a list of images, and a list of corresponding dimensions
-       *
-       * @code
-       *    FILE *fp = std::fopen("mosaicfile.czi", "rb");
-       *    czi = pylibczi::Reader(fp);
-       *    auto dims = czi.read_dims();  // dims = {{DimensionIndex::T, {0, 10}}, {DimensionIndex::C, {0,3}}
-       *    // the dims above mean that T and C need to be constrained to single values
-       *
-       *    auto c_dims = CDimCoordinate{ { DimensionIndex::T,0 }, { DimensionIndex::C,1 } };
-       *    auto img_p = czi.read_mosaic( c_dims, scaleFactor=0.15 );
-       *    if(img_p->is_type_match<uint16_t>(){ // supported types are uint8_t, uint16_t, and float
-       *        std::shared_ptr<uint16_t> img = img_p->get_derived<uint16_t>(); // cast the image to the memory size it was acquired as
-       *        // do what you like with the Image<uint16_t>
-       *    }
-       * @endcode
-       */
-      ImagesContainerBase::ImagesContainerBasePtr
-      readMosaic(libCZI::CDimCoordinate plane_coord_, float scale_factor_ = 1.0, libCZI::IntRect im_box_ = {0, 0, -1, -1});
-      // changed from {.w=-1, .h=-1} to above to support MSVC and GCC - lagging on C++14 std
-
-      /*!
-       * Convert the libCZI::DimensionIndex to a character
-       * @param di_, The libCZI::DimensionIndex to be converted
-       * @return The character representing the DimensionIndex
-       */
-      static char dimToChar(libCZI::DimensionIndex di_) { return libCZI::Utils::DimensionToChar(di_); }
-
-      bool shapeIsConsistent() { return !m_specifyScene; }
-
-      virtual ~Reader()
-      {
-          m_czireader->Close();
-      }
-
-      /*!
-       * Get the full size of the mosaic image without scaling. If you're selecting a sub-region it must be within the box returned.
-       * @return an IntRect {x, y, w, h}
-       */
-      libCZI::IntRect mosaicShape()
-      {
-        return m_statistics.boundingBoxLayer0Only;
-      }
-
-      /*!
-       * @brief get the shape of the loaded images
-       * @param images_ the ImageVector of images to get the shape of
-       * @param is_mosaic_ a boolean telling the function if it's a mosaic file
-       * @return a vector of (Dimension letter, Dimension size)
-       */
-      static Shape getShape(pylibczi::ImageVector& images_, bool is_mosaic_)
-      {
-        return images_.getShape();
-      }
-
-      /*!
-       * @brief get the pyramid 0 (acquired data) shape
-       * @param scene_index_ specifies scene but defaults to the first scene,
-       * Scenes can have different sizes
-       * @return std::vector<libCZI::IntRect> containing (x0, y0, w, h)
-       */
-      libCZI::IntRect getSceneYXSize(int scene_index_ = -1)
-      {
-        std::vector<libCZI::IntRect> matches = getAllSceneYXSize(scene_index_);
-        return matches.front();
-      }
-
-      /*!
-       * @brief get the pyramid 0 (acquired data) shape
-       * @param scene_index_ specifies scene but defaults to the first scene,
-       * Scenes can have different sizes
-       * @param get_all_matches_ if true return all matching bounding boxes
-       * @return std::vector<libCZI::IntRect> containing (x0, y0, w, h)
-       */
-      std::vector<libCZI::IntRect> getAllSceneYXSize(
-        int scene_index_ = -1,
-        bool get_all_matches_ = false);
-
-      std::string pixelType()
-      {
-        // each subblock can apparently have a different pixelType 🙄
-        if (m_pixelType == libCZI::PixelType::Invalid)
-          m_pixelType = getFirstPixelType();
-        return libCZI::Utils::PixelTypeToInformalString(m_pixelType);
-      }
-
-    private:
-      Reader::SubblockIndexVec getMatches(SubblockSortable& match_);
-
-      static bool isPyramid0(const libCZI::SubBlockInfo& info_)
-      {
-          return (info_.logicalRect.w==info_.physicalSize.w && info_.logicalRect.h==info_.physicalSize.h);
-      }
-
-      void getMemory(SubblockIndexVec& matches_);
-
-      static bool isValidRegion(const libCZI::IntRect& in_box_, const libCZI::IntRect& czi_box_);
-
-      void checkSceneShapes(void);
-
-      libCZI::PixelType getFirstPixelType(void);
-  };
+  ImagesContainerBase::ImagesContainerBasePtr readMosaic(libCZI::CDimCoordinate plane_coord_,
+                                                         float scale_factor_ = 1.0,
+                                                         libCZI::IntRect im_box_ = { 0, 0, -1, -1 });
+  // changed from {.w=-1, .h=-1} to above to support MSVC and GCC - lagging on C++14 std
+
+  /*!
+   * Convert the libCZI::DimensionIndex to a character
+   * @param di_, The libCZI::DimensionIndex to be converted
+   * @return The character representing the DimensionIndex
+   */
+  static char dimToChar(libCZI::DimensionIndex di_) { return libCZI::Utils::DimensionToChar(di_); }
+
+  bool shapeIsConsistent() const { return !m_specifyScene; }
+
+  virtual ~Reader() { m_czireader->Close(); }
+
+  /*!
+   * Get the full size of the mosaic image without scaling. If you're selecting a sub-region it must be within the box
+   * returned.
+   * @return an IntRect {x, y, w, h}
+   */
+  libCZI::IntRect mosaicShape() const { return m_statistics.boundingBoxLayer0Only; }
+
+  /*!
+   * @brief get the shape of the loaded images
+   * @param images_ the ImageVector of images to get the shape of
+   * @param is_mosaic_ a boolean telling the function if it's a mosaic file
+   * @return a vector of (Dimension letter, Dimension size)
+   */
+  static Shape getShape(pylibczi::ImageVector& images_, bool is_mosaic_) { return images_.getShape(); }
+
+  /*!
+   * @brief get the pyramid 0 (acquired data) shape
+   * @param scene_index_ specifies scene but defaults to the first scene,
+   * Scenes can have different sizes
+   * @return std::vector<libCZI::IntRect> containing (x0, y0, w, h)
+   */
+  libCZI::IntRect getSceneYXSize(int scene_index_ = -1)
+  {
+    std::vector<libCZI::IntRect> matches = getAllSceneYXSize(scene_index_);
+    return matches.front();
+  }
+
+  /*!
+   * @brief get the pyramid 0 (acquired data) shape
+   * @param scene_index_ specifies scene but defaults to the first scene,
+   * Scenes can have different sizes
+   * @param get_all_matches_ if true return all matching bounding boxes
+   * @return std::vector<libCZI::IntRect> containing (x0, y0, w, h)
+   */
+  std::vector<libCZI::IntRect> getAllSceneYXSize(int scene_index_ = -1, bool get_all_matches_ = false);
+
+  std::string pixelType()
+  {
+    // each subblock can apparently have a different pixelType 🙄
+    if (m_pixelType == libCZI::PixelType::Invalid)
+      m_pixelType = getFirstPixelType();
+    return libCZI::Utils::PixelTypeToInformalString(m_pixelType);
+  }
+
+private:
+  Reader::SubblockIndexVec getMatches(SubblockSortable& match_);
+
+  static bool isPyramid0(const libCZI::SubBlockInfo& info_)
+  {
+    return (info_.logicalRect.w == info_.physicalSize.w && info_.logicalRect.h == info_.physicalSize.h);
+  }
+
+  static bool isValidRegion(const libCZI::IntRect& in_box_, const libCZI::IntRect& czi_box_);
+
+  void checkSceneShapes();
+
+  libCZI::PixelType getFirstPixelType();
+};
 
 }
 
diff --git a/_aicspylibczi/exceptions.h b/_aicspylibczi/exceptions.h
index 32474638..de6d3856 100644
--- a/_aicspylibczi/exceptions.h
+++ b/_aicspylibczi/exceptions.h
@@ -12,221 +12,125 @@
 
 namespace pylibczi {
 
-class FilePtrException : public std::exception
+class FilePtrException : public std::runtime_error
 {
-  std::string m_message;
-
 public:
-  explicit FilePtrException(std::string message_)
-    : exception()
-    , m_message(std::move(message_))
+  explicit FilePtrException(const std::string& message_)
+    : std::runtime_error("File Pointer Exception: " + message_)
   {}
-  const char* what() const noexcept override
-  {
-    std::string tmp("File Pointer Exception: ");
-    tmp += m_message;
-    return tmp.c_str();
-  }
-};
-
-class ThreadingRequestedCoresException : public std::exception
-{
-  std::stringstream m_message;
-
-public:
-  explicit ThreadingRequestedCoresException(int cores_, int requested_cores_, std::string message_)
-  {
-    m_message << "Requested " << requested_cores_ << " but there are only " << cores_ << "available. " << message_;
-  }
-
-  const char* what() const noexcept override { return m_message.str().c_str(); }
 };
 
-class PixelTypeException : public std::exception
+class PixelTypeException : public std::runtime_error
 {
-  libCZI::PixelType m_pixelType;
-  std::string m_message;
-
   static const std::map<libCZI::PixelType, const std::string> s_byName;
 
 public:
-  PixelTypeException(libCZI::PixelType pixel_type_, std::string message_)
-    : exception()
-    , m_pixelType(pixel_type_)
-    , m_message(std::move(message_))
-  {}
-
-  const char* what() const noexcept override
+  PixelTypeException(libCZI::PixelType pixel_type_, const std::string& message_)
+    : std::runtime_error("")
   {
-    auto tname = s_byName.find(m_pixelType);
+    auto tname = s_byName.find(pixel_type_);
     std::string name((tname == s_byName.end()) ? "Unknown type" : tname->second);
-    std::string tmp("PixelType( " + name + " ): " + m_message);
-    return tmp.c_str();
+    static_cast<std::runtime_error&>(*this) = std::runtime_error("PixelType( " + name + " ): " + message_);
   }
 };
 
-class RegionSelectionException : public std::exception
+class RegionSelectionException : public std::runtime_error
 {
-  libCZI::IntRect m_requested;
-  libCZI::IntRect m_image;
-  std::string m_message;
-
 public:
   RegionSelectionException(const libCZI::IntRect& requested_box_,
                            const libCZI::IntRect& image_box_,
-                           std::string message_)
-    : m_requested(requested_box_)
-    , m_image(image_box_)
-    , m_message(std::move(message_))
-  {}
-
-  const char* what() const noexcept override
+                           const std::string& message_)
+    : std::runtime_error("")
   {
-    std::stringstream front;
+    std::stringstream front;  // x ∉ Y means x is not an element of Y, or x is not in Y
     front << "Requirement violated requested region is not a subset of the "
              "defined image! \n\t "
-          << m_requested << " ⊄ " << m_image << "\n\t" << m_message << std::endl;
-    return front.str().c_str();
+          << requested_box_ << " ⊄ " << image_box_ << "\n\t" << message_ << std::endl;
+    static_cast<std::runtime_error&>(*this) = std::runtime_error(front.str());
   }
 };
 
-class ImageAccessUnderspecifiedException : public std::exception
+class ImageAccessUnderspecifiedException : public std::runtime_error
 {
-  size_t m_given, m_required;
-  std::string m_message;
-
 public:
-  ImageAccessUnderspecifiedException(size_t given_, size_t required_, std::string message_)
-    : m_given(given_)
-    , m_required(required_)
-    , m_message(std::move(message_))
+  ImageAccessUnderspecifiedException(size_t given_, size_t required_, const std::string& message_)
+    : std::runtime_error("Dimensions underspecified, given " + std::to_string(given_) + " dimensions but " +
+                         std::to_string(required_) + " needed! \n\t" + message_)
   {}
-
-  const char* what() const noexcept override
-  {
-    std::stringstream tmp;
-    tmp << "Dimensions underspecified, given " << m_given << " dimensions but " << m_required << " needed! \n\t"
-        << m_message << std::endl;
-    return tmp.str().c_str();
-  }
 };
 
-class ImageIteratorException : public std::exception
+class ImageIteratorException : public std::runtime_error
 {
-  std::string m_message;
-
 public:
   explicit ImageIteratorException(const std::string& message_)
-    : m_message("ImageIteratorException: " + message_)
+    : std::runtime_error("ImageIteratorException: " + message_)
   {}
-
-  const char* what() const noexcept override { return m_message.c_str(); }
 };
 
-class ImageSplitChannelException : public std::exception
+class ImageSplitChannelException : public std::runtime_error
 {
-  std::string m_message;
-  int m_channel;
-
 public:
   ImageSplitChannelException(const std::string& message_, int channel_)
-    : m_message("ImageSplitChannelExcetion: " + message_)
-    , m_channel(channel_)
+    : std::runtime_error("ImageSplitChannelExcetion: " + message_ +
+                         " Channel should be zero or unset but has a value of " + std::to_string(channel_) +
+                         " not sure how to procede in assigning channels.")
   {}
-
-  const char* what() const noexcept override
-  {
-    std::stringstream tmp;
-    tmp << m_message << " Channel should be zero or unset but has a value of " << m_channel
-        << " not sure how to procede in assigning channels." << std::endl;
-    return tmp.str().c_str();
-  }
 };
 
-class ImageCopyAllocFailed : public std::bad_alloc
+class ImageCopyAllocFailed
+  : public std::runtime_error
+  , public std::bad_alloc
 {
-  std::string m_message;
-  unsigned long m_size;
-
 public:
-  ImageCopyAllocFailed(std::string message_, unsigned long alloc_size_)
-    : m_message(std::move(message_))
-    , m_size(alloc_size_)
-  {}
-
-  const char* what() const noexcept override
+  ImageCopyAllocFailed(const std::string& message_, unsigned long alloc_size_)
+    : std::runtime_error("")
   {
     std::stringstream tmp;
-    auto gbSize = static_cast<double>(m_size);
+    auto gbSize = static_cast<double>(alloc_size_);
     gbSize /= 1073741824.0; // 1024 * 1024 * 1024
-    tmp << "ImageCopyAllocFailed [" << std::setprecision(1) << gbSize << " GB requested]: " << m_message << std::endl;
-    return tmp.str().c_str();
+    tmp << "ImageCopyAllocFailed [" << std::setprecision(1) << gbSize << " GB requested]: " << message_ << std::endl;
+    static_cast<std::runtime_error&>(*this) = std::runtime_error(tmp.str());
   }
+
+  // both parent classes define what so it's necessary to override in order to call the appropriate one.
+  const char* what() const noexcept override { return std::runtime_error::what(); }
 };
 
-class CDimCoordinatesOverspecifiedException : public std::exception
+class CDimCoordinatesOverspecifiedException : public std::runtime_error
 {
-  std::string m_message;
-
 public:
-  explicit CDimCoordinatesOverspecifiedException(std::string message_)
-    : m_message(std::move(message_))
+  explicit CDimCoordinatesOverspecifiedException(const std::string& message_)
+    : std::runtime_error("The coordinates are overspecified = you have specified a Dimension "
+                         "or Dimension value that is not valid. " +
+                         message_)
   {}
-
-  const char* what() const noexcept override
-  {
-    std::stringstream tmp;
-    tmp << "The coordinates are overspecified = you have specified a Dimension "
-           "or Dimension value that is not valid. "
-        << m_message << std::endl;
-    return tmp.str().c_str();
-  }
 };
 
-class CDimCoordinatesUnderspecifiedException : public std::exception
+class CDimCoordinatesUnderspecifiedException : public std::runtime_error // std::exception
 {
-  std::string m_message;
-
 public:
-  explicit CDimCoordinatesUnderspecifiedException(std::string message_)
-    : m_message(std::move(message_))
+  explicit CDimCoordinatesUnderspecifiedException(const std::string& message_)
+    : std::runtime_error("The coordinates are underspecified = you have not specified a "
+                         "Dimension that is required. " +
+                         message_)
   {}
-
-  const char* what() const noexcept override
-  {
-    std::stringstream tmp;
-    tmp << "The coordinates are underspecified = you have not specified a "
-           "Dimension that is required. "
-        << m_message << std::endl;
-    return tmp.str().c_str();
-  }
 };
 
-class CdimSelectionZeroImagesException : public std::exception
+class CdimSelectionZeroImagesException : public std::runtime_error
 {
-  libCZI::CDimCoordinate m_requestedPlaneCoordinate; // requested
-  libCZI::CDimBounds m_planeCoordinateBounds;        // image file
-  std::string m_message;
-
 public:
   CdimSelectionZeroImagesException(libCZI::CDimCoordinate& requested_plane_coordinate_,
                                    libCZI::CDimBounds& plane_coordinate_bounds_,
-                                   std::string message_)
-    : m_requestedPlaneCoordinate(requested_plane_coordinate_)
-    , m_planeCoordinateBounds(plane_coordinate_bounds_)
-    , m_message(std::move(message_))
-  {
-    std::cout << this->what();
-  }
-
-  const char* what() const noexcept override
+                                   const std::string& message_)
+    : std::runtime_error("")
   {
     std::stringstream tmp;
-    tmp << "Specified Dims resulted in NO image frames: " << m_requestedPlaneCoordinate << " ∉ "
-        << m_planeCoordinateBounds << std::endl;
-    return tmp.str().c_str();
+    tmp << "Specified Dims resulted in NO image frames: " << requested_plane_coordinate_ << " ∉ "
+        << plane_coordinate_bounds_ << " " << message_ << std::endl;
+    static_cast<std::runtime_error&>(*this) = std::runtime_error(tmp.str());
   }
 };
+
 }
 
 #endif //_PYLIBCZI_EXCEPTIONS_H


Test patch:
diff --git a/c_tests/resources/tiles.czi b/c_tests/resources/tiles.czi
new file mode 100755
index 00000000..c6ca441f
Binary files /dev/null and b/c_tests/resources/tiles.czi differ
diff --git a/c_tests/resources/tiles_time_z.czi b/c_tests/resources/tiles_time_z.czi
new file mode 100755
index 00000000..e11049e1
Binary files /dev/null and b/c_tests/resources/tiles_time_z.czi differ
diff --git a/c_tests/resources/tiles_z.czi b/c_tests/resources/tiles_z.czi
new file mode 100755
index 00000000..e281e8e1
Binary files /dev/null and b/c_tests/resources/tiles_z.czi differ
diff --git a/c_tests/test_Reader.cpp b/c_tests/test_Reader.cpp
index 4b2ac85b..52eb77f7 100644
--- a/c_tests/test_Reader.cpp
+++ b/c_tests/test_Reader.cpp
@@ -1,16 +1,60 @@
 #include <algorithm>
 #include <chrono>
-#include <cstdio>
+#include <memory>
 #include <sstream>
 
 #include "catch.hpp"
 
 #include "../_aicspylibczi/Reader.h"
-#include "../_aicspylibczi/exceptions.h"
 #include "../_aicspylibczi/pb_helpers.h"
 
 #define CORES_FOR_THREADS 1 // for local testing increase this but for GH Actions it must be 1
 
+void
+printDims(const std::string& msg, const pylibczi::Reader::DimsShape& dimsVec)
+{
+  std::cout << msg << std::endl;
+  using DI = pylibczi::DimIndex;
+  for (const auto& x : dimsVec) {
+    for (const auto& y : x) {
+      char yChar;
+      switch (y.first) {
+        case DI::X:
+          yChar = 'X';
+          break;
+        case DI::Y:
+          yChar = 'Y';
+          break;
+        case DI::Z:
+          yChar = 'Z';
+          break;
+        case DI::M:
+          yChar = 'M';
+          break;
+        case DI::C:
+          yChar = 'C';
+          break;
+        case DI::T:
+          yChar = 'T';
+          break;
+        case DI::V:
+          yChar = 'V';
+          break;
+        case DI::B:
+          yChar = 'B';
+          break;
+        case DI::S:
+          yChar = 'S';
+          break;
+        default:
+          yChar = 'N';
+          break;
+      }
+      std::cout << yChar << "\t [ " << y.second.first << ", " << y.second.second << " ]" << std::endl;
+    }
+  }
+}
+
 class CziCreator
 {
   std::unique_ptr<pylibczi::Reader> m_czi;
@@ -53,7 +97,7 @@ class CziCreatorIStream
     : m_czi()
   {
     auto fp = std::shared_ptr<libCZI::IStream>(new CSimpleStreamImplCppStreams(L"resources/s_1_t_1_c_1_z_1.czi"));
-    m_czi = std::unique_ptr<pylibczi::Reader>(new pylibczi::Reader(fp));
+    m_czi = std::make_unique<pylibczi::Reader>(fp);
   }
   pylibczi::Reader* get() { return m_czi.get(); }
 };
@@ -105,6 +149,39 @@ class CziCreator5
   pylibczi::Reader* get() { return m_czi.get(); }
 };
 
+class CziCreatorTiles
+{
+  std::unique_ptr<pylibczi::Reader> m_czi;
+
+public:
+  CziCreatorTiles()
+    : m_czi(new pylibczi::Reader(L"resources/tiles.czi"))
+  {}
+  pylibczi::Reader* get() { return m_czi.get(); }
+};
+
+class CziCreatorTilesTZ
+{
+  std::unique_ptr<pylibczi::Reader> m_czi;
+
+public:
+  CziCreatorTilesTZ()
+    : m_czi(new pylibczi::Reader(L"resources/tiles_time_z.czi"))
+  {}
+  pylibczi::Reader* get() { return m_czi.get(); }
+};
+
+class CziCreatorTilesZ
+{
+  std::unique_ptr<pylibczi::Reader> m_czi;
+
+public:
+  CziCreatorTilesZ()
+    : m_czi(new pylibczi::Reader(L"resources/tiles_z.czi"))
+  {}
+  pylibczi::Reader* get() { return m_czi.get(); }
+};
+
 TEST_CASE_METHOD(CziCreatorIStream, "test_reader_constructor", "[Reader]")
 {
   REQUIRE_NOTHROW(get());
@@ -310,7 +387,6 @@ TEST_CASE_METHOD(CziMCreator, "test_mosaic_readScaled", "[Reader_mosaic_readScal
   REQUIRE(imvec.size() == 1);
 }
 
-
 TEST_CASE_METHOD(CziMCreator, "test_mosaic_shape", "[Reader_mosaic_shape]")
 {
   auto czi = get();
@@ -371,7 +447,7 @@ TEST_CASE_METHOD(CziBgrCreator, "test_bgr_flatten", "[Reader_read_flatten_bgr]")
   auto shape = imgCont.second;
   REQUIRE(pr.size() == 1);
 
-  for (auto x : pr) {
+  for (const auto& x : pr) {
     REQUIRE(x->shape()[0] == 3);
     REQUIRE(x->shape()[1] == 624);
     REQUIRE(x->shape()[2] == 924);
@@ -476,7 +552,7 @@ TEST_CASE_METHOD(CziBgrCreator2, "test_bgr2_flatten", "[Reader_read_flatten_bgr2
 
   REQUIRE(pr.size() == 1);
 
-  for (auto x : pr) {
+  for (const auto& x : pr) {
     REQUIRE(x->shape()[0] == 3);
     REQUIRE(x->shape()[1] == 81);
     REQUIRE(x->shape()[2] == 147);
@@ -538,6 +614,114 @@ TEST_CASE_METHOD(CziCreatorOrder, "test_image_order", "[Reader_image_order]")
            });
 }
 
+TEST_CASE_METHOD(CziCreatorTiles, "test_tile_no_scene", "[Reader_tile_no_scene]")
+{
+  auto czi = get();
+
+  REQUIRE(czi->isMosaic());
+
+  auto dims = czi->dimsString();
+  REQUIRE(dims == std::string("BVTCZMYX"));
+
+  auto dSizes = czi->dimSizes();
+  std::vector<int> dSizesAns = { 1, 1, 1, 2, 1, 4, 256, 256 };
+  REQUIRE(dSizes == dSizesAns);
+
+  using DI = pylibczi::DimIndex;
+  pylibczi::Reader::DimsShape ans{
+    { { DI::X, { 0, 256 } },
+      { DI::Y, { 0, 256 } },
+      { DI::M, { 0, 4 } },
+      { DI::Z, { 0, 1 } },
+      { DI::C, { 0, 2 } },
+      { DI::T, { 0, 1 } },
+      { DI::V, { 0, 1 } },
+      { DI::B, { 0, 1 } } },
+  };
+
+  auto dimsVec = czi->readDimsRange();
+  REQUIRE(czi->shapeIsConsistent());
+  REQUIRE(dimsVec.size() == 1);
+  REQUIRE(dimsVec == ans);
+
+  libCZI::CDimCoordinate dm = { { libCZI::DimensionIndex::C, 0 } };
+  REQUIRE_NOTHROW(czi->readMosaic(dm));
+
+  REQUIRE_NOTHROW(czi->readSelected(dm));
+}
+
+TEST_CASE_METHOD(CziCreatorTilesTZ, "test_tile_tz_no_scene", "[Reader_tile_tz_no_scene]")
+{
+  auto czi = get();
+
+  REQUIRE(czi->isMosaic());
+
+  auto dims = czi->dimsString();
+  REQUIRE(dims == std::string("BVTCZMYX"));
+
+  auto dSizes = czi->dimSizes();
+  std::vector<int> dSizesAns = { 1, 1, 2, 2, 2, 4, 256, 256 };
+  REQUIRE(dSizes == dSizesAns);
+
+  using DI = pylibczi::DimIndex;
+  pylibczi::Reader::DimsShape ans{
+    { { DI::X, { 0, 256 } },
+      { DI::Y, { 0, 256 } },
+      { DI::M, { 0, 4 } },
+      { DI::Z, { 0, 2 } },
+      { DI::C, { 0, 2 } },
+      { DI::T, { 0, 2 } },
+      { DI::V, { 0, 1 } },
+      { DI::B, { 0, 1 } } },
+  };
+
+  auto dimsVec = czi->readDimsRange();
+  REQUIRE(czi->shapeIsConsistent());
+  REQUIRE(dimsVec.size() == 1);
+  REQUIRE(dimsVec == ans);
+
+  libCZI::CDimCoordinate dm = { { libCZI::DimensionIndex::C, 0 },
+                                { libCZI::DimensionIndex::Z, 1 },
+                                { libCZI::DimensionIndex::T, 0 } };
+  REQUIRE_NOTHROW(czi->readMosaic(dm));
+}
+
+TEST_CASE_METHOD(CziCreatorTilesZ, "test_tile_z_no_scene", "[Reader_tile_z_no_scene]")
+{
+  auto czi = get();
+
+  REQUIRE(czi->isMosaic());
+
+  auto dims = czi->dimsString();
+  REQUIRE(dims == std::string("BVTCZMYX"));
+
+  auto dSizes = czi->dimSizes();
+  std::vector<int> dSizesAns = { 1, 1, 1, 2, 2, 4, 256, 256 };
+  REQUIRE(dSizes == dSizesAns);
+
+  using DI = pylibczi::DimIndex;
+  pylibczi::Reader::DimsShape ans{
+    { { DI::X, { 0, 256 } },
+      { DI::Y, { 0, 256 } },
+      { DI::M, { 0, 4 } },
+      { DI::Z, { 0, 2 } },
+      { DI::C, { 0, 2 } },
+      { DI::T, { 0, 1 } },
+      { DI::V, { 0, 1 } },
+      { DI::B, { 0, 1 } } },
+  };
+
+  auto dimsVec = czi->readDimsRange();
+  REQUIRE(czi->shapeIsConsistent());
+  REQUIRE(dimsVec.size() == 1);
+  REQUIRE(dimsVec == ans);
+
+  libCZI::CDimCoordinate dm = { { libCZI::DimensionIndex::C, 0 }, { libCZI::DimensionIndex::Z, 1 } };
+  REQUIRE_NOTHROW(czi->readMosaic(dm));
+
+  REQUIRE_NOTHROW(czi->readSelected(dm));
+}
+
 #ifdef LOCAL_TEST
 
 TEST_CASE_METHOD(CziCreatorBig, "test_big_czifile", "[Reader_timed_read]")
@@ -578,7 +762,7 @@ TEST_CASE_METHOD(CziCreatorBigM, "test_bigm_czifile", "[Reader_bbox]")
   REQUIRE(ans[2].y == 19201);
   REQUIRE(ans[2].w == 950);
   REQUIRE(ans[2].h == 650);
-  libCZI::CDimCoordinate cdims = {{libCZI::DimensionIndex::C, 0}};
+  libCZI::CDimCoordinate cdims = { { libCZI::DimensionIndex::C, 0 } };
   // REQUIRE_NOTHROW not compatible with MSVC as is.
 }