1use std::path::{Path, PathBuf};
6
7use atfits_rs::copy_header_only;
8use fitsio::FitsFile;
9use ndarray::Array2;
10use thiserror::Error;
11
12use crate::beam::Beam;
13use crate::smooth::BrightnessUnit;
14
15#[derive(Debug, Error)]
16pub enum FitsError {
17 #[error("FITS I/O error: {0}")]
18 Fitsio(#[from] fitsio::errors::Error),
19 #[error("missing keyword: {0}")]
20 MissingKeyword(String),
21 #[error("unsupported NAXIS={0} (expected 2 or 4)")]
22 UnsupportedNaxis(i64),
23 #[error("I/O error: {0}")]
24 Io(#[from] std::io::Error),
25}
26
27impl From<atfits_rs::AtfitsError> for FitsError {
29 fn from(e: atfits_rs::AtfitsError) -> Self {
30 use atfits_rs::AtfitsError as A;
31 match e {
32 A::Fits(e) => FitsError::Fitsio(e),
33 A::Io(e) => FitsError::Io(e),
34 A::MissingKeyword(s) | A::TargetAxisMissing(s) => FitsError::MissingKeyword(s),
35 A::UnsupportedNaxis(n) => FitsError::UnsupportedNaxis(n),
36 A::Other(s) => FitsError::Io(std::io::Error::other(s)),
37 }
38 }
39}
40
41pub struct FitsImageData {
42 pub path: PathBuf,
43 pub image: Array2<f32>,
44 pub is_4d: bool,
45 pub dx_deg: f64,
47 pub dy_deg: f64,
49 pub beam: Beam,
50 pub unit: BrightnessUnit,
52 pub header_cards: Vec<(String, String)>,
54}
55
56pub fn read_fits(path: &Path) -> Result<FitsImageData, FitsError> {
58 let path_str = path.to_string_lossy().into_owned();
59 let mut fptr = FitsFile::open(&path_str)?;
60 let hdu = fptr.primary_hdu()?;
61
62 let naxis: i64 = hdu.read_key(&mut fptr, "NAXIS")?;
63 let naxis1: i64 = hdu.read_key(&mut fptr, "NAXIS1")?; let naxis2: i64 = hdu.read_key(&mut fptr, "NAXIS2")?; if naxis != 2 && naxis != 4 {
67 return Err(FitsError::UnsupportedNaxis(naxis));
68 }
69
70 let nx = naxis1 as usize;
71 let ny = naxis2 as usize;
72
73 let data: Vec<f32> = hdu.read_image(&mut fptr)?;
75 let image = Array2::from_shape_vec((ny, nx), data)
76 .map_err(|e| FitsError::Io(std::io::Error::other(e.to_string())))?;
77
78 let cdelt1: f64 = hdu.read_key(&mut fptr, "CDELT1")?;
80 let cdelt2: f64 = hdu.read_key(&mut fptr, "CDELT2")?;
81 let dx_deg = cdelt1.abs();
82 let dy_deg = cdelt2.abs();
83
84 let bmaj: f64 = hdu
86 .read_key(&mut fptr, "BMAJ")
87 .map_err(|_| FitsError::MissingKeyword("BMAJ".into()))?;
88 let bmin: f64 = hdu.read_key(&mut fptr, "BMIN").unwrap_or(bmaj);
89 let bpa: f64 = hdu.read_key(&mut fptr, "BPA").unwrap_or(0.0);
90
91 let beam = Beam::new(bmaj, bmin, bpa)
92 .map_err(|e| FitsError::Io(std::io::Error::other(e.to_string())))?;
93
94 let unit = match hdu.read_key::<String>(&mut fptr, "BUNIT") {
96 Ok(s) => BrightnessUnit::from_bunit(&s),
97 Err(_) => {
98 tracing::warn!(
99 "No BUNIT keyword in {}; assuming Jy/beam (flux scaling applied).",
100 path.display()
101 );
102 BrightnessUnit::default()
103 }
104 };
105
106 Ok(FitsImageData {
107 path: path.to_path_buf(),
108 image,
109 is_4d: naxis == 4,
110 dx_deg,
111 dy_deg,
112 beam,
113 unit,
114 header_cards: vec![],
115 })
116}
117
118pub fn write_fits(
124 image: &Array2<f32>,
125 out_path: &Path,
126 template_path: &Path,
127 new_beam: &Beam,
128 _was_4d: bool,
129) -> Result<(), FitsError> {
130 copy_header_only(template_path, out_path)?;
135
136 let out_str = out_path.to_string_lossy().into_owned();
137 let mut fptr = FitsFile::edit(&out_str)?;
138 let hdu = fptr.primary_hdu()?;
139
140 let flat: Vec<f32> = image.iter().cloned().collect();
142 hdu.write_image(&mut fptr, &flat)?;
143
144 hdu.write_key(&mut fptr, "BMAJ", new_beam.major_deg)?;
146 hdu.write_key(&mut fptr, "BMIN", new_beam.minor_deg)?;
147 hdu.write_key(&mut fptr, "BPA", new_beam.pa_deg)?;
148
149 Ok(())
150}
151
152pub use atfits_rs::output_path;