1use std::path::{Path, PathBuf};
6
7use fitsio::FitsFile;
8use ndarray::Array2;
9use thiserror::Error;
10
11use crate::beam::Beam;
12use crate::smooth::BrightnessUnit;
13
14#[derive(Debug, Error)]
15pub enum FitsError {
16 #[error("FITS I/O error: {0}")]
17 Fitsio(#[from] fitsio::errors::Error),
18 #[error("missing keyword: {0}")]
19 MissingKeyword(String),
20 #[error("unsupported NAXIS={0} (expected 2 or 4)")]
21 UnsupportedNaxis(i64),
22 #[error("I/O error: {0}")]
23 Io(#[from] std::io::Error),
24}
25
26pub struct FitsImageData {
27 pub path: PathBuf,
28 pub image: Array2<f32>,
29 pub is_4d: bool,
30 pub dx_deg: f64,
32 pub dy_deg: f64,
34 pub beam: Beam,
35 pub unit: BrightnessUnit,
37 pub header_cards: Vec<(String, String)>,
39}
40
41pub fn read_fits(path: &Path) -> Result<FitsImageData, FitsError> {
43 let path_str = path.to_string_lossy().into_owned();
44 let mut fptr = FitsFile::open(&path_str)?;
45 let hdu = fptr.primary_hdu()?;
46
47 let naxis: i64 = hdu.read_key(&mut fptr, "NAXIS")?;
48 let naxis1: i64 = hdu.read_key(&mut fptr, "NAXIS1")?; let naxis2: i64 = hdu.read_key(&mut fptr, "NAXIS2")?; if naxis != 2 && naxis != 4 {
52 return Err(FitsError::UnsupportedNaxis(naxis));
53 }
54
55 let nx = naxis1 as usize;
56 let ny = naxis2 as usize;
57
58 let data: Vec<f32> = hdu.read_image(&mut fptr)?;
60 let image = Array2::from_shape_vec((ny, nx), data)
61 .map_err(|e| FitsError::Io(std::io::Error::other(e.to_string())))?;
62
63 let cdelt1: f64 = hdu.read_key(&mut fptr, "CDELT1")?;
65 let cdelt2: f64 = hdu.read_key(&mut fptr, "CDELT2")?;
66 let dx_deg = cdelt1.abs();
67 let dy_deg = cdelt2.abs();
68
69 let bmaj: f64 = hdu
71 .read_key(&mut fptr, "BMAJ")
72 .map_err(|_| FitsError::MissingKeyword("BMAJ".into()))?;
73 let bmin: f64 = hdu.read_key(&mut fptr, "BMIN").unwrap_or(bmaj);
74 let bpa: f64 = hdu.read_key(&mut fptr, "BPA").unwrap_or(0.0);
75
76 let beam = Beam::new(bmaj, bmin, bpa)
77 .map_err(|e| FitsError::Io(std::io::Error::other(e.to_string())))?;
78
79 let unit = match hdu.read_key::<String>(&mut fptr, "BUNIT") {
81 Ok(s) => BrightnessUnit::from_bunit(&s),
82 Err(_) => {
83 tracing::warn!(
84 "No BUNIT keyword in {}; assuming Jy/beam (flux scaling applied).",
85 path.display()
86 );
87 BrightnessUnit::default()
88 }
89 };
90
91 Ok(FitsImageData {
92 path: path.to_path_buf(),
93 image,
94 is_4d: naxis == 4,
95 dx_deg,
96 dy_deg,
97 beam,
98 unit,
99 header_cards: vec![],
100 })
101}
102
103pub fn write_fits(
109 image: &Array2<f32>,
110 out_path: &Path,
111 template_path: &Path,
112 new_beam: &Beam,
113 _was_4d: bool,
114) -> Result<(), FitsError> {
115 copy_header_only(template_path, out_path)?;
120
121 let out_str = out_path.to_string_lossy().into_owned();
122 let mut fptr = FitsFile::edit(&out_str)?;
123 let hdu = fptr.primary_hdu()?;
124
125 let flat: Vec<f32> = image.iter().cloned().collect();
127 hdu.write_image(&mut fptr, &flat)?;
128
129 hdu.write_key(&mut fptr, "BMAJ", new_beam.major_deg)?;
131 hdu.write_key(&mut fptr, "BMIN", new_beam.minor_deg)?;
132 hdu.write_key(&mut fptr, "BPA", new_beam.pa_deg)?;
133
134 Ok(())
135}
136
137fn copy_header_only(input: &Path, output: &Path) -> Result<(), FitsError> {
143 let mut in_fptr = FitsFile::open(input.to_string_lossy().into_owned())?;
144 in_fptr.primary_hdu()?; let mut out_fptr = FitsFile::create(output).overwrite().open()?;
146
147 let mut status = 0;
148 unsafe {
149 fitsio::sys::ffcphd(in_fptr.as_raw(), out_fptr.as_raw(), &mut status);
150 }
151 fitsio::errors::check_status(status)?;
152 Ok(())
153}
154
155pub fn output_path(
157 input: &Path,
158 suffix: Option<&str>,
159 prefix: Option<&str>,
160 outdir: Option<&Path>,
161) -> PathBuf {
162 let stem = input.file_stem().unwrap_or_default().to_string_lossy();
163 let ext = input.extension().unwrap_or_default().to_string_lossy();
164
165 let filename = match suffix {
166 Some(s) => format!("{stem}.{s}.{ext}"),
167 None => format!("{stem}.{ext}"),
168 };
169 let filename = match prefix {
170 Some(p) => format!("{p}{filename}"),
171 None => filename,
172 };
173
174 let dir = outdir.unwrap_or_else(|| input.parent().unwrap_or(Path::new(".")));
175 dir.join(filename)
176}