use super::{FitsError, FitsResult};
const CARD: usize = 80;
const BLOCK: usize = 2880;
pub(super) const RESERVED: &[&str] = &[
"SIMPLE", "BITPIX", "NAXIS", "EXTEND", "END", "XTENSION", "PCOUNT", "GCOUNT", "BSCALE",
"BZERO", "TFIELDS", "THEAP", "GROUPS", "BLOCKED", "CONTINUE", "COMMENT", "HISTORY", "DATE-OBS",
"COLORSPC", "BAYERPAT",
];
fn is_fixed(key: &str) -> bool {
!key.is_empty()
&& key.len() <= 8
&& key
.bytes()
.all(|b| b.is_ascii_uppercase() || b.is_ascii_digit() || b == b'-' || b == b'_')
}
pub(super) struct Header {
buf: Vec<u8>,
longstrn: bool,
}
impl Header {
pub(super) fn new() -> Self {
Self {
buf: Vec::with_capacity(BLOCK),
longstrn: false,
}
}
fn raw(&mut self, text: &str) {
debug_assert!(text.len() <= CARD, "card over 80 cols: {text:?}");
let start = self.buf.len();
self.buf.extend_from_slice(text.as_bytes());
self.buf.resize(start + CARD, b' ');
}
fn fixed_prefix(key: &str) -> String {
format!("{key:<8}= ")
}
pub(super) fn logical(
&mut self,
key: &str,
val: bool,
comment: Option<&str>,
) -> FitsResult<()> {
self.valued(key, if val { "T" } else { "F" }, comment, true)
}
pub(super) fn integer(&mut self, key: &str, val: i64, comment: Option<&str>) -> FitsResult<()> {
self.valued(key, &val.to_string(), comment, true)
}
pub(super) fn real(&mut self, key: &str, val: f64, comment: Option<&str>) -> FitsResult<()> {
if !val.is_finite() {
self.commentary("COMMENT", &format!("{key} omitted: non-finite value"));
return Ok(());
}
let s = format!("{val:?}").replace('e', "E");
self.valued(key, &s, comment, true)
}
fn valued(
&mut self,
key: &str,
val: &str,
comment: Option<&str>,
numeric: bool,
) -> FitsResult<()> {
let mut card = if is_fixed(key) {
let mut c = Self::fixed_prefix(key);
if numeric && val.len() <= 20 {
c.push_str(&format!("{val:>20}"));
} else {
c.push_str(val);
}
c
} else {
let c = format!("HIERARCH {key} = {val}");
if c.len() > CARD {
return Err(FitsError::KeywordTooLong(key.to_string()));
}
c
};
if let Some(cmt) = comment
&& card.len() + 3 < CARD {
let room = CARD - card.len() - 3;
card.push_str(" / ");
card.push_str(&cmt[..cmt.len().min(room)]);
}
self.raw(&card);
Ok(())
}
pub(super) fn commentary(&mut self, key: &str, text: &str) {
for chunk in text.as_bytes().chunks(CARD - 8) {
let line = std::str::from_utf8(chunk).unwrap_or("");
self.raw(&format!("{key:<8}{line}"));
}
}
pub(super) fn string(&mut self, key: &str, val: &str, comment: Option<&str>) -> FitsResult<()> {
let escaped = val.replace('\'', "''");
let prefix = if is_fixed(key) {
Self::fixed_prefix(key)
} else {
let p = format!("HIERARCH {key} = ");
if p.len() + 10 > CARD {
return Err(FitsError::KeywordTooLong(key.to_string()));
}
p
};
let first_room = CARD - prefix.len() - 2;
if escaped.len() <= first_room {
let body = pad8(&escaped);
let mut card = format!("{prefix}'{body}'");
if let Some(cmt) = comment
&& card.len() + 3 < CARD {
let room = CARD - card.len() - 3;
card.push_str(" / ");
card.push_str(&cmt[..cmt.len().min(room)]);
}
self.raw(&card);
return Ok(());
}
if !self.longstrn {
let card = format!("{:<8}= 'OGIP 1.0'", "LONGSTRN");
self.raw(&card);
self.longstrn = true;
}
let bytes = escaped.as_bytes();
let mut pos = 0;
let take = first_room.saturating_sub(1).min(bytes.len());
let seg = std::str::from_utf8(&bytes[..take]).unwrap_or("");
self.raw(&format!("{prefix}'{seg}&'"));
pos += take;
while pos < bytes.len() {
let room = CARD - 10 - 2; let last = pos + room >= bytes.len();
let n = if last { bytes.len() - pos } else { room - 1 };
let seg = std::str::from_utf8(&bytes[pos..pos + n]).unwrap_or("");
pos += n;
let tail = if last { "'" } else { "&'" };
let mut card = format!("CONTINUE '{seg}{tail}");
if last
&& let Some(cmt) = comment
&& card.len() + 3 < CARD {
let room = CARD - card.len() - 3;
card.push_str(" / ");
card.push_str(&cmt[..cmt.len().min(room)]);
}
self.raw(&card);
}
Ok(())
}
pub(super) fn finish(mut self) -> Vec<u8> {
self.raw("END");
let rem = self.buf.len() % BLOCK;
if rem != 0 {
self.buf.resize(self.buf.len() + (BLOCK - rem), b' ');
}
self.buf
}
}
fn pad8(s: &str) -> String {
if s.len() >= 8 {
s.to_string()
} else {
format!("{s:<8}")
}
}
pub(super) fn pad_writer<W: std::io::Write>(w: &mut W, data_len: usize) -> std::io::Result<()> {
let rem = data_len % BLOCK;
if rem != 0 {
w.write_all(&vec![0u8; BLOCK - rem])?;
}
Ok(())
}
pub(super) fn is_reserved(key: &str) -> bool {
let k = key.to_ascii_uppercase();
RESERVED.contains(&k.as_str())
|| k.starts_with("NAXIS")
|| k.starts_with('Z')
|| k.starts_with('T') && k[1..].chars().all(|c| c.is_ascii_digit()) && k.len() > 1
}