Skip to main content

trx_rs/formats/
mod.rs

1pub mod tck;
2pub mod trk;
3pub mod tt;
4pub mod vtk;
5
6use std::path::Path;
7
8use crate::any_trx_file::AnyTrxFile;
9use crate::dtype::DType;
10use crate::error::{Result, TrxError};
11use crate::header::Header;
12use crate::tractogram::Tractogram;
13pub use vtk::{
14    inspect_vtk_declared_space, vtk_import_warnings, VtkCoordinateMode, VtkCoordinateSpace,
15};
16
17/// Supported tractogram file formats.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum Format {
20    /// TRX format (directory or `.trx` zip archive).
21    Trx,
22    /// TrackVis `.trk` / `.trk.gz`.
23    Trk,
24    /// MRtrix `.tck` / `.tck.gz`.
25    Tck,
26    /// VTK legacy polydata `.vtk`.
27    Vtk,
28    /// DSI Studio Tiny Track `.tt.gz` (import only).
29    TinyTrack,
30}
31
32/// Options that control tractogram conversion behaviour.
33#[derive(Clone, Debug)]
34pub struct ConversionOptions {
35    /// Optional header override for formats that do not carry TRX-style metadata.
36    pub header: Option<Header>,
37    /// Positions dtype to use when writing TRX output.
38    pub trx_positions_dtype: DType,
39    /// How VTK coordinates should be interpreted when reading.
40    pub vtk_coordinate_mode: VtkCoordinateMode,
41}
42
43impl Default for ConversionOptions {
44    fn default() -> Self {
45        Self {
46            header: None,
47            trx_positions_dtype: DType::Float32,
48            vtk_coordinate_mode: VtkCoordinateMode::AssumeRas,
49        }
50    }
51}
52
53/// Detect the tractogram format from a file path or directory.
54///
55/// Returns `Err` if the path does not match a known format.
56pub fn detect_format(path: &Path) -> Result<Format> {
57    let file_name = path
58        .file_name()
59        .and_then(|name| name.to_str())
60        .ok_or_else(|| {
61            TrxError::Argument(format!("cannot determine format for {}", path.display()))
62        })?;
63
64    if file_name.ends_with(".trx") || path.is_dir() {
65        return Ok(Format::Trx);
66    }
67    if file_name.ends_with(".trk") || file_name.ends_with(".trk.gz") {
68        return Ok(Format::Trk);
69    }
70    if file_name.ends_with(".tck") || file_name.ends_with(".tck.gz") {
71        return Ok(Format::Tck);
72    }
73    if file_name.ends_with(".vtk") {
74        return Ok(Format::Vtk);
75    }
76    if file_name.ends_with(".tt") || file_name.ends_with(".tt.gz") {
77        return Ok(Format::TinyTrack);
78    }
79
80    Err(TrxError::Format(format!(
81        "unsupported tractogram format for {}",
82        path.display()
83    )))
84}
85
86/// Read a tractogram from any supported format into the neutral in-memory representation.
87///
88/// Dispatches to the format-specific reader based on [`detect_format`].
89pub fn read_tractogram(path: &Path, options: &ConversionOptions) -> Result<Tractogram> {
90    match detect_format(path)? {
91        Format::Trx => Ok(Tractogram::from(&AnyTrxFile::load(path)?)),
92        Format::Trk => trk::read_trk(path, options.header.clone()),
93        Format::Tck => tck::read_tck(path, options.header.clone()),
94        Format::Vtk => vtk::read_vtk(path, options.header.clone(), options.vtk_coordinate_mode),
95        Format::TinyTrack => tt::read_tt(path),
96    }
97}
98
99/// Write a tractogram to any supported output format.
100///
101/// Dispatches to the format-specific writer based on [`detect_format`].
102pub fn write_tractogram(
103    path: &Path,
104    tractogram: &Tractogram,
105    options: &ConversionOptions,
106) -> Result<()> {
107    match detect_format(path)? {
108        Format::Trx => {
109            let mut tractogram = tractogram.clone();
110            if let Some(header) = &options.header {
111                tractogram.set_spatial_metadata(header.voxel_to_rasmm, header.dimensions);
112            }
113            match tractogram.to_trx(options.trx_positions_dtype)? {
114                AnyTrxFile::F16(file) => file.save(path),
115                AnyTrxFile::F32(file) => file.save(path),
116                AnyTrxFile::F64(file) => file.save(path),
117            }
118        }
119        Format::Trk => {
120            let mut tractogram = tractogram.clone();
121            if let Some(header) = &options.header {
122                tractogram.set_spatial_metadata(header.voxel_to_rasmm, header.dimensions);
123            }
124            crate::legacy_io::write_trk(path, &tractogram, None).map_err(|err| {
125                TrxError::Format(format!(
126                    "failed to write TrackVis file {}: {err}",
127                    path.display()
128                ))
129            })
130        }
131        Format::Tck => tck::write_tck(path, tractogram),
132        Format::Vtk => vtk::write_vtk(path, tractogram),
133        Format::TinyTrack => Err(TrxError::Format(
134            "Tiny Track (.tt/.tt.gz) conversion is not implemented yet".into(),
135        )),
136    }
137}
138
139/// Convert between tractogram file formats in one step.
140///
141/// Reads from `input`, writes to `output`, applying the given [`ConversionOptions`].
142pub fn convert(input: &Path, output: &Path, options: &ConversionOptions) -> Result<()> {
143    if detect_format(input)? == Format::Trk && detect_format(output)? == Format::Trx {
144        return trk::convert_trk_to_trx(input, output, options);
145    }
146    let tractogram = read_tractogram(input, options)?;
147    write_tractogram(output, &tractogram, options)
148}
149
150#[cfg(test)]
151mod tests {
152    use super::{ConversionOptions, VtkCoordinateMode};
153
154    #[test]
155    fn conversion_options_default_to_ras_for_vtk_parity() {
156        assert_eq!(
157            ConversionOptions::default().vtk_coordinate_mode,
158            VtkCoordinateMode::AssumeRas
159        );
160    }
161}