pub(crate) mod card;
pub(crate) mod value;
use std::collections::HashMap;
use crate::bitpix::Bitpix;
use crate::block::CARD_SIZE;
use crate::data::Scaling;
use crate::error::FitsError;
use crate::error::Result;
use crate::header::card::Card;
use crate::header::card::CardKind;
use crate::header::card::validate_keyword;
use crate::header::value::Value;
use crate::keyword::key;
use crate::time::{EpochTime, FitsTime, PhaseAxis, TimeBounds};
use crate::wcs::Wcs;
#[derive(Debug, Clone, Default)]
pub struct Header {
pub(crate) cards: Vec<Card>,
index: HashMap<String, usize>,
}
impl Header {
pub fn parse(bytes: &[u8]) -> Result<Header> {
let ncards = bytes.len() / CARD_SIZE;
let mut cards: Vec<Card> = Vec::with_capacity(ncards);
let mut index = HashMap::with_capacity(ncards);
for chunk in bytes.chunks_exact(CARD_SIZE) {
let card = Card::parse(
chunk
.try_into()
.expect("chunks_exact yields CARD_SIZE slices"),
)?;
match card.kind {
CardKind::End => return Ok(Header { cards, index }),
CardKind::Continue if fold_continuation(&mut cards, &card) => {}
_ => {
let mut card = card;
if card.kind == CardKind::Continue {
card.kind = CardKind::Commentary;
card.value = None;
}
if matches!(card.kind, CardKind::Value | CardKind::Hierarch) {
index.entry(card.keyword.clone()).or_insert(cards.len());
}
cards.push(card);
}
}
}
Err(FitsError::MissingEnd)
}
pub fn get(&self, keyword: &str) -> Option<&Value> {
self.index
.get(keyword)
.and_then(|&i| self.cards[i].value.as_ref())
}
pub fn get_logical(&self, keyword: &str) -> Option<bool> {
self.get(keyword)?.as_logical()
}
pub fn get_integer(&self, keyword: &str) -> Option<i64> {
self.get(keyword)?.as_integer()
}
pub fn get_real(&self, keyword: &str) -> Option<f64> {
self.get(keyword)?.as_real()
}
pub fn get_text(&self, keyword: &str) -> Option<&str> {
self.get(keyword)?.as_text()
}
pub fn bitpix(&self) -> Result<Bitpix> {
let code = self
.get_integer("BITPIX")
.ok_or(FitsError::MissingKeyword { name: "BITPIX" })?;
Bitpix::from_code(code)
}
pub fn naxis(&self) -> Result<usize> {
let n = self
.get_integer("NAXIS")
.ok_or(FitsError::MissingKeyword { name: "NAXIS" })?;
match usize::try_from(n) {
Ok(n) if n <= 999 => Ok(n),
_ => Err(FitsError::KeywordOutOfRange { name: "NAXIS" }),
}
}
pub fn axes(&self) -> Result<Vec<usize>> {
let naxis = self.naxis()?;
let mut axes = Vec::with_capacity(naxis);
for n in 1..=naxis {
let len = self
.get_integer(key!("NAXIS{n}").as_str())
.ok_or(FitsError::MissingKeyword { name: "NAXISn" })?;
axes.push(
usize::try_from(len)
.map_err(|_| FitsError::KeywordOutOfRange { name: "NAXISn" })?,
);
}
Ok(axes)
}
pub fn scaling(&self) -> Scaling {
Scaling::from_header(self)
}
pub fn wcs(&self, alt: Option<char>) -> Result<Wcs> {
Wcs::from_header(self, alt)
}
pub fn wcs_pixel_list(&self, columns: &[usize], alt: Option<char>) -> Result<Wcs> {
Wcs::from_pixel_list(self, columns, alt)
}
pub fn wcs_array_column(&self, column: usize, alt: Option<char>) -> Result<Wcs> {
Wcs::from_array_column(self, column, alt)
}
pub fn time(&self) -> FitsTime {
FitsTime::from_header(self)
}
pub fn obs_mjd(&self) -> Option<f64> {
FitsTime::obs_mjd(self)
}
pub fn epoch(&self) -> Option<EpochTime> {
FitsTime::epoch(self)
}
pub fn time_bounds(&self) -> TimeBounds {
FitsTime::bounds(self)
}
pub fn phase_axis(&self, axis: usize) -> Option<PhaseAxis> {
FitsTime::phase_axis(self, axis)
}
pub fn new() -> Header {
Header::default()
}
pub fn set(&mut self, keyword: &str, value: impl Into<Value>) -> &mut Self {
assert!(
validate_keyword(keyword).is_ok(),
"Header::set: invalid FITS keyword {keyword:?}"
);
let value = value.into();
if let Some(&i) = self.index.get(keyword) {
self.cards[i].value = Some(value);
} else {
self.index.insert(keyword.to_string(), self.cards.len());
self.cards.push(Card::value(keyword, value));
}
self
}
#[cfg(feature = "compression")]
pub(crate) fn remove(&mut self, keyword: &str) -> &mut Self {
if self.index.contains_key(keyword) {
self.cards.retain(|c| c.keyword != keyword);
self.reindex();
}
self
}
#[cfg(feature = "compression")]
fn reindex(&mut self) {
self.index.clear();
for (i, card) in self.cards.iter().enumerate() {
if matches!(card.kind, CardKind::Value | CardKind::Hierarch) {
self.index.entry(card.keyword.clone()).or_insert(i);
}
}
}
pub fn comment(&mut self, keyword: &str, text: &str) -> &mut Self {
if let Some(&i) = self.index.get(keyword) {
self.cards[i].comment = Some(text.to_string());
}
self
}
pub fn push_comment(&mut self, text: &str) -> &mut Self {
self.cards.push(Card::commentary("COMMENT", text));
self
}
pub fn push_history(&mut self, text: &str) -> &mut Self {
self.cards.push(Card::commentary("HISTORY", text));
self
}
}
fn fold_continuation(cards: &mut [Card], cont: &Card) -> bool {
let Some(prev) = cards.last_mut() else {
return false;
};
let Some(Value::Text(acc)) = prev.value.as_mut() else {
return false;
};
if !acc.ends_with('&') {
return false;
}
acc.pop(); if let Some(Value::Text(sub)) = &cont.value {
acc.push_str(sub);
}
if cont.comment.is_some() {
prev.comment = cont.comment.clone();
}
true
}
#[cfg(test)]
pub(crate) fn from_card_lines(lines: &[&str]) -> Header {
let mut buf = Vec::with_capacity((lines.len() + 1) * CARD_SIZE);
for line in lines {
let mut card = [b' '; CARD_SIZE];
card[..line.len()].copy_from_slice(line.as_bytes());
buf.extend_from_slice(&card);
}
let mut end = [b' '; CARD_SIZE];
end[..3].copy_from_slice(b"END");
buf.extend_from_slice(&end);
Header::parse(&buf).unwrap()
}
#[cfg(test)]
mod tests;