From b0898cbf1f90eb2b61791baefce1c6df9ea8fa3d Mon Sep 17 00:00:00 2001 From: ptahmose Date: Mon, 12 Feb 2024 11:56:25 +0100 Subject: [PATCH] some fixes for readerwriter (#95) * some fixes for czireaderwriter * implement some missing methods * add unittests for newly implemented methods of CziReaderWriter --- CMakeLists.txt | 2 +- Src/libCZI/CMakeLists.txt | 2 + Src/libCZI/CZIReader.cpp | 104 ++------------- Src/libCZI/CziReaderCommon.cpp | 118 +++++++++++++++++ Src/libCZI/CziReaderCommon.h | 34 +++++ Src/libCZI/CziReaderWriter.cpp | 86 ++++++------- Src/libCZI/CziWriter.cpp | 4 +- Src/libCZI/Doc/version-history.markdown | 1 + Src/libCZI/libCZI_ReadWrite.h | 4 +- Src/libCZI_UnitTests/test_readerwriter.cpp | 142 ++++++++++++++++++++- 10 files changed, 351 insertions(+), 146 deletions(-) create mode 100644 Src/libCZI/CziReaderCommon.cpp create mode 100644 Src/libCZI/CziReaderCommon.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 5432c0c5..0b6251f9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.15) cmake_policy(SET CMP0091 NEW) # enable new "MSVC runtime library selection" (https://cmake.org/cmake/help/latest/variable/CMAKE_MSVC_RUNTIME_LIBRARY.html) project(libCZI - VERSION 0.58.0 + VERSION 0.58.1 HOMEPAGE_URL "https://github.com/ZEISS/libczi" DESCRIPTION "libCZI is an Open Source Cross-Platform C++ library to read and write CZI") diff --git a/Src/libCZI/CMakeLists.txt b/Src/libCZI/CMakeLists.txt index 629e02fb..de1494b9 100644 --- a/Src/libCZI/CMakeLists.txt +++ b/Src/libCZI/CMakeLists.txt @@ -20,6 +20,7 @@ set(LIBCZISRCFILES CziMetadataSegment.cpp CziParse.cpp CZIReader.cpp + CziReaderCommon.cpp CziReaderWriter.cpp CziStructs.cpp CziSubBlock.cpp @@ -60,6 +61,7 @@ set(LIBCZISRCFILES CziMetadataDocumentInfo2.h CziMetadataSegment.h CziParse.h + CziReaderCommon.h CZIReader.h CziReaderWriter.h CziStructs.h diff --git a/Src/libCZI/CZIReader.cpp b/Src/libCZI/CZIReader.cpp index 8f09f75c..cb19dd10 100644 --- a/Src/libCZI/CZIReader.cpp +++ b/Src/libCZI/CZIReader.cpp @@ -10,6 +10,7 @@ #include "CziUtils.h" #include "utilities.h" #include "CziAttachment.h" +#include "CziReaderCommon.h" using namespace std; using namespace libCZI; @@ -101,15 +102,7 @@ CCZIReader::CCZIReader() : isOperational(false) this->subBlkDir.EnumSubBlocks( [&](int index, const CCziSubBlockDirectory::SubBlkEntry& entry)->bool { - SubBlockInfo info; - info.compressionModeRaw = entry.Compression; - info.pixelType = CziUtils::PixelTypeFromInt(entry.PixelType); - info.coordinate = entry.coordinate; - info.logicalRect = IntRect{ entry.x,entry.y,entry.width,entry.height }; - info.physicalSize = IntSize{ (std::uint32_t)entry.storedWidth, (std::uint32_t)entry.storedHeight }; - info.mIndex = entry.mIndex; - info.pyramidType = CziUtils::PyramidTypeFromByte(entry.pyramid_type_from_spare); - return funcEnum(index, info); + return funcEnum(index, CziReaderCommon::ConvertToSubBlockInfo(entry)); }); } @@ -135,30 +128,7 @@ CCZIReader::CCZIReader() : isOperational(false) /*virtual*/void CCZIReader::EnumSubset(const IDimCoordinate* planeCoordinate, const IntRect* roi, bool onlyLayer0, const std::function& funcEnum) { this->ThrowIfNotOperational(); - - // TODO: - // Ok... for a first tentative, experimental and quick-n-dirty implementation, simply - // walk through all the subblocks. We surely want to have something more elaborated - // here. - this->EnumerateSubBlocks( - [&](int index, const SubBlockInfo& info)->bool - { - // TODO: we only deal with layer 0 currently... or, more precisely, we do not take "zoom" into account at all - // -> well... added that boolean "onlyLayer0" - is this sufficient...? - if (onlyLayer0 == false || (info.physicalSize.w == info.logicalRect.w && info.physicalSize.h == info.logicalRect.h)) - { - if (planeCoordinate == nullptr || CziUtils::CompareCoordinate(planeCoordinate, &info.coordinate) == true) - { - if (roi == nullptr || Utilities::DoIntersect(*roi, info.logicalRect)) - { - bool b = funcEnum(index, info); - return b; - } - } - } - - return true; - }); + CziReaderCommon::EnumSubset(this, planeCoordinate, roi, onlyLayer0, funcEnum); } /*virtual*/std::shared_ptr CCZIReader::ReadSubBlock(int index) @@ -176,41 +146,7 @@ CCZIReader::CCZIReader() : isOperational(false) /*virtual*/bool CCZIReader::TryGetSubBlockInfoOfArbitrarySubBlockInChannel(int channelIndex, SubBlockInfo& info) { this->ThrowIfNotOperational(); - - // TODO: we should be able to gather this information when constructing the subblock-list - // for the time being... just walk through the whole list - // - bool foundASubBlock = false; - SubBlockStatistics s = this->subBlkDir.GetStatistics(); - if (!s.dimBounds.IsValid(DimensionIndex::C)) - { - // in this case -> just take the first subblock... - this->EnumerateSubBlocks( - [&](int index, const SubBlockInfo& sbinfo)->bool - { - info = sbinfo; - foundASubBlock = true; - return false; - }); - } - else - { - this->EnumerateSubBlocks( - [&](int index, const SubBlockInfo& sbinfo)->bool - { - int c; - if (sbinfo.coordinate.TryGetPosition(DimensionIndex::C, &c) == true && c == channelIndex) - { - info = sbinfo; - foundASubBlock = true; - return false; - } - - return true; - }); - } - - return foundASubBlock; + return CziReaderCommon::TryGetSubBlockInfoOfArbitrarySubBlockInChannel(this, channelIndex, info); } /*virtual*/bool CCZIReader::TryGetSubBlockInfo(int index, SubBlockInfo* info) const @@ -223,13 +159,7 @@ CCZIReader::CCZIReader() : isOperational(false) if (info != nullptr) { - info->compressionModeRaw = entry.Compression; - info->pixelType = CziUtils::PixelTypeFromInt(entry.PixelType); - info->coordinate = entry.coordinate; - info->logicalRect = IntRect{ entry.x,entry.y,entry.width,entry.height }; - info->physicalSize = IntSize{ static_cast(entry.storedWidth), static_cast(entry.storedHeight) }; - info->mIndex = entry.mIndex; - info->pyramidType = CziUtils::PyramidTypeFromByte(entry.pyramid_type_from_spare); + *info = CziReaderCommon::ConvertToSubBlockInfo(entry); } return true; @@ -273,25 +203,11 @@ CCZIReader::CCZIReader() : isOperational(false) /*virtual*/void CCZIReader::EnumerateSubset(const char* contentFileType, const char* name, const std::function& funcEnum) { this->ThrowIfNotOperational(); - libCZI::AttachmentInfo ai; - ai.contentFileType[sizeof(ai.contentFileType) - 1] = '\0'; - this->attachmentDir.EnumAttachments( - [&](int index, const CCziAttachmentsDirectory::AttachmentEntry& ae) - { - if (contentFileType == nullptr || strcmp(contentFileType, ae.ContentFileType) == 0) - { - if (name == nullptr || strcmp(name, ae.Name) == 0) - { - ai.contentGuid = ae.ContentGuid; - memcpy(ai.contentFileType, ae.ContentFileType, sizeof(ae.ContentFileType)); - ai.name = ae.Name; - bool b = funcEnum(index, ai); - return b; - } - } - - return true; - }); + CziReaderCommon::EnumerateSubset( + std::bind(&CCziAttachmentsDirectory::EnumAttachments, &this->attachmentDir, std::placeholders::_1), + contentFileType, + name, + funcEnum); } /*virtual*/std::shared_ptr CCZIReader::ReadAttachment(int index) diff --git a/Src/libCZI/CziReaderCommon.cpp b/Src/libCZI/CziReaderCommon.cpp new file mode 100644 index 00000000..91ee3141 --- /dev/null +++ b/Src/libCZI/CziReaderCommon.cpp @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: 2024 Carl Zeiss Microscopy GmbH +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "CziReaderCommon.h" + +#include "CziUtils.h" +#include "utilities.h" + +using namespace std; +using namespace libCZI; + +/*static*/void CziReaderCommon::EnumSubset( + libCZI::ISubBlockRepository* repository, + const libCZI::IDimCoordinate* planeCoordinate, + const libCZI::IntRect* roi, + bool onlyLayer0, + const std::function& funcEnum) +{ + // Ok... for a first tentative, experimental and quick-n-dirty implementation, simply + // walk through all the subblocks. We surely want to have something more elaborated + // here. + repository->EnumerateSubBlocks( + [&](int index, const SubBlockInfo& info)->bool + { + if (onlyLayer0 == false || (info.physicalSize.w == info.logicalRect.w && info.physicalSize.h == info.logicalRect.h)) + { + if (planeCoordinate == nullptr || CziUtils::CompareCoordinate(planeCoordinate, &info.coordinate) == true) + { + if (roi == nullptr || Utilities::DoIntersect(*roi, info.logicalRect)) + { + const bool b = funcEnum(index, info); + return b; + } + } + } + + return true; + }); +} + +/*static*/bool CziReaderCommon::TryGetSubBlockInfoOfArbitrarySubBlockInChannel( + libCZI::ISubBlockRepository* repository, + int channelIndex, + libCZI::SubBlockInfo& info) +{ + bool foundASubBlock = false; + SubBlockStatistics s = repository->GetStatistics(); + if (!s.dimBounds.IsValid(DimensionIndex::C)) + { + // in this case -> just take the first subblock... + repository->EnumerateSubBlocks( + [&](int index, const SubBlockInfo& sbinfo)->bool + { + info = sbinfo; + foundASubBlock = true; + return false; + }); + } + else + { + repository->EnumerateSubBlocks( + [&](int index, const SubBlockInfo& sbinfo)->bool + { + int c; + if (sbinfo.coordinate.TryGetPosition(DimensionIndex::C, &c) == true && c == channelIndex) + { + info = sbinfo; + foundASubBlock = true; + return false; + } + + return true; + }); + } + + return foundASubBlock; +} + +/*static*/void CziReaderCommon::EnumerateSubset( + const std::function&)>& func, + const char* contentFileType, + const char* name, + const std::function& funcEnum) +{ + libCZI::AttachmentInfo ai; + ai.contentFileType[sizeof(ai.contentFileType) - 1] = '\0'; + func( + [&](int index, const CCziAttachmentsDirectoryBase::AttachmentEntry& ae) + { + if (contentFileType == nullptr || strcmp(contentFileType, ae.ContentFileType) == 0) + { + if (name == nullptr || strcmp(name, ae.Name) == 0) + { + ai.contentGuid = ae.ContentGuid; + memcpy(ai.contentFileType, ae.ContentFileType, sizeof(ae.ContentFileType)); + ai.name = ae.Name; + bool b = funcEnum(index, ai); + return b; + } + } + + return true; + }); +} + +/*static*/libCZI::SubBlockInfo CziReaderCommon::ConvertToSubBlockInfo(const CCziSubBlockDirectory::SubBlkEntry& entry) +{ + SubBlockInfo info; + info.compressionModeRaw = entry.Compression; + info.pixelType = CziUtils::PixelTypeFromInt(entry.PixelType); + info.coordinate = entry.coordinate; + info.logicalRect = IntRect{ entry.x,entry.y,entry.width,entry.height }; + info.physicalSize = IntSize{ (std::uint32_t)entry.storedWidth, (std::uint32_t)entry.storedHeight }; + info.mIndex = entry.mIndex; + info.pyramidType = CziUtils::PyramidTypeFromByte(entry.pyramid_type_from_spare); + return info; +} diff --git a/Src/libCZI/CziReaderCommon.h b/Src/libCZI/CziReaderCommon.h new file mode 100644 index 00000000..ab43da7a --- /dev/null +++ b/Src/libCZI/CziReaderCommon.h @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2024 Carl Zeiss Microscopy GmbH +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include "libCZI.h" +#include "CziAttachment.h" +#include + +/// Here we gather some common functionality that is used by both the CziReader and the CziReaderWriter classes. +class CziReaderCommon +{ +public: + static void EnumSubset( + libCZI::ISubBlockRepository* repository, + const libCZI::IDimCoordinate* planeCoordinate, + const libCZI::IntRect* roi, + bool onlyLayer0, + const std::function& funcEnum); + + static bool TryGetSubBlockInfoOfArbitrarySubBlockInChannel( + libCZI::ISubBlockRepository* repository, + int channelIndex, + libCZI::SubBlockInfo& info); + + static void EnumerateSubset( + const std::function&)>& func, + const char* contentFileType, + const char* name, + const std::function& funcEnum); + + static libCZI::SubBlockInfo ConvertToSubBlockInfo(const CCziSubBlockDirectory::SubBlkEntry& entry); +}; diff --git a/Src/libCZI/CziReaderWriter.cpp b/Src/libCZI/CziReaderWriter.cpp index ffe83669..c8241179 100644 --- a/Src/libCZI/CziReaderWriter.cpp +++ b/Src/libCZI/CziReaderWriter.cpp @@ -11,6 +11,7 @@ #include "utilities.h" #include "CziSubBlock.h" #include "CziAttachment.h" +#include "CziReaderCommon.h" using namespace libCZI; using namespace std; @@ -130,8 +131,8 @@ void ICziReaderWriter::ReplaceSubBlock(int key, const libCZI::AddSubBlockInfoStr wi.writeFunc = std::bind(&CCziReaderWriter::WriteToOutputStream, this, placeholders::_1, placeholders::_2, placeholders::_3, placeholders::_4, placeholders::_5); wi.useSpecifiedAllocatedSize = false; - auto sizeOfSbBlk = CWriterUtils::WriteSubBlock(wi, addSbBlkInfo); - this->nextSegmentInfo.SetNextSegmentPos(wi.segmentPos + sizeOfSbBlk /*+ sizeof(SegmentHeader)*/); + const auto sizeOfSbBlk = CWriterUtils::WriteSubBlock(wi, addSbBlkInfo); + this->nextSegmentInfo.SetNextSegmentPos(wi.segmentPos + sizeOfSbBlk); } /*virtual*/void CCziReaderWriter::SyncAddAttachment(const libCZI::AddAttachmentInfo& addAttachmentInfo) @@ -268,16 +269,16 @@ void CCziReaderWriter::Finish() sbBlkDirWriteInfo.segmentPosForNewSegment = this->nextSegmentInfo.GetNextSegmentPos(); sbBlkDirWriteInfo.enumEntriesFunc = [&](const std::function& f)->void - { - this->sbBlkDirectory.EnumEntries( - [&](size_t index, const CCziSubBlockDirectoryBase::SubBlkEntry& e)->bool - { - f(index, e); - return true; - }); - }; + { + this->sbBlkDirectory.EnumEntries( + [&](size_t index, const CCziSubBlockDirectoryBase::SubBlkEntry& e)->bool + { + f(index, e); + return true; + }); + }; sbBlkDirWriteInfo.writeFunc = std::bind(&CCziReaderWriter::WriteToOutputStream, this, placeholders::_1, placeholders::_2, placeholders::_3, placeholders::_4, placeholders::_5); - auto posAndSize = CWriterUtils::WriteSubBlkDirectory(sbBlkDirWriteInfo); + const auto posAndSize = CWriterUtils::WriteSubBlkDirectory(sbBlkDirWriteInfo); this->subBlockDirectorySegment.SetPositionAndAllocatedSize(get<0>(posAndSize), get<1>(posAndSize), false); if (get<0>(posAndSize) == sbBlkDirWriteInfo.segmentPosForNewSegment) { @@ -306,16 +307,16 @@ void CCziReaderWriter::Finish() attchmntDirWriteInfo.segmentPosForNewSegment = this->nextSegmentInfo.GetNextSegmentPos(); attchmntDirWriteInfo.entryCnt = this->attachmentDirectory.GetEntryCnt(); attchmntDirWriteInfo.enumEntriesFunc = [&](const std::function& f)->void - { - this->attachmentDirectory.EnumEntries( - [&](size_t index, const CCziAttachmentsDirectoryBase::AttachmentEntry& e)->bool - { - f(index, e); - return true; - }); - }; + { + this->attachmentDirectory.EnumEntries( + [&](size_t index, const CCziAttachmentsDirectoryBase::AttachmentEntry& e)->bool + { + f(index, e); + return true; + }); + }; attchmntDirWriteInfo.writeFunc = std::bind(&CCziReaderWriter::WriteToOutputStream, this, placeholders::_1, placeholders::_2, placeholders::_3, placeholders::_4, placeholders::_5); - auto posAndSize = CWriterUtils::WriteAttachmentDirectory(attchmntDirWriteInfo); + const auto posAndSize = CWriterUtils::WriteAttachmentDirectory(attchmntDirWriteInfo); this->attachmentDirectorySegment.SetPositionAndAllocatedSize(get<0>(posAndSize), get<1>(posAndSize), false); if (get<0>(posAndSize) == attchmntDirWriteInfo.segmentPosForNewSegment) { @@ -380,7 +381,7 @@ void CCziReaderWriter::ReadCziStructure() if (this->info->GetForceFileGuid()) { // we then immediately update the File-Guid - auto newGuid = this->UpdateFileHeaderGuid(); + const auto newGuid = this->UpdateFileHeaderGuid(); memcpy(&fileHeaderSegment.FileGuid, &newGuid, sizeof(newGuid)); memcpy(&fileHeaderSegment.PrimaryFileGuid, &newGuid, sizeof(newGuid)); } @@ -415,13 +416,14 @@ void CCziReaderWriter::ReadCziStructure() }, &attchmntDirSegmentSize); + this->attachmentDirectory.SetModified(false); this->attachmentDirectorySegment.SetPositionAndAllocatedSize(pos, attchmntDirSegmentSize.AllocatedSize, false); } pos = this->hdrSegmentData.GetMetadataPosition(); if (pos != 0) { - auto segmentSize = CCZIParse::ReadSegmentHeader(CCZIParse::SegmentType::Metadata, this->stream.get(), this->hdrSegmentData.GetMetadataPosition()); + const auto segmentSize = CCZIParse::ReadSegmentHeader(CCZIParse::SegmentType::Metadata, this->stream.get(), this->hdrSegmentData.GetMetadataPosition()); this->metadataSegment.SetPositionAndAllocatedSize(pos, segmentSize.AllocatedSize, false); } } @@ -481,7 +483,7 @@ void CCziReaderWriter::DetermineNextSubBlockOffset() lastSegmentPos = sbBlkEntry.FilePosition; } - return true; + return true; }); this->attachmentDirectory.EnumEntries( @@ -492,7 +494,7 @@ void CCziReaderWriter::DetermineNextSubBlockOffset() lastSegmentPos = attEntry.FilePosition; } - return true; + return true; }); if (this->hdrSegmentData.GetIsSubBlockDirectoryPositionValid()) @@ -640,20 +642,14 @@ void CCziReaderWriter::WriteToOutputStream(std::uint64_t offset, const void* pv, this->sbBlkDirectory.EnumEntries( [&](int index, const CCziSubBlockDirectory::SubBlkEntry& entry)->bool { - SubBlockInfo info; - info.coordinate = entry.coordinate; - info.logicalRect = IntRect{ entry.x,entry.y,entry.width,entry.height }; - info.physicalSize = IntSize{ (std::uint32_t)entry.storedWidth, (std::uint32_t)entry.storedHeight }; - info.mIndex = entry.mIndex; - info.pixelType = CziUtils::PixelTypeFromInt(entry.PixelType); - return funcEnum(index, info); + return funcEnum(index, CziReaderCommon::ConvertToSubBlockInfo(entry)); }); } /*virtual*/void CCziReaderWriter::EnumSubset(const libCZI::IDimCoordinate* planeCoordinate, const libCZI::IntRect* roi, bool onlyLayer0, const std::function& funcEnum) { this->ThrowIfNotOperational(); - throw std::runtime_error("Not Implemented"); + CziReaderCommon::EnumSubset(this, planeCoordinate, roi, onlyLayer0, funcEnum); } /*virtual*/std::shared_ptr CCziReaderWriter::ReadSubBlock(int index) @@ -692,13 +688,7 @@ void CCziReaderWriter::WriteToOutputStream(std::uint64_t offset, const void* pv, if (info != nullptr) { - info->compressionModeRaw = entry.Compression; - info->pixelType = CziUtils::PixelTypeFromInt(entry.PixelType); - info->coordinate = entry.coordinate; - info->logicalRect = IntRect{ entry.x,entry.y,entry.width,entry.height }; - info->physicalSize = IntSize{ static_cast(entry.storedWidth), static_cast(entry.storedHeight) }; - info->mIndex = entry.mIndex; - info->pyramidType = CziUtils::PyramidTypeFromByte(entry.pyramid_type_from_spare); + *info = CziReaderCommon::ConvertToSubBlockInfo(entry); } return true; @@ -707,7 +697,7 @@ void CCziReaderWriter::WriteToOutputStream(std::uint64_t offset, const void* pv, /*virtual*/bool CCziReaderWriter::TryGetSubBlockInfoOfArbitrarySubBlockInChannel(int channelIndex, libCZI::SubBlockInfo& info) { this->ThrowIfNotOperational(); - throw std::runtime_error("Not Implemented"); + return CziReaderCommon::TryGetSubBlockInfoOfArbitrarySubBlockInChannel(this, channelIndex, info); } /*virtual*/libCZI::SubBlockStatistics CCziReaderWriter::GetStatistics() @@ -729,18 +719,22 @@ void CCziReaderWriter::WriteToOutputStream(std::uint64_t offset, const void* pv, [&](int index, const CCziAttachmentsDirectoryBase::AttachmentEntry& entry)->bool { libCZI::AttachmentInfo info; - info.contentGuid = entry.ContentGuid; - memcpy(info.contentFileType, entry.ContentFileType, sizeof(entry.ContentFileType)); - info.name = entry.Name; - bool b = funcEnum(index, info); - return b; + info.contentGuid = entry.ContentGuid; + memcpy(info.contentFileType, entry.ContentFileType, sizeof(entry.ContentFileType)); + info.name = entry.Name; + bool b = funcEnum(index, info); + return b; }); } /*virtual*/void CCziReaderWriter::EnumerateSubset(const char* contentFileType, const char* name, const std::function& funcEnum) { this->ThrowIfNotOperational(); - throw std::runtime_error("Not Implemented"); + CziReaderCommon::EnumerateSubset( + std::bind(&CReaderWriterCziAttachmentsDirectory::EnumEntries, &this->attachmentDirectory, std::placeholders::_1), + contentFileType, + name, + funcEnum); } /*virtual*/std::shared_ptr CCziReaderWriter::ReadAttachment(int index) diff --git a/Src/libCZI/CziWriter.cpp b/Src/libCZI/CziWriter.cpp index 767bd180..5981e920 100644 --- a/Src/libCZI/CziWriter.cpp +++ b/Src/libCZI/CziWriter.cpp @@ -733,10 +733,11 @@ void libCZI::ICziWriter::SyncAddSubBlock(const AddSubBlockInfoStridedBitmap& add std::unique_ptr upAttchmntData((AttachmentEntryA1*)malloc(sizeAttchmntEntries), [](AttachmentEntryA1* p)->void {free(p); }); + size_t index_count = 0; info.enumEntriesFunc( [&](size_t index, const CCziAttachmentsDirectoryBase::AttachmentEntry& entry)->bool { - AttachmentEntryA1* p = (upAttchmntData.get() + index); + AttachmentEntryA1* p = upAttchmntData.get() + index_count; p->SchemaType[0] = 'A'; p->SchemaType[1] = '1'; memset(&p->_spare[0], 0, sizeof(p->_spare)); @@ -747,6 +748,7 @@ void libCZI::ICziWriter::SyncAddSubBlock(const AddSubBlockInfoStridedBitmap& add memcpy(&p->Name[0], &entry.Name[0], sizeof(p->Name)); ConvertToHostByteOrder::Convert(p); + ++index_count; return true; }); diff --git a/Src/libCZI/Doc/version-history.markdown b/Src/libCZI/Doc/version-history.markdown index 0d6c74ef..ca025360 100644 --- a/Src/libCZI/Doc/version-history.markdown +++ b/Src/libCZI/Doc/version-history.markdown @@ -17,3 +17,4 @@ version history {#version_history} 0.57.2 | [90](https://github.com/ZEISS/libczi/pull/90) | improve thread-safety of CziReader 0.57.3 | [91](https://github.com/ZEISS/libczi/pull/91) | improve error-message 0.58.0 | [92](https://github.com/ZEISS/libczi/pull/92) | export a list with properties for streams-property-bag + 0.58.1 | [95](https://github.com/ZEISS/libczi/pull/95) | some fixes for CziReaderWriter \ No newline at end of file diff --git a/Src/libCZI/libCZI_ReadWrite.h b/Src/libCZI/libCZI_ReadWrite.h index 427692c8..d2a19757 100644 --- a/Src/libCZI/libCZI_ReadWrite.h +++ b/Src/libCZI/libCZI_ReadWrite.h @@ -33,7 +33,9 @@ namespace libCZI /// - The indices (or "keys") for a subblock/attachment do not change during the lifetime of the object (even if deleting some). /// - Contrary to ICziWriter, this object does not attempt to verify the consistency of the coordinates - which is due the fact /// that we aim at allowing arbitrary modifications. We do not require to specify in advance the number of dimensions or the bounds. - /// - The information returned by ISubBlockRepository::GetStatistics is valid (taking into consideration the current state). + /// - The information returned by ISubBlockRepository::GetStatistics is valid (taking into consideration the current state). + /// - When enumerating subblocks/attachments, mutations (i.e. adding/removing items) of the respective subblock/attachment-collection are not allowed + /// and result in undefined behavior. class LIBCZI_API ICziReaderWriter : public ISubBlockRepository, public IAttachmentRepository { public: diff --git a/Src/libCZI_UnitTests/test_readerwriter.cpp b/Src/libCZI_UnitTests/test_readerwriter.cpp index 9e2c9414..cf082652 100644 --- a/Src/libCZI_UnitTests/test_readerwriter.cpp +++ b/Src/libCZI_UnitTests/test_readerwriter.cpp @@ -8,6 +8,7 @@ #include "utils.h" #include "MemInputOutputStream.h" #include "SegmentWalker.h" +#include using namespace libCZI; using namespace std; @@ -1349,9 +1350,9 @@ INSTANTIATE_TEST_SUITE_P( CziReaderWriter, SparePyramidTypeFixture, testing::Values( - SubBlockPyramidType::None, - SubBlockPyramidType::SingleSubBlock, - SubBlockPyramidType::MultiSubBlock)); + SubBlockPyramidType::None, + SubBlockPyramidType::SingleSubBlock, + SubBlockPyramidType::MultiSubBlock)); TEST(CziReaderWriter, TryAddingDuplicateAttachmentToCziReaderWriterAndExpectError) { @@ -1387,3 +1388,138 @@ TEST(CziReaderWriter, TryAddingDuplicateAttachmentToCziReaderWriterAndExpectErro add_attachment_info.contentGuid = GUID{ 111, 2, 3, {4, 5, 6, 7, 8, 9, 10, 11} }; reader_writer->SyncAddAttachment(add_attachment_info); } + +TEST(CziReaderWriter, TestEnumerateSubBlocks) +{ + auto testCzi = CreateTestCzi(); + + const auto input_output_stream = make_shared(get<0>(testCzi).get(), get<1>(testCzi)); + const auto reader_writer = CreateCZIReaderWriter(); + reader_writer->Create(input_output_stream); + + vector indices; + reader_writer->EnumerateSubBlocks( + [&](int index, const SubBlockInfo& info)->bool + { + indices.push_back(index); + return true; + }); + + // Check the size + ASSERT_EQ(indices.size(), 50) << "Vector does not contain exactly 50 elements."; + + // Verify each number from 0 to 49 is present + bool allNumbersPresent = true; + for (int i = 0; i < 50; ++i) + { + if (std::find(indices.begin(), indices.end(), i) == indices.end()) + { + allNumbersPresent = false; + break; + } + } + + ASSERT_TRUE(allNumbersPresent) << "Not all numbers from 0 to 49 are present in the vector."; + + indices.clear(); + constexpr auto query_rect = IntRect{ 8,0,8,1 }; + reader_writer->EnumSubset( + nullptr, + &query_rect, + true, + [&](int index, const SubBlockInfo& info)->bool + { + indices.push_back(index); + return true; + }); + + // Check the size + ASSERT_EQ(indices.size(), 20) << "Vector does not contain exactly 20 elements."; + + for (auto it = indices.begin(); it != indices.end(); ++it) + { + // check that the index is not present more than once + ASSERT_FALSE(std::find(it + 1, indices.end(), *it) != indices.end()); + + // check that the subblock is within the query rectangle + SubBlockInfo sub_block_info; + bool b = reader_writer->TryGetSubBlockInfo(*it, &sub_block_info); + ASSERT_TRUE(b); + b = query_rect.IntersectsWith(sub_block_info.logicalRect); + ASSERT_TRUE(b); + } +} + +TEST(CziReaderWriter, AttachmentEnumerateSubset) +{ + auto testCzi = CreateTestCzi2(); + + const auto input_output_stream = make_shared(get<0>(testCzi).get(), get<1>(testCzi)); + const auto reader_writer = CreateCZIReaderWriter(); + reader_writer->Create(input_output_stream); + + // compare the results of EnumerateAttachments and EnumerateSubset (where no subset is specified), + // this should give the same result + vector indices_from_enumerate; + reader_writer->EnumerateAttachments( + [&](int index, const AttachmentInfo& info)->bool + { + indices_from_enumerate.push_back(index); + return true; + }); + + vector indices_from_enumerate_subset; + reader_writer->EnumerateSubset( + nullptr, nullptr, + [&](int index, const AttachmentInfo& info)->bool + { + indices_from_enumerate_subset.push_back(index); + return true; + }); + + // The matcher will check if 'actual' contains all the elements of 'expected', ignoring order + EXPECT_THAT(indices_from_enumerate, ::testing::UnorderedElementsAreArray(indices_from_enumerate_subset)); + + // Now use a condition to filter the attachments (only those with name "ATTACHMENT1" should be found). + // Note that our CZI-document only has one attachment with this name, so the result should be the same as before. + indices_from_enumerate_subset.clear(); + reader_writer->EnumerateSubset( + nullptr, "ATTACHMENT1", + [&](int index, const AttachmentInfo& info)->bool + { + indices_from_enumerate_subset.push_back(index); + return true; + }); + + EXPECT_THAT(indices_from_enumerate, ::testing::UnorderedElementsAreArray(indices_from_enumerate_subset)); + + indices_from_enumerate_subset.clear(); + + // Now use a condition which is not met by any attachment (so the result should be an empty vector). + reader_writer->EnumerateSubset( + nullptr, "XXXXXXXXXXX", + [&](int index, const AttachmentInfo& info)->bool + { + indices_from_enumerate_subset.push_back(index); + return true; + }); + + EXPECT_EQ(indices_from_enumerate_subset.size(), 0); +} + +TEST(CziReaderWriter, TryGetSubBlockInfoOfArbitrarySubBlockInChannel) +{ + auto testCzi = CreateTestCzi2(); + + const auto input_output_stream = make_shared(get<0>(testCzi).get(), get<1>(testCzi)); + const auto reader_writer = CreateCZIReaderWriter(); + reader_writer->Create(input_output_stream); + + SubBlockInfo sub_block_info; + bool b = reader_writer->TryGetSubBlockInfoOfArbitrarySubBlockInChannel(0, sub_block_info); + ASSERT_TRUE(b); + EXPECT_EQ(sub_block_info.pixelType, PixelType::Gray8); + + b = reader_writer->TryGetSubBlockInfoOfArbitrarySubBlockInChannel(1, sub_block_info); + EXPECT_FALSE(b); +}