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 pixdim = header.pixdim;
96 let vsx = pixdim[1] as f64;
97 let vsy = pixdim[2] as f64;
98 let vsz = pixdim[3] as f64;
99
100 let scl_slope = if header.scl_slope == 0.0 { 1.0 } else { header.scl_slope as f64 };
102 let scl_inter = header.scl_inter as f64;
103
104 let affine = get_affine(header);
106
107 let volume = obj.into_volume();
109 let array: Array<f64, _> = volume.into_ndarray()
110 .map_err(|e| format!("Failed to convert to ndarray: {}", e))?;
111
112 let shape = array.shape();
114
115 if shape.len() < 3 {
117 return Err(format!("Expected at least 3D array, got {}D", shape.len()));
118 }
119
120 let (dim0, dim1, dim2) = (shape[0], shape[1], shape[2]);
122 let expected_size = dim0 * dim1 * dim2;
123
124 let mut data = Vec::with_capacity(expected_size);
127
128 if shape.len() == 3 {
130 for k in 0..dim2 {
131 for j in 0..dim1 {
132 for i in 0..dim0 {
133 data.push(array[[i, j, k]]);
134 }
135 }
136 }
137 } else if shape.len() >= 4 {
138 for k in 0..dim2 {
140 for j in 0..dim1 {
141 for i in 0..dim0 {
142 data.push(array[[i, j, k, 0]]);
143 }
144 }
145 }
146 }
147
148 Ok(NiftiData {
151 data,
152 dims: (dim0, dim1, dim2),
153 voxel_size: (vsx, vsy, vsz),
154 affine,
155 scl_slope,
156 scl_inter,
157 })
158}
159
160pub fn load_nifti_4d(bytes: &[u8]) -> Result<(Vec<f64>, (usize, usize, usize, usize), (f64, f64, f64), [f64; 16]), String> {
162 let obj: InMemNiftiObject = if is_gzip(bytes) {
163 let cursor = Cursor::new(bytes);
164 let decoder = GzDecoder::new(cursor);
165 InMemNiftiObject::from_reader(decoder)
166 .map_err(|e| format!("Failed to read gzipped NIfTI: {}", e))?
167 } else {
168 let cursor = Cursor::new(bytes);
169 InMemNiftiObject::from_reader(cursor)
170 .map_err(|e| format!("Failed to read NIfTI: {}", e))?
171 };
172
173 let header = obj.header();
174 let dim = header.dim;
175 let _ndim = dim[0] as usize;
176
177 let pixdim = header.pixdim;
178 let vsx = pixdim[1] as f64;
179 let vsy = pixdim[2] as f64;
180 let vsz = pixdim[3] as f64;
181
182 let affine = get_affine(header);
183
184 let volume = obj.into_volume();
186 let array: Array<f64, _> = volume.into_ndarray()
187 .map_err(|e| format!("Failed to convert to ndarray: {}", e))?;
188
189 let shape = array.shape();
190
191 let (dim0, dim1, dim2) = (shape[0], shape[1], shape[2]);
193 let dim3 = if shape.len() >= 4 { shape[3] } else { 1 };
194
195 let mut data = Vec::with_capacity(dim0 * dim1 * dim2 * dim3);
198
199 if shape.len() == 3 {
200 for k in 0..dim2 {
202 for j in 0..dim1 {
203 for i in 0..dim0 {
204 data.push(array[[i, j, k]]);
205 }
206 }
207 }
208 } else if shape.len() >= 4 {
209 for t in 0..dim3 {
211 for k in 0..dim2 {
212 for j in 0..dim1 {
213 for i in 0..dim0 {
214 data.push(array[[i, j, k, t]]);
215 }
216 }
217 }
218 }
219 }
220
221 Ok((data, (dim0, dim1, dim2, dim3), (vsx, vsy, vsz), affine))
223}
224
225fn get_affine(header: &NiftiHeader) -> [f64; 16] {
227 if header.sform_code > 0 {
229 let s = &header.srow_x;
230 let t = &header.srow_y;
231 let u = &header.srow_z;
232 [
233 s[0] as f64, s[1] as f64, s[2] as f64, s[3] as f64,
234 t[0] as f64, t[1] as f64, t[2] as f64, t[3] as f64,
235 u[0] as f64, u[1] as f64, u[2] as f64, u[3] as f64,
236 0.0, 0.0, 0.0, 1.0,
237 ]
238 } else {
239 let vsx = header.pixdim[1] as f64;
241 let vsy = header.pixdim[2] as f64;
242 let vsz = header.pixdim[3] as f64;
243 [
244 vsx, 0.0, 0.0, 0.0,
245 0.0, vsy, 0.0, 0.0,
246 0.0, 0.0, vsz, 0.0,
247 0.0, 0.0, 0.0, 1.0,
248 ]
249 }
250}
251
252pub fn save_nifti(
256 data: &[f64],
257 dims: (usize, usize, usize),
258 voxel_size: (f64, f64, f64),
259 affine: &[f64; 16],
260) -> Result<Vec<u8>, String> {
261 use std::io::Write;
262
263 let (nx, ny, nz) = dims;
264 let (vsx, vsy, vsz) = voxel_size;
265
266 let mut header = [0u8; 348];
268
269 header[0..4].copy_from_slice(&348i32.to_le_bytes());
271
272 let dim: [i16; 8] = [3, nx as i16, ny as i16, nz as i16, 1, 1, 1, 1];
274 for (i, &d) in dim.iter().enumerate() {
275 let offset = 40 + i * 2;
276 header[offset..offset + 2].copy_from_slice(&d.to_le_bytes());
277 }
278
279 header[70..72].copy_from_slice(&16i16.to_le_bytes());
281
282 header[72..74].copy_from_slice(&32i16.to_le_bytes());
284
285 let pixdim: [f32; 8] = [1.0, vsx as f32, vsy as f32, vsz as f32, 1.0, 1.0, 1.0, 1.0];
287 for (i, &p) in pixdim.iter().enumerate() {
288 let offset = 76 + i * 4;
289 header[offset..offset + 4].copy_from_slice(&p.to_le_bytes());
290 }
291
292 header[108..112].copy_from_slice(&352.0f32.to_le_bytes());
294
295 header[112..116].copy_from_slice(&1.0f32.to_le_bytes());
297
298 header[116..120].copy_from_slice(&0.0f32.to_le_bytes());
300
301 header[254..256].copy_from_slice(&1i16.to_le_bytes());
303
304 for i in 0..4 {
306 let offset = 280 + i * 4;
307 header[offset..offset + 4].copy_from_slice(&(affine[i] as f32).to_le_bytes());
308 }
309 for i in 0..4 {
310 let offset = 296 + i * 4;
311 header[offset..offset + 4].copy_from_slice(&(affine[4 + i] as f32).to_le_bytes());
312 }
313 for i in 0..4 {
314 let offset = 312 + i * 4;
315 header[offset..offset + 4].copy_from_slice(&(affine[8 + i] as f32).to_le_bytes());
316 }
317
318 header[344..348].copy_from_slice(b"n+1\0");
320
321 let mut buffer = Vec::with_capacity(352 + data.len() * 4);
323
324 buffer.write_all(&header).map_err(|e| format!("Write header failed: {}", e))?;
326
327 buffer.write_all(&[0u8; 4]).map_err(|e| format!("Write extension failed: {}", e))?;
329
330 for &val in data {
332 buffer.write_all(&(val as f32).to_le_bytes())
333 .map_err(|e| format!("Write data failed: {}", e))?;
334 }
335
336 Ok(buffer)
337}
338
339pub fn save_nifti_gz(
341 data: &[f64],
342 dims: (usize, usize, usize),
343 voxel_size: (f64, f64, f64),
344 affine: &[f64; 16],
345) -> Result<Vec<u8>, String> {
346 use flate2::write::GzEncoder;
347 use flate2::Compression;
348 use std::io::Write;
349
350 let uncompressed = save_nifti(data, dims, voxel_size, affine)?;
352
353 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
355 encoder.write_all(&uncompressed)
356 .map_err(|e| format!("Gzip compression failed: {}", e))?;
357
358 encoder.finish()
359 .map_err(|e| format!("Gzip finish failed: {}", e))
360}
361
362pub fn read_nifti_dims(path: &std::path::Path) -> Result<(usize, usize, usize), String> {
367 use std::io::Read;
368
369 let mut file = std::fs::File::open(path)
370 .map_err(|e| format!("Failed to open '{}': {}", path.display(), e))?;
371
372 let mut header_bytes = [0u8; 348];
374
375 let path_str = path.to_string_lossy();
376 if path_str.ends_with(".gz") {
377 let mut decoder = GzDecoder::new(file);
379 decoder.read_exact(&mut header_bytes)
380 .map_err(|e| format!("Failed to read gzipped NIfTI header '{}': {}", path.display(), e))?;
381 } else {
382 file.read_exact(&mut header_bytes)
383 .map_err(|e| format!("Failed to read NIfTI header '{}': {}", path.display(), e))?;
384 }
385
386 let sizeof_hdr_le = i32::from_le_bytes([header_bytes[0], header_bytes[1], header_bytes[2], header_bytes[3]]);
388 let is_le = sizeof_hdr_le == 348;
389
390 if !is_le {
391 let sizeof_hdr_be = i32::from_be_bytes([header_bytes[0], header_bytes[1], header_bytes[2], header_bytes[3]]);
392 if sizeof_hdr_be != 348 {
393 return Err(format!(
394 "Invalid NIfTI header in '{}': sizeof_hdr={} (LE) / {} (BE), expected 348",
395 path.display(), sizeof_hdr_le, sizeof_hdr_be
396 ));
397 }
398 }
399
400 let read_i16 = |offset: usize| -> i16 {
402 if is_le {
403 i16::from_le_bytes([header_bytes[offset], header_bytes[offset + 1]])
404 } else {
405 i16::from_be_bytes([header_bytes[offset], header_bytes[offset + 1]])
406 }
407 };
408
409 let ndim = read_i16(40);
410 if ndim < 3 {
411 return Err(format!("Expected at least 3D volume in '{}', got {}D", path.display(), ndim));
412 }
413
414 let nx = read_i16(42) as usize;
415 let ny = read_i16(44) as usize;
416 let nz = read_i16(46) as usize;
417
418 Ok((nx, ny, nz))
419}
420
421pub fn read_nifti_file(path: &std::path::Path) -> Result<NiftiData, String> {
425 let bytes = std::fs::read(path)
426 .map_err(|e| format!("Failed to read file '{}': {}", path.display(), e))?;
427 load_nifti(&bytes)
428}
429
430pub fn save_nifti_to_file(
435 path: &std::path::Path,
436 data: &[f64],
437 dims: (usize, usize, usize),
438 voxel_size: (f64, f64, f64),
439 affine: &[f64; 16],
440) -> Result<(), String> {
441 let path_str = path.to_string_lossy();
442 let bytes = if path_str.ends_with(".nii.gz") {
443 save_nifti_gz(data, dims, voxel_size, affine)?
444 } else {
445 save_nifti(data, dims, voxel_size, affine)?
446 };
447
448 std::fs::write(path, &bytes)
449 .map_err(|e| format!("Failed to write file '{}': {}", path.display(), e))
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455
456 #[test]
457 fn test_affine_identity() {
458 let mut header = NiftiHeader::default();
459 header.pixdim[1] = 1.0;
460 header.pixdim[2] = 2.0;
461 header.pixdim[3] = 3.0;
462 header.sform_code = 0;
463
464 let affine = get_affine(&header);
465 assert_eq!(affine[0], 1.0);
466 assert_eq!(affine[5], 2.0);
467 assert_eq!(affine[10], 3.0);
468 }
469
470 #[test]
471 fn test_gzip_detection() {
472 assert!(is_gzip(&[0x1f, 0x8b, 0x00]));
473 assert!(!is_gzip(&[0x00, 0x00, 0x00]));
474 assert!(!is_gzip(&[0x1f])); }
476
477 #[test]
478 fn test_save_nifti_header() {
479 let data = vec![0.0; 8]; let dims = (2, 2, 2);
481 let voxel_size = (1.0, 1.0, 1.0);
482 let affine = [
483 1.0, 0.0, 0.0, 0.0,
484 0.0, 1.0, 0.0, 0.0,
485 0.0, 0.0, 1.0, 0.0,
486 0.0, 0.0, 0.0, 1.0,
487 ];
488
489 let bytes = save_nifti(&data, dims, voxel_size, &affine).unwrap();
490
491 assert_eq!(bytes.len(), 352 + 8 * 4); assert_eq!(&bytes[344..348], b"n+1\0");
496
497 let sizeof_hdr = i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
499 assert_eq!(sizeof_hdr, 348);
500 }
501
502 #[test]
503 fn test_save_and_read_nifti_roundtrip() {
504 let dims = (4, 4, 4);
505 let n = dims.0 * dims.1 * dims.2;
506 let voxel_size = (1.0, 2.0, 3.0);
507 let affine = [
508 1.0, 0.0, 0.0, 10.0,
509 0.0, 2.0, 0.0, 20.0,
510 0.0, 0.0, 3.0, 30.0,
511 0.0, 0.0, 0.0, 1.0,
512 ];
513
514 let data: Vec<f64> = (0..n).map(|i| (i as f64) * 0.5 + 1.0).collect();
516
517 let tmp_dir = std::env::temp_dir();
519 let tmp_path = tmp_dir.join("test_nifti_roundtrip.nii");
520
521 save_nifti_to_file(&tmp_path, &data, dims, voxel_size, &affine).unwrap();
522
523 let loaded = read_nifti_file(&tmp_path).unwrap();
525
526 assert_eq!(loaded.dims, dims, "Dimensions should match");
528
529 assert!((loaded.voxel_size.0 - voxel_size.0).abs() < 1e-5, "Voxel size X mismatch");
531 assert!((loaded.voxel_size.1 - voxel_size.1).abs() < 1e-5, "Voxel size Y mismatch");
532 assert!((loaded.voxel_size.2 - voxel_size.2).abs() < 1e-5, "Voxel size Z mismatch");
533
534 assert_eq!(loaded.data.len(), n, "Data length should match");
536 for i in 0..n {
537 assert!(
538 (loaded.data[i] - data[i]).abs() < 0.01,
539 "Data mismatch at index {}: expected {}, got {}",
540 i, data[i], loaded.data[i]
541 );
542 }
543
544 std::fs::remove_file(&tmp_path).ok();
546 }
547
548 #[test]
549 fn test_save_and_read_nifti_f32() {
550 let dims = (4, 4, 4);
551 let n = dims.0 * dims.1 * dims.2;
552 let voxel_size = (1.5, 1.5, 1.5);
553 let affine = [
554 1.5, 0.0, 0.0, 0.0,
555 0.0, 1.5, 0.0, 0.0,
556 0.0, 0.0, 1.5, 0.0,
557 0.0, 0.0, 0.0, 1.0,
558 ];
559
560 let data: Vec<f64> = (0..n).map(|i| (i as f32 * 0.1) as f64).collect();
562
563 let tmp_dir = std::env::temp_dir();
564 let tmp_path = tmp_dir.join("test_nifti_f32.nii");
565
566 save_nifti_to_file(&tmp_path, &data, dims, voxel_size, &affine).unwrap();
567 let loaded = read_nifti_file(&tmp_path).unwrap();
568
569 assert_eq!(loaded.dims, dims);
570 assert_eq!(loaded.data.len(), n);
571
572 for i in 0..n {
574 assert!(
575 (loaded.data[i] - data[i]).abs() < 1e-5,
576 "f32 data mismatch at index {}: expected {}, got {}",
577 i, data[i], loaded.data[i]
578 );
579 }
580
581 std::fs::remove_file(&tmp_path).ok();
582 }
583
584 #[test]
585 fn test_save_nifti_gzip() {
586 let dims = (4, 4, 4);
587 let n = dims.0 * dims.1 * dims.2;
588 let voxel_size = (1.0, 1.0, 1.0);
589 let affine = [
590 1.0, 0.0, 0.0, 0.0,
591 0.0, 1.0, 0.0, 0.0,
592 0.0, 0.0, 1.0, 0.0,
593 0.0, 0.0, 0.0, 1.0,
594 ];
595
596 let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
597
598 let tmp_dir = std::env::temp_dir();
599 let tmp_path = tmp_dir.join("test_nifti_gz.nii.gz");
600
601 save_nifti_to_file(&tmp_path, &data, dims, voxel_size, &affine).unwrap();
602
603 let bytes = std::fs::read(&tmp_path).unwrap();
605 assert!(is_gzip(&bytes), "File should be gzip compressed");
606
607 let loaded = read_nifti_file(&tmp_path).unwrap();
609 assert_eq!(loaded.dims, dims);
610 assert_eq!(loaded.data.len(), n);
611
612 for i in 0..n {
613 assert!(
614 (loaded.data[i] - data[i]).abs() < 0.01,
615 "Gzip roundtrip mismatch at index {}: expected {}, got {}",
616 i, data[i], loaded.data[i]
617 );
618 }
619
620 std::fs::remove_file(&tmp_path).ok();
621 }
622
623 #[test]
624 fn test_load_nifti_invalid_bytes() {
625 let result = load_nifti(&[0u8; 10]);
627 assert!(result.is_err(), "Loading invalid bytes should error");
628 }
629
630 #[test]
631 fn test_load_nifti_invalid_gzip() {
632 let result = load_nifti(&[0x1f, 0x8b, 0x00, 0x00, 0x00]);
634 assert!(result.is_err(), "Loading invalid gzip should error");
635 }
636
637 #[test]
638 fn test_get_header_info_small_file() {
639 let info = get_header_info(&[0u8; 10]);
640 assert!(info.contains("too small"), "Should report file too small");
641 }
642
643 #[test]
644 fn test_get_header_info_normal() {
645 let mut bytes = vec![0u8; 348];
647 bytes[0..4].copy_from_slice(&348i32.to_le_bytes());
649 bytes[344..348].copy_from_slice(b"n+1\0");
651 bytes[70..72].copy_from_slice(&16i16.to_le_bytes());
653
654 let info = get_header_info(&bytes);
655 assert!(info.contains("sizeof_hdr=348"), "Should contain sizeof_hdr");
656 assert!(info.contains("datatype=16"), "Should contain datatype");
657 }
658
659 #[test]
660 fn test_affine_sform() {
661 let mut header = NiftiHeader::default();
663 header.sform_code = 1;
664 header.srow_x = [1.0, 0.0, 0.0, 10.0];
665 header.srow_y = [0.0, 2.0, 0.0, 20.0];
666 header.srow_z = [0.0, 0.0, 3.0, 30.0];
667
668 let affine = get_affine(&header);
669 assert_eq!(affine[0], 1.0);
670 assert_eq!(affine[3], 10.0);
671 assert_eq!(affine[5], 2.0);
672 assert_eq!(affine[7], 20.0);
673 assert_eq!(affine[10], 3.0);
674 assert_eq!(affine[11], 30.0);
675 assert_eq!(affine[15], 1.0);
676 }
677
678 #[test]
679 fn test_save_nifti_header_details() {
680 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]; let dims = (2, 2, 2);
682 let voxel_size = (1.5, 2.5, 3.5);
683 let affine = [
684 1.5, 0.0, 0.0, 5.0,
685 0.0, 2.5, 0.0, 10.0,
686 0.0, 0.0, 3.5, 15.0,
687 0.0, 0.0, 0.0, 1.0,
688 ];
689
690 let bytes = save_nifti(&data, dims, voxel_size, &affine).unwrap();
691
692 let datatype = i16::from_le_bytes([bytes[70], bytes[71]]);
694 assert_eq!(datatype, 16);
695
696 let bitpix = i16::from_le_bytes([bytes[72], bytes[73]]);
698 assert_eq!(bitpix, 32);
699
700 let ndim = i16::from_le_bytes([bytes[40], bytes[41]]);
702 assert_eq!(ndim, 3);
703
704 let nx = i16::from_le_bytes([bytes[42], bytes[43]]);
706 assert_eq!(nx, 2);
707
708 let vox_offset = f32::from_le_bytes([bytes[108], bytes[109], bytes[110], bytes[111]]);
710 assert_eq!(vox_offset, 352.0);
711
712 let scl_slope = f32::from_le_bytes([bytes[112], bytes[113], bytes[114], bytes[115]]);
714 assert_eq!(scl_slope, 1.0);
715
716 let sform_code = i16::from_le_bytes([bytes[254], bytes[255]]);
718 assert_eq!(sform_code, 1);
719
720 let pixdim1 = f32::from_le_bytes([bytes[80], bytes[81], bytes[82], bytes[83]]);
722 assert!((pixdim1 - 1.5).abs() < 1e-6);
723 }
724
725 #[test]
726 fn test_save_nifti_data_values() {
727 let data = vec![1.0f64, 2.0, -3.0, 4.5, 0.0, 100.0, -0.5, 999.0]; let dims = (2, 2, 2);
729 let voxel_size = (1.0, 1.0, 1.0);
730 let affine = [
731 1.0, 0.0, 0.0, 0.0,
732 0.0, 1.0, 0.0, 0.0,
733 0.0, 0.0, 1.0, 0.0,
734 0.0, 0.0, 0.0, 1.0,
735 ];
736
737 let bytes = save_nifti(&data, dims, voxel_size, &affine).unwrap();
738
739 for i in 0..8 {
741 let offset = 352 + i * 4;
742 let val = f32::from_le_bytes([
743 bytes[offset], bytes[offset + 1],
744 bytes[offset + 2], bytes[offset + 3],
745 ]);
746 assert!(
747 (val as f64 - data[i]).abs() < 0.01,
748 "Data value {} mismatch: saved {}, expected {}",
749 i, val, data[i]
750 );
751 }
752 }
753
754 #[test]
755 fn test_save_nifti_gz_bytes() {
756 let data = vec![0.0; 8]; let dims = (2, 2, 2);
758 let voxel_size = (1.0, 1.0, 1.0);
759 let affine = [
760 1.0, 0.0, 0.0, 0.0,
761 0.0, 1.0, 0.0, 0.0,
762 0.0, 0.0, 1.0, 0.0,
763 0.0, 0.0, 0.0, 1.0,
764 ];
765
766 let bytes = save_nifti_gz(&data, dims, voxel_size, &affine).unwrap();
767 assert!(is_gzip(&bytes), "save_nifti_gz should produce gzip bytes");
768
769 let loaded = load_nifti(&bytes).unwrap();
771 assert_eq!(loaded.dims, dims);
772 }
773
774 #[test]
775 fn test_read_nonexistent_file() {
776 let result = read_nifti_file(std::path::Path::new("/tmp/nonexistent_file_12345.nii"));
777 assert!(result.is_err(), "Reading nonexistent file should error");
778 match result {
779 Err(err) => {
780 assert!(err.contains("Failed to read file"), "Error should mention file reading: {}", err);
781 }
782 Ok(_) => panic!("Should have returned an error"),
783 }
784 }
785
786 #[test]
787 fn test_save_nifti_large_volume() {
788 let dims = (8, 8, 8);
790 let n = dims.0 * dims.1 * dims.2;
791 let voxel_size = (0.5, 0.5, 0.5);
792 let affine = [
793 0.5, 0.0, 0.0, -2.0,
794 0.0, 0.5, 0.0, -2.0,
795 0.0, 0.0, 0.5, -2.0,
796 0.0, 0.0, 0.0, 1.0,
797 ];
798
799 let data: Vec<f64> = (0..n).map(|i| (i as f64).sin()).collect();
800
801 let bytes = save_nifti(&data, dims, voxel_size, &affine).unwrap();
802 assert_eq!(bytes.len(), 352 + n * 4);
803
804 let loaded = load_nifti(&bytes).unwrap();
806 assert_eq!(loaded.dims, dims);
807 assert_eq!(loaded.data.len(), n);
808
809 for i in 0..n {
810 assert!(
811 (loaded.data[i] - data[i]).abs() < 0.01,
812 "Roundtrip mismatch at {}: expected {}, got {}",
813 i, data[i], loaded.data[i]
814 );
815 }
816 }
817
818 #[test]
819 fn test_nifti_roundtrip_affine() {
820 let dims = (4, 4, 4);
822 let n = dims.0 * dims.1 * dims.2;
823 let voxel_size = (1.0, 2.0, 3.0);
824 let affine = [
825 1.0, 0.1, 0.2, 10.0,
826 0.3, 2.0, 0.4, 20.0,
827 0.5, 0.6, 3.0, 30.0,
828 0.0, 0.0, 0.0, 1.0,
829 ];
830
831 let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
832
833 let tmp_dir = std::env::temp_dir();
834 let tmp_path = tmp_dir.join("test_nifti_affine_rt.nii");
835
836 save_nifti_to_file(&tmp_path, &data, dims, voxel_size, &affine).unwrap();
837 let loaded = read_nifti_file(&tmp_path).unwrap();
838
839 for i in 0..16 {
841 assert!(
842 (loaded.affine[i] - affine[i]).abs() < 0.01,
843 "Affine[{}] mismatch: expected {}, got {}",
844 i, affine[i], loaded.affine[i]
845 );
846 }
847
848 std::fs::remove_file(&tmp_path).ok();
849 }
850
851 #[test]
852 fn test_nifti_scl_slope_intercept() {
853 let dims = (4, 4, 4);
855 let n = dims.0 * dims.1 * dims.2;
856 let voxel_size = (1.0, 1.0, 1.0);
857 let affine = [
858 1.0, 0.0, 0.0, 0.0,
859 0.0, 1.0, 0.0, 0.0,
860 0.0, 0.0, 1.0, 0.0,
861 0.0, 0.0, 0.0, 1.0,
862 ];
863
864 let data = vec![1.0; n];
865 let bytes = save_nifti(&data, dims, voxel_size, &affine).unwrap();
866 let loaded = load_nifti(&bytes).unwrap();
867
868 assert!((loaded.scl_slope - 1.0).abs() < 1e-5);
870 assert!((loaded.scl_inter - 0.0).abs() < 1e-5);
871 }
872}