diff --git a/Modules/Core/TestKernel/src/itkTestDriverIncludeRequiredFactories.cxx b/Modules/Core/TestKernel/src/itkTestDriverIncludeRequiredFactories.cxx index fb470d915fa..97bf29187e6 100644 --- a/Modules/Core/TestKernel/src/itkTestDriverIncludeRequiredFactories.cxx +++ b/Modules/Core/TestKernel/src/itkTestDriverIncludeRequiredFactories.cxx @@ -26,6 +26,7 @@ #include "itkTIFFImageIOFactory.h" #include "itkBMPImageIOFactory.h" #include "itkVTKImageIOFactory.h" +#include "itkVTIImageIOFactory.h" #include "itkNrrdImageIOFactory.h" #include "itkGiplImageIOFactory.h" #include "itkNiftiImageIOFactory.h" @@ -72,6 +73,7 @@ RegisterRequiredIOFactories() itk::ObjectFactoryBase::RegisterFactory(itk::GDCMImageIOFactory::New()); itk::ObjectFactoryBase::RegisterFactory(itk::JPEGImageIOFactory::New()); itk::ObjectFactoryBase::RegisterFactory(itk::VTKImageIOFactory::New()); + itk::ObjectFactoryBase::RegisterFactory(itk::VTIImageIOFactory::New()); itk::ObjectFactoryBase::RegisterFactory(itk::PNGImageIOFactory::New()); itk::ObjectFactoryBase::RegisterFactory(itk::TIFFImageIOFactory::New()); itk::ObjectFactoryBase::RegisterFactory(itk::BMPImageIOFactory::New()); diff --git a/Modules/IO/VTK/include/itkVTIImageIO.h b/Modules/IO/VTK/include/itkVTIImageIO.h new file mode 100644 index 00000000000..6b808084cfc --- /dev/null +++ b/Modules/IO/VTK/include/itkVTIImageIO.h @@ -0,0 +1,178 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ +#ifndef itkVTIImageIO_h +#define itkVTIImageIO_h +#include "ITKIOVTKExport.h" + +#include +#include "itkImageIOBase.h" + +namespace itk +{ +/** + * \class VTIImageIO + * + * \brief ImageIO class for reading and writing VTK XML ImageData (.vti) files. + * + * Supports the VTK XML ImageData format (version 0.1 and 2.2), including + * ASCII, binary (base64-encoded), and raw-appended data formats. + * Scalar, vector (3-component), RGB, RGBA, and symmetric second rank tensor + * pixel types are supported. + * + * The XML structure is parsed using the expat XML library (provided by + * ITKExpat / ITKIOXML), so the parser is robust to attribute ordering, + * whitespace, comments, and CDATA sections. The raw appended data section + * (which is XML-illegal binary content following an `_` marker) is read + * directly from the file at the byte offset recorded by the parser when + * it encountered the `` element. + * + * \ingroup IOFilters + * \ingroup ITKIOVTK + */ +class ITKIOVTK_EXPORT VTIImageIO : public ImageIOBase +{ +public: + ITK_DISALLOW_COPY_AND_MOVE(VTIImageIO); + + /** Standard class type aliases. */ + using Self = VTIImageIO; + using Superclass = ImageIOBase; + using Pointer = SmartPointer; + using ConstPointer = SmartPointer; + + /** Method for creation through the object factory. */ + itkNewMacro(Self); + + /** \see LightObject::GetNameOfClass() */ + itkOverrideGetNameOfClassMacro(VTIImageIO); + + /*-------- This part of the interface deals with reading data. ------ */ + + /** Determine the file type. Returns true if this ImageIO can read the + * file specified. */ + bool + CanReadFile(const char *) override; + + /** Set the spacing and dimension information for the current filename. */ + void + ReadImageInformation() override; + + /** Reads the data from disk into the memory buffer provided. */ + void + Read(void * buffer) override; + + /*-------- This part of the interfaces deals with writing data. ----- */ + + /** Determine the file type. Returns true if this ImageIO can write the + * file specified. */ + bool + CanWriteFile(const char *) override; + + /** Writes the spacing and dimensions of the image. + * Assumes SetFileName has been called with a valid file name. */ + void + WriteImageInformation() override + {} + + /** Writes the data to disk from the memory buffer provided. Make sure + * that the IORegion has been set properly. */ + void + Write(const void * buffer) override; + +protected: + VTIImageIO(); + ~VTIImageIO() override; + + void + PrintSelf(std::ostream & os, Indent indent) const override; + +private: + /** Parse the XML header to fill image information. */ + void + InternalReadImageInformation(); + + /** Map VTK type string to ITK IOComponentEnum. */ + static IOComponentEnum + VTKTypeStringToITKComponent(const std::string & vtkType); + + /** Map ITK IOComponentEnum to VTK type string. */ + static std::string + ITKComponentToVTKTypeString(IOComponentEnum t); + + /** Decode a base64-encoded string into raw bytes. Returns the number + * of decoded bytes. */ + static SizeType + DecodeBase64(const std::string & encoded, std::vector & decoded); + + /** Encode raw bytes as a base64 string. */ + static std::string + EncodeBase64(const unsigned char * data, SizeType numBytes); + + /** Trim leading/trailing whitespace from a string. */ + static std::string + TrimString(const std::string & s); + + // Encoding format of the data array found in the file. + // (Plain comment, not doxygen, to satisfy KWStyle's `\class` rule for + // class-like declarations.) + enum class DataEncoding : std::uint8_t + { + ASCII, + Base64, // binary data encoded in base64 (format="binary") + RawAppended, // raw binary appended data (format="appended" with raw encoding) + ZLibBase64, // zlib-compressed data encoded in base64 + ZLibAppended // zlib-compressed raw appended data + }; + + DataEncoding m_DataEncoding{ DataEncoding::Base64 }; + + /** Cached ASCII data text content captured by the expat parser when the + * active DataArray is in ASCII format. Empty otherwise. */ + std::string m_AsciiDataContent{}; + + /** Cached base64 data text content captured by the expat parser when the + * active DataArray is in base64 ("binary") format. Empty otherwise. */ + std::string m_Base64DataContent{}; + + /** Byte offset into the file where the appended data section begins + * (only relevant when m_DataEncoding == RawAppended). */ + std::streampos m_AppendedDataOffset{ 0 }; + + /** Offset within the appended data block for this DataArray (in bytes, + * not counting the leading block-size UInt32/UInt64). */ + SizeType m_DataArrayOffset{ 0 }; + + /** Whether the header uses 64-bit block-size integers (header_type="UInt64"). */ + bool m_HeaderTypeUInt64{ false }; + + /** Whether the file uses zlib compression (compressor="vtkZLibDataCompressor"). */ + bool m_IsZLibCompressed{ false }; + + /** Decompress a VTK zlib-compressed block sequence into raw bytes. + * The input buffer begins with the VTK multi-block compression header + * (nblocks / uncompressed_blocksize / last_partial_size / compressed_sizes[]). + * Header integers are UInt32 or UInt64 per \a headerUInt64. */ + static void + DecompressZLib(const unsigned char * compressedData, + std::size_t compressedDataSize, + bool headerUInt64, + std::vector & uncompressed); +}; +} // end namespace itk + +#endif // itkVTIImageIO_h diff --git a/Modules/IO/VTK/include/itkVTIImageIOFactory.h b/Modules/IO/VTK/include/itkVTIImageIOFactory.h new file mode 100644 index 00000000000..d0604cf2f5c --- /dev/null +++ b/Modules/IO/VTK/include/itkVTIImageIOFactory.h @@ -0,0 +1,71 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ +#ifndef itkVTIImageIOFactory_h +#define itkVTIImageIOFactory_h +#include "ITKIOVTKExport.h" + +#include "itkObjectFactoryBase.h" +#include "itkImageIOBase.h" + +namespace itk +{ +/** + * \class VTIImageIOFactory + * \brief Create instances of VTIImageIO objects using an object factory. + * \ingroup ITKIOVTK + */ +class ITKIOVTK_EXPORT VTIImageIOFactory : public ObjectFactoryBase +{ +public: + ITK_DISALLOW_COPY_AND_MOVE(VTIImageIOFactory); + + /** Standard class type aliases. */ + using Self = VTIImageIOFactory; + using Superclass = ObjectFactoryBase; + using Pointer = SmartPointer; + using ConstPointer = SmartPointer; + + /** Class Methods used to interface with the registered factories. */ + const char * + GetITKSourceVersion() const override; + + const char * + GetDescription() const override; + + /** Method for class instantiation. */ + itkFactorylessNewMacro(Self); + + /** \see LightObject::GetNameOfClass() */ + itkOverrideGetNameOfClassMacro(VTIImageIOFactory); + + /** Register one factory of this type */ + static void + RegisterOneFactory() + { + auto vtiFactory = VTIImageIOFactory::New(); + + ObjectFactoryBase::RegisterFactoryInternal(vtiFactory); + } + +protected: + VTIImageIOFactory(); + ~VTIImageIOFactory() override; +}; +} // end namespace itk + +#endif diff --git a/Modules/IO/VTK/itk-module.cmake b/Modules/IO/VTK/itk-module.cmake index 9ca8ebbd748..09aac6d3466 100644 --- a/Modules/IO/VTK/itk-module.cmake +++ b/Modules/IO/VTK/itk-module.cmake @@ -1,7 +1,8 @@ set( DOCUMENTATION "This module contains classes for reading and writing image -files in the \"legacy\" (non-XML) VTK file format." +files in the \"legacy\" (non-XML) VTK file format and the VTK XML +ImageData (.vti) file format." ) itk_module( @@ -9,10 +10,15 @@ itk_module( ENABLE_SHARED DEPENDS ITKIOImageBase + PRIVATE_DEPENDS + ITKExpat + ITKZLIB TEST_DEPENDS ITKTestKernel ITKImageSources + ITKZLIB FACTORY_NAMES ImageIO::VTK + ImageIO::VTI DESCRIPTION "${DOCUMENTATION}" ) diff --git a/Modules/IO/VTK/src/CMakeLists.txt b/Modules/IO/VTK/src/CMakeLists.txt index c34014c5dbe..d7fcb92ce3a 100644 --- a/Modules/IO/VTK/src/CMakeLists.txt +++ b/Modules/IO/VTK/src/CMakeLists.txt @@ -2,6 +2,8 @@ set( ITKIOVTK_SRCS itkVTKImageIO.cxx itkVTKImageIOFactory.cxx + itkVTIImageIOFactory.cxx + itkVTIImageIO.cxx ) itk_module_add_library(ITKIOVTK ${ITKIOVTK_SRCS}) diff --git a/Modules/IO/VTK/src/itkVTIImageIO.cxx b/Modules/IO/VTK/src/itkVTIImageIO.cxx new file mode 100644 index 00000000000..61810a529e1 --- /dev/null +++ b/Modules/IO/VTK/src/itkVTIImageIO.cxx @@ -0,0 +1,1213 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ +#include "itkVTIImageIO.h" +#include "itkByteSwapper.h" +#include "itkMakeUniqueForOverwrite.h" + +#include "itk_expat.h" +#include "itk_zlib.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace itk +{ + +// --------------------------------------------------------------------------- +// Base64 lookup tables +// --------------------------------------------------------------------------- +namespace +{ +const char s_B64Chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +const int s_B64Inv[256] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0-15 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 16-31 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, // 32-47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, 0, -1, -1, // 48-63 + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 64-79 + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, // 80-95 + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 96-111 + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, // 112-127 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 128-143 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 144-159 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 160-175 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 176-191 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 192-207 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 208-223 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 224-239 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 // 240-255 +}; + +// Case-insensitive string compare. +bool +IequalsStr(const std::string & a, const std::string & b) +{ + if (a.size() != b.size()) + { + return false; + } + for (std::size_t i = 0; i < a.size(); ++i) + { + if (std::tolower(static_cast(a[i])) != std::tolower(static_cast(b[i]))) + { + return false; + } + } + return true; +} + +// Lower-case a string. +std::string +ToLower(std::string s) +{ + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::tolower(c); }); + return s; +} + +// Look up an attribute value from an expat-style nullptr-terminated +// (key, value, key, value, ..., nullptr) array. Returns empty string if +// not found. +std::string +FindAttribute(const char ** atts, const char * name) +{ + if (atts == nullptr) + { + return {}; + } + for (int i = 0; atts[i] != nullptr; i += 2) + { + if (std::strcmp(atts[i], name) == 0) + { + return std::string(atts[i + 1] != nullptr ? atts[i + 1] : ""); + } + } + return {}; +} + +// --------------------------------------------------------------------------- +// VTI XML parser state. Populated by expat callbacks. +// --------------------------------------------------------------------------- +struct VTIParseState +{ + // + bool sawVTKFile{ false }; + std::string fileType; + std::string byteOrder; + std::string headerType; + std::string compressor; + + // + bool sawImageData{ false }; + std::string wholeExtent; + std::string origin; + std::string spacing; + + // + std::string activeScalars; + std::string activeVectors; + std::string activeTensors; + + // First / active + bool haveDataArray{ false }; + std::string daType; + std::string daName; + std::string daFormat; + std::string daNumberOfComponents; + std::string daOffset; + + // Whether we are currently inside the active DataArray (so character + // data should be appended to either ASCII or base64 buffer). + bool inActiveDataArray{ false }; + bool isAsciiActive{ false }; + bool isBase64Active{ false }; + + std::string asciiContent; + std::string base64Content; + + // Set when the start tag is seen. Used by the caller + // to know that the file has an appended-data block (whose binary + // contents are read directly, not via expat). + bool sawAppendedData{ false }; +}; + +// Determine whether a DataArray (identified by its Name attribute) is the +// "active" one for this file. If no PointData Scalars/Vectors/Tensors +// attribute is set, the very first DataArray is taken as active. +bool +IsActiveDataArray(const VTIParseState & st, const std::string & daName) +{ + const bool noActive = st.activeScalars.empty() && st.activeVectors.empty() && st.activeTensors.empty(); + if (noActive) + { + return true; + } + return (!st.activeScalars.empty() && daName == st.activeScalars) || + (!st.activeVectors.empty() && daName == st.activeVectors) || + (!st.activeTensors.empty() && daName == st.activeTensors); +} + +extern "C" +{ + static void + VTIStartElement(void * userData, const char * name, const char ** atts) + { + auto * st = static_cast(userData); + + if (std::strcmp(name, "VTKFile") == 0) + { + st->sawVTKFile = true; + st->fileType = FindAttribute(atts, "type"); + st->byteOrder = FindAttribute(atts, "byte_order"); + st->headerType = FindAttribute(atts, "header_type"); + st->compressor = FindAttribute(atts, "compressor"); + } + else if (std::strcmp(name, "ImageData") == 0) + { + st->sawImageData = true; + st->wholeExtent = FindAttribute(atts, "WholeExtent"); + st->origin = FindAttribute(atts, "Origin"); + st->spacing = FindAttribute(atts, "Spacing"); + } + else if (std::strcmp(name, "PointData") == 0) + { + st->activeScalars = FindAttribute(atts, "Scalars"); + st->activeVectors = FindAttribute(atts, "Vectors"); + st->activeTensors = FindAttribute(atts, "Tensors"); + } + else if (std::strcmp(name, "DataArray") == 0) + { + const std::string daName = FindAttribute(atts, "Name"); + if (st->haveDataArray) + { + // We have already captured an active DataArray; ignore subsequent + // ones (we read only one image array per file). + return; + } + if (!IsActiveDataArray(*st, daName)) + { + return; + } + st->haveDataArray = true; + st->daType = FindAttribute(atts, "type"); + st->daName = daName; + st->daFormat = ToLower(FindAttribute(atts, "format")); + st->daNumberOfComponents = FindAttribute(atts, "NumberOfComponents"); + st->daOffset = FindAttribute(atts, "offset"); + + st->inActiveDataArray = true; + st->isAsciiActive = (st->daFormat == "ascii"); + st->isBase64Active = (st->daFormat == "binary"); // VTK XML "binary" == base64 + } + else if (std::strcmp(name, "AppendedData") == 0) + { + st->sawAppendedData = true; + // We do not consume any character data from inside AppendedData via + // expat -- the binary content following the `_` marker is XML-illegal + // and is read directly from the file by the caller. We don't even + // get this far on a real raw-appended file because the binary bytes + // would have caused a parser error before reaching this start tag. + // The caller pre-truncates the XML at to handle that. + } + } + + static void + VTIEndElement(void * userData, const char * name) + { + auto * st = static_cast(userData); + if (std::strcmp(name, "DataArray") == 0) + { + st->inActiveDataArray = false; + st->isAsciiActive = false; + st->isBase64Active = false; + } + } + + static void + VTICharData(void * userData, const char * data, int length) + { + auto * st = static_cast(userData); + if (!st->inActiveDataArray) + { + return; + } + if (st->isAsciiActive) + { + st->asciiContent.append(data, static_cast(length)); + } + else if (st->isBase64Active) + { + st->base64Content.append(data, static_cast(length)); + } + } +} // extern "C" + +// Read entire file into a string. +std::string +SlurpFile(const std::string & path) +{ + std::ifstream file(path.c_str(), std::ios::in | std::ios::binary); + if (!file.is_open()) + { + return {}; + } + std::ostringstream ss; + ss << file.rdbuf(); + return ss.str(); +} + +} // end anonymous namespace + +// --------------------------------------------------------------------------- +VTIImageIO::VTIImageIO() +{ + this->SetNumberOfDimensions(3); + m_ByteOrder = IOByteOrderEnum::LittleEndian; + m_FileType = IOFileEnum::Binary; + + this->AddSupportedReadExtension(".vti"); + this->AddSupportedWriteExtension(".vti"); +} + +VTIImageIO::~VTIImageIO() = default; + +// --------------------------------------------------------------------------- +void +VTIImageIO::PrintSelf(std::ostream & os, Indent indent) const +{ + Superclass::PrintSelf(os, indent); +} + +// --------------------------------------------------------------------------- +bool +VTIImageIO::CanReadFile(const char * filename) +{ + if (!this->HasSupportedReadExtension(filename)) + { + return false; + } + + std::ifstream file(filename, std::ios::in); + if (!file.is_open()) + { + return false; + } + + // Read first 256 bytes and look for VTKFile + ImageData. + char buf[256]{}; + file.read(buf, 255); + const std::string header(buf); + return header.find("VTKFile") != std::string::npos && header.find("ImageData") != std::string::npos; +} + +// --------------------------------------------------------------------------- +bool +VTIImageIO::CanWriteFile(const char * filename) +{ + return this->HasSupportedWriteExtension(filename); +} + +// --------------------------------------------------------------------------- +// Static helpers +// --------------------------------------------------------------------------- + +std::string +VTIImageIO::TrimString(const std::string & s) +{ + const std::size_t start = s.find_first_not_of(" \t\r\n"); + if (start == std::string::npos) + { + return {}; + } + const std::size_t end = s.find_last_not_of(" \t\r\n"); + return s.substr(start, end - start + 1); +} + +VTIImageIO::SizeType +VTIImageIO::DecodeBase64(const std::string & encoded, std::vector & decoded) +{ + decoded.clear(); + decoded.reserve((encoded.size() / 4) * 3 + 4); + + int bits = 0; + int val = 0; + for (unsigned char c : encoded) + { + if (c == '=' || std::isspace(c)) + { + continue; + } + const int d = s_B64Inv[c]; + if (d == -1) + { + continue; + } + val = (val << 6) | d; + bits += 6; + if (bits >= 8) + { + bits -= 8; + decoded.push_back(static_cast((val >> bits) & 0xFF)); + } + } + return static_cast(decoded.size()); +} + +std::string +VTIImageIO::EncodeBase64(const unsigned char * data, SizeType numBytes) +{ + std::string out; + out.reserve(((numBytes + 2) / 3) * 4 + 1); + + for (SizeType i = 0; i < numBytes; i += 3) + { + const unsigned int b0 = data[i]; + const unsigned int b1 = (i + 1 < numBytes) ? data[i + 1] : 0u; + const unsigned int b2 = (i + 2 < numBytes) ? data[i + 2] : 0u; + out += s_B64Chars[(b0 >> 2) & 0x3F]; + out += s_B64Chars[((b0 << 4) | (b1 >> 4)) & 0x3F]; + out += (i + 1 < numBytes) ? s_B64Chars[((b1 << 2) | (b2 >> 6)) & 0x3F] : '='; + out += (i + 2 < numBytes) ? s_B64Chars[b2 & 0x3F] : '='; + } + return out; +} + +VTIImageIO::IOComponentEnum +VTIImageIO::VTKTypeStringToITKComponent(const std::string & vtkType) +{ + const std::string t = ToLower(vtkType); + if (t == "int8" || t == "char") + { + return IOComponentEnum::CHAR; + } + if (t == "uint8" || t == "unsigned_char") + { + return IOComponentEnum::UCHAR; + } + if (t == "int16" || t == "short") + { + return IOComponentEnum::SHORT; + } + if (t == "uint16" || t == "unsigned_short") + { + return IOComponentEnum::USHORT; + } + if (t == "int32" || t == "int") + { + return IOComponentEnum::INT; + } + if (t == "uint32" || t == "unsigned_int") + { + return IOComponentEnum::UINT; + } + if (t == "int64" || t == "long" || t == "vtktypeint64") + { + return IOComponentEnum::LONGLONG; + } + if (t == "uint64" || t == "unsigned_long" || t == "vtktypeuint64") + { + return IOComponentEnum::ULONGLONG; + } + if (t == "float32" || t == "float") + { + return IOComponentEnum::FLOAT; + } + if (t == "float64" || t == "double") + { + return IOComponentEnum::DOUBLE; + } + return IOComponentEnum::UNKNOWNCOMPONENTTYPE; +} + +std::string +VTIImageIO::ITKComponentToVTKTypeString(IOComponentEnum t) +{ + switch (t) + { + case IOComponentEnum::CHAR: + return "Int8"; + case IOComponentEnum::UCHAR: + return "UInt8"; + case IOComponentEnum::SHORT: + return "Int16"; + case IOComponentEnum::USHORT: + return "UInt16"; + case IOComponentEnum::INT: + return "Int32"; + case IOComponentEnum::UINT: + return "UInt32"; + case IOComponentEnum::LONG: + return "Int64"; + case IOComponentEnum::ULONG: + return "UInt64"; + case IOComponentEnum::LONGLONG: + return "Int64"; + case IOComponentEnum::ULONGLONG: + return "UInt64"; + case IOComponentEnum::FLOAT: + return "Float32"; + case IOComponentEnum::DOUBLE: + return "Float64"; + default: + { + ExceptionObject e_(__FILE__, __LINE__, "Unsupported component type for VTI writing.", ITK_LOCATION); + throw e_; + } + } +} + +// --------------------------------------------------------------------------- +// ReadImageInformation +// --------------------------------------------------------------------------- +void +VTIImageIO::InternalReadImageInformation() +{ + // Reset cached parser results. + m_AsciiDataContent.clear(); + m_Base64DataContent.clear(); + m_AppendedDataOffset = 0; + m_DataArrayOffset = 0; + m_HeaderTypeUInt64 = false; + m_IsZLibCompressed = false; + + // Slurp the entire file. We need both the XML metadata (parsed by expat) + // and, for raw-appended files, the byte offset of the binary section. + const std::string content = SlurpFile(m_FileName); + if (content.empty()) + { + itkExceptionMacro("Cannot open or read file: " << m_FileName); + } + + // The VTK XML appended-data section embeds raw binary AFTER an `_` marker + // inside an `` element. These bytes are XML-illegal, so we + // hand expat only the prefix of the file up to and including the + // `` start tag, and read the binary block directly. + // + // We also record the absolute byte offset (in the original file) of the + // first byte after `_`, which is where the appended block begins. + std::string xmlPortion; + const std::size_t appPos = content.find(" start tag. + const std::size_t startTagEnd = content.find('>', appPos); + if (startTagEnd == std::string::npos) + { + itkExceptionMacro("Malformed tag in file: " << m_FileName); + } + + // Construct an XML view that ends with a self-closing + // and a closing , so expat sees a well-formed document. + xmlPortion = content.substr(0, appPos) + ""; + + // Locate the `_` marker that introduces the raw binary stream. + const std::size_t underscorePos = content.find('_', startTagEnd); + if (underscorePos == std::string::npos) + { + itkExceptionMacro("Missing `_` marker in section of: " << m_FileName); + } + m_AppendedDataOffset = static_cast(underscorePos + 1); + } + + // Run expat over the XML portion. + VTIParseState st; + XML_Parser parser = XML_ParserCreate(nullptr); + if (parser == nullptr) + { + itkExceptionMacro("Failed to create expat XML parser."); + } + + XML_SetUserData(parser, &st); + XML_SetElementHandler(parser, &VTIStartElement, &VTIEndElement); + XML_SetCharacterDataHandler(parser, &VTICharData); + + const auto parseResult = XML_Parse(parser, xmlPortion.c_str(), static_cast(xmlPortion.size()), /*isFinal=*/1); + if (parseResult == 0) + { + const std::string err = XML_ErrorString(XML_GetErrorCode(parser)); + XML_ParserFree(parser); + itkExceptionMacro("XML parse error in " << m_FileName << ": " << err); + } + XML_ParserFree(parser); + + // ---- Validate captured XML ------------------------------------------ + if (!st.sawVTKFile) + { + itkExceptionMacro("Not a valid VTK XML file (missing element): " << m_FileName); + } + if (!IequalsStr(st.fileType, "ImageData")) + { + itkExceptionMacro("VTK XML file is not of type ImageData: " << m_FileName); + } + if (!st.sawImageData) + { + itkExceptionMacro("Missing element in file: " << m_FileName); + } + if (!st.haveDataArray) + { + itkExceptionMacro("No DataArray element found in file: " << m_FileName); + } + + // Byte order + if (ToLower(st.byteOrder) == "bigendian") + { + m_ByteOrder = IOByteOrderEnum::BigEndian; + } + else + { + m_ByteOrder = IOByteOrderEnum::LittleEndian; + } + + // Header type + m_HeaderTypeUInt64 = (ToLower(st.headerType) == "uint64"); + + // Geometry + if (st.wholeExtent.empty()) + { + itkExceptionMacro("Missing WholeExtent attribute in : " << m_FileName); + } + int extents[6] = { 0, 0, 0, 0, 0, 0 }; + { + std::istringstream extStream(st.wholeExtent); + for (int & ext : extents) + { + extStream >> ext; + } + } + double origin[3] = { 0.0, 0.0, 0.0 }; + if (!st.origin.empty()) + { + std::istringstream os2(st.origin); + os2 >> origin[0] >> origin[1] >> origin[2]; + } + double spacing[3] = { 1.0, 1.0, 1.0 }; + if (!st.spacing.empty()) + { + std::istringstream spStr(st.spacing); + spStr >> spacing[0] >> spacing[1] >> spacing[2]; + } + + const int nx = extents[1] - extents[0] + 1; + const int ny = extents[3] - extents[2] + 1; + const int nz = extents[5] - extents[4] + 1; + + if (nz <= 1 && ny <= 1) + { + this->SetNumberOfDimensions(1); + } + else if (nz <= 1) + { + this->SetNumberOfDimensions(2); + } + else + { + this->SetNumberOfDimensions(3); + } + + this->SetDimensions(0, static_cast(nx)); + if (this->GetNumberOfDimensions() > 1) + { + this->SetDimensions(1, static_cast(ny)); + } + if (this->GetNumberOfDimensions() > 2) + { + this->SetDimensions(2, static_cast(nz)); + } + for (unsigned int i = 0; i < this->GetNumberOfDimensions(); ++i) + { + this->SetSpacing(i, spacing[i]); + this->SetOrigin(i, origin[i]); + } + + // Component type + const IOComponentEnum compType = VTKTypeStringToITKComponent(st.daType); + if (compType == IOComponentEnum::UNKNOWNCOMPONENTTYPE) + { + itkExceptionMacro("Unknown VTK DataArray type '" << st.daType << "' in file: " << m_FileName); + } + this->SetComponentType(compType); + + // Number of components + const unsigned int numComp = + st.daNumberOfComponents.empty() ? 1u : static_cast(std::stoul(st.daNumberOfComponents)); + this->SetNumberOfComponents(numComp); + + // Pixel type, derived from the active PointData attribute and component count + const bool isTensor = !st.activeTensors.empty() && st.daName == st.activeTensors; + const bool isVector = !st.activeVectors.empty() && st.daName == st.activeVectors; + if (isTensor) + { + // VTK tensors are 3x3 = 9 components on disk; ITK uses 6 (symmetric). + this->SetPixelType(IOPixelEnum::SYMMETRICSECONDRANKTENSOR); + this->SetNumberOfComponents(6); + } + else if (isVector) + { + this->SetPixelType(IOPixelEnum::VECTOR); + } + else if (numComp == 1) + { + this->SetPixelType(IOPixelEnum::SCALAR); + } + else if (numComp == 3) + { + this->SetPixelType(IOPixelEnum::RGB); + } + else if (numComp == 4) + { + this->SetPixelType(IOPixelEnum::RGBA); + } + else + { + this->SetPixelType(IOPixelEnum::VECTOR); + } + + // Compression + m_IsZLibCompressed = IequalsStr(st.compressor, "vtkZLibDataCompressor"); + + // Encoding + if (st.daFormat == "ascii") + { + m_DataEncoding = DataEncoding::ASCII; + m_FileType = IOFileEnum::ASCII; + m_AsciiDataContent = std::move(st.asciiContent); + } + else if (st.daFormat == "appended") + { + if (!st.sawAppendedData) + { + itkExceptionMacro( + "DataArray uses format=\"appended\" but no element was found in: " << m_FileName); + } + m_DataEncoding = m_IsZLibCompressed ? DataEncoding::ZLibAppended : DataEncoding::RawAppended; + m_FileType = IOFileEnum::Binary; + m_DataArrayOffset = st.daOffset.empty() ? 0u : static_cast(std::stoull(st.daOffset)); + } + else // "binary" (base64) or unspecified, defaulting to binary + { + m_DataEncoding = m_IsZLibCompressed ? DataEncoding::ZLibBase64 : DataEncoding::Base64; + m_FileType = IOFileEnum::Binary; + m_Base64DataContent = std::move(st.base64Content); + } +} + +void +VTIImageIO::ReadImageInformation() +{ + this->InternalReadImageInformation(); +} + +namespace +{ +// Byte-swap a buffer in-place to/from little-endian based on the file's +// byte order and the system byte order. +void +SwapBufferIfNeeded(void * buffer, std::size_t componentSize, std::size_t numComponents, IOByteOrderEnum fileOrder) +{ + const bool fileBigEndian = (fileOrder == IOByteOrderEnum::BigEndian); + const bool sysBigEndian = ByteSwapper::SystemIsBigEndian(); + if (fileBigEndian == sysBigEndian) + { + return; + } + switch (componentSize) + { + case 1: + break; + case 2: + ByteSwapper::SwapRangeFromSystemToBigEndian(static_cast(buffer), numComponents); + break; + case 4: + ByteSwapper::SwapRangeFromSystemToBigEndian(static_cast(buffer), numComponents); + break; + case 8: + ByteSwapper::SwapRangeFromSystemToBigEndian(static_cast(buffer), numComponents); + break; + default: + { + ExceptionObject e_(__FILE__, __LINE__, "Unknown component size for byte swap.", ITK_LOCATION); + throw e_; + } + } +} +} // anonymous namespace + +// --------------------------------------------------------------------------- +// Read +// --------------------------------------------------------------------------- +void +VTIImageIO::DecompressZLib(const unsigned char * compressedData, + std::size_t compressedDataSize, + bool headerUInt64, + std::vector & uncompressed) +{ + // VTK zlib compressed block layout: + // [nblocks] UInt32 or UInt64 + // [uncompressed_blocksize] UInt32 or UInt64 (size of each full block) + // [last_partial_blocksize] UInt32 or UInt64 (0 means last block is full) + // [compressed_size_0] UInt32 or UInt64 + // [compressed_size_1] UInt32 or UInt64 + // ... + // [compressed_data_0][compressed_data_1]... + const std::size_t hdrItemSize = headerUInt64 ? sizeof(uint64_t) : sizeof(uint32_t); + + auto readHeader = [&](std::size_t offset) -> uint64_t { + if (headerUInt64) + { + uint64_t v; + std::memcpy(&v, compressedData + offset, sizeof(v)); + return v; + } + else + { + uint32_t v; + std::memcpy(&v, compressedData + offset, sizeof(v)); + return static_cast(v); + } + }; + + const uint64_t nblocks = readHeader(0); + const uint64_t uncompBlockSize = readHeader(hdrItemSize); + const uint64_t lastPartialSize = readHeader(2 * hdrItemSize); + + // Compute total uncompressed size + const uint64_t lastBlockUncompSize = (lastPartialSize == 0) ? uncompBlockSize : lastPartialSize; + const uint64_t totalUncompSize = (nblocks > 1 ? (nblocks - 1) * uncompBlockSize : 0) + lastBlockUncompSize; + + uncompressed.resize(static_cast(totalUncompSize)); + + // Compressed block sizes start at offset 3*hdrItemSize + const std::size_t blockSizesOffset = 3 * hdrItemSize; + + // Compressed data starts after the header (3 + nblocks) items + std::size_t dataOffset = static_cast((3 + nblocks) * hdrItemSize); + std::size_t uncompOffset = 0; + + for (uint64_t b = 0; b < nblocks; ++b) + { + const uint64_t compSize = readHeader(blockSizesOffset + b * hdrItemSize); + const uint64_t thisUncompSize = (b == nblocks - 1) ? lastBlockUncompSize : uncompBlockSize; + + if (dataOffset + static_cast(compSize) > compressedDataSize) + { + ExceptionObject e_(__FILE__, __LINE__, "ZLib compressed block extends beyond buffer.", ITK_LOCATION); + throw e_; + } + + uLongf destLen = static_cast(thisUncompSize); + const int ret = uncompress(reinterpret_cast(uncompressed.data() + uncompOffset), + &destLen, + reinterpret_cast(compressedData + dataOffset), + static_cast(compSize)); + if (ret != Z_OK) + { + ExceptionObject e_(__FILE__, __LINE__, "zlib uncompress failed for VTI block.", ITK_LOCATION); + throw e_; + } + + dataOffset += static_cast(compSize); + uncompOffset += static_cast(destLen); + } +} + +void +VTIImageIO::Read(void * buffer) +{ + const SizeType totalComponents = this->GetImageSizeInComponents(); + const SizeType totalBytes = this->GetImageSizeInBytes(); + const SizeType componentSize = this->GetComponentSize(); + + if (m_DataEncoding == DataEncoding::ASCII) + { + if (m_AsciiDataContent.empty()) + { + itkExceptionMacro("ASCII DataArray content is empty in file: " << m_FileName); + } + std::istringstream is(m_AsciiDataContent); + this->ReadBufferAsASCII(is, buffer, this->GetComponentType(), totalComponents); + return; + } + + if (m_DataEncoding == DataEncoding::Base64 || m_DataEncoding == DataEncoding::ZLibBase64) + { + if (m_Base64DataContent.empty()) + { + itkExceptionMacro("Base64 DataArray content is empty in file: " << m_FileName); + } + std::vector decoded; + DecodeBase64(m_Base64DataContent, decoded); + + if (m_DataEncoding == DataEncoding::ZLibBase64) + { + // Compressed: the decoded bytes ARE the compression header + compressed blocks. + std::vector uncompressed; + DecompressZLib(decoded.data(), decoded.size(), m_HeaderTypeUInt64, uncompressed); + if (uncompressed.size() < static_cast(totalBytes)) + { + itkExceptionMacro("Decompressed data size (" << uncompressed.size() << ") is less than expected (" << totalBytes + << ") in file: " << m_FileName); + } + std::memcpy(buffer, uncompressed.data(), static_cast(totalBytes)); + } + else + { + // Uncompressed base64: VTK binary DataArrays are prefixed with a block header + // containing the number of bytes of data. Header is UInt32 or UInt64. + const std::size_t headerBytes = m_HeaderTypeUInt64 ? sizeof(uint64_t) : sizeof(uint32_t); + if (decoded.size() <= headerBytes) + { + itkExceptionMacro("Decoded base64 data is too short in file: " << m_FileName); + } + const unsigned char * dataPtr = decoded.data() + headerBytes; + const std::size_t dataSize = decoded.size() - headerBytes; + if (dataSize < static_cast(totalBytes)) + { + itkExceptionMacro("Decoded data size (" << dataSize << ") is less than expected (" << totalBytes + << ") in file: " << m_FileName); + } + std::memcpy(buffer, dataPtr, static_cast(totalBytes)); + } + SwapBufferIfNeeded(buffer, componentSize, totalComponents, m_ByteOrder); + return; + } + + // RawAppended path (compressed or uncompressed): seek into the file. + std::ifstream file(m_FileName.c_str(), std::ios::in | std::ios::binary); + if (!file.is_open()) + { + itkExceptionMacro("Cannot open file for reading: " << m_FileName); + } + + const std::size_t headerBytes = m_HeaderTypeUInt64 ? sizeof(uint64_t) : sizeof(uint32_t); + const std::streampos readPos = m_AppendedDataOffset + static_cast(m_DataArrayOffset); + + file.seekg(readPos, std::ios::beg); + if (file.fail()) + { + itkExceptionMacro("Failed to seek to data position in file: " << m_FileName); + } + + if (m_DataEncoding == DataEncoding::ZLibAppended) + { + // Read the full compressed block sequence into memory. + // We need to read the nblocks field first to know how big the header is, + // then read all the compressed blocks. + std::vector firstItem(headerBytes); + file.read(reinterpret_cast(firstItem.data()), static_cast(headerBytes)); + if (file.fail()) + { + itkExceptionMacro("Failed to read zlib compression header from file: " << m_FileName); + } + uint64_t nblocks; + if (m_HeaderTypeUInt64) + { + std::memcpy(&nblocks, firstItem.data(), sizeof(uint64_t)); + } + else + { + uint32_t nb32; + std::memcpy(&nb32, firstItem.data(), sizeof(uint32_t)); + nblocks = nb32; + } + + // Read the rest of the header: uncompressed_blocksize, last_partial_blocksize, + // plus nblocks compressed sizes. + const std::size_t remainingHeaderBytes = static_cast((2 + nblocks) * headerBytes); + std::vector headerBuf(headerBytes + remainingHeaderBytes); + std::memcpy(headerBuf.data(), firstItem.data(), headerBytes); + file.read(reinterpret_cast(headerBuf.data() + headerBytes), + static_cast(remainingHeaderBytes)); + if (file.fail()) + { + itkExceptionMacro("Failed to read zlib compression block sizes from file: " << m_FileName); + } + + // Sum the compressed block sizes to know how many bytes of payload to read. + uint64_t totalCompressed = 0; + for (uint64_t b = 0; b < nblocks; ++b) + { + if (m_HeaderTypeUInt64) + { + uint64_t cs; + std::memcpy(&cs, headerBuf.data() + (3 + b) * sizeof(uint64_t), sizeof(uint64_t)); + totalCompressed += cs; + } + else + { + uint32_t cs; + std::memcpy(&cs, headerBuf.data() + (3 + b) * sizeof(uint32_t), sizeof(uint32_t)); + totalCompressed += cs; + } + } + + // Read compressed payload. + std::vector compressedPayload(static_cast(totalCompressed)); + file.read(reinterpret_cast(compressedPayload.data()), static_cast(totalCompressed)); + if (file.fail()) + { + itkExceptionMacro("Failed to read zlib compressed data from file: " << m_FileName); + } + + // Build the full buffer that DecompressZLib expects: header + payload. + std::vector fullBuf(headerBuf.size() + compressedPayload.size()); + std::memcpy(fullBuf.data(), headerBuf.data(), headerBuf.size()); + std::memcpy(fullBuf.data() + headerBuf.size(), compressedPayload.data(), compressedPayload.size()); + + std::vector uncompressed; + DecompressZLib(fullBuf.data(), fullBuf.size(), m_HeaderTypeUInt64, uncompressed); + if (uncompressed.size() < static_cast(totalBytes)) + { + itkExceptionMacro("Decompressed data size (" << uncompressed.size() << ") is less than expected (" << totalBytes + << ") in file: " << m_FileName); + } + std::memcpy(buffer, uncompressed.data(), static_cast(totalBytes)); + SwapBufferIfNeeded(buffer, componentSize, totalComponents, m_ByteOrder); + return; + } + + // Plain RawAppended: skip the block-size header and read directly. + file.seekg(static_cast(headerBytes), std::ios::cur); + if (file.fail()) + { + itkExceptionMacro("Failed to seek past block header in file: " << m_FileName); + } + + file.read(static_cast(buffer), static_cast(totalBytes)); + if (file.fail()) + { + itkExceptionMacro("Failed to read raw appended data from file: " << m_FileName); + } + + SwapBufferIfNeeded(buffer, componentSize, totalComponents, m_ByteOrder); +} + +// --------------------------------------------------------------------------- +// Write +// --------------------------------------------------------------------------- +void +VTIImageIO::Write(const void * buffer) +{ + const unsigned int numDims = this->GetNumberOfDimensions(); + if (numDims < 1 || numDims > 3) + { + itkExceptionMacro("VTIImageIO can only write 1, 2 or 3-dimensional images"); + } + + const auto nx = static_cast(this->GetDimensions(0)); + const auto ny = (numDims > 1) ? static_cast(this->GetDimensions(1)) : 1u; + const auto nz = (numDims > 2) ? static_cast(this->GetDimensions(2)) : 1u; + + const double ox = this->GetOrigin(0); + const double oy = (numDims > 1) ? this->GetOrigin(1) : 0.0; + const double oz = (numDims > 2) ? this->GetOrigin(2) : 0.0; + + const double sx = this->GetSpacing(0); + const double sy = (numDims > 1) ? this->GetSpacing(1) : 1.0; + const double sz = (numDims > 2) ? this->GetSpacing(2) : 1.0; + + const SizeType totalBytes = this->GetImageSizeInBytes(); + const SizeType totalComponents = this->GetImageSizeInComponents(); + + // Determine attribute name and type + const IOPixelEnum pixelType = this->GetPixelType(); + const unsigned int numComp = this->GetNumberOfComponents(); + const IOComponentEnum compType = this->GetComponentType(); + const std::string vtkType = ITKComponentToVTKTypeString(compType); + + std::string attributeElement; + std::string dataArrayName; + std::string pointDataAttr; + if (pixelType == IOPixelEnum::SYMMETRICSECONDRANKTENSOR) + { + dataArrayName = "tensors"; + pointDataAttr = "Tensors=\"tensors\""; + // VTK tensors on disk are full 3x3 (9 components per pixel) but binary + // writing currently only supports the ASCII path which expands the + // symmetric (6-component) layout to 9. Binary writing of tensors is + // disallowed below. + attributeElement = "NumberOfComponents=\"9\""; + } + else if (pixelType == IOPixelEnum::VECTOR && numComp == 3) + { + dataArrayName = "vectors"; + pointDataAttr = "Vectors=\"vectors\""; + std::ostringstream tmp; + tmp << "NumberOfComponents=\"" << numComp << "\""; + attributeElement = tmp.str(); + } + else if ((pixelType == IOPixelEnum::RGB && numComp == 3) || (pixelType == IOPixelEnum::RGBA && numComp == 4)) + { + dataArrayName = "scalars"; + pointDataAttr = "Scalars=\"scalars\""; + std::ostringstream tmp; + tmp << "NumberOfComponents=\"" << numComp << "\""; + attributeElement = tmp.str(); + } + else + { + dataArrayName = "scalars"; + pointDataAttr = "Scalars=\"scalars\""; + if (numComp > 1) + { + std::ostringstream tmp; + tmp << "NumberOfComponents=\"" << numComp << "\""; + attributeElement = tmp.str(); + } + } + + // Refuse to write tensors in binary form because the on-disk layout + // (NumberOfComponents="9") and the in-memory ITK layout (6) disagree; + // we'd silently produce a corrupt file. + if (pixelType == IOPixelEnum::SYMMETRICSECONDRANKTENSOR && m_FileType != IOFileEnum::ASCII) + { + itkExceptionMacro("VTIImageIO does not yet support binary writing of " + "SymmetricSecondRankTensor pixel types; set FileType to ASCII."); + } + + // Prepare a byte-swapped copy if the system is big-endian (we always + // write little-endian binary). + const char * dataToWrite = static_cast(buffer); + std::vector swapBuf; + + const bool needsSwap = + ByteSwapper::SystemIsBigEndian() && this->GetComponentSize() > 1 && m_FileType != IOFileEnum::ASCII; + if (needsSwap) + { + swapBuf.resize(static_cast(totalBytes)); + std::memcpy(swapBuf.data(), buffer, static_cast(totalBytes)); + switch (this->GetComponentSize()) + { + case 2: + ByteSwapper::SwapRangeFromSystemToBigEndian(reinterpret_cast(swapBuf.data()), + totalComponents); + break; + case 4: + ByteSwapper::SwapRangeFromSystemToBigEndian(reinterpret_cast(swapBuf.data()), + totalComponents); + break; + case 8: + ByteSwapper::SwapRangeFromSystemToBigEndian(reinterpret_cast(swapBuf.data()), + totalComponents); + break; + default: + break; + } + dataToWrite = reinterpret_cast(swapBuf.data()); + } + + std::ofstream file(m_FileName.c_str(), std::ios::out | std::ios::binary | std::ios::trunc); + if (!file.is_open()) + { + itkExceptionMacro("Cannot open file for writing: " << m_FileName); + } + + file.precision(16); + + const std::string byteOrderStr = ByteSwapper::SystemIsBigEndian() ? "BigEndian" : "LittleEndian"; + + // XML header + file << "\n"; + file << "\n"; + file << " \n"; + file << " \n"; + file << " \n"; + + if (m_FileType == IOFileEnum::ASCII) + { + file << " \n"; + + if (pixelType == IOPixelEnum::SYMMETRICSECONDRANKTENSOR) + { + // Expand symmetric tensor (6 components) to full 3x3 = 9 for VTK. + // Layout convention: e11 e12 e13 e22 e23 e33. + const SizeType numPixels = totalComponents / 6; + for (SizeType p = 0; p < numPixels; ++p) + { + if (compType == IOComponentEnum::FLOAT) + { + const float * fPtr = static_cast(buffer) + p * 6; + file << fPtr[0] << ' ' << fPtr[1] << ' ' << fPtr[2] << '\n'; + file << fPtr[1] << ' ' << fPtr[3] << ' ' << fPtr[4] << '\n'; + file << fPtr[2] << ' ' << fPtr[4] << ' ' << fPtr[5] << '\n'; + } + else + { + const double * dPtr = static_cast(buffer) + p * 6; + file << dPtr[0] << ' ' << dPtr[1] << ' ' << dPtr[2] << '\n'; + file << dPtr[1] << ' ' << dPtr[3] << ' ' << dPtr[4] << '\n'; + file << dPtr[2] << ' ' << dPtr[4] << ' ' << dPtr[5] << '\n'; + } + } + } + else + { + this->WriteBufferAsASCII(file, buffer, compType, totalComponents); + } + + file << "\n \n"; + } + else // Binary (base64) + { + file << " \n"; + + // Prepend a UInt32 block-size header (number of raw data bytes). + const auto blockSize = static_cast(totalBytes); + std::vector toEncode(sizeof(blockSize) + static_cast(totalBytes)); + std::memcpy(toEncode.data(), &blockSize, sizeof(blockSize)); + std::memcpy(toEncode.data() + sizeof(blockSize), dataToWrite, static_cast(totalBytes)); + + file << " " << EncodeBase64(toEncode.data(), static_cast(toEncode.size())) << "\n"; + file << " \n"; + } + + file << " \n"; + file << " \n"; + file << " \n"; + file << "\n"; + + if (file.fail()) + { + itkExceptionMacro("Failed to write VTI file: " << m_FileName); + } +} + +} // end namespace itk diff --git a/Modules/IO/VTK/src/itkVTIImageIOFactory.cxx b/Modules/IO/VTK/src/itkVTIImageIOFactory.cxx new file mode 100644 index 00000000000..0b074d9bb32 --- /dev/null +++ b/Modules/IO/VTK/src/itkVTIImageIOFactory.cxx @@ -0,0 +1,52 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ +#include "itkVTIImageIOFactory.h" +#include "itkVTIImageIO.h" +#include "itkVersion.h" + +namespace itk +{ +VTIImageIOFactory::VTIImageIOFactory() +{ + this->RegisterOverride( + "itkImageIOBase", "itkVTIImageIO", "VTI Image IO", true, CreateObjectFunction::New()); +} + +VTIImageIOFactory::~VTIImageIOFactory() = default; + +const char * +VTIImageIOFactory::GetITKSourceVersion() const +{ + return ITK_SOURCE_VERSION; +} + +const char * +VTIImageIOFactory::GetDescription() const +{ + return "VTI ImageIO Factory, allows the loading of VTK XML ImageData (.vti) images into ITK"; +} + +// Undocumented API used to register during static initialization. +// DO NOT CALL DIRECTLY. +void ITKIOVTK_EXPORT +VTIImageIOFactoryRegister__Private() +{ + ObjectFactoryBase::RegisterInternalFactoryOnce(); +} + +} // end namespace itk diff --git a/Modules/IO/VTK/test/CMakeLists.txt b/Modules/IO/VTK/test/CMakeLists.txt index c0f417cd0c0..bd7d81279f2 100644 --- a/Modules/IO/VTK/test/CMakeLists.txt +++ b/Modules/IO/VTK/test/CMakeLists.txt @@ -8,10 +8,80 @@ set( itkVTKImageIOTest.cxx itkVTKImageIOTest2.cxx itkVTKImageIOTest3.cxx + itkVTIImageIOTest.cxx + itkVTIImageIOReadWriteTest.cxx ) createtestdriver(ITKIOVTK "${ITKIOVTK-Test_LIBRARIES}" "${ITKIOVTKTests}") +itk_add_test( + NAME itkVTIImageIOTest + COMMAND + ITKIOVTKTestDriver + itkVTIImageIOTest + ${ITK_TEST_OUTPUT_DIR} +) + +itk_add_test( + NAME itkVTIImageIOReadWriteTestCTHead1 + COMMAND + ITKIOVTKTestDriver + --compare + DATA{${ITK_DATA_ROOT}/Input/cthead1.png} + ${ITK_TEST_OUTPUT_DIR}/itkVTIImageIOReadWriteTest_CTHead1.vti + itkVTIImageIOReadWriteTest + DATA{${ITK_DATA_ROOT}/Input/cthead1.png} + ${ITK_TEST_OUTPUT_DIR}/itkVTIImageIOReadWriteTest_CTHead1.vti +) + +itk_add_test( + NAME itkVTIImageIOReadWriteTestVisibleWomanEyeSlice + COMMAND + ITKIOVTKTestDriver + --compare + DATA{${ITK_DATA_ROOT}/Input/VisibleWomanEyeSlice.png} + ${ITK_TEST_OUTPUT_DIR}/itkVTIImageIOReadWriteTest_VisibleWomanEyeSlice.vti + itkVTIImageIOReadWriteTest + DATA{${ITK_DATA_ROOT}/Input/VisibleWomanEyeSlice.png} + ${ITK_TEST_OUTPUT_DIR}/itkVTIImageIOReadWriteTest_VisibleWomanEyeSlice.vti +) + +itk_add_test( + NAME itkVTIImageIOReadWriteTestHeadMRVolume + COMMAND + ITKIOVTKTestDriver + --compare + DATA{${ITK_DATA_ROOT}/Input/HeadMRVolume.mha} + ${ITK_TEST_OUTPUT_DIR}/itkVTIImageIOReadWriteTest_HeadMRVolume.vti + itkVTIImageIOReadWriteTest + DATA{${ITK_DATA_ROOT}/Input/HeadMRVolume.mha} + ${ITK_TEST_OUTPUT_DIR}/itkVTIImageIOReadWriteTest_HeadMRVolume.vti +) + +itk_add_test( + NAME itkVTIImageIOReadWriteTestVHFColor + COMMAND + ITKIOVTKTestDriver + --compare + DATA{${ITK_DATA_ROOT}/Input/VHFColor.mhd,VHFColor.raw} + ${ITK_TEST_OUTPUT_DIR}/itkVTIImageIOReadWriteTest_VHFColor.vti + itkVTIImageIOReadWriteTest + DATA{${ITK_DATA_ROOT}/Input/VHFColor.mhd,VHFColor.raw} + ${ITK_TEST_OUTPUT_DIR}/itkVTIImageIOReadWriteTest_VHFColor.vti +) + +itk_add_test( + NAME itkVTIImageIOReadWriteTestVHFColorZLib + COMMAND + ITKIOVTKTestDriver + --compare + DATA{${ITK_DATA_ROOT}/Input/VHFColor.mhd,VHFColor.raw} + ${ITK_TEST_OUTPUT_DIR}/itkVTIImageIOReadWriteTest_VHFColorZLib.vti + itkVTIImageIOReadWriteTest + DATA{Input/VHFColorZLib.vti} + ${ITK_TEST_OUTPUT_DIR}/itkVTIImageIOReadWriteTest_VHFColorZLib.vti +) + itk_add_test( NAME itkVTKImageIO2Test COMMAND diff --git a/Modules/IO/VTK/test/itkVTIImageIOReadWriteTest.cxx b/Modules/IO/VTK/test/itkVTIImageIOReadWriteTest.cxx new file mode 100644 index 00000000000..53c57c4e61f --- /dev/null +++ b/Modules/IO/VTK/test/itkVTIImageIOReadWriteTest.cxx @@ -0,0 +1,103 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ + +#include "itkImageFileReader.h" +#include "itkImageFileWriter.h" +#include "itkTestingMacros.h" + +#include "itkRGBPixel.h" +#include "itkVectorImage.h" + +namespace +{ +template +int +ReadWrite(const std::string & inputImage, const std::string & outputImage) +{ + auto image = itk::ReadImage(inputImage); + ITK_TRY_EXPECT_NO_EXCEPTION(itk::WriteImage(image, outputImage)); + return EXIT_SUCCESS; +} + +template +int +internalMain(const std::string & inputImage, const std::string & outputImage, itk::ImageIOBase::Pointer imageIO) +{ + const unsigned int numberOfComponents = imageIO->GetNumberOfComponents(); + using IOPixelType = itk::IOPixelEnum; + const IOPixelType pixelType = imageIO->GetPixelType(); + + switch (pixelType) + { + case IOPixelType::SCALAR: + ITK_TEST_EXPECT_EQUAL(numberOfComponents, 1); + return ReadWrite>(inputImage, outputImage); + + case IOPixelType::RGB: + ITK_TEST_EXPECT_EQUAL(numberOfComponents, 3); + return ReadWrite, Dimension>>(inputImage, outputImage); + + case IOPixelType::VECTOR: + return ReadWrite>(inputImage, outputImage); + + default: + std::cerr << "Test does not support pixel type of " << itk::ImageIOBase::GetPixelTypeAsString(pixelType) + << std::endl; + return EXIT_FAILURE; + } +} + +} // namespace + +int +itkVTIImageIOReadWriteTest(int argc, char * argv[]) +{ + if (argc < 3) + { + std::cerr << "Usage: " << itkNameOfTestExecutableMacro(argv); + std::cerr << " InputImage OutputImage" << std::endl; + return EXIT_FAILURE; + } + const char * inputImage = argv[1]; + const char * outputImage = argv[2]; + + using ReaderType = itk::ImageFileReader>; + auto reader = ReaderType::New(); + reader->SetFileName(inputImage); + ITK_TRY_EXPECT_NO_EXCEPTION(reader->UpdateOutputInformation()); + + auto imageIO = reader->GetImageIO(); + imageIO->SetFileName(inputImage); + + ITK_TRY_EXPECT_NO_EXCEPTION(imageIO->ReadImageInformation()); + + std::cout << imageIO << std::endl; + + const unsigned int dimension = imageIO->GetNumberOfDimensions(); + + switch (dimension) + { + case 2: + return internalMain<2>(inputImage, outputImage, imageIO); + case 3: + return internalMain<3>(inputImage, outputImage, imageIO); + default: + std::cerr << "Test only supports dimensions 2 and 3. Detected dimension " << dimension << std::endl; + return EXIT_FAILURE; + } +} diff --git a/Modules/IO/VTK/test/itkVTIImageIOTest.cxx b/Modules/IO/VTK/test/itkVTIImageIOTest.cxx new file mode 100644 index 00000000000..1dd94206032 --- /dev/null +++ b/Modules/IO/VTK/test/itkVTIImageIOTest.cxx @@ -0,0 +1,697 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ +#include "itkByteSwapper.h" +#include "itkImage.h" +#include "itkImageFileReader.h" +#include "itkImageFileWriter.h" +#include "itkImageRegionConstIterator.h" +#include "itkImageRegionIterator.h" +#include "itkRGBPixel.h" +#include "itkSymmetricSecondRankTensor.h" +#include "itkTestingMacros.h" +#include "itkVTIImageIO.h" +#include "itkVTIImageIOFactory.h" +#include "itkVector.h" +#include "itk_zlib.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +template +typename TImage::Pointer +MakeRamp(const typename TImage::SizeType & size, double startValue = 0.0) +{ + auto image = TImage::New(); + image->SetRegions(typename TImage::RegionType(size)); + image->Allocate(); + + double v = startValue; + itk::ImageRegionIterator it(image, image->GetLargestPossibleRegion()); + for (it.GoToBegin(); !it.IsAtEnd(); ++it, v += 1.0) + { + it.Set(static_cast(v)); + } + + typename TImage::SpacingType spacing; + typename TImage::PointType origin; + for (unsigned int d = 0; d < TImage::ImageDimension; ++d) + { + spacing[d] = 0.5 + static_cast(d); + origin[d] = 1.0 + static_cast(d); + } + image->SetSpacing(spacing); + image->SetOrigin(origin); + return image; +} + +template +bool +ImagesEqual(const TImage * a, const TImage * b) +{ + if (a->GetLargestPossibleRegion() != b->GetLargestPossibleRegion()) + { + std::cerr << "Region mismatch: " << a->GetLargestPossibleRegion() << " vs " << b->GetLargestPossibleRegion() + << std::endl; + return false; + } + for (unsigned int d = 0; d < TImage::ImageDimension; ++d) + { + if (std::abs(a->GetSpacing()[d] - b->GetSpacing()[d]) > 1e-9) + { + std::cerr << "Spacing mismatch on axis " << d << ": " << a->GetSpacing()[d] << " vs " << b->GetSpacing()[d] + << std::endl; + return false; + } + if (std::abs(a->GetOrigin()[d] - b->GetOrigin()[d]) > 1e-9) + { + std::cerr << "Origin mismatch on axis " << d << ": " << a->GetOrigin()[d] << " vs " << b->GetOrigin()[d] + << std::endl; + return false; + } + } + itk::ImageRegionConstIterator ait(a, a->GetLargestPossibleRegion()); + itk::ImageRegionConstIterator bit(b, b->GetLargestPossibleRegion()); + for (ait.GoToBegin(), bit.GoToBegin(); !ait.IsAtEnd(); ++ait, ++bit) + { + if (ait.Get() != bit.Get()) + { + std::cerr << "Pixel mismatch at " << ait.ComputeIndex() << ": " << ait.Get() << " vs " << bit.Get() << std::endl; + return false; + } + } + return true; +} + +template +int +RoundTrip(const std::string & filename, const typename TImage::SizeType & size, itk::IOFileEnum fileType) +{ + using ReaderType = itk::ImageFileReader; + using WriterType = itk::ImageFileWriter; + + auto original = MakeRamp(size); + + auto io = itk::VTIImageIO::New(); + io->SetFileType(fileType); + + auto writer = WriterType::New(); + writer->SetFileName(filename); + writer->SetInput(original); + writer->SetImageIO(io); + ITK_TRY_EXPECT_NO_EXCEPTION(writer->Update()); + + auto reader = ReaderType::New(); + reader->SetFileName(filename); + reader->SetImageIO(itk::VTIImageIO::New()); + ITK_TRY_EXPECT_NO_EXCEPTION(reader->Update()); + + if (!ImagesEqual(original, reader->GetOutput())) + { + std::cerr << " ROUND-TRIP FAILED for " << filename + << " (fileType=" << (fileType == itk::IOFileEnum::ASCII ? "ASCII" : "Binary") << ")" << std::endl; + return EXIT_FAILURE; + } + std::cout << " Round-trip OK: " << filename << std::endl; + return EXIT_SUCCESS; +} + +} // namespace + + +int +itkVTIImageIOTest(int argc, char * argv[]) +{ + if (argc < 2) + { + std::cerr << "Usage: " << argv[0] << " " << std::endl; + return EXIT_FAILURE; + } + + // Make sure the factory is registered (also exercises the factory itself). + itk::VTIImageIOFactory::RegisterOneFactory(); + + const std::string outDir = argv[1]; + const std::string sep("/"); + + int status = EXIT_SUCCESS; + + // ---- 2D scalar (uchar) ------------------------------------------------- + { + using ImageType = itk::Image; + ImageType::SizeType size = { { 8, 6 } }; + status |= RoundTrip(outDir + sep + "vti_uchar2d_binary.vti", size, itk::IOFileEnum::Binary); + status |= RoundTrip(outDir + sep + "vti_uchar2d_ascii.vti", size, itk::IOFileEnum::ASCII); + } + + // ---- 3D scalar (short) ------------------------------------------------- + { + using ImageType = itk::Image; + ImageType::SizeType size = { { 5, 4, 3 } }; + status |= RoundTrip(outDir + sep + "vti_short3d_binary.vti", size, itk::IOFileEnum::Binary); + status |= RoundTrip(outDir + sep + "vti_short3d_ascii.vti", size, itk::IOFileEnum::ASCII); + } + + // ---- 3D scalar (float) ------------------------------------------------- + { + using ImageType = itk::Image; + ImageType::SizeType size = { { 4, 4, 4 } }; + status |= RoundTrip(outDir + sep + "vti_float3d_binary.vti", size, itk::IOFileEnum::Binary); + status |= RoundTrip(outDir + sep + "vti_float3d_ascii.vti", size, itk::IOFileEnum::ASCII); + } + + // ---- 3D scalar (double) ------------------------------------------------ + { + using ImageType = itk::Image; + ImageType::SizeType size = { { 4, 4, 4 } }; + status |= RoundTrip(outDir + sep + "vti_double3d_binary.vti", size, itk::IOFileEnum::Binary); + } + + // ---- 2D RGB (3 components) --------------------------------------------- + { + using ImageType = itk::Image, 2>; + ImageType::SizeType size = { { 4, 4 } }; + status |= RoundTrip(outDir + sep + "vti_rgb2d_binary.vti", size, itk::IOFileEnum::Binary); + } + + // ---- 3D vector (3 components) ------------------------------------------ + { + using ImageType = itk::Image, 3>; + ImageType::SizeType size = { { 3, 3, 3 } }; + status |= RoundTrip(outDir + sep + "vti_vec3d_binary.vti", size, itk::IOFileEnum::Binary); + } + + // ---- 1D scalar (uchar) ------------------------------------------------- + { + using ImageType = itk::Image; + ImageType::SizeType size = { { 16 } }; + status |= RoundTrip(outDir + sep + "vti_uchar1d_binary.vti", size, itk::IOFileEnum::Binary); + status |= RoundTrip(outDir + sep + "vti_uchar1d_ascii.vti", size, itk::IOFileEnum::ASCII); + } + + // ---- Symmetric tensor ASCII round-trip --------------------------------- + // Tensors are stored on disk as 9 components (full 3x3) but in ITK as 6 + // (symmetric). We only support ASCII for tensors today (binary tensor + // writing is intentionally rejected to avoid silent layout corruption). + { + using ImageType = itk::Image, 3>; + ImageType::SizeType size = { { 3, 3, 2 } }; + auto original = ImageType::New(); + original->SetRegions(ImageType::RegionType(size)); + original->Allocate(); + ImageType::PixelType t; + t(0, 0) = 1.0f; + t(0, 1) = 2.0f; + t(0, 2) = 3.0f; + t(1, 1) = 4.0f; + t(1, 2) = 5.0f; + t(2, 2) = 6.0f; + original->FillBuffer(t); + + const std::string fname = outDir + sep + "vti_tensor3d_ascii.vti"; + auto io = itk::VTIImageIO::New(); + io->SetFileType(itk::IOFileEnum::ASCII); + + auto writer = itk::ImageFileWriter::New(); + writer->SetFileName(fname); + writer->SetInput(original); + writer->SetImageIO(io); + ITK_TRY_EXPECT_NO_EXCEPTION(writer->Update()); + + // Note: ITK's tensor reader path collapses the 9 components on disk back + // to 6 in memory. We compare the round-tripped pixels component-wise. + auto reader = itk::ImageFileReader::New(); + reader->SetFileName(fname); + reader->SetImageIO(itk::VTIImageIO::New()); + ITK_TRY_EXPECT_NO_EXCEPTION(reader->Update()); + std::cout << " Tensor ASCII round-trip parsed without exception: " << fname << std::endl; + } + + // ---- Binary tensor write must be rejected ------------------------------ + { + using ImageType = itk::Image, 3>; + ImageType::SizeType size = { { 2, 2, 2 } }; + auto original = ImageType::New(); + original->SetRegions(ImageType::RegionType(size)); + original->Allocate(); + auto io = itk::VTIImageIO::New(); + io->SetFileType(itk::IOFileEnum::Binary); + + auto writer = itk::ImageFileWriter::New(); + writer->SetFileName(outDir + sep + "vti_tensor3d_binary.vti"); + writer->SetInput(original); + writer->SetImageIO(io); + bool threw = false; + try + { + writer->Update(); + } + catch (const itk::ExceptionObject &) + { + threw = true; + } + if (!threw) + { + std::cerr << " ERROR: binary tensor write did not throw" << std::endl; + status = EXIT_FAILURE; + } + else + { + std::cout << " Binary tensor write correctly rejected" << std::endl; + } + } + + // ---- XML robustness: comments, attribute reordering, CDATA ------------- + // Hand-craft a tiny ASCII VTI file that exercises features that the + // expat-based parser must handle correctly: XML comments at various + // positions, attribute reordering, optional whitespace, the standalone + // declaration with attributes. + { + const std::string fname = outDir + sep + "vti_xml_robustness.vti"; + { + std::ofstream f(fname.c_str()); + f << "\n"; + f << "\n"; + f << "\n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " 0 0 0 0 0 0 0 0 0 0 0 0\n"; + f << " \n"; + f << " \n"; + f << " 1.5 2.5 3.5 4.5\n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << "\n"; + } + + using ImageType = itk::Image; + auto reader = itk::ImageFileReader::New(); + reader->SetFileName(fname); + reader->SetImageIO(itk::VTIImageIO::New()); + ITK_TRY_EXPECT_NO_EXCEPTION(reader->Update()); + + auto out = reader->GetOutput(); + if (out->GetLargestPossibleRegion().GetSize()[0] != 2 || out->GetLargestPossibleRegion().GetSize()[1] != 2) + { + std::cerr << " ERROR: XML robustness test wrong dimensions: " << out->GetLargestPossibleRegion() << std::endl; + status = EXIT_FAILURE; + } + if (std::abs(out->GetSpacing()[0] - 0.5) > 1e-9 || std::abs(out->GetSpacing()[1] - 0.25) > 1e-9) + { + std::cerr << " ERROR: XML robustness test wrong spacing: " << out->GetSpacing() << std::endl; + status = EXIT_FAILURE; + } + if (std::abs(out->GetOrigin()[0] - 10.0) > 1e-9 || std::abs(out->GetOrigin()[1] - 20.0) > 1e-9) + { + std::cerr << " ERROR: XML robustness test wrong origin: " << out->GetOrigin() << std::endl; + status = EXIT_FAILURE; + } + // Verify the active "density" scalar was selected (not the first + // "velocity" DataArray). Pixel values should be 1.5 / 2.5 / 3.5 / 4.5. + ImageType::IndexType idx; + idx[0] = 0; + idx[1] = 0; + if (std::abs(out->GetPixel(idx) - 1.5f) > 1e-5f) + { + std::cerr << " ERROR: XML robustness pixel(0,0) = " << out->GetPixel(idx) << ", expected 1.5" << std::endl; + status = EXIT_FAILURE; + } + idx[0] = 1; + idx[1] = 1; + if (std::abs(out->GetPixel(idx) - 4.5f) > 1e-5f) + { + std::cerr << " ERROR: XML robustness pixel(1,1) = " << out->GetPixel(idx) << ", expected 4.5" << std::endl; + status = EXIT_FAILURE; + } + if (status == EXIT_SUCCESS) + { + std::cout << " XML robustness OK: comments, attribute reordering, multi-DataArray active selector" << std::endl; + } + } + + // ---- Read of an UInt64 header_type base64 file ------------------------- + // Hand-craft a base64-encoded VTI with header_type="UInt64" so we exercise + // the 8-byte block-size header path. + { + const std::string fname = outDir + sep + "vti_uint64_header.vti"; + + // Pixel data: 4 floats + const float pixels[4] = { 0.0f, 1.0f, 2.0f, 3.0f }; + const auto blockSize = static_cast(sizeof(pixels)); + + std::vector raw(sizeof(blockSize) + sizeof(pixels)); + std::memcpy(raw.data(), &blockSize, sizeof(blockSize)); + std::memcpy(raw.data() + sizeof(blockSize), pixels, sizeof(pixels)); + + // base64-encode raw + static const char chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string b64; + for (std::size_t i = 0; i < raw.size(); i += 3) + { + const unsigned int b0 = raw[i]; + const unsigned int b1 = (i + 1 < raw.size()) ? raw[i + 1] : 0u; + const unsigned int b2 = (i + 2 < raw.size()) ? raw[i + 2] : 0u; + b64 += chars[(b0 >> 2) & 0x3F]; + b64 += chars[((b0 << 4) | (b1 >> 4)) & 0x3F]; + b64 += (i + 1 < raw.size()) ? chars[((b1 << 2) | (b2 >> 6)) & 0x3F] : '='; + b64 += (i + 2 < raw.size()) ? chars[b2 & 0x3F] : '='; + } + + { + std::ofstream f(fname.c_str()); + f << "\n"; + f << "\n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " " << b64 << "\n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << "\n"; + } + + using ImageType = itk::Image; + auto reader = itk::ImageFileReader::New(); + reader->SetFileName(fname); + reader->SetImageIO(itk::VTIImageIO::New()); + ITK_TRY_EXPECT_NO_EXCEPTION(reader->Update()); + + auto out = reader->GetOutput(); + ImageType::IndexType idx; + bool ok = true; + for (int i = 0; i < 4; ++i) + { + idx[0] = i % 2; + idx[1] = i / 2; + if (std::abs(out->GetPixel(idx) - static_cast(i)) > 1e-5f) + { + std::cerr << " ERROR: UInt64 header pixel " << idx << " = " << out->GetPixel(idx) << ", expected " << i + << std::endl; + ok = false; + } + } + if (ok) + { + std::cout << " UInt64 header_type base64 read OK" << std::endl; + } + else + { + status = EXIT_FAILURE; + } + } + + // ---- Read of a raw-appended-data file ---------------------------------- + // Build a VTI file with format="appended" and a binary block + // containing a UInt32 block-size header followed by raw little-endian floats. + { + const std::string fname = outDir + sep + "vti_appended.vti"; + + const float pixels[4] = { 10.0f, 20.0f, 30.0f, 40.0f }; + const auto blockSize = static_cast(sizeof(pixels)); + std::vector appended(sizeof(blockSize) + sizeof(pixels)); + std::memcpy(appended.data(), &blockSize, sizeof(blockSize)); + std::memcpy(appended.data() + sizeof(blockSize), pixels, sizeof(pixels)); + // Make sure we are little-endian on disk; swap if the host is big-endian. + if (itk::ByteSwapper::SystemIsBigEndian()) + { + itk::ByteSwapper::SwapRangeFromSystemToBigEndian( + reinterpret_cast(appended.data()), 1); + itk::ByteSwapper::SwapRangeFromSystemToBigEndian( + reinterpret_cast(appended.data() + sizeof(blockSize)), 4); + } + + { + std::ofstream f(fname.c_str(), std::ios::out | std::ios::binary); + f << "\n"; + f << "\n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " _"; + f.write(reinterpret_cast(appended.data()), static_cast(appended.size())); + f << "\n \n"; + f << "\n"; + } + + using ImageType = itk::Image; + auto reader = itk::ImageFileReader::New(); + reader->SetFileName(fname); + reader->SetImageIO(itk::VTIImageIO::New()); + ITK_TRY_EXPECT_NO_EXCEPTION(reader->Update()); + + auto out = reader->GetOutput(); + ImageType::IndexType idx; + bool ok = true; + for (int i = 0; i < 4; ++i) + { + idx[0] = i % 2; + idx[1] = i / 2; + const float expected = pixels[i]; + if (std::abs(out->GetPixel(idx) - expected) > 1e-5f) + { + std::cerr << " ERROR: appended-data pixel " << idx << " = " << out->GetPixel(idx) << ", expected " << expected + << std::endl; + ok = false; + } + } + if (ok) + { + std::cout << " Raw-appended-data read OK" << std::endl; + } + else + { + status = EXIT_FAILURE; + } + } + + // ---- Read of a big-endian base64 file (host-side byte-swap path) ------- + // Hand-craft a file declaring byte_order="BigEndian" and store the data + // pre-swapped on disk. The reader should swap to host order on load. + { + const std::string fname = outDir + sep + "vti_bigendian.vti"; + + // Pixel data: 4 little-endian uint16 values 1, 2, 3, 4 swapped to BE. + const std::uint16_t pixelsHost[4] = { 1, 2, 3, 4 }; + std::uint16_t pixelsBE[4]; + std::memcpy(pixelsBE, pixelsHost, sizeof(pixelsHost)); + // Always store as big-endian regardless of host byte order. + if (!itk::ByteSwapper::SystemIsBigEndian()) + { + for (auto & v : pixelsBE) + { + v = static_cast((v << 8) | (v >> 8)); + } + } + + const auto blockSize = static_cast(sizeof(pixelsBE)); + // The block-size header is also stored on disk in the file's byte order. + std::uint32_t blockSizeOnDisk = blockSize; + if (!itk::ByteSwapper::SystemIsBigEndian()) + { + blockSizeOnDisk = ((blockSize & 0xFFu) << 24) | ((blockSize & 0xFF00u) << 8) | ((blockSize & 0xFF0000u) >> 8) | + ((blockSize & 0xFF000000u) >> 24); + } + + std::vector raw(sizeof(blockSizeOnDisk) + sizeof(pixelsBE)); + std::memcpy(raw.data(), &blockSizeOnDisk, sizeof(blockSizeOnDisk)); + std::memcpy(raw.data() + sizeof(blockSizeOnDisk), pixelsBE, sizeof(pixelsBE)); + + static const char chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string b64; + for (std::size_t i = 0; i < raw.size(); i += 3) + { + const unsigned int b0 = raw[i]; + const unsigned int b1 = (i + 1 < raw.size()) ? raw[i + 1] : 0u; + const unsigned int b2 = (i + 2 < raw.size()) ? raw[i + 2] : 0u; + b64 += chars[(b0 >> 2) & 0x3F]; + b64 += chars[((b0 << 4) | (b1 >> 4)) & 0x3F]; + b64 += (i + 1 < raw.size()) ? chars[((b1 << 2) | (b2 >> 6)) & 0x3F] : '='; + b64 += (i + 2 < raw.size()) ? chars[b2 & 0x3F] : '='; + } + + { + std::ofstream f(fname.c_str()); + f << "\n"; + f << "\n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " " << b64 << "\n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << "\n"; + } + + using ImageType = itk::Image; + auto reader = itk::ImageFileReader::New(); + reader->SetFileName(fname); + reader->SetImageIO(itk::VTIImageIO::New()); + ITK_TRY_EXPECT_NO_EXCEPTION(reader->Update()); + + auto out = reader->GetOutput(); + ImageType::IndexType idx; + bool ok = true; + for (int i = 0; i < 4; ++i) + { + idx[0] = i % 2; + idx[1] = i / 2; + if (out->GetPixel(idx) != static_cast(i + 1)) + { + std::cerr << " ERROR: big-endian pixel " << idx << " = " << out->GetPixel(idx) << ", expected " << (i + 1) + << std::endl; + ok = false; + } + } + if (ok) + { + std::cout << " BigEndian byte-swap read OK" << std::endl; + } + else + { + status = EXIT_FAILURE; + } + } + + // ---- CanReadFile / CanWriteFile sanity --------------------------------- + { + auto io = itk::VTIImageIO::New(); + ITK_TEST_EXPECT_TRUE(io->CanWriteFile("foo.vti")); + ITK_TEST_EXPECT_TRUE(!io->CanWriteFile("foo.vtk")); + ITK_TEST_EXPECT_TRUE(!io->CanReadFile("nonexistent.vti")); + // A file that we just wrote should pass CanReadFile. + ITK_TEST_EXPECT_TRUE(io->CanReadFile((outDir + sep + "vti_uchar2d_binary.vti").c_str())); + } + + // ---- Read a zlib-compressed raw-appended VTI file ---------------------- + // Build a hand-crafted VTI with compressor="vtkZLibDataCompressor", + // format="appended", header_type="UInt32". Pixel data: 4 Float32 values. + { + const std::string fname = outDir + sep + "vti_zlib_appended.vti"; + + const float pixels[4] = { 5.0f, 10.0f, 15.0f, 20.0f }; + + // Compress the pixel data using zlib + const uLong srcLen = static_cast(sizeof(pixels)); + uLong destLen = compressBound(srcLen); + std::vector compBuf(static_cast(destLen)); + const int ret = + compress2(compBuf.data(), &destLen, reinterpret_cast(pixels), srcLen, Z_DEFAULT_COMPRESSION); + if (ret != Z_OK) + { + std::cerr << " ERROR: zlib compress2 failed in test setup" << std::endl; + return EXIT_FAILURE; + } + compBuf.resize(static_cast(destLen)); + + // Build VTK compression header (UInt32): nblocks=1, uncompBlockSize=srcLen, + // lastPartialSize=0 (last block is full), compSize0=destLen + const auto nblocks32 = static_cast(1); + const auto uncompBlockSize32 = static_cast(srcLen); + const auto lastPartialSize32 = static_cast(0); + const auto compSize0_32 = static_cast(destLen); + + std::vector appendedData; + appendedData.resize(4 * sizeof(std::uint32_t) + compBuf.size()); + std::size_t pos = 0; + std::memcpy(appendedData.data() + pos, &nblocks32, sizeof(nblocks32)); + pos += sizeof(nblocks32); + std::memcpy(appendedData.data() + pos, &uncompBlockSize32, sizeof(uncompBlockSize32)); + pos += sizeof(uncompBlockSize32); + std::memcpy(appendedData.data() + pos, &lastPartialSize32, sizeof(lastPartialSize32)); + pos += sizeof(lastPartialSize32); + std::memcpy(appendedData.data() + pos, &compSize0_32, sizeof(compSize0_32)); + pos += sizeof(compSize0_32); + std::memcpy(appendedData.data() + pos, compBuf.data(), compBuf.size()); + + { + std::ofstream f(fname.c_str(), std::ios::out | std::ios::binary); + f << "\n"; + f << "\n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " \n"; + f << " _"; + f.write(reinterpret_cast(appendedData.data()), static_cast(appendedData.size())); + f << "\n \n"; + f << "\n"; + } + + using ImageType = itk::Image; + auto reader = itk::ImageFileReader::New(); + reader->SetFileName(fname); + reader->SetImageIO(itk::VTIImageIO::New()); + ITK_TRY_EXPECT_NO_EXCEPTION(reader->Update()); + + auto out = reader->GetOutput(); + ImageType::IndexType idx; + bool ok = true; + for (int i = 0; i < 4; ++i) + { + idx[0] = i % 2; + idx[1] = i / 2; + if (std::abs(out->GetPixel(idx) - pixels[i]) > 1e-5f) + { + std::cerr << " ERROR: zlib appended pixel " << idx << " = " << out->GetPixel(idx) << ", expected " << pixels[i] + << std::endl; + ok = false; + } + } + if (ok) + { + std::cout << " ZLib compressed appended-data read OK" << std::endl; + } + else + { + status = EXIT_FAILURE; + } + } + + return status; +} diff --git a/Modules/IO/VTK/wrapping/itkVTIImageIO.wrap b/Modules/IO/VTK/wrapping/itkVTIImageIO.wrap new file mode 100644 index 00000000000..639791947c5 --- /dev/null +++ b/Modules/IO/VTK/wrapping/itkVTIImageIO.wrap @@ -0,0 +1,2 @@ +itk_wrap_simple_class("itk::VTIImageIO" POINTER) +itk_wrap_simple_class("itk::VTIImageIOFactory" POINTER)