1use std::collections::HashMap;
2use std::fs::File;
3use std::io::Read;
4use std::path::Path;
5
6use flate2::read::MultiGzDecoder;
7
8use crate::any_trx_file::AnyTrxFile;
9use crate::dtype::DType;
10use crate::error::{Result, TrxError};
11use crate::header::Header;
12use crate::mmap_backing::{vec_to_bytes, MmapBacking};
13use crate::tractogram::Tractogram;
14use crate::trx_file::{DataArray, TrxFile, TrxParts};
15
16const TRK_HEADER_SIZE: usize = 1000;
17const MAX_NAMED_SCALARS_PER_POINT: usize = 10;
18const MAX_NAMED_PROPERTIES_PER_STREAMLINE: usize = 10;
19
20pub fn read_trk(path: &Path, _header_override: Option<Header>) -> Result<Tractogram> {
25 let parsed = parse_trk(path)?;
26
27 let mut tractogram = Tractogram::with_header(parsed.header);
28 for streamline in parsed.streamlines {
29 tractogram.push_streamline(&streamline)?;
30 }
31 for (name, array) in parsed.dpv {
32 tractogram.insert_dpv(name, array);
33 }
34 for (name, array) in parsed.dps {
35 tractogram.insert_dps(name, array);
36 }
37 Ok(tractogram)
38}
39
40pub fn convert_trk_to_trx(
42 input: &Path,
43 output: &Path,
44 options: &crate::formats::ConversionOptions,
45) -> Result<()> {
46 let parsed = parse_trk(input)?;
47 let positions: Vec<[f32; 3]> = parsed.streamlines.iter().flatten().copied().collect();
48 let offsets = build_offsets(&parsed.streamlines)?;
49
50 let file = TrxFile::from_parts(TrxParts {
51 header: Header {
52 nb_streamlines: parsed.streamlines.len() as u64,
53 nb_vertices: positions.len() as u64,
54 ..parsed.header
55 },
56 positions_backing: MmapBacking::Owned(vec_to_bytes(positions)),
57 offsets_backing: MmapBacking::Owned(vec_to_bytes(offsets)),
58 dps: parsed.dps,
59 dpv: parsed.dpv,
60 groups: HashMap::new(),
61 dpg: HashMap::new(),
62 tempdir: None,
63 });
64
65 let any = AnyTrxFile::F32(file);
66 let any = if options.trx_positions_dtype == DType::Float32 {
67 any
68 } else {
69 any.convert_positions_dtype(options.trx_positions_dtype)?
70 };
71 any.save(output)
72}
73
74struct ParsedTrk {
75 header: Header,
76 streamlines: Vec<Vec<[f32; 3]>>,
77 dpv: HashMap<String, DataArray>,
78 dps: HashMap<String, DataArray>,
79}
80
81#[derive(Clone, Copy)]
82enum Endianness {
83 Little,
84 Big,
85}
86
87impl Endianness {
88 fn read_i16(self, bytes: &[u8]) -> i16 {
89 match self {
90 Endianness::Little => i16::from_le_bytes(bytes.try_into().unwrap()),
91 Endianness::Big => i16::from_be_bytes(bytes.try_into().unwrap()),
92 }
93 }
94
95 fn read_i32(self, bytes: &[u8]) -> i32 {
96 match self {
97 Endianness::Little => i32::from_le_bytes(bytes.try_into().unwrap()),
98 Endianness::Big => i32::from_be_bytes(bytes.try_into().unwrap()),
99 }
100 }
101
102 fn read_f32(self, bytes: &[u8]) -> f32 {
103 match self {
104 Endianness::Little => f32::from_le_bytes(bytes.try_into().unwrap()),
105 Endianness::Big => f32::from_be_bytes(bytes.try_into().unwrap()),
106 }
107 }
108}
109
110#[derive(Clone)]
111struct SliceSpec {
112 name: String,
113 start: usize,
114 len: usize,
115}
116
117fn parse_trk(path: &Path) -> Result<ParsedTrk> {
118 let bytes = read_maybe_gzip(path)?;
119 if bytes.len() < TRK_HEADER_SIZE {
120 return Err(TrxError::Format(
121 "TRK file is smaller than the 1000-byte header".into(),
122 ));
123 }
124 if &bytes[..5] != b"TRACK" {
125 return Err(TrxError::Format(
126 "file does not start with TrackVis magic".into(),
127 ));
128 }
129
130 let header_bytes = &bytes[..TRK_HEADER_SIZE];
131 let endian = detect_endianness(header_bytes)?;
132 let version = endian.read_i32(&header_bytes[992..996]);
133 if version != 2 {
134 return Err(TrxError::Format(format!(
135 "unsupported TrackVis version {version}; only v2 is supported"
136 )));
137 }
138
139 let dimensions = [
140 parse_positive_i16(endian.read_i16(&header_bytes[6..8]), "dim[0]")? as usize,
141 parse_positive_i16(endian.read_i16(&header_bytes[8..10]), "dim[1]")? as usize,
142 parse_positive_i16(endian.read_i16(&header_bytes[10..12]), "dim[2]")? as usize,
143 ];
144 let voxel_sizes = [
145 parse_positive_f32(endian.read_f32(&header_bytes[12..16]), "voxel_size[0]")?,
146 parse_positive_f32(endian.read_f32(&header_bytes[16..20]), "voxel_size[1]")?,
147 parse_positive_f32(endian.read_f32(&header_bytes[20..24]), "voxel_size[2]")?,
148 ];
149
150 let n_scalars = parse_nonnegative_i16(endian.read_i16(&header_bytes[36..38]), "n_scalars")?;
151 let n_properties =
152 parse_nonnegative_i16(endian.read_i16(&header_bytes[238..240]), "n_properties")?;
153
154 let scalar_specs = parse_name_specs(
155 &header_bytes[38..238],
156 n_scalars,
157 MAX_NAMED_SCALARS_PER_POINT,
158 "scalars",
159 )?;
160 let property_specs = parse_name_specs(
161 &header_bytes[240..440],
162 n_properties,
163 MAX_NAMED_PROPERTIES_PER_STREAMLINE,
164 "properties",
165 )?;
166
167 let voxel_to_rasmm_f32 = parse_affine_f32(endian, &header_bytes[440..504])?;
168 if affine_is_all_zero(&voxel_to_rasmm_f32) {
169 return Err(TrxError::Format(
170 "TRK vox_to_ras is missing or zero; convert it with a more permissive tool first"
171 .into(),
172 ));
173 }
174 let affine_codes = affine_to_axcodes(&voxel_to_rasmm_f32)?;
175 let header_codes = parse_voxel_order(&header_bytes[948..952])?;
176
177 let declared_count = endian.read_i32(&header_bytes[988..992]);
178 let mut cursor = TRK_HEADER_SIZE;
179 let mut count = 0usize;
180 let mut streamlines = Vec::new();
181 let mut dpv_buffers = allocate_field_buffers(&scalar_specs);
182 let mut dps_buffers = allocate_field_buffers(&property_specs);
183
184 while cursor < bytes.len() {
185 if declared_count > 0 && count >= declared_count as usize {
186 break;
187 }
188
189 if cursor + 4 > bytes.len() {
190 return Err(TrxError::Format(
191 "TRK payload ended while reading streamline length".into(),
192 ));
193 }
194 let len = endian.read_i32(&bytes[cursor..cursor + 4]);
195 cursor += 4;
196 let len = usize::try_from(len)
197 .map_err(|_| TrxError::Format("TRK streamline length cannot be negative".into()))?;
198
199 let mut streamline = Vec::with_capacity(len);
200 for _ in 0..len {
201 if cursor + 12 > bytes.len() {
202 return Err(TrxError::Format(
203 "TRK payload ended while reading streamline points".into(),
204 ));
205 }
206 let point_voxmm = [
207 endian.read_f32(&bytes[cursor..cursor + 4]),
208 endian.read_f32(&bytes[cursor + 4..cursor + 8]),
209 endian.read_f32(&bytes[cursor + 8..cursor + 12]),
210 ];
211 cursor += 12;
212 let point_world = trackvis_to_rasmm(
213 point_voxmm,
214 voxel_sizes,
215 dimensions,
216 header_codes,
217 affine_codes,
218 &voxel_to_rasmm_f32,
219 )?;
220 streamline.push(point_world);
221
222 if n_scalars > 0 {
223 let scalar_values =
224 read_f32_row(&bytes, &mut cursor, n_scalars, endian, "scalars")?;
225 append_slices(&mut dpv_buffers, &scalar_specs, &scalar_values);
226 }
227 }
228
229 if n_properties > 0 {
230 let property_values =
231 read_f32_row(&bytes, &mut cursor, n_properties, endian, "properties")?;
232 append_slices(&mut dps_buffers, &property_specs, &property_values);
233 }
234
235 streamlines.push(streamline);
236 count += 1;
237 }
238
239 if declared_count > 0 && count != declared_count as usize {
240 return Err(TrxError::Format(format!(
241 "TRK header declares {declared_count} streamlines but parsed {count}"
242 )));
243 }
244
245 let header = Header {
246 voxel_to_rasmm: voxel_to_rasmm_f32.map(|row| row.map(f64::from)),
247 dimensions: dimensions.map(|value| value as u64),
248 nb_streamlines: streamlines.len() as u64,
249 nb_vertices: streamlines.iter().map(Vec::len).sum::<usize>() as u64,
250 extra: Default::default(),
251 };
252
253 Ok(ParsedTrk {
254 header,
255 streamlines,
256 dpv: finalize_field_buffers(dpv_buffers),
257 dps: finalize_field_buffers(dps_buffers),
258 })
259}
260
261fn detect_endianness(header: &[u8]) -> Result<Endianness> {
262 let le = i32::from_le_bytes(header[996..1000].try_into().unwrap());
263 if le == TRK_HEADER_SIZE as i32 {
264 return Ok(Endianness::Little);
265 }
266 let be = i32::from_be_bytes(header[996..1000].try_into().unwrap());
267 if be == TRK_HEADER_SIZE as i32 {
268 return Ok(Endianness::Big);
269 }
270 Err(TrxError::Format("TRK header size is invalid".into()))
271}
272
273fn parse_positive_i16(value: i16, label: &str) -> Result<i16> {
274 if value <= 0 {
275 return Err(TrxError::Format(format!(
276 "TRK {label} must be positive, got {value}"
277 )));
278 }
279 Ok(value)
280}
281
282fn parse_nonnegative_i16(value: i16, label: &str) -> Result<usize> {
283 usize::try_from(value)
284 .map_err(|_| TrxError::Format(format!("TRK {label} cannot be negative, got {value}")))
285}
286
287fn parse_positive_f32(value: f32, label: &str) -> Result<f32> {
288 if !value.is_finite() || value <= 0.0 {
289 return Err(TrxError::Format(format!(
290 "TRK {label} must be positive, got {value}"
291 )));
292 }
293 Ok(value)
294}
295
296fn parse_affine_f32(endian: Endianness, bytes: &[u8]) -> Result<[[f32; 4]; 4]> {
297 let mut affine = [[0.0f32; 4]; 4];
298 for (row, row_values) in affine.iter_mut().enumerate() {
299 for (col, value) in row_values.iter_mut().enumerate() {
300 let start = (row * 4 + col) * 4;
301 *value = endian.read_f32(&bytes[start..start + 4]);
302 }
303 }
304 if !affine.iter().flatten().all(|value| value.is_finite()) {
305 return Err(TrxError::Format(
306 "TRK vox_to_ras contains non-finite values".into(),
307 ));
308 }
309 Ok(affine)
310}
311
312fn affine_is_all_zero(affine: &[[f32; 4]; 4]) -> bool {
313 affine
314 .iter()
315 .flatten()
316 .all(|value| value.abs() <= f32::EPSILON)
317}
318
319fn parse_voxel_order(bytes: &[u8]) -> Result<[char; 3]> {
320 let text = bytes
321 .iter()
322 .copied()
323 .take_while(|byte| *byte != 0)
324 .map(char::from)
325 .collect::<String>()
326 .trim()
327 .to_ascii_uppercase();
328 let chars: Vec<char> = text.chars().collect();
329 if chars.len() < 3 {
330 return Err(TrxError::Format(
331 "TRK voxel order is missing or incomplete; convert it with another tool first".into(),
332 ));
333 }
334 Ok([chars[0], chars[1], chars[2]])
335}
336
337fn affine_to_axcodes(affine: &[[f32; 4]; 4]) -> Result<[char; 3]> {
338 let mut used_world_axes = [false; 3];
339 let mut out = ['R'; 3];
340 for (voxel_axis, slot) in out.iter_mut().enumerate() {
341 let mut best_axis = 0usize;
342 let mut best_value = 0.0f32;
343 for (world_axis, row) in affine.iter().enumerate().take(3) {
344 let value = row[voxel_axis].abs();
345 if value > best_value {
346 best_value = value;
347 best_axis = world_axis;
348 }
349 }
350 if best_value <= f32::EPSILON || used_world_axes[best_axis] {
351 return Err(TrxError::Format(
352 "TRK vox_to_ras affine is ambiguous or singular".into(),
353 ));
354 }
355 used_world_axes[best_axis] = true;
356 let sign = affine[best_axis][voxel_axis].signum();
357 *slot = match (best_axis, sign >= 0.0) {
358 (0, true) => 'R',
359 (0, false) => 'L',
360 (1, true) => 'A',
361 (1, false) => 'P',
362 (2, true) => 'S',
363 (2, false) => 'I',
364 _ => unreachable!(),
365 };
366 }
367 Ok(out)
368}
369
370fn parse_name_specs(
371 bytes: &[u8],
372 total: usize,
373 max_entries: usize,
374 fallback_name: &str,
375) -> Result<Vec<SliceSpec>> {
376 let mut specs = Vec::new();
377 let mut cursor = 0usize;
378 for entry in 0..max_entries {
379 let start = entry * 20;
380 let end = start + 20;
381 let (name, count) = decode_name_field(&bytes[start..end])?;
382 if count == 0 {
383 continue;
384 }
385 specs.push(SliceSpec {
386 name,
387 start: cursor,
388 len: count,
389 });
390 cursor += count;
391 }
392
393 if cursor < total {
394 specs.push(SliceSpec {
395 name: fallback_name.to_string(),
396 start: cursor,
397 len: total - cursor,
398 });
399 cursor = total;
400 }
401 if cursor != total {
402 return Err(TrxError::Format(format!(
403 "TrackVis named field layout is inconsistent with declared column count {total}"
404 )));
405 }
406 Ok(specs)
407}
408
409fn decode_name_field(bytes: &[u8]) -> Result<(String, usize)> {
410 let decoded = bytes.iter().map(|&byte| byte as char).collect::<String>();
411 let trimmed = decoded.trim_end_matches('\0');
412 if trimmed.is_empty() {
413 return Ok((String::new(), 0));
414 }
415
416 let mut parts = trimmed.split('\0');
417 let name = parts.next().unwrap().to_string();
418 let count = match parts.next() {
419 Some(count) => count
420 .parse::<usize>()
421 .map_err(|_| TrxError::Format(format!("invalid TrackVis name encoding '{trimmed}'")))?,
422 None => 1,
423 };
424 if parts.next().is_some() {
425 return Err(TrxError::Format(format!(
426 "invalid TrackVis name encoding '{trimmed}'"
427 )));
428 }
429 Ok((name, count))
430}
431
432fn read_f32_row(
433 bytes: &[u8],
434 cursor: &mut usize,
435 count: usize,
436 endian: Endianness,
437 label: &str,
438) -> Result<Vec<f32>> {
439 let byte_count = count
440 .checked_mul(4)
441 .ok_or_else(|| TrxError::Format(format!("TRK {label} row is too large")))?;
442 if *cursor + byte_count > bytes.len() {
443 return Err(TrxError::Format(format!(
444 "TRK payload ended while reading {label}"
445 )));
446 }
447 let row = (0..count)
448 .map(|index| {
449 let start = *cursor + index * 4;
450 endian.read_f32(&bytes[start..start + 4])
451 })
452 .collect();
453 *cursor += byte_count;
454 Ok(row)
455}
456
457fn allocate_field_buffers(specs: &[SliceSpec]) -> HashMap<String, (usize, Vec<f32>)> {
458 specs
459 .iter()
460 .map(|spec| (spec.name.clone(), (spec.len, Vec::new())))
461 .collect()
462}
463
464fn append_slices(
465 buffers: &mut HashMap<String, (usize, Vec<f32>)>,
466 specs: &[SliceSpec],
467 row: &[f32],
468) {
469 for spec in specs {
470 if let Some((_, values)) = buffers.get_mut(&spec.name) {
471 values.extend_from_slice(&row[spec.start..spec.start + spec.len]);
472 }
473 }
474}
475
476fn finalize_field_buffers(
477 buffers: HashMap<String, (usize, Vec<f32>)>,
478) -> HashMap<String, DataArray> {
479 buffers
480 .into_iter()
481 .map(|(name, (ncols, values))| {
482 (
483 name,
484 DataArray::owned_bytes(vec_to_bytes(values), ncols, DType::Float32),
485 )
486 })
487 .collect()
488}
489
490fn trackvis_to_rasmm(
491 point_voxmm: [f32; 3],
492 voxel_sizes: [f32; 3],
493 dimensions: [usize; 3],
494 header_codes: [char; 3],
495 affine_codes: [char; 3],
496 voxel_to_rasmm: &[[f32; 4]; 4],
497) -> Result<[f32; 3]> {
498 let voxel_center = [
499 point_voxmm[0] / voxel_sizes[0] - 0.5,
500 point_voxmm[1] / voxel_sizes[1] - 0.5,
501 point_voxmm[2] / voxel_sizes[2] - 0.5,
502 ];
503 let oriented = reorient_voxel_coords(voxel_center, dimensions, header_codes, affine_codes)?;
504 Ok(apply_affine(voxel_to_rasmm, oriented))
505}
506
507fn reorient_voxel_coords(
508 point: [f32; 3],
509 dimensions: [usize; 3],
510 from: [char; 3],
511 to: [char; 3],
512) -> Result<[f32; 3]> {
513 let mut out = [0.0f32; 3];
514 for (dst_axis, dst_code) in to.iter().enumerate() {
515 let dst_family = axis_family(*dst_code)?;
516 let dst_sign = axis_sign(*dst_code)?;
517 let src_axis = from
518 .iter()
519 .position(|code| axis_family(*code).ok() == Some(dst_family))
520 .ok_or_else(|| {
521 TrxError::Format("TRK voxel order does not match affine orientation".into())
522 })?;
523 let src_sign = axis_sign(from[src_axis])?;
524 out[dst_axis] = if src_sign == dst_sign {
525 point[src_axis]
526 } else {
527 (dimensions[src_axis] as f32 - 1.0) - point[src_axis]
528 };
529 }
530 Ok(out)
531}
532
533fn axis_family(code: char) -> Result<usize> {
534 match code {
535 'L' | 'R' => Ok(0),
536 'P' | 'A' => Ok(1),
537 'I' | 'S' => Ok(2),
538 other => Err(TrxError::Format(format!(
539 "unsupported anatomical axis code '{other}'"
540 ))),
541 }
542}
543
544fn axis_sign(code: char) -> Result<i8> {
545 match code {
546 'R' | 'A' | 'S' => Ok(1),
547 'L' | 'P' | 'I' => Ok(-1),
548 other => Err(TrxError::Format(format!(
549 "unsupported anatomical axis code '{other}'"
550 ))),
551 }
552}
553
554fn apply_affine(affine: &[[f32; 4]; 4], point: [f32; 3]) -> [f32; 3] {
555 [
556 affine[0][0] * point[0] + affine[0][1] * point[1] + affine[0][2] * point[2] + affine[0][3],
557 affine[1][0] * point[0] + affine[1][1] * point[1] + affine[1][2] * point[2] + affine[1][3],
558 affine[2][0] * point[0] + affine[2][1] * point[1] + affine[2][2] * point[2] + affine[2][3],
559 ]
560}
561
562fn build_offsets(streamlines: &[Vec<[f32; 3]>]) -> Result<Vec<u32>> {
563 let mut offsets = Vec::with_capacity(streamlines.len() + 1);
564 offsets.push(0);
565 let mut total = 0usize;
566 for streamline in streamlines {
567 total += streamline.len();
568 offsets
569 .push(u32::try_from(total).map_err(|_| {
570 TrxError::Format("TRK file has more than u32::MAX vertices".into())
571 })?);
572 }
573 Ok(offsets)
574}
575
576fn read_maybe_gzip(path: &Path) -> Result<Vec<u8>> {
577 let mut bytes = Vec::new();
578 let file = File::open(path)?;
579 if path
580 .file_name()
581 .and_then(|name| name.to_str())
582 .is_some_and(|name| name.ends_with(".gz"))
583 {
584 let mut decoder = MultiGzDecoder::new(file);
585 decoder.read_to_end(&mut bytes)?;
586 } else {
587 let mut file = file;
588 file.read_to_end(&mut bytes)?;
589 }
590 Ok(bytes)
591}