astro_rs/fits/mod.rs
1//! Serialize and deserialize FITS data.
2//! See <https://archive.stsci.edu/fits/fits_standard/fits_standard.html> for the FITS API.
3
4mod hdu_types;
5mod header;
6mod header_value;
7
8use std::fmt::Debug;
9use std::io::{BufReader, BufWriter, Cursor, Read, Write};
10use std::slice::IterMut;
11
12pub use hdu_types::*;
13pub use header::*;
14pub use header_value::*;
15
16/// The expected keyword for the name of an extension.
17pub const EXTNAME_KEYWORD: [u8; 8] = *b"EXTNAME ";
18const BLANK_KEYWORD: [u8; 8] = *b" ";
19
20/// A representation of the entirety of a FITS file.
21#[derive(Debug)]
22pub struct HduList<R> {
23 reader: BufReader<R>,
24 hdus: Vec<Hdu>,
25}
26
27impl Default for HduList<Cursor<Vec<u8>>> {
28 fn default() -> Self {
29 Self {
30 reader: BufReader::new(Cursor::new(Vec::new())),
31 hdus: Default::default(),
32 }
33 }
34}
35
36impl<R: Read> HduList<R> {
37 /// Constructs an empty HduList.
38 pub fn new(reader: BufReader<R>) -> Self {
39 HduList {
40 reader,
41 hdus: Vec::new(),
42 }
43 }
44
45 /// Retrieves the HDU at the given index, or None if an HDU doesn't exist at the index.
46 ///
47 /// # Examples
48 ///
49 /// ```
50 /// use astro_rs::fits::*;
51 ///
52 /// let mut hdu_list = HduList::default();
53 /// assert!(hdu_list.get_by_index(0).is_none());
54 ///
55 /// hdu_list.push(primary_hdu::default());
56 /// assert!(hdu_list.get_by_index(0).is_some());
57 /// ```
58 pub fn get_by_index(&mut self, index: usize) -> Option<&mut Hdu> {
59 let mut cur_hdus = self.hdus.len();
60 while cur_hdus <= index {
61 let new_hdu = self.read_hdu()?;
62 self.hdus.push(new_hdu);
63 cur_hdus += 1;
64 }
65 Some(&mut self.hdus[index])
66 }
67
68 /// Retrieves the HDU with the given value for the `EXTNAME` keyword, or None if an HDU
69 /// with the given name doesn't exist.
70 ///
71 /// # Examples
72 ///
73 /// ```
74 /// use astro_rs::fits::*;
75 ///
76 /// let mut hdu_list = HduList::default();
77 /// // empty list
78 /// assert!(hdu_list.get_by_name("hdu_name").is_none());
79 ///
80 /// // name does not match
81 /// let mut img_hdu = image_hdu::default();
82 /// let name_card = FitsHeaderCard::from(*b"EXTNAME = 'name_of_hdu' ");
83 /// img_hdu.header.cards.insert(img_hdu.header.cards.len() - 1, name_card);
84 /// hdu_list.push(img_hdu);
85 /// assert!(hdu_list.get_by_name("hdu_name").is_none());
86 ///
87 /// // name matches
88 /// let mut img_hdu = image_hdu::default();
89 /// let name_card = FitsHeaderCard::from(*b"EXTNAME = 'hdu_name' ");
90 /// img_hdu.header.cards.insert(img_hdu.header.cards.len() - 1, name_card);
91 /// hdu_list.push(img_hdu);
92 /// assert!(hdu_list.get_by_name("hdu_name").is_some());
93 /// ```
94 pub fn get_by_name(&mut self, name: &str) -> Option<&mut Hdu> {
95 let index = self
96 .hdus
97 .iter_mut()
98 .position(|hdu| hdu.get_name() == name)
99 .or_else(|| {
100 let mut index = self.hdus.len();
101 loop {
102 let mut new_hdu = self.read_hdu()?;
103 if new_hdu.get_name() == name {
104 self.hdus.push(new_hdu);
105 break;
106 }
107
108 self.hdus.push(new_hdu);
109 index += 1;
110 }
111 Some(index)
112 });
113
114 Some(&mut self.hdus[index?])
115 }
116
117 /// Returns a mutable pointer to the first HDU, or `None` if the list is empty.
118 ///
119 /// # Examples
120 ///
121 /// ```
122 /// use astro_rs::fits::*;
123 ///
124 /// let mut hdu_list = HduList::default();
125 /// assert!(hdu_list.first_mut().is_none());
126 ///
127 /// hdu_list.push(primary_hdu::default());
128 /// assert!(hdu_list.first_mut().is_some());
129 /// ```
130 pub fn first_mut(&mut self) -> Option<&mut Hdu> {
131 if self.hdus.is_empty() {
132 let new_hdu = self.read_hdu()?;
133 self.hdus.push(new_hdu);
134 }
135 Some(&mut self.hdus[0])
136 }
137
138 /// Deserializes all HDUs if necessary, then returns a mutable iterator over the HDUs.
139 ///
140 /// # Examples
141 ///
142 /// ```
143 /// use astro_rs::fits::*;
144 ///
145 /// let mut hdu_list = HduList::default();
146 /// hdu_list.push(primary_hdu::default());
147 ///
148 /// // find the primary HDU
149 /// assert!(hdu_list
150 /// .iter_mut()
151 /// .find_map(|hdu| if hdu.header.get_card(SIMPLE_KEYWORD).is_some() {
152 /// Some(hdu)
153 /// } else {
154 /// None
155 /// })
156 /// .is_some())
157 /// ```
158 pub fn iter_mut(&mut self) -> IterMut<Hdu> {
159 while let Some(new_hdu) = self.read_hdu() {
160 self.hdus.push(new_hdu);
161 }
162 self.hdus.iter_mut()
163 }
164
165 /// Deserializes all HDUs up to `index` if necessary, then inserts the given `hdu`.
166 ///
167 /// # Panics
168 ///
169 /// Panics if `index` is out of bounds.
170 ///
171 /// # Examples
172 ///
173 /// ```should_panic
174 /// use astro_rs::fits::*;
175 ///
176 /// let mut hdu_list = HduList::default();
177 /// // panics, index is out of bounds
178 /// hdu_list.insert(1, image_hdu::default());
179 /// ```
180 ///
181 /// ```
182 /// use astro_rs::fits::*;
183 ///
184 /// let mut hdu_list = HduList::default();
185 /// hdu_list.push(primary_hdu::default());
186 /// hdu_list.insert(1, image_hdu::default());
187 /// assert_eq!(hdu_list.iter_mut().count(), 2);
188 /// ```
189 pub fn insert(&mut self, index: usize, hdu: Hdu) {
190 let mut cur_hdus = self.hdus.len();
191 while cur_hdus < index {
192 if let Some(new_hdu) = self.read_hdu() {
193 self.hdus.push(new_hdu);
194 cur_hdus += 1;
195 } else {
196 panic!("{} is out of bounds (max {})", index, cur_hdus);
197 }
198 }
199 self.hdus.insert(index, hdu);
200 }
201
202 /// Appends `hdu` to the end of the HDU list.
203 ///
204 /// # Examples
205 ///
206 /// ```
207 /// use astro_rs::fits::*;
208 ///
209 /// let mut hdu_list = HduList::default();
210 /// hdu_list.push(primary_hdu::default());
211 /// assert_eq!(hdu_list.iter_mut().count(), 1);
212 /// hdu_list.push(image_hdu::default());
213 /// assert_eq!(hdu_list.iter_mut().count(), 2);
214 /// ```
215 pub fn push(&mut self, hdu: Hdu) {
216 while let Some(new_hdu) = self.read_hdu() {
217 self.hdus.push(new_hdu);
218 }
219 self.hdus.push(hdu);
220 }
221
222 /// Writes the HDU list via the given writer.
223 ///
224 /// # Examples
225 ///
226 /// ```
227 /// use astro_rs::fits::*;
228 /// use std::io::*;
229 ///
230 /// let in_cursor = Cursor::new(SIMPLE_KEYWORD.to_vec());
231 /// let mut hdu_list = HduList::new(BufReader::new(in_cursor));
232 /// let out_cursor = Cursor::new(Vec::new());
233 /// let mut out_writer = BufWriter::new(out_cursor);
234 /// hdu_list.write(&mut out_writer)?;
235 /// assert_eq!(out_writer.get_ref().get_ref(), &SIMPLE_KEYWORD.to_vec());
236 ///
237 /// # Ok::<(), std::io::Error>(())
238 /// ```
239 pub fn write<W: Write>(&mut self, writer: &mut BufWriter<W>) -> Result<(), std::io::Error> {
240 for hdu in &self.hdus {
241 writer.write_all(&hdu.clone().to_bytes())?;
242 }
243 std::io::copy(&mut self.reader, writer)?;
244 writer.flush()?;
245 Ok(())
246 }
247
248 /// Validates the existence and format of the SIMPLE header card.
249 ///
250 /// # Examples
251 ///
252 /// ```
253 /// use astro_rs::fits::*;
254 ///
255 /// let mut hdu_list = HduList::default();
256 /// // empty header
257 /// assert!(!hdu_list.is_header_valid()?);
258 ///
259 /// let mut hdu = Hdu::new();
260 ///
261 /// // non-empty header missing simple card
262 /// let bitpix_card = FitsHeaderCard::from(*b"BITPIX = -32 / FITS BITS/PIXEL ");
263 /// hdu.header.cards.insert(0, bitpix_card);
264 /// hdu_list.push(hdu);
265 /// assert!(!hdu_list.is_header_valid()?);
266 ///
267 /// // valid header
268 /// let simple_card = FitsHeaderCard::from(*b"SIMPLE = T / FITS STANDARD ");
269 /// hdu_list.first_mut().unwrap().header.cards.insert(0, simple_card);
270 /// assert!(hdu_list.is_header_valid()?);
271 /// # Ok::<(), astro_rs::fits::FitsHeaderError>(())
272 /// ```
273 pub fn is_header_valid(&mut self) -> Result<bool, FitsHeaderError> {
274 Ok(*self
275 .get_by_index(0)
276 .and_then(|hdu| hdu.header.get_card(SIMPLE_KEYWORD))
277 .and_then(|card| card.get_value::<bool>().ok())
278 .unwrap_or_default())
279 }
280
281 fn read_hdu(&mut self) -> Option<Hdu> {
282 let mut header_raw = Vec::new();
283 let mut new_header_bytes = vec![0; FITS_RECORD_LEN];
284 self.reader.read_exact(&mut new_header_bytes).ok()?;
285 header_raw.append(&mut new_header_bytes);
286
287 // search for the END keyword.
288 // this should be the last keyword in the header, so if something other than ' ' is found, stop searching
289 loop {
290 let mut end_found = false;
291 for card in 1..=FITS_RECORD_LEN / HEADER_CARD_LEN {
292 let card_index = header_raw.len() - card * HEADER_CARD_LEN;
293 match header_raw[card_index..card_index + HEADER_KEYWORD_LEN]
294 .try_into()
295 .unwrap()
296 {
297 END_KEYWORD => {
298 end_found = true;
299 break;
300 }
301 BLANK_KEYWORD => continue,
302 _ => {
303 end_found = false;
304 break;
305 }
306 }
307 }
308 if end_found {
309 break;
310 }
311 new_header_bytes = vec![0; FITS_RECORD_LEN];
312 self.reader.read_exact(&mut new_header_bytes).ok()?;
313 header_raw.append(&mut new_header_bytes);
314 }
315
316 let mut header = FitsHeader::from_bytes(header_raw);
317 let mut data_raw = Vec::new();
318
319 let naxis = *header
320 .get_card(NAXIS_KEYWORD)
321 .and_then(|card| card.get_value::<u16>().ok())
322 .unwrap_or_default();
323 if naxis != 0 {
324 if let Some(bitpix) = header
325 .get_card(BITPIX_KEYWORD)
326 .and_then(|card| card.get_value::<Bitpix>().ok())
327 {
328 let mut data_len = 1;
329 let mut naxisx_keyword = FitsHeaderKeyword::from(NAXIS_KEYWORD);
330 for x in 1..=naxis {
331 naxisx_keyword.append_number(x);
332
333 let naxisx = *header
334 .get_card(naxisx_keyword)
335 .and_then(|card| card.get_value::<u32>().ok())
336 .unwrap_or_default() as usize;
337 data_len *= naxisx;
338 }
339 data_len *= bitpix.value() / 8;
340 if data_len % FITS_RECORD_LEN != 0 {
341 let num_records = (data_len / FITS_RECORD_LEN) + 1;
342 data_len = num_records * FITS_RECORD_LEN;
343 }
344 data_raw = vec![0; data_len];
345 let _ = self.reader.read_exact(&mut data_raw);
346 }
347 }
348 Some(Hdu { header, data_raw })
349 }
350}
351
352/// A Header Data Unit within a FITS file.
353#[derive(Debug, Default, Clone)]
354pub struct Hdu {
355 /// The header section of the HDU.
356 pub header: FitsHeader,
357 data_raw: Vec<u8>,
358}
359
360impl Hdu {
361 /// Constructs an HDU with the given header and data.
362 pub fn new() -> Self {
363 Self::default()
364 }
365
366 /// Serializes the contents of the HDU to bytes.
367 pub fn to_bytes(mut self) -> Vec<u8> {
368 let mut result = self.header.to_bytes();
369 result.append(&mut self.data_raw);
370 let remainder = result.len() % FITS_RECORD_LEN;
371 if remainder != 0 {
372 let num_cards = (result.len() / FITS_RECORD_LEN) + 1;
373 let new_len = num_cards * FITS_RECORD_LEN;
374 result.resize(new_len, 0);
375 }
376 result
377 }
378
379 /// Gets the name of the HDU, or an empty string if the name cannot be determined.
380 ///
381 /// # Examples
382 ///
383 /// ```
384 /// use astro_rs::fits::*;
385 ///
386 /// let mut img_hdu = image_hdu::default();
387 /// let name_card = FitsHeaderCard::from(*b"EXTNAME = 'hdu_name' ");
388 /// img_hdu.header.cards.insert(img_hdu.header.cards.len() - 1, name_card);
389 /// assert_eq!(img_hdu.get_name(), String::from("hdu_name"));
390 /// ```
391 pub fn get_name(&mut self) -> String {
392 self.header
393 .get_card(EXTNAME_KEYWORD)
394 .and_then(|card| card.get_value::<String>().ok())
395 .map(|name| name.trim().to_owned())
396 .unwrap_or_default()
397 }
398
399 /// Gets the data section of the HDU.
400 pub fn data_raw(&self) -> &Vec<u8> {
401 &self.data_raw
402 }
403
404 /// Sets the data section of the HDU.
405 pub fn set_data_raw(&mut self, data_raw: Vec<u8>) {
406 self.data_raw = data_raw;
407 }
408
409 /// Attempts to create a new FitsDataCollection from the data section of the HDU.
410 pub fn get_data<T: FitsDataCollection>(&self) -> Result<T, FitsHeaderError> {
411 T::from_bytes(&self.data_raw)
412 }
413
414 /// Sets the data section of the HDU.
415 pub fn set_data<T: FitsDataCollection>(&mut self, data: &T) {
416 self.data_raw = data.to_bytes();
417 }
418
419 /// Creates a Vec containing the dimensions of the data section of the HDU as defined by the NAXIS keywords.
420 pub fn get_dimensions(&mut self) -> Vec<usize> {
421 let naxis = *self
422 .header
423 .get_card(NAXIS_KEYWORD)
424 .and_then(|card| card.get_value::<u16>().ok())
425 .unwrap_or_default();
426 if naxis == 0 {
427 return Vec::new();
428 }
429 let mut result = Vec::with_capacity(naxis as usize);
430 let mut naxisx_keyword = FitsHeaderKeyword::from(NAXIS_KEYWORD);
431 for x in 1..=naxis {
432 naxisx_keyword.append_number(x);
433
434 let naxisx = *self
435 .header
436 .get_card(naxisx_keyword)
437 .and_then(|card| card.get_value::<u32>().ok())
438 .unwrap_or_default() as usize;
439 result.push(naxisx);
440 }
441 result
442 }
443}
444
445/// A trait that allows data to be serialized/deserialized as the data section of an HDU.
446pub trait FitsDataCollection: Debug {
447 /// Attempts to deserialize a data collection from the given bytes.
448 fn from_bytes(raw: &[u8]) -> Result<Self, FitsHeaderError>
449 where
450 Self: Sized;
451
452 /// Serializes the data collection to bytes.
453 fn to_bytes(&self) -> Vec<u8>;
454}
455
456impl FitsDataCollection for Vec<u8> {
457 fn from_bytes(raw: &[u8]) -> Result<Self, FitsHeaderError> {
458 Ok(raw.to_owned())
459 }
460
461 fn to_bytes(&self) -> Vec<u8> {
462 self.to_owned()
463 }
464}
465
466impl FitsDataCollection for Vec<i16> {
467 fn from_bytes(raw: &[u8]) -> Result<Self, FitsHeaderError> {
468 let mut data = Vec::with_capacity(raw.len() / 2);
469 for chunk in raw.chunks_exact(2) {
470 data.push(i16::from_be_bytes(chunk.try_into().unwrap()));
471 }
472 Ok(data)
473 }
474
475 fn to_bytes(&self) -> Vec<u8> {
476 let mut data = Vec::with_capacity(self.len() * 2);
477 for chunk in self {
478 data.extend_from_slice(&chunk.to_be_bytes());
479 }
480 data
481 }
482}
483
484impl FitsDataCollection for Vec<i32> {
485 fn from_bytes(raw: &[u8]) -> Result<Self, FitsHeaderError> {
486 let mut data = Vec::with_capacity(raw.len() / 4);
487 for chunk in raw.chunks_exact(4) {
488 data.push(i32::from_be_bytes(chunk.try_into().unwrap()));
489 }
490 Ok(data)
491 }
492
493 fn to_bytes(&self) -> Vec<u8> {
494 let mut data = Vec::with_capacity(self.len() * 4);
495 for chunk in self {
496 data.extend_from_slice(&chunk.to_be_bytes());
497 }
498 data
499 }
500}
501
502impl FitsDataCollection for Vec<f32> {
503 fn from_bytes(raw: &[u8]) -> Result<Self, FitsHeaderError> {
504 let mut data = Vec::with_capacity(raw.len() / 4);
505 for chunk in raw.chunks_exact(4) {
506 data.push(f32::from_be_bytes(chunk.try_into().unwrap()));
507 }
508 Ok(data)
509 }
510
511 fn to_bytes(&self) -> Vec<u8> {
512 let mut data = Vec::with_capacity(self.len() * 4);
513 for chunk in self {
514 data.extend_from_slice(&chunk.to_be_bytes());
515 }
516 data
517 }
518}
519
520impl FitsDataCollection for Vec<f64> {
521 fn from_bytes(raw: &[u8]) -> Result<Self, FitsHeaderError> {
522 let mut data = Vec::with_capacity(raw.len() / 8);
523 for chunk in raw.chunks_exact(8) {
524 data.push(f64::from_be_bytes(chunk.try_into().unwrap()));
525 }
526 Ok(data)
527 }
528
529 fn to_bytes(&self) -> Vec<u8> {
530 let mut data = Vec::with_capacity(self.len() * 8);
531 for chunk in self {
532 data.extend_from_slice(&chunk.to_be_bytes());
533 }
534 data
535 }
536}
537
538#[macro_use]
539pub(crate) mod hdu_macros {
540 /// Creates a box of the given value and casts it to an implicit return type.
541 macro_rules! return_box {
542 ($result: expr) => {{
543 let b = Box::new($result);
544 let ptr = Box::into_raw(b);
545 let new_ptr = ptr.cast();
546 Box::from_raw(new_ptr)
547 }};
548 }
549
550 pub(crate) use return_box;
551}