1use std::path::Path;
16
17use ndarray::Array3;
18use tiff::decoder::Decoder;
19use tiff::decoder::DecodingResult;
20
21use crate::error::IoError;
22
23pub fn load_tiff_stack(path: &Path) -> Result<Array3<f64>, IoError> {
34 let file = std::fs::File::open(path)
35 .map_err(|e| IoError::FileNotFound(path.to_string_lossy().into_owned(), e))?;
36 let mut decoder = Decoder::new(file).map_err(|e| IoError::TiffDecode(format!("{}", e)))?;
37
38 let mut frames: Vec<Vec<f64>> = Vec::new();
39 let mut width = 0u32;
40 let mut height = 0u32;
41
42 loop {
43 let (w, h) = decoder
44 .dimensions()
45 .map_err(|e| IoError::TiffDecode(format!("{}", e)))?;
46
47 if frames.is_empty() {
48 width = w;
49 height = h;
50 } else if w != width || h != height {
51 return Err(IoError::DimensionMismatch {
52 expected: (width, height),
53 got: (w, h),
54 frame: frames.len(),
55 });
56 }
57
58 let data = decoder
59 .read_image()
60 .map_err(|e| IoError::TiffDecode(format!("{}", e)))?;
61
62 let pixels = decode_to_f64(data)?;
63 let expected_len = (width as usize) * (height as usize);
64 if pixels.len() != expected_len {
65 return Err(IoError::TiffDecode(format!(
66 "Frame {} has {} pixels, expected {}",
67 frames.len(),
68 pixels.len(),
69 expected_len
70 )));
71 }
72 frames.push(pixels);
73
74 if !decoder.more_images() {
75 break;
76 }
77 decoder
78 .next_image()
79 .map_err(|e| IoError::TiffDecode(format!("{}", e)))?;
80 }
81
82 let n_frames = frames.len();
83 if n_frames == 0 {
84 return Err(IoError::TiffDecode("TIFF file contains no frames".into()));
85 }
86
87 let flat: Vec<f64> = frames.into_iter().flatten().collect();
89 Array3::from_shape_vec((n_frames, height as usize, width as usize), flat)
90 .map_err(|e| IoError::TiffDecode(format!("Shape error: {}", e)))
91}
92
93pub fn load_tiff_auto(path: &Path) -> Result<Array3<f64>, IoError> {
105 match std::fs::metadata(path) {
106 Ok(meta) => {
107 if meta.is_file() {
108 load_tiff_stack(path)
109 } else if meta.is_dir() {
110 load_tiff_directory(path)
111 } else {
112 Err(IoError::FileNotFound(
113 path.to_string_lossy().into_owned(),
114 std::io::Error::new(
115 std::io::ErrorKind::InvalidInput,
116 "path is neither a regular file nor a directory",
117 ),
118 ))
119 }
120 }
121 Err(e) => Err(IoError::FileNotFound(
122 path.to_string_lossy().into_owned(),
123 e,
124 )),
125 }
126}
127
128pub fn load_tiff_directory(dir: &Path) -> Result<Array3<f64>, IoError> {
139 load_tiff_folder(dir, None).map_err(|e| match e {
140 IoError::NoMatchingFiles { .. } => {
142 IoError::TiffDecode("No TIFF files found in directory".into())
143 }
144 other => other,
145 })
146}
147
148pub fn load_tiff_folder(dir: &Path, pattern: Option<&str>) -> Result<Array3<f64>, IoError> {
171 if !dir.is_dir() {
172 return Err(IoError::NotADirectory(dir.to_string_lossy().into_owned()));
173 }
174
175 let entries: Vec<_> = std::fs::read_dir(dir)
178 .map_err(|e| IoError::FileNotFound(dir.to_string_lossy().into_owned(), e))?
179 .collect::<Result<Vec<_>, _>>()
180 .map_err(|e| IoError::FileNotFound(dir.to_string_lossy().into_owned(), e))?;
181
182 let mut paths: Vec<_> = entries
183 .iter()
184 .filter_map(|entry| {
185 let p = entry.path();
187 if !p.is_file() {
189 return None;
190 }
191 let is_tiff = p
192 .extension()
193 .and_then(|ext| ext.to_str())
194 .map(|ext| matches!(ext.to_lowercase().as_str(), "tif" | "tiff"))
195 .unwrap_or(false);
196 if !is_tiff {
197 return None;
198 }
199 if let Some(pat) = pattern {
200 let matches = entry
201 .file_name()
202 .to_str()
203 .map(|name| glob_match(pat, name))
204 .unwrap_or(false);
205 if !matches {
206 return None;
207 }
208 }
209 Some(p)
210 })
211 .collect();
212
213 paths.sort();
214
215 if paths.is_empty() {
216 return Err(IoError::NoMatchingFiles {
217 directory: dir.to_string_lossy().into_owned(),
218 pattern: pattern.unwrap_or("*.tif / *.tiff").to_string(),
219 });
220 }
221
222 load_frames_from_paths(&paths)
223}
224
225fn load_frames_from_paths(paths: &[std::path::PathBuf]) -> Result<Array3<f64>, IoError> {
231 debug_assert!(
232 !paths.is_empty(),
233 "load_frames_from_paths called with empty paths"
234 );
235 let mut frames: Vec<Vec<f64>> = Vec::new();
236 let mut width = 0u32;
237 let mut height = 0u32;
238
239 for (i, path) in paths.iter().enumerate() {
240 let file = std::fs::File::open(path)
241 .map_err(|e| IoError::FileNotFound(path.to_string_lossy().into_owned(), e))?;
242 let mut decoder = Decoder::new(file).map_err(|e| IoError::TiffDecode(format!("{}", e)))?;
243
244 let (w, h) = decoder
245 .dimensions()
246 .map_err(|e| IoError::TiffDecode(format!("{}", e)))?;
247
248 if i == 0 {
249 width = w;
250 height = h;
251 } else if w != width || h != height {
252 return Err(IoError::DimensionMismatch {
253 expected: (width, height),
254 got: (w, h),
255 frame: i,
256 });
257 }
258
259 let data = decoder
260 .read_image()
261 .map_err(|e| IoError::TiffDecode(format!("{}", e)))?;
262
263 if decoder.more_images() {
267 return Err(IoError::InvalidParameter(format!(
268 "File '{}' contains multiple frames; use load_tiff_stack() for multi-frame TIFFs",
269 path.display()
270 )));
271 }
272
273 let pixels = decode_to_f64(data)?;
274 let expected_len = (width as usize) * (height as usize);
275 if pixels.len() != expected_len {
276 return Err(IoError::TiffDecode(format!(
277 "Frame {} has {} pixels, expected {}",
278 i,
279 pixels.len(),
280 expected_len
281 )));
282 }
283 frames.push(pixels);
284 }
285
286 let n_frames = frames.len();
287 let flat: Vec<f64> = frames.into_iter().flatten().collect();
288 Array3::from_shape_vec((n_frames, height as usize, width as usize), flat)
289 .map_err(|e| IoError::TiffDecode(format!("Shape error: {}", e)))
290}
291
292fn glob_match(pattern: &str, name: &str) -> bool {
301 let p: Vec<char> = pattern.to_lowercase().chars().collect();
302 let n: Vec<char> = name.to_lowercase().chars().collect();
303
304 let (mut pi, mut ni) = (0usize, 0usize);
305 let (mut star_pi, mut star_ni) = (None::<usize>, 0usize);
307
308 while ni < n.len() {
309 if pi < p.len() && p[pi] == '*' {
310 star_pi = Some(pi);
312 star_ni = ni;
313 pi += 1; } else if pi < p.len() && (p[pi] == '?' || p[pi] == n[ni]) {
315 pi += 1;
316 ni += 1;
317 } else if let Some(sp) = star_pi {
318 star_ni += 1;
320 ni = star_ni;
321 pi = sp + 1;
322 } else {
323 return false;
324 }
325 }
326
327 while pi < p.len() && p[pi] == '*' {
329 pi += 1;
330 }
331
332 pi == p.len()
333}
334
335fn decode_to_f64(data: DecodingResult) -> Result<Vec<f64>, IoError> {
337 match data {
338 DecodingResult::U8(v) => Ok(v.into_iter().map(|x| x as f64).collect()),
339 DecodingResult::U16(v) => Ok(v.into_iter().map(|x| x as f64).collect()),
340 DecodingResult::U32(v) => Ok(v.into_iter().map(|x| x as f64).collect()),
341 DecodingResult::U64(v) => Ok(v.into_iter().map(|x| x as f64).collect()),
342 DecodingResult::F32(v) => Ok(v.into_iter().map(|x| x as f64).collect()),
343 DecodingResult::F64(v) => Ok(v),
344 DecodingResult::I8(v) => Ok(v.into_iter().map(|x| x as f64).collect()),
345 DecodingResult::I16(v) => Ok(v.into_iter().map(|x| x as f64).collect()),
346 DecodingResult::I32(v) => Ok(v.into_iter().map(|x| x as f64).collect()),
347 DecodingResult::I64(v) => Ok(v.into_iter().map(|x| x as f64).collect()),
348 DecodingResult::F16(v) => Ok(v.into_iter().map(f64::from).collect()),
349 }
350}
351
352#[derive(Debug, Clone)]
354pub struct TiffStackInfo {
355 pub n_frames: usize,
357 pub height: usize,
359 pub width: usize,
361}
362
363impl TiffStackInfo {
364 pub fn from_array(arr: &Array3<f64>) -> Self {
366 let shape = arr.shape();
367 Self {
368 n_frames: shape[0],
369 height: shape[1],
370 width: shape[2],
371 }
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use tiff::encoder::TiffEncoder;
379
380 fn write_test_tiff(path: &Path, frames: &[Vec<u16>], width: u32, height: u32) {
382 let file = std::fs::File::create(path).unwrap();
383 let mut encoder = TiffEncoder::new(file).unwrap();
384 for frame in frames {
385 encoder
386 .write_image::<tiff::encoder::colortype::Gray16>(width, height, frame)
387 .unwrap();
388 }
389 }
390
391 #[test]
392 fn test_load_single_frame_tiff() {
393 let dir = tempfile::tempdir().unwrap();
394 let path = dir.path().join("test.tiff");
395
396 let data: Vec<u16> = vec![1, 2, 3, 4, 5, 6];
398 write_test_tiff(&path, &[data], 3, 2);
399
400 let arr = load_tiff_stack(&path).unwrap();
401 assert_eq!(arr.shape(), &[1, 2, 3]);
402 assert_eq!(arr[[0, 0, 0]], 1.0);
403 assert_eq!(arr[[0, 0, 2]], 3.0);
404 assert_eq!(arr[[0, 1, 0]], 4.0);
405 assert_eq!(arr[[0, 1, 2]], 6.0);
406 }
407
408 #[test]
409 fn test_load_multi_frame_tiff() {
410 let dir = tempfile::tempdir().unwrap();
411 let path = dir.path().join("multi.tiff");
412
413 let frame1: Vec<u16> = vec![10, 20, 30, 40];
414 let frame2: Vec<u16> = vec![50, 60, 70, 80];
415 let frame3: Vec<u16> = vec![90, 100, 110, 120];
416 write_test_tiff(&path, &[frame1, frame2, frame3], 2, 2);
417
418 let arr = load_tiff_stack(&path).unwrap();
419 assert_eq!(arr.shape(), &[3, 2, 2]);
420 assert_eq!(arr[[0, 0, 0]], 10.0);
422 assert_eq!(arr[[0, 1, 1]], 40.0);
423 assert_eq!(arr[[2, 0, 0]], 90.0);
425 assert_eq!(arr[[2, 1, 1]], 120.0);
426 }
427
428 #[test]
429 fn test_load_tiff_directory() {
430 let dir = tempfile::tempdir().unwrap();
431
432 for i in 0..3u16 {
434 let path = dir.path().join(format!("frame_{:04}.tiff", i));
435 let data: Vec<u16> = (0..4).map(|j| (i + 1) * 10 + j).collect();
436 write_test_tiff(&path, &[data], 2, 2);
437 }
438
439 let arr = load_tiff_directory(dir.path()).unwrap();
440 assert_eq!(arr.shape(), &[3, 2, 2]);
441 assert_eq!(arr[[0, 0, 0]], 10.0);
443 assert_eq!(arr[[2, 0, 0]], 30.0);
445 assert_eq!(arr[[2, 1, 1]], 33.0);
446 }
447
448 #[test]
449 fn test_load_tiff_folder_no_pattern() {
450 let dir = tempfile::tempdir().unwrap();
451
452 for i in 0..2u16 {
454 let path = dir.path().join(format!("frame_{:04}.tif", i));
455 let data: Vec<u16> = (0..4).map(|j| (i + 1) * 10 + j).collect();
456 write_test_tiff(&path, &[data], 2, 2);
457 }
458 let path = dir.path().join("frame_0002.tiff");
459 write_test_tiff(&path, &[vec![30, 31, 32, 33]], 2, 2);
460
461 std::fs::write(dir.path().join("frame_0001.tif.bak"), b"not a tiff").unwrap();
463
464 let arr = load_tiff_folder(dir.path(), None).unwrap();
465 assert_eq!(arr.shape(), &[3, 2, 2]);
466 }
467
468 #[test]
469 fn test_load_tiff_folder_with_pattern() {
470 let dir = tempfile::tempdir().unwrap();
471
472 for i in 0..3u16 {
473 let path = dir.path().join(format!("frame_{:04}.tif", i));
474 let data: Vec<u16> = (0..4).map(|j| (i + 1) * 10 + j).collect();
475 write_test_tiff(&path, &[data], 2, 2);
476 }
477
478 let arr = load_tiff_folder(dir.path(), Some("*.tif")).unwrap();
479 assert_eq!(arr.shape(), &[3, 2, 2]);
480 assert_eq!(arr[[0, 0, 0]], 10.0);
481 assert_eq!(arr[[2, 1, 1]], 33.0);
482 }
483
484 #[test]
485 fn test_load_tiff_folder_custom_pattern() {
486 let dir = tempfile::tempdir().unwrap();
487
488 for i in 0..2u16 {
490 let path = dir.path().join(format!("scan_{:04}.tif", i));
491 let data: Vec<u16> = (0..4).map(|j| (i + 1) * 10 + j).collect();
492 write_test_tiff(&path, &[data], 2, 2);
493 }
494 let extra = dir.path().join("other_0001.tif");
496 write_test_tiff(&extra, &[vec![99, 99, 99, 99]], 2, 2);
497
498 let arr = load_tiff_folder(dir.path(), Some("scan_*.tif")).unwrap();
499 assert_eq!(arr.shape(), &[2, 2, 2]);
500 assert_eq!(arr[[0, 0, 0]], 10.0);
501 }
502
503 #[test]
504 fn test_load_tiff_folder_no_matching_files() {
505 let dir = tempfile::tempdir().unwrap();
506
507 let path = dir.path().join("frame_0001.tiff");
509 write_test_tiff(&path, &[vec![1, 2, 3, 4]], 2, 2);
510
511 let result = load_tiff_folder(dir.path(), Some("*.png"));
512 assert!(result.is_err());
513 let err = result.unwrap_err();
514 assert!(
515 matches!(err, IoError::NoMatchingFiles { .. }),
516 "Expected NoMatchingFiles, got: {:?}",
517 err,
518 );
519 }
520
521 #[test]
522 fn test_load_tiff_folder_case_insensitive() {
523 let dir = tempfile::tempdir().unwrap();
524
525 let path = dir.path().join("frame_0001.TIF");
527 write_test_tiff(&path, &[vec![1, 2, 3, 4]], 2, 2);
528
529 let arr = load_tiff_folder(dir.path(), Some("*.tif")).unwrap();
531 assert_eq!(arr.shape(), &[1, 2, 2]);
532 }
533
534 #[test]
535 fn test_glob_match_basic() {
536 assert!(glob_match("*.tif", "frame_0001.tif"));
537 assert!(glob_match("*.tif", "a.tif"));
538 assert!(!glob_match("*.tif", "frame_0001.tiff"));
539 assert!(!glob_match("*.tif", "frame_0001.png"));
540 }
541
542 #[test]
543 fn test_glob_match_question_mark() {
544 assert!(glob_match("frame_?.tif", "frame_1.tif"));
545 assert!(!glob_match("frame_?.tif", "frame_12.tif"));
546 assert!(glob_match("?.tif", "\u{00e9}.tif")); }
549
550 #[test]
551 fn test_glob_match_case_insensitive() {
552 assert!(glob_match("*.tif", "FILE.TIF"));
553 assert!(glob_match("*.TIF", "file.tif"));
554 }
555
556 #[test]
557 fn test_glob_match_pattern_longer_than_name() {
558 assert!(!glob_match("abcdef.tif", "a.tif"));
559 }
560
561 #[test]
562 fn test_glob_match_empty_strings() {
563 assert!(glob_match("", ""));
564 assert!(!glob_match("", "foo"));
565 assert!(glob_match("*", ""));
566 }
567
568 #[test]
569 fn test_glob_match_pathological_pattern() {
570 let pattern = "*a*a*a*a*a*b";
573 let name = "aaaaaaaaaaaaaaaaaaaac";
574 assert!(!glob_match(pattern, name));
575 }
576
577 #[test]
578 fn test_load_tiff_folder_empty_directory() {
579 let dir = tempfile::tempdir().unwrap();
580 let result = load_tiff_folder(dir.path(), None);
581 assert!(result.is_err());
582 let err = result.unwrap_err();
583 assert!(
584 matches!(err, IoError::NoMatchingFiles { .. }),
585 "Expected NoMatchingFiles, got: {:?}",
586 err,
587 );
588 }
589
590 #[test]
591 fn test_load_tiff_folder_not_a_directory() {
592 let dir = tempfile::tempdir().unwrap();
593 let file_path = dir.path().join("frame_0001.tif");
594 write_test_tiff(&file_path, &[vec![1, 2, 3, 4]], 2, 2);
595
596 let result = load_tiff_folder(&file_path, None);
597 assert!(result.is_err());
598 let err = result.unwrap_err();
599 assert!(
600 matches!(err, IoError::NotADirectory(..)),
601 "Expected NotADirectory, got: {:?}",
602 err,
603 );
604 }
605
606 #[test]
607 fn test_load_tiff_folder_dimension_mismatch() {
608 let dir = tempfile::tempdir().unwrap();
609
610 write_test_tiff(
612 &dir.path().join("frame_0000.tif"),
613 &[vec![1, 2, 3, 4]],
614 2,
615 2,
616 );
617 write_test_tiff(
619 &dir.path().join("frame_0001.tif"),
620 &[vec![1, 2, 3, 4, 5, 6]],
621 3,
622 2,
623 );
624
625 let result = load_tiff_folder(dir.path(), None);
626 assert!(result.is_err());
627 let err = result.unwrap_err();
628 assert!(
629 matches!(err, IoError::DimensionMismatch { .. }),
630 "Expected DimensionMismatch, got: {:?}",
631 err,
632 );
633 }
634
635 #[test]
636 fn test_nonexistent_file() {
637 let result = load_tiff_stack(Path::new("/nonexistent/file.tiff"));
638 assert!(result.is_err());
639 }
640
641 #[test]
642 fn test_tiff_stack_info() {
643 let arr = Array3::<f64>::zeros((10, 512, 512));
644 let info = TiffStackInfo::from_array(&arr);
645 assert_eq!(info.n_frames, 10);
646 assert_eq!(info.height, 512);
647 assert_eq!(info.width, 512);
648 }
649
650 #[test]
651 fn test_load_tiff_auto_file() {
652 let dir = tempfile::tempdir().unwrap();
653 let path = dir.path().join("multi.tiff");
654
655 let frame1: Vec<u16> = vec![10, 20, 30, 40];
656 let frame2: Vec<u16> = vec![50, 60, 70, 80];
657 write_test_tiff(&path, &[frame1, frame2], 2, 2);
658
659 let arr = load_tiff_auto(&path).unwrap();
660 assert_eq!(arr.shape(), &[2, 2, 2]);
661 assert_eq!(arr[[0, 0, 0]], 10.0);
662 assert_eq!(arr[[1, 1, 1]], 80.0);
663 }
664
665 #[test]
666 fn test_load_tiff_auto_directory() {
667 let dir = tempfile::tempdir().unwrap();
668
669 for i in 0..2u16 {
670 let path = dir.path().join(format!("frame_{:04}.tif", i));
671 let data: Vec<u16> = (0..4).map(|j| (i + 1) * 10 + j).collect();
672 write_test_tiff(&path, &[data], 2, 2);
673 }
674
675 let arr = load_tiff_auto(dir.path()).unwrap();
676 assert_eq!(arr.shape(), &[2, 2, 2]);
677 assert_eq!(arr[[0, 0, 0]], 10.0);
678 }
679
680 #[test]
681 fn test_load_tiff_auto_nonexistent() {
682 let result = load_tiff_auto(Path::new("/nonexistent/path"));
683 assert!(result.is_err());
684 }
685
686 #[test]
688 fn test_load_tiff_folder_rejects_multi_frame() {
689 let dir = tempfile::tempdir().unwrap();
690
691 let path = dir.path().join("multi.tiff");
693 let frame1: Vec<u16> = vec![1, 2, 3, 4];
694 let frame2: Vec<u16> = vec![5, 6, 7, 8];
695 write_test_tiff(&path, &[frame1, frame2], 2, 2);
696
697 let result = load_tiff_folder(dir.path(), None);
698 assert!(
699 result.is_err(),
700 "Multi-frame TIFF in folder should be rejected"
701 );
702 let err = format!("{}", result.unwrap_err());
703 assert!(
704 err.contains("multiple frames"),
705 "Error should mention multiple frames, got: {err}"
706 );
707 }
708}