1use std::io::Cursor;
7use nifti::{NiftiObject, InMemNiftiObject, NiftiHeader};
8use nifti::volume::ndarray::IntoNdArray;
9use flate2::read::GzDecoder;
10use ndarray::Array;
11
12pub struct NiftiData {
14 pub data: Vec<f64>,
16 pub dims: (usize, usize, usize),
18 pub voxel_size: (f64, f64, f64),
20 pub affine: [f64; 16],
22 pub scl_slope: f64,
24 pub scl_inter: f64,
26}
27
28fn is_gzip(bytes: &[u8]) -> bool {
30 bytes.len() >= 2 && bytes[0] == 0x1f && bytes[1] == 0x8b
31}
32
33fn get_header_info(bytes: &[u8]) -> String {
35 if bytes.len() < 348 {
36 return format!("File too small ({} bytes, need at least 348)", bytes.len());
37 }
38
39 let sizeof_hdr = i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
41
42 let magic = if bytes.len() >= 348 {
44 String::from_utf8_lossy(&bytes[344..348]).to_string()
45 } else {
46 "N/A".to_string()
47 };
48
49 let datatype = if bytes.len() >= 72 {
51 i16::from_le_bytes([bytes[70], bytes[71]])
52 } else {
53 -1
54 };
55
56 format!("sizeof_hdr={}, magic='{}', datatype={}", sizeof_hdr, magic, datatype)
57}
58
59pub fn load_nifti(bytes: &[u8]) -> Result<NiftiData, String> {
63 let obj: InMemNiftiObject = if is_gzip(bytes) {
64 let cursor = Cursor::new(bytes);
65 let decoder = GzDecoder::new(cursor);
66 InMemNiftiObject::from_reader(decoder)
67 .map_err(|e| {
68 let mut decoder2 = GzDecoder::new(Cursor::new(bytes));
70 let mut decompressed = Vec::new();
71 let info = if std::io::Read::read_to_end(&mut decoder2, &mut decompressed).is_ok() {
72 get_header_info(&decompressed)
73 } else {
74 "Could not decompress".to_string()
75 };
76 format!("Failed to read gzipped NIfTI: {} ({})", e, info)
77 })?
78 } else {
79 let info = get_header_info(bytes);
80 let cursor = Cursor::new(bytes);
81 InMemNiftiObject::from_reader(cursor)
82 .map_err(|e| format!("Failed to read NIfTI: {} ({})", e, info))?
83 };
84
85 let header = obj.header();
86
87 let dim = header.dim;
89 let ndim = dim[0] as usize;
90 if ndim < 3 {
91 return Err(format!("Expected at least 3D volume, got {}D", ndim));
92 }
93
94 let nx = dim[1] as usize;
95 let ny = dim[2] as usize;
96 let nz = dim[3] as usize;
97
98 let pixdim = header.pixdim;
100 let vsx = pixdim[1] as f64;
101 let vsy = pixdim[2] as f64;
102 let vsz = pixdim[3] as f64;
103
104 let scl_slope = if header.scl_slope == 0.0 { 1.0 } else { header.scl_slope as f64 };
106 let scl_inter = header.scl_inter as f64;
107
108 let affine = get_affine(header);
110
111 let volume = obj.into_volume();
113 let array: Array<f64, _> = volume.into_ndarray()
114 .map_err(|e| format!("Failed to convert to ndarray: {}", e))?;
115
116 let shape = array.shape();
118
119 if shape.len() < 3 {
121 return Err(format!("Expected at least 3D array, got {}D", shape.len()));
122 }
123
124 let (dim0, dim1, dim2) = (shape[0], shape[1], shape[2]);
126 let expected_size = dim0 * dim1 * dim2;
127
128 let mut data = Vec::with_capacity(expected_size);
131
132 if shape.len() == 3 {
134 for k in 0..dim2 {
135 for j in 0..dim1 {
136 for i in 0..dim0 {
137 data.push(array[[i, j, k]]);
138 }
139 }
140 }
141 } else if shape.len() >= 4 {
142 for k in 0..dim2 {
144 for j in 0..dim1 {
145 for i in 0..dim0 {
146 data.push(array[[i, j, k, 0]]);
147 }
148 }
149 }
150 }
151
152 Ok(NiftiData {
155 data,
156 dims: (dim0, dim1, dim2),
157 voxel_size: (vsx, vsy, vsz),
158 affine,
159 scl_slope,
160 scl_inter,
161 })
162}
163
164pub fn load_nifti_4d(bytes: &[u8]) -> Result<(Vec<f64>, (usize, usize, usize, usize), (f64, f64, f64), [f64; 16]), String> {
166 let obj: InMemNiftiObject = if is_gzip(bytes) {
167 let cursor = Cursor::new(bytes);
168 let decoder = GzDecoder::new(cursor);
169 InMemNiftiObject::from_reader(decoder)
170 .map_err(|e| format!("Failed to read gzipped NIfTI: {}", e))?
171 } else {
172 let cursor = Cursor::new(bytes);
173 InMemNiftiObject::from_reader(cursor)
174 .map_err(|e| format!("Failed to read NIfTI: {}", e))?
175 };
176
177 let header = obj.header();
178 let dim = header.dim;
179 let ndim = dim[0] as usize;
180
181 let nx = dim[1] as usize;
182 let ny = dim[2] as usize;
183 let nz = dim[3] as usize;
184 let nt = if ndim >= 4 { dim[4] as usize } else { 1 };
185
186 let pixdim = header.pixdim;
187 let vsx = pixdim[1] as f64;
188 let vsy = pixdim[2] as f64;
189 let vsz = pixdim[3] as f64;
190
191 let affine = get_affine(header);
192
193 let volume = obj.into_volume();
195 let array: Array<f64, _> = volume.into_ndarray()
196 .map_err(|e| format!("Failed to convert to ndarray: {}", e))?;
197
198 let shape = array.shape();
199
200 let (dim0, dim1, dim2) = (shape[0], shape[1], shape[2]);
202 let dim3 = if shape.len() >= 4 { shape[3] } else { 1 };
203
204 let mut data = Vec::with_capacity(dim0 * dim1 * dim2 * dim3);
207
208 if shape.len() == 3 {
209 for k in 0..dim2 {
211 for j in 0..dim1 {
212 for i in 0..dim0 {
213 data.push(array[[i, j, k]]);
214 }
215 }
216 }
217 } else if shape.len() >= 4 {
218 for t in 0..dim3 {
220 for k in 0..dim2 {
221 for j in 0..dim1 {
222 for i in 0..dim0 {
223 data.push(array[[i, j, k, t]]);
224 }
225 }
226 }
227 }
228 }
229
230 Ok((data, (dim0, dim1, dim2, dim3), (vsx, vsy, vsz), affine))
232}
233
234fn get_affine(header: &NiftiHeader) -> [f64; 16] {
236 if header.sform_code > 0 {
238 let s = &header.srow_x;
239 let t = &header.srow_y;
240 let u = &header.srow_z;
241 [
242 s[0] as f64, s[1] as f64, s[2] as f64, s[3] as f64,
243 t[0] as f64, t[1] as f64, t[2] as f64, t[3] as f64,
244 u[0] as f64, u[1] as f64, u[2] as f64, u[3] as f64,
245 0.0, 0.0, 0.0, 1.0,
246 ]
247 } else {
248 let vsx = header.pixdim[1] as f64;
250 let vsy = header.pixdim[2] as f64;
251 let vsz = header.pixdim[3] as f64;
252 [
253 vsx, 0.0, 0.0, 0.0,
254 0.0, vsy, 0.0, 0.0,
255 0.0, 0.0, vsz, 0.0,
256 0.0, 0.0, 0.0, 1.0,
257 ]
258 }
259}
260
261pub fn save_nifti(
265 data: &[f64],
266 dims: (usize, usize, usize),
267 voxel_size: (f64, f64, f64),
268 affine: &[f64; 16],
269) -> Result<Vec<u8>, String> {
270 use std::io::Write;
271
272 let (nx, ny, nz) = dims;
273 let (vsx, vsy, vsz) = voxel_size;
274
275 let mut header = [0u8; 348];
277
278 header[0..4].copy_from_slice(&348i32.to_le_bytes());
280
281 let dim: [i16; 8] = [3, nx as i16, ny as i16, nz as i16, 1, 1, 1, 1];
283 for (i, &d) in dim.iter().enumerate() {
284 let offset = 40 + i * 2;
285 header[offset..offset + 2].copy_from_slice(&d.to_le_bytes());
286 }
287
288 header[70..72].copy_from_slice(&16i16.to_le_bytes());
290
291 header[72..74].copy_from_slice(&32i16.to_le_bytes());
293
294 let pixdim: [f32; 8] = [1.0, vsx as f32, vsy as f32, vsz as f32, 1.0, 1.0, 1.0, 1.0];
296 for (i, &p) in pixdim.iter().enumerate() {
297 let offset = 76 + i * 4;
298 header[offset..offset + 4].copy_from_slice(&p.to_le_bytes());
299 }
300
301 header[108..112].copy_from_slice(&352.0f32.to_le_bytes());
303
304 header[112..116].copy_from_slice(&1.0f32.to_le_bytes());
306
307 header[116..120].copy_from_slice(&0.0f32.to_le_bytes());
309
310 header[254..256].copy_from_slice(&1i16.to_le_bytes());
312
313 for i in 0..4 {
315 let offset = 280 + i * 4;
316 header[offset..offset + 4].copy_from_slice(&(affine[i] as f32).to_le_bytes());
317 }
318 for i in 0..4 {
319 let offset = 296 + i * 4;
320 header[offset..offset + 4].copy_from_slice(&(affine[4 + i] as f32).to_le_bytes());
321 }
322 for i in 0..4 {
323 let offset = 312 + i * 4;
324 header[offset..offset + 4].copy_from_slice(&(affine[8 + i] as f32).to_le_bytes());
325 }
326
327 header[344..348].copy_from_slice(b"n+1\0");
329
330 let mut buffer = Vec::with_capacity(352 + data.len() * 4);
332
333 buffer.write_all(&header).map_err(|e| format!("Write header failed: {}", e))?;
335
336 buffer.write_all(&[0u8; 4]).map_err(|e| format!("Write extension failed: {}", e))?;
338
339 for &val in data {
341 buffer.write_all(&(val as f32).to_le_bytes())
342 .map_err(|e| format!("Write data failed: {}", e))?;
343 }
344
345 Ok(buffer)
346}
347
348pub fn save_nifti_gz(
350 data: &[f64],
351 dims: (usize, usize, usize),
352 voxel_size: (f64, f64, f64),
353 affine: &[f64; 16],
354) -> Result<Vec<u8>, String> {
355 use flate2::write::GzEncoder;
356 use flate2::Compression;
357 use std::io::Write;
358
359 let uncompressed = save_nifti(data, dims, voxel_size, affine)?;
361
362 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
364 encoder.write_all(&uncompressed)
365 .map_err(|e| format!("Gzip compression failed: {}", e))?;
366
367 encoder.finish()
368 .map_err(|e| format!("Gzip finish failed: {}", e))
369}
370
371pub fn read_nifti_dims(path: &std::path::Path) -> Result<(usize, usize, usize), String> {
376 use std::io::Read;
377
378 let mut file = std::fs::File::open(path)
379 .map_err(|e| format!("Failed to open '{}': {}", path.display(), e))?;
380
381 let mut header_bytes = [0u8; 348];
383
384 let path_str = path.to_string_lossy();
385 if path_str.ends_with(".gz") {
386 let mut decoder = GzDecoder::new(file);
388 decoder.read_exact(&mut header_bytes)
389 .map_err(|e| format!("Failed to read gzipped NIfTI header '{}': {}", path.display(), e))?;
390 } else {
391 file.read_exact(&mut header_bytes)
392 .map_err(|e| format!("Failed to read NIfTI header '{}': {}", path.display(), e))?;
393 }
394
395 let sizeof_hdr_le = i32::from_le_bytes([header_bytes[0], header_bytes[1], header_bytes[2], header_bytes[3]]);
397 let is_le = sizeof_hdr_le == 348;
398
399 if !is_le {
400 let sizeof_hdr_be = i32::from_be_bytes([header_bytes[0], header_bytes[1], header_bytes[2], header_bytes[3]]);
401 if sizeof_hdr_be != 348 {
402 return Err(format!(
403 "Invalid NIfTI header in '{}': sizeof_hdr={} (LE) / {} (BE), expected 348",
404 path.display(), sizeof_hdr_le, sizeof_hdr_be
405 ));
406 }
407 }
408
409 let read_i16 = |offset: usize| -> i16 {
411 if is_le {
412 i16::from_le_bytes([header_bytes[offset], header_bytes[offset + 1]])
413 } else {
414 i16::from_be_bytes([header_bytes[offset], header_bytes[offset + 1]])
415 }
416 };
417
418 let ndim = read_i16(40);
419 if ndim < 3 {
420 return Err(format!("Expected at least 3D volume in '{}', got {}D", path.display(), ndim));
421 }
422
423 let nx = read_i16(42) as usize;
424 let ny = read_i16(44) as usize;
425 let nz = read_i16(46) as usize;
426
427 Ok((nx, ny, nz))
428}
429
430pub fn read_nifti_file(path: &std::path::Path) -> Result<NiftiData, String> {
434 let bytes = std::fs::read(path)
435 .map_err(|e| format!("Failed to read file '{}': {}", path.display(), e))?;
436 load_nifti(&bytes)
437}
438
439pub fn save_nifti_to_file(
444 path: &std::path::Path,
445 data: &[f64],
446 dims: (usize, usize, usize),
447 voxel_size: (f64, f64, f64),
448 affine: &[f64; 16],
449) -> Result<(), String> {
450 let path_str = path.to_string_lossy();
451 let bytes = if path_str.ends_with(".nii.gz") {
452 save_nifti_gz(data, dims, voxel_size, affine)?
453 } else {
454 save_nifti(data, dims, voxel_size, affine)?
455 };
456
457 std::fs::write(path, &bytes)
458 .map_err(|e| format!("Failed to write file '{}': {}", path.display(), e))
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464
465 #[test]
466 fn test_affine_identity() {
467 let mut header = NiftiHeader::default();
468 header.pixdim[1] = 1.0;
469 header.pixdim[2] = 2.0;
470 header.pixdim[3] = 3.0;
471 header.sform_code = 0;
472
473 let affine = get_affine(&header);
474 assert_eq!(affine[0], 1.0);
475 assert_eq!(affine[5], 2.0);
476 assert_eq!(affine[10], 3.0);
477 }
478
479 #[test]
480 fn test_gzip_detection() {
481 assert!(is_gzip(&[0x1f, 0x8b, 0x00]));
482 assert!(!is_gzip(&[0x00, 0x00, 0x00]));
483 assert!(!is_gzip(&[0x1f])); }
485
486 #[test]
487 fn test_save_nifti_header() {
488 let data = vec![0.0; 8]; let dims = (2, 2, 2);
490 let voxel_size = (1.0, 1.0, 1.0);
491 let affine = [
492 1.0, 0.0, 0.0, 0.0,
493 0.0, 1.0, 0.0, 0.0,
494 0.0, 0.0, 1.0, 0.0,
495 0.0, 0.0, 0.0, 1.0,
496 ];
497
498 let bytes = save_nifti(&data, dims, voxel_size, &affine).unwrap();
499
500 assert_eq!(bytes.len(), 352 + 8 * 4); assert_eq!(&bytes[344..348], b"n+1\0");
505
506 let sizeof_hdr = i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
508 assert_eq!(sizeof_hdr, 348);
509 }
510
511 #[test]
512 fn test_save_and_read_nifti_roundtrip() {
513 let dims = (4, 4, 4);
514 let n = dims.0 * dims.1 * dims.2;
515 let voxel_size = (1.0, 2.0, 3.0);
516 let affine = [
517 1.0, 0.0, 0.0, 10.0,
518 0.0, 2.0, 0.0, 20.0,
519 0.0, 0.0, 3.0, 30.0,
520 0.0, 0.0, 0.0, 1.0,
521 ];
522
523 let data: Vec<f64> = (0..n).map(|i| (i as f64) * 0.5 + 1.0).collect();
525
526 let tmp_dir = std::env::temp_dir();
528 let tmp_path = tmp_dir.join("test_nifti_roundtrip.nii");
529
530 save_nifti_to_file(&tmp_path, &data, dims, voxel_size, &affine).unwrap();
531
532 let loaded = read_nifti_file(&tmp_path).unwrap();
534
535 assert_eq!(loaded.dims, dims, "Dimensions should match");
537
538 assert!((loaded.voxel_size.0 - voxel_size.0).abs() < 1e-5, "Voxel size X mismatch");
540 assert!((loaded.voxel_size.1 - voxel_size.1).abs() < 1e-5, "Voxel size Y mismatch");
541 assert!((loaded.voxel_size.2 - voxel_size.2).abs() < 1e-5, "Voxel size Z mismatch");
542
543 assert_eq!(loaded.data.len(), n, "Data length should match");
545 for i in 0..n {
546 assert!(
547 (loaded.data[i] - data[i]).abs() < 0.01,
548 "Data mismatch at index {}: expected {}, got {}",
549 i, data[i], loaded.data[i]
550 );
551 }
552
553 std::fs::remove_file(&tmp_path).ok();
555 }
556
557 #[test]
558 fn test_save_and_read_nifti_f32() {
559 let dims = (4, 4, 4);
560 let n = dims.0 * dims.1 * dims.2;
561 let voxel_size = (1.5, 1.5, 1.5);
562 let affine = [
563 1.5, 0.0, 0.0, 0.0,
564 0.0, 1.5, 0.0, 0.0,
565 0.0, 0.0, 1.5, 0.0,
566 0.0, 0.0, 0.0, 1.0,
567 ];
568
569 let data: Vec<f64> = (0..n).map(|i| (i as f32 * 0.1) as f64).collect();
571
572 let tmp_dir = std::env::temp_dir();
573 let tmp_path = tmp_dir.join("test_nifti_f32.nii");
574
575 save_nifti_to_file(&tmp_path, &data, dims, voxel_size, &affine).unwrap();
576 let loaded = read_nifti_file(&tmp_path).unwrap();
577
578 assert_eq!(loaded.dims, dims);
579 assert_eq!(loaded.data.len(), n);
580
581 for i in 0..n {
583 assert!(
584 (loaded.data[i] - data[i]).abs() < 1e-5,
585 "f32 data mismatch at index {}: expected {}, got {}",
586 i, data[i], loaded.data[i]
587 );
588 }
589
590 std::fs::remove_file(&tmp_path).ok();
591 }
592
593 #[test]
594 fn test_save_nifti_gzip() {
595 let dims = (4, 4, 4);
596 let n = dims.0 * dims.1 * dims.2;
597 let voxel_size = (1.0, 1.0, 1.0);
598 let affine = [
599 1.0, 0.0, 0.0, 0.0,
600 0.0, 1.0, 0.0, 0.0,
601 0.0, 0.0, 1.0, 0.0,
602 0.0, 0.0, 0.0, 1.0,
603 ];
604
605 let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
606
607 let tmp_dir = std::env::temp_dir();
608 let tmp_path = tmp_dir.join("test_nifti_gz.nii.gz");
609
610 save_nifti_to_file(&tmp_path, &data, dims, voxel_size, &affine).unwrap();
611
612 let bytes = std::fs::read(&tmp_path).unwrap();
614 assert!(is_gzip(&bytes), "File should be gzip compressed");
615
616 let loaded = read_nifti_file(&tmp_path).unwrap();
618 assert_eq!(loaded.dims, dims);
619 assert_eq!(loaded.data.len(), n);
620
621 for i in 0..n {
622 assert!(
623 (loaded.data[i] - data[i]).abs() < 0.01,
624 "Gzip roundtrip mismatch at index {}: expected {}, got {}",
625 i, data[i], loaded.data[i]
626 );
627 }
628
629 std::fs::remove_file(&tmp_path).ok();
630 }
631
632 #[test]
633 fn test_load_nifti_invalid_bytes() {
634 let result = load_nifti(&[0u8; 10]);
636 assert!(result.is_err(), "Loading invalid bytes should error");
637 }
638
639 #[test]
640 fn test_load_nifti_invalid_gzip() {
641 let result = load_nifti(&[0x1f, 0x8b, 0x00, 0x00, 0x00]);
643 assert!(result.is_err(), "Loading invalid gzip should error");
644 }
645
646 #[test]
647 fn test_get_header_info_small_file() {
648 let info = get_header_info(&[0u8; 10]);
649 assert!(info.contains("too small"), "Should report file too small");
650 }
651
652 #[test]
653 fn test_get_header_info_normal() {
654 let mut bytes = vec![0u8; 348];
656 bytes[0..4].copy_from_slice(&348i32.to_le_bytes());
658 bytes[344..348].copy_from_slice(b"n+1\0");
660 bytes[70..72].copy_from_slice(&16i16.to_le_bytes());
662
663 let info = get_header_info(&bytes);
664 assert!(info.contains("sizeof_hdr=348"), "Should contain sizeof_hdr");
665 assert!(info.contains("datatype=16"), "Should contain datatype");
666 }
667
668 #[test]
669 fn test_affine_sform() {
670 let mut header = NiftiHeader::default();
672 header.sform_code = 1;
673 header.srow_x = [1.0, 0.0, 0.0, 10.0];
674 header.srow_y = [0.0, 2.0, 0.0, 20.0];
675 header.srow_z = [0.0, 0.0, 3.0, 30.0];
676
677 let affine = get_affine(&header);
678 assert_eq!(affine[0], 1.0);
679 assert_eq!(affine[3], 10.0);
680 assert_eq!(affine[5], 2.0);
681 assert_eq!(affine[7], 20.0);
682 assert_eq!(affine[10], 3.0);
683 assert_eq!(affine[11], 30.0);
684 assert_eq!(affine[15], 1.0);
685 }
686
687 #[test]
688 fn test_save_nifti_header_details() {
689 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]; let dims = (2, 2, 2);
691 let voxel_size = (1.5, 2.5, 3.5);
692 let affine = [
693 1.5, 0.0, 0.0, 5.0,
694 0.0, 2.5, 0.0, 10.0,
695 0.0, 0.0, 3.5, 15.0,
696 0.0, 0.0, 0.0, 1.0,
697 ];
698
699 let bytes = save_nifti(&data, dims, voxel_size, &affine).unwrap();
700
701 let datatype = i16::from_le_bytes([bytes[70], bytes[71]]);
703 assert_eq!(datatype, 16);
704
705 let bitpix = i16::from_le_bytes([bytes[72], bytes[73]]);
707 assert_eq!(bitpix, 32);
708
709 let ndim = i16::from_le_bytes([bytes[40], bytes[41]]);
711 assert_eq!(ndim, 3);
712
713 let nx = i16::from_le_bytes([bytes[42], bytes[43]]);
715 assert_eq!(nx, 2);
716
717 let vox_offset = f32::from_le_bytes([bytes[108], bytes[109], bytes[110], bytes[111]]);
719 assert_eq!(vox_offset, 352.0);
720
721 let scl_slope = f32::from_le_bytes([bytes[112], bytes[113], bytes[114], bytes[115]]);
723 assert_eq!(scl_slope, 1.0);
724
725 let sform_code = i16::from_le_bytes([bytes[254], bytes[255]]);
727 assert_eq!(sform_code, 1);
728
729 let pixdim1 = f32::from_le_bytes([bytes[80], bytes[81], bytes[82], bytes[83]]);
731 assert!((pixdim1 - 1.5).abs() < 1e-6);
732 }
733
734 #[test]
735 fn test_save_nifti_data_values() {
736 let data = vec![1.0f64, 2.0, -3.0, 4.5, 0.0, 100.0, -0.5, 999.0]; let dims = (2, 2, 2);
738 let voxel_size = (1.0, 1.0, 1.0);
739 let affine = [
740 1.0, 0.0, 0.0, 0.0,
741 0.0, 1.0, 0.0, 0.0,
742 0.0, 0.0, 1.0, 0.0,
743 0.0, 0.0, 0.0, 1.0,
744 ];
745
746 let bytes = save_nifti(&data, dims, voxel_size, &affine).unwrap();
747
748 for i in 0..8 {
750 let offset = 352 + i * 4;
751 let val = f32::from_le_bytes([
752 bytes[offset], bytes[offset + 1],
753 bytes[offset + 2], bytes[offset + 3],
754 ]);
755 assert!(
756 (val as f64 - data[i]).abs() < 0.01,
757 "Data value {} mismatch: saved {}, expected {}",
758 i, val, data[i]
759 );
760 }
761 }
762
763 #[test]
764 fn test_save_nifti_gz_bytes() {
765 let data = vec![0.0; 8]; let dims = (2, 2, 2);
767 let voxel_size = (1.0, 1.0, 1.0);
768 let affine = [
769 1.0, 0.0, 0.0, 0.0,
770 0.0, 1.0, 0.0, 0.0,
771 0.0, 0.0, 1.0, 0.0,
772 0.0, 0.0, 0.0, 1.0,
773 ];
774
775 let bytes = save_nifti_gz(&data, dims, voxel_size, &affine).unwrap();
776 assert!(is_gzip(&bytes), "save_nifti_gz should produce gzip bytes");
777
778 let loaded = load_nifti(&bytes).unwrap();
780 assert_eq!(loaded.dims, dims);
781 }
782
783 #[test]
784 fn test_read_nonexistent_file() {
785 let result = read_nifti_file(std::path::Path::new("/tmp/nonexistent_file_12345.nii"));
786 assert!(result.is_err(), "Reading nonexistent file should error");
787 match result {
788 Err(err) => {
789 assert!(err.contains("Failed to read file"), "Error should mention file reading: {}", err);
790 }
791 Ok(_) => panic!("Should have returned an error"),
792 }
793 }
794
795 #[test]
796 fn test_save_nifti_large_volume() {
797 let dims = (8, 8, 8);
799 let n = dims.0 * dims.1 * dims.2;
800 let voxel_size = (0.5, 0.5, 0.5);
801 let affine = [
802 0.5, 0.0, 0.0, -2.0,
803 0.0, 0.5, 0.0, -2.0,
804 0.0, 0.0, 0.5, -2.0,
805 0.0, 0.0, 0.0, 1.0,
806 ];
807
808 let data: Vec<f64> = (0..n).map(|i| (i as f64).sin()).collect();
809
810 let bytes = save_nifti(&data, dims, voxel_size, &affine).unwrap();
811 assert_eq!(bytes.len(), 352 + n * 4);
812
813 let loaded = load_nifti(&bytes).unwrap();
815 assert_eq!(loaded.dims, dims);
816 assert_eq!(loaded.data.len(), n);
817
818 for i in 0..n {
819 assert!(
820 (loaded.data[i] - data[i]).abs() < 0.01,
821 "Roundtrip mismatch at {}: expected {}, got {}",
822 i, data[i], loaded.data[i]
823 );
824 }
825 }
826
827 #[test]
828 fn test_nifti_roundtrip_affine() {
829 let dims = (4, 4, 4);
831 let n = dims.0 * dims.1 * dims.2;
832 let voxel_size = (1.0, 2.0, 3.0);
833 let affine = [
834 1.0, 0.1, 0.2, 10.0,
835 0.3, 2.0, 0.4, 20.0,
836 0.5, 0.6, 3.0, 30.0,
837 0.0, 0.0, 0.0, 1.0,
838 ];
839
840 let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
841
842 let tmp_dir = std::env::temp_dir();
843 let tmp_path = tmp_dir.join("test_nifti_affine_rt.nii");
844
845 save_nifti_to_file(&tmp_path, &data, dims, voxel_size, &affine).unwrap();
846 let loaded = read_nifti_file(&tmp_path).unwrap();
847
848 for i in 0..16 {
850 assert!(
851 (loaded.affine[i] - affine[i]).abs() < 0.01,
852 "Affine[{}] mismatch: expected {}, got {}",
853 i, affine[i], loaded.affine[i]
854 );
855 }
856
857 std::fs::remove_file(&tmp_path).ok();
858 }
859
860 #[test]
861 fn test_nifti_scl_slope_intercept() {
862 let dims = (4, 4, 4);
864 let n = dims.0 * dims.1 * dims.2;
865 let voxel_size = (1.0, 1.0, 1.0);
866 let affine = [
867 1.0, 0.0, 0.0, 0.0,
868 0.0, 1.0, 0.0, 0.0,
869 0.0, 0.0, 1.0, 0.0,
870 0.0, 0.0, 0.0, 1.0,
871 ];
872
873 let data = vec![1.0; n];
874 let bytes = save_nifti(&data, dims, voxel_size, &affine).unwrap();
875 let loaded = load_nifti(&bytes).unwrap();
876
877 assert!((loaded.scl_slope - 1.0).abs() < 1e-5);
879 assert!((loaded.scl_inter - 0.0).abs() < 1e-5);
880 }
881}