1use crate::fits::Fits;
2use crate::fs::fs_ascii_table_hdu::FsAsciiTableHDU;
3use crate::fs::fs_bin_table_hdu::FsBinTableHDU;
4use crate::fs::fs_image_hdu::FsImageHDU;
5use crate::fs::is_fits_file;
6use crate::fs::open_fits_file::open_fits_file;
7use crate::hdu::{ExtensionHDU, HDU};
8use crate::header::header::BLOCK_NUM_BYTES;
9use crate::header::{ExtensionType, Header};
10use log::{debug, info};
11use std::error::Error;
12use std::fs;
13use std::io::Seek;
14use std::path::{Path, PathBuf};
15
16#[derive(Debug, Clone)]
18pub struct FsFits {
19 path: PathBuf,
20 primary_hdu: FsImageHDU,
21 extension_hdus: Vec<ExtensionHDU<Self>>,
22}
23
24impl FsFits {
25 pub fn open(path: &Path) -> Result<Self, Box<dyn Error + Send + Sync>> {
27 Self::assert_file_type(path)?;
28 debug!("Opening FITS file: {:?}", path);
29 let mut reader = open_fits_file(path)?;
30
31 let header =
32 Header::from_reader(&mut reader)?.ok_or("Could not read primary FITS header")?;
33 debug!("Opened primary header: {:?}", header);
34 header.validate_primary()?;
35
36 let primary_hdu = FsImageHDU::new_primary(path, header);
37
38 let mut extension_hdus = vec![];
39 let mut offset = primary_hdu.byte_size();
40
41 loop {
42 reader.seek(std::io::SeekFrom::Start(offset))?;
43
44 if let Some(header) = Header::from_reader(&mut reader)? {
45 header.validate_extension()?;
46 debug!("Found extension header: {:?}", header);
47
48 let extension_type = header.extension().ok_or(
49 "This is not a valid fits extension. Card XTENSION is missing or invalid",
50 )?;
51
52 match extension_type {
53 ExtensionType::Image => {
54 let extension_hdu = FsImageHDU::new_extension(path, header, offset)?;
55 offset += extension_hdu.byte_size();
56 extension_hdus.push(ExtensionHDU::Image(extension_hdu));
57 }
58 ExtensionType::BinTable if header.is_compressed_image() => {
62 let extension_hdu = FsImageHDU::new_extension(path, header, offset)?;
63 offset += extension_hdu.byte_size();
64 extension_hdus.push(ExtensionHDU::Image(extension_hdu));
65 }
66 ExtensionType::BinTable => {
67 let extension_hdu = FsBinTableHDU::new(path, header, offset)?;
68 offset += extension_hdu.byte_size();
69 extension_hdus.push(ExtensionHDU::BinTable(extension_hdu));
70 }
71 ExtensionType::AsciiTable => {
72 let extension_hdu = FsAsciiTableHDU::new(path, header, offset)?;
73 offset += extension_hdu.byte_size();
74 extension_hdus.push(ExtensionHDU::AsciiTable(extension_hdu));
75 }
76 }
77 } else {
78 break;
79 }
80 }
81 info!("Opened FITS file: {:?}", path);
82 Ok(Self {
83 path: path.to_path_buf(),
84 primary_hdu,
85 extension_hdus,
86 })
87 }
88
89 #[cfg(feature = "tokio")]
91 pub async fn open_async(path: &Path) -> Result<Self, Box<dyn Error + Send + Sync>> {
92 let path = path.to_path_buf();
93 tokio::task::spawn_blocking(move || Self::open(&path)).await?
94 }
95
96 pub fn new(path: &Path) -> Self {
98 Self {
99 path: path.to_path_buf(),
100 primary_hdu: FsImageHDU::new_primary(path, Header::default()),
101 extension_hdus: vec![],
102 }
103 }
104
105 pub fn save(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
111 self.save_as(&self.path)
112 }
113
114 pub fn save_as(&self, path: &Path) -> Result<(), Box<dyn Error + Send + Sync>> {
116 let bytes = self.to_vec()?;
117
118 let temporary = path.with_extension(format!(
119 "{}.fits-io-tmp",
120 path.extension().unwrap_or_default().to_string_lossy()
121 ));
122
123 fs::write(&temporary, &bytes)?;
124 if let Err(error) = fs::rename(&temporary, path) {
125 let _ = fs::remove_file(&temporary);
128 return Err(error.into());
129 }
130
131 info!("Wrote FITS file: {:?}", path);
132
133 Ok(())
134 }
135
136 pub fn path(&self) -> &Path {
138 &self.path
139 }
140
141 fn assert_file_type(path: &Path) -> Result<(), Box<dyn Error + Send + Sync>> {
142 if is_fits_file(path) {
143 Ok(())
144 } else {
145 Err("Invalid file type".into())
146 }
147 }
148}
149
150impl Fits for FsFits {
151 type ImageHDU = FsImageHDU;
152 type BinTableHDU = FsBinTableHDU;
153 type AsciiTableHDU = FsAsciiTableHDU;
154
155 fn primary_hdu(&self) -> &Self::ImageHDU {
156 &self.primary_hdu
157 }
158
159 fn primary_hdu_mut(&mut self) -> &mut Self::ImageHDU {
160 &mut self.primary_hdu
161 }
162
163 fn extension_count(&self) -> usize {
164 self.extension_hdus.len()
165 }
166
167 fn extension_hdu(&self, index: usize) -> Option<&ExtensionHDU<Self>> {
168 self.extension_hdus.get(index)
169 }
170
171 fn extension_hdu_mut(&mut self, index: usize) -> Option<&mut ExtensionHDU<Self>> {
172 self.extension_hdus.get_mut(index)
173 }
174
175 fn extension_hdus(&self) -> impl Iterator<Item = &ExtensionHDU<Self>> {
176 self.extension_hdus.iter()
177 }
178
179 fn extension_hdus_mut(&mut self) -> impl Iterator<Item = &mut ExtensionHDU<Self>> {
180 self.extension_hdus.iter_mut()
181 }
182
183 fn push_extension(&mut self, extension: ExtensionHDU<Self>) {
184 self.extension_hdus.push(extension);
185 }
186
187 fn remove_extension(&mut self, index: usize) -> Option<ExtensionHDU<Self>> {
188 (index < self.extension_hdus.len()).then(|| self.extension_hdus.remove(index))
189 }
190
191 fn to_vec(&self) -> Result<Vec<u8>, Box<dyn Error + Send + Sync>> {
196 let mut bytes = Vec::new();
197
198 if self.primary_hdu.header().is_compressed_image() {
204 append_hdu(&mut bytes, &empty_primary_header(), &[], DataPadding::Zero)?;
205 append_hdu(
206 &mut bytes,
207 &self
208 .primary_hdu
209 .header()
210 .conformed(Some(ExtensionType::BinTable)),
211 &self.primary_hdu.data_bytes()?,
212 DataPadding::Zero,
213 )?;
214 } else {
215 append_hdu(
216 &mut bytes,
217 &self.primary_hdu.header().conformed(None),
218 &self.primary_hdu.data_bytes()?,
219 DataPadding::Zero,
220 )?;
221 }
222
223 for extension in &self.extension_hdus {
224 let (header, data, padding) = match extension {
225 ExtensionHDU::Image(hdu) => (
228 hdu.header()
229 .conformed(Some(extension_type_of(hdu.header()))),
230 hdu.data_bytes()?,
231 DataPadding::Zero,
232 ),
233 ExtensionHDU::BinTable(hdu) => (
234 hdu.header().conformed(Some(ExtensionType::BinTable)),
235 hdu.data_bytes()?,
236 DataPadding::Zero,
237 ),
238 ExtensionHDU::AsciiTable(hdu) => (
241 hdu.header().conformed(Some(ExtensionType::AsciiTable)),
242 hdu.data_bytes()?,
243 DataPadding::Blank,
244 ),
245 };
246
247 append_hdu(&mut bytes, &header, &data, padding)?;
248 }
249
250 Ok(bytes)
251 }
252}
253
254#[derive(Debug, Clone, Copy)]
256enum DataPadding {
257 Zero,
258 Blank,
259}
260
261impl DataPadding {
262 fn byte(self) -> u8 {
263 match self {
264 DataPadding::Zero => 0,
265 DataPadding::Blank => b' ',
266 }
267 }
268}
269
270fn append_hdu(
276 bytes: &mut Vec<u8>,
277 header: &Header,
278 data: &[u8],
279 padding: DataPadding,
280) -> Result<(), Box<dyn Error + Send + Sync>> {
281 header.validate_against_data(data.len())?;
282
283 let mut data = data.to_vec();
284 let overhang = data.len() % BLOCK_NUM_BYTES;
285 if overhang != 0 {
286 data.resize(data.len() + BLOCK_NUM_BYTES - overhang, padding.byte());
287 }
288
289 bytes.extend_from_slice(&header.checksummed_bytes(&data));
290 bytes.extend_from_slice(&data);
291
292 Ok(())
293}
294
295fn extension_type_of(header: &Header) -> ExtensionType {
300 if header.is_compressed_image() {
301 ExtensionType::BinTable
302 } else {
303 ExtensionType::Image
304 }
305}
306
307fn empty_primary_header() -> Header {
310 let mut header = Header::default();
311
312 let _ = header.set_card(crate::header::card_keys::EXTEND, true);
315
316 header.conformed(None)
317}