Skip to content

Commit ddf579b

Browse files
hjmjohnsonclaude
andcommitted
ENH: Add support for reading/writing VTK XML ImageData (.vti) format
Closes #6030's parent issue thread; replaces the WIP implementation in the original PR with one that addresses both the CI failures and the review feedback. == Why a rewrite == The original WIP commit on this branch failed CI on every C++ platform and on Python wrapping, and Bradley Lowekamp's review asked for the XML parsing to use ITK's existing expat library rather than ad-hoc string matching. Rather than patch the WIP implementation, this commit restarts the file from the same upstream/main base with: - expat-based XML parsing for the file header - Python wrapping registration - KWStyle-clean doxygen comments - an ENH: prefix that satisfies kwrobot/ghostflow - a comprehensive round-trip and corner-case test suite == Algorithm == VTIImageIO inherits from itk::ImageIOBase and supports the serial-piece subset of the VTK XML ImageData (.vti) format. Reading 1. The whole file is slurped into memory. 2. If the file contains an `<AppendedData>` element, the XML view fed to expat is truncated at that element and replaced with a self-closing `<AppendedData/></VTKFile>` so the parser sees a well-formed document. The byte offset of the `_` marker that introduces the raw binary block is recorded for later seek-and-read. 3. The truncated XML view is parsed with expat (XML_Parser / XML_SetElementHandler / XML_SetCharacterDataHandler). Element handlers populate a VTIParseState with the VTKFile, ImageData, PointData, and active DataArray attributes, and the character data handler captures inline ASCII or base64 contents. 4. After parsing, geometry/spacing/origin/component-type/pixel-type are populated on the ImageIOBase from the captured state. 5. Read() then routes to one of three branches: - ASCII: parse the cached character data via ReadBufferAsASCII - base64: decode the cached base64 string, strip the UInt32/UInt64 block-size header, memcpy into the user buffer, then byte-swap if file != host endianness - raw appended: open the file, seek to m_AppendedDataOffset + m_DataArrayOffset + headerBytes, read, then byte-swap if necessary The expat-based reader handles the things string matching cannot: attribute reordering, optional whitespace, XML comments at any depth, the standalone <?xml ... ?> declaration, and PointData with multiple DataArrays where only one is the active scalar/vector/tensor. Writing - ASCII or binary (base64) DataMode, selectable via SetFileType(). - SymmetricSecondRankTensor pixels are expanded from ITK's 6-component storage to VTK's full 9-component layout for ASCII output. Binary tensor writing is intentionally rejected with an exception because the on-disk NumberOfComponents="9" header would silently disagree with a 6-component memory buffer; this is verified by a test. - Output is always written in the host byte order; the file declares its byte_order accordingly. - The block-size header for base64 binary is currently UInt32 (the UInt64 reader path is exercised by a hand-crafted test file below). Compression (zlib/lz4) and the VTK direction matrix are intentionally out of scope for this PR. == CI failures fixed == - itkVTIImageIO.h:124 KWStyle: comment doesn't have \class The DataEncoding `enum class` had a `/** ... */` doxygen comment; KWStyle's class-comment rule treats `enum class` like a class. Replaced with a plain `//` comment. - KeyError: 'VTIImageIOFactory' (Python tests) Added Modules/IO/VTK/wrapping/itkVTIImageIO.wrap registering both VTIImageIO and VTIImageIOFactory with the auto-loaded wrap module. - ghostflow-check-main: 'WIP:' prefix not in allowed list Commit message now uses ENH: prefix. - Module dependency: ITKExpat added as a PRIVATE_DEPENDS of ITKIOVTK so the new XML parser actually links. Updated the FACTORY_NAMES to include `ImageIO::VTI`. == Test strategy and coverage == The new itkVTIImageIOTest.cxx exercises the filter through 3 classes of cases: 1. Round-trip tests via ImageFileWriter -> ImageFileReader for the common pixel type and dimensionality combinations: uchar 1D / 2D, short 3D, float 3D, double 3D, RGB 2D, vector<float,3> in 3D, both ASCII and binary (base64) encodings where applicable. Each round-trip checks region, spacing, origin, and per-pixel bit-equivalence. 2. Behavior tests: - Symmetric tensor ASCII round-trip (writes 9-component layout, reads back into 6-component ITK layout). - Symmetric tensor binary write must throw (silent layout corruption guard). 3. Hand-crafted-file readability tests for code paths the writer never produces but the reader must support: - XML robustness: comments at multiple positions, attribute reordering, multiple DataArrays in PointData with the active Scalars selector pointing at the second array. Verifies dimensions, spacing, origin, AND that the correct active array is selected. - header_type="UInt64": base64 file with an 8-byte block-size header; verifies the dual UInt32/UInt64 header path. - format="appended" with raw binary in <AppendedData>: verifies the file-truncation + offset-seek path. - byte_order="BigEndian": base64 file with the data and the block-size header pre-swapped to big-endian; verifies the byte-swap-on-read path. - CanReadFile / CanWriteFile sanity. This matches the relevant subset of VTK's own TestDataObjectXMLIO.cxx coverage matrix (DataMode x ByteOrder x HeaderType x DataObjectType) for the features this PR claims. ZLIB/LZ4 compression and the VTK direction matrix are intentionally not exercised since this PR does not claim those features. == Local results == $ cmake --build build-ssim -j48 --target ITKIOVTKTestDriver [4/4] Linking CXX executable bin/ITKIOVTKTestDriver $ ctest -R itkVTIImageIOTest --output-on-failure Test #1259: itkVTIImageIOTest .... Passed 0.03 sec 100% tests passed, 0 tests failed out of 1 pre-commit (gersemi, clang-format, kw-pre-commit) clean on every touched file. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 36bf35c commit ddf579b

9 files changed

Lines changed: 1943 additions & 1 deletion

File tree

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*=========================================================================
2+
*
3+
* Copyright NumFOCUS
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0.txt
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*=========================================================================*/
18+
#ifndef itkVTIImageIO_h
19+
#define itkVTIImageIO_h
20+
#include "ITKIOVTKExport.h"
21+
22+
#include <fstream>
23+
#include "itkImageIOBase.h"
24+
25+
namespace itk
26+
{
27+
/**
28+
* \class VTIImageIO
29+
*
30+
* \brief ImageIO class for reading and writing VTK XML ImageData (.vti) files.
31+
*
32+
* Supports the VTK XML ImageData format (version 0.1 and 2.2), including
33+
* ASCII, binary (base64-encoded), and raw-appended data formats.
34+
* Scalar, vector (3-component), RGB, RGBA, and symmetric second rank tensor
35+
* pixel types are supported.
36+
*
37+
* The XML structure is parsed using the expat XML library (provided by
38+
* ITKExpat / ITKIOXML), so the parser is robust to attribute ordering,
39+
* whitespace, comments, and CDATA sections. The raw appended data section
40+
* (which is XML-illegal binary content following an `_` marker) is read
41+
* directly from the file at the byte offset recorded by the parser when
42+
* it encountered the `<AppendedData>` element.
43+
*
44+
* \ingroup IOFilters
45+
* \ingroup ITKIOVTK
46+
*/
47+
class ITKIOVTK_EXPORT VTIImageIO : public ImageIOBase
48+
{
49+
public:
50+
ITK_DISALLOW_COPY_AND_MOVE(VTIImageIO);
51+
52+
/** Standard class type aliases. */
53+
using Self = VTIImageIO;
54+
using Superclass = ImageIOBase;
55+
using Pointer = SmartPointer<Self>;
56+
using ConstPointer = SmartPointer<const Self>;
57+
58+
/** Method for creation through the object factory. */
59+
itkNewMacro(Self);
60+
61+
/** \see LightObject::GetNameOfClass() */
62+
itkOverrideGetNameOfClassMacro(VTIImageIO);
63+
64+
/*-------- This part of the interface deals with reading data. ------ */
65+
66+
/** Determine the file type. Returns true if this ImageIO can read the
67+
* file specified. */
68+
bool
69+
CanReadFile(const char *) override;
70+
71+
/** Set the spacing and dimension information for the current filename. */
72+
void
73+
ReadImageInformation() override;
74+
75+
/** Reads the data from disk into the memory buffer provided. */
76+
void
77+
Read(void * buffer) override;
78+
79+
/*-------- This part of the interfaces deals with writing data. ----- */
80+
81+
/** Determine the file type. Returns true if this ImageIO can write the
82+
* file specified. */
83+
bool
84+
CanWriteFile(const char *) override;
85+
86+
/** Writes the spacing and dimensions of the image.
87+
* Assumes SetFileName has been called with a valid file name. */
88+
void
89+
WriteImageInformation() override
90+
{}
91+
92+
/** Writes the data to disk from the memory buffer provided. Make sure
93+
* that the IORegion has been set properly. */
94+
void
95+
Write(const void * buffer) override;
96+
97+
protected:
98+
VTIImageIO();
99+
~VTIImageIO() override;
100+
101+
void
102+
PrintSelf(std::ostream & os, Indent indent) const override;
103+
104+
private:
105+
/** Parse the XML header to fill image information. */
106+
void
107+
InternalReadImageInformation();
108+
109+
/** Map VTK type string to ITK IOComponentEnum. */
110+
static IOComponentEnum
111+
VTKTypeStringToITKComponent(const std::string & vtkType);
112+
113+
/** Map ITK IOComponentEnum to VTK type string. */
114+
static std::string
115+
ITKComponentToVTKTypeString(IOComponentEnum t);
116+
117+
/** Decode a base64-encoded string into raw bytes. Returns the number
118+
* of decoded bytes. */
119+
static SizeType
120+
DecodeBase64(const std::string & encoded, std::vector<unsigned char> & decoded);
121+
122+
/** Encode raw bytes as a base64 string. */
123+
static std::string
124+
EncodeBase64(const unsigned char * data, SizeType numBytes);
125+
126+
/** Trim leading/trailing whitespace from a string. */
127+
static std::string
128+
TrimString(const std::string & s);
129+
130+
// Encoding format of the data array found in the file.
131+
// (Plain comment, not doxygen, to satisfy KWStyle's `\class` rule for
132+
// class-like declarations.)
133+
enum class DataEncoding : std::uint8_t
134+
{
135+
ASCII,
136+
Base64, // binary data encoded in base64 (format="binary")
137+
RawAppended // raw binary appended data (format="appended" with raw encoding)
138+
};
139+
140+
DataEncoding m_DataEncoding{ DataEncoding::Base64 };
141+
142+
/** Cached ASCII data text content captured by the expat parser when the
143+
* active DataArray is in ASCII format. Empty otherwise. */
144+
std::string m_AsciiDataContent{};
145+
146+
/** Cached base64 data text content captured by the expat parser when the
147+
* active DataArray is in base64 ("binary") format. Empty otherwise. */
148+
std::string m_Base64DataContent{};
149+
150+
/** Byte offset into the file where the appended data section begins
151+
* (only relevant when m_DataEncoding == RawAppended). */
152+
std::streampos m_AppendedDataOffset{ 0 };
153+
154+
/** Offset within the appended data block for this DataArray (in bytes,
155+
* not counting the leading block-size UInt32/UInt64). */
156+
SizeType m_DataArrayOffset{ 0 };
157+
158+
/** Whether the header uses 64-bit block-size integers (header_type="UInt64"). */
159+
bool m_HeaderTypeUInt64{ false };
160+
};
161+
} // end namespace itk
162+
163+
#endif // itkVTIImageIO_h
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*=========================================================================
2+
*
3+
* Copyright NumFOCUS
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0.txt
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*=========================================================================*/
18+
#ifndef itkVTIImageIOFactory_h
19+
#define itkVTIImageIOFactory_h
20+
#include "ITKIOVTKExport.h"
21+
22+
#include "itkObjectFactoryBase.h"
23+
#include "itkImageIOBase.h"
24+
25+
namespace itk
26+
{
27+
/**
28+
* \class VTIImageIOFactory
29+
* \brief Create instances of VTIImageIO objects using an object factory.
30+
* \ingroup ITKIOVTK
31+
*/
32+
class ITKIOVTK_EXPORT VTIImageIOFactory : public ObjectFactoryBase
33+
{
34+
public:
35+
ITK_DISALLOW_COPY_AND_MOVE(VTIImageIOFactory);
36+
37+
/** Standard class type aliases. */
38+
using Self = VTIImageIOFactory;
39+
using Superclass = ObjectFactoryBase;
40+
using Pointer = SmartPointer<Self>;
41+
using ConstPointer = SmartPointer<const Self>;
42+
43+
/** Class Methods used to interface with the registered factories. */
44+
const char *
45+
GetITKSourceVersion() const override;
46+
47+
const char *
48+
GetDescription() const override;
49+
50+
/** Method for class instantiation. */
51+
itkFactorylessNewMacro(Self);
52+
53+
/** \see LightObject::GetNameOfClass() */
54+
itkOverrideGetNameOfClassMacro(VTIImageIOFactory);
55+
56+
/** Register one factory of this type */
57+
static void
58+
RegisterOneFactory()
59+
{
60+
auto vtiFactory = VTIImageIOFactory::New();
61+
62+
ObjectFactoryBase::RegisterFactoryInternal(vtiFactory);
63+
}
64+
65+
protected:
66+
VTIImageIOFactory();
67+
~VTIImageIOFactory() override;
68+
};
69+
} // end namespace itk
70+
71+
#endif

Modules/IO/VTK/itk-module.cmake

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
set(
22
DOCUMENTATION
33
"This module contains classes for reading and writing image
4-
files in the \"legacy\" (non-XML) VTK file format."
4+
files in the \"legacy\" (non-XML) VTK file format and the VTK XML
5+
ImageData (.vti) file format."
56
)
67

78
itk_module(
89
ITKIOVTK
910
ENABLE_SHARED
1011
DEPENDS
1112
ITKIOImageBase
13+
PRIVATE_DEPENDS
14+
ITKExpat
1215
TEST_DEPENDS
1316
ITKTestKernel
1417
ITKImageSources
1518
FACTORY_NAMES
1619
ImageIO::VTK
20+
ImageIO::VTI
1721
DESCRIPTION "${DOCUMENTATION}"
1822
)

Modules/IO/VTK/src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ set(
22
ITKIOVTK_SRCS
33
itkVTKImageIOFactory.cxx
44
itkVTKImageIO.cxx
5+
itkVTIImageIOFactory.cxx
6+
itkVTIImageIO.cxx
57
)
68

79
itk_module_add_library(ITKIOVTK ${ITKIOVTK_SRCS})

0 commit comments

Comments
 (0)