Skip to content

VTI — VTK XML ImageData (.vti) ​

A regular lattice whose geometry is three attributes rather than a point array. It is the natural home for a generated grid — grid, voxelize with fill="all", and compute_sdf with structure="voxel" all produce exactly the shape this format stores — and the only format in meshio++ that round-trips such a grid's header. See the VTK XML file formats documentation.

Format namevti
Extensions.vti
Read / Write✓ / ✓
Extra dependencies— (zlib for compressed output, as VTU)

Reading & writing ​

python
import meshioplusplus

field = meshioplusplus.compute_sdf(skin, resolution=(128, 128, 128))
meshioplusplus.vti.write("field.vti", field,
    binary=True,            # base64-encode the arrays instead of writing text
    compression="zlib",     # "zlib", "lz4", "zstd", or None
    header_type=None,       # "UInt32" (default) or "UInt64"
)

back = meshioplusplus.vti.read("field.vti")

meshioplusplus.read/write dispatch on the extension, so .vti needs no explicit format name.

File structure ​

xml
<?xml version="1.0"?>
<VTKFile type="ImageData" version="0.1" byte_order="LittleEndian"
         compressor="vtkZLibDataCompressor">
<ImageData WholeExtent="0 16 0 16 0 16"
           Origin="-0.17320508075688773 -0.17320508075688773 -0.17320508075688773"
           Spacing="0.084150635094610967 0.084150635094610967 0.084150635094610967">
<Piece Extent="0 16 0 16 0 16">
<PointData>
  <DataArray type="Float64" Name="sdf:distance" format="binary">…</DataArray>
</PointData>
<CellData/>
</Piece>
</ImageData>
</VTKFile>

There is no <Points> section: Origin, Spacing and WholeExtent are the geometry. WholeExtent counts points, so 0 16 on an axis means 16 cells and 17 planes.

The mesh side of the deal ​

A Mesh has no implicit geometry, so the two directions are not symmetric:

  • read expands the extent into explicit points and one hexahedron block, through the same numbering grid produces (x fastest, then y, then z). That is what makes read(write(m)) == m an identity rather than a coincidence.
  • write requires a lattice. A mesh that is not one has no extent to write and raises WriteError by name.

A partial grid cannot be written either

voxelize's surface/inside fills and compute_sdf's octree produce meshes made of the right cells in the right places — and ImageData still has no way to express their holes. Writing one would silently fill them in, so it is refused. Write those as .vtu, which stores the cells explicitly.

The recovery is exact rather than a fit: grid writes every coordinate as origin + index * spacing evaluated independently, so all points on one plane carry bit-identical values and an exact sort-and-unique recovers the planes with no tolerance. Only the spacing needs one — consecutive gaps of origin + i*h differ in the last bits — so uniformity is checked to a relative 1e-9, which rejects a genuinely graded mesh by orders of magnitude rather than by ulps.

Data mapping ​

meshio++VTI
point_data<PointData>, one <DataArray> per array, sorted by name
cell_data<CellData> — a lattice has one block, so one array each
field_datanot written (see below)
point_sets / cell_setsnot written

Both Origin and Spacing are written with %.17g — the round-trip width for a double. The stream default of six significant digits would lose about ten digits of a real origin, placing the grid ~1e-7 off its own points with nothing downstream to flag it.

Why this format exists for compute_sdf ​

A generated grid carries an sdf:* field_data header describing itself. The .vti writer does not persist that header: its geometry attributes carry the same lattice information, and reading them reconstructs the mesh. Numeric <FieldData> arrays supplied by another producer are read; use .vtu if field data and named regions must also be written.

Quirks & limitations ​

  • Both readers support <AppendedData encoding="raw"|"base64">, sharing VTU's framing decoder: UInt32/UInt64 headers, either byte order, and optional zlib/LZ4/ZSTD compression. Raw offsets address bytes; base64 offsets address encoded characters. Missing native codecs fail explicitly; Python needs the corresponding optional module for LZ4/ZSTD. The writer still emits inline arrays, not appended output.
  • Multiple pieces and partial extents are read by both engines. Each piece's Extent sizes its own geometry and arrays and must lie inside WholeExtent; a multi-piece file must specify each piece's extent. Points and cells concatenate in document order, without welding shared boundaries or discarding ghost cells. Compatible data arrays present in every piece survive; otherwise they are dropped with a warning. Numeric field data and piece-local named regions use the VTP convention. Metadata counts sum the pieces, and its bounding box encloses the actual pieces (not an unfilled part of WholeExtent).
  • A non-identity Direction (a rotated lattice) raises: an axis-aligned hexahedron grid cannot express it without baking the rotation into the coordinates, which is a different mesh from the one the file describes.
  • lzma is rejected by both readers. Python has the module; declining it is a deliberate parity choice, so that the two readers accept the same files.
  • header_type="UInt64" is honoured on read; the writer always emits the default UInt32, as the VTU writer does.
  • The writer always declares LittleEndian; the readers honour byte_order.

Notes ​

The C++ reader/writer share detail/vtk_xml.hpp's <DataArray> codec and detail/vtu_binary.hpp's base64 + 32 KiB block framing with VTU and VTP verbatim, so --codec zlib|lz4|zstd works here exactly as it does there.

Testing does not rely on the round trip through this library's own writer, which a consistently wrong Origin or a transposed extent would survive unchanged. tests/cpp/test_vti.cpp and tests/python/test_vti.py additionally assert on the raw written bytes and read hand-written files whose expected geometry is stated independently — a non-zero Origin, an extent not starting at zero, an inverted extent, a foreign Piece.

See also ​

Released under the MIT License.