use std::path::Path;
use super::{Categorization, FileCategorizer};
use limnifs_core::codec::CODEC_RICEPP;
const RICEPP_ENABLED: bool = true;
const FITS_MAGIC: &[u8] = b"SIMPLE =";
const FITS_RECORD_LEN: usize = 80;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FitsParams {
pub bitpix: i32,
pub naxis: u32,
pub naxis1: u32,
pub naxis2: u32,
}
impl FitsParams {
#[must_use]
pub fn encode(&self) -> [u8; 16] {
let mut out = [0u8; 16];
out[0..4].copy_from_slice(&self.bitpix.to_le_bytes());
out[4..8].copy_from_slice(&self.naxis.to_le_bytes());
out[8..12].copy_from_slice(&self.naxis1.to_le_bytes());
out[12..16].copy_from_slice(&self.naxis2.to_le_bytes());
out
}
}
pub struct FitsCategorizer;
impl FileCategorizer for FitsCategorizer {
fn name(&self) -> &'static str {
"fits"
}
fn categories(&self) -> &'static [&'static str] {
&["fits/image"]
}
fn first_byte_hint(&self) -> Option<&'static [u8]> {
Some(b"S")
}
fn categorize(&self, _path: &Path, data: &[u8]) -> Option<Categorization> {
if !RICEPP_ENABLED {
return None;
}
let params = parse_fits(data)?;
Some(Categorization {
codec_id: CODEC_RICEPP,
codec_params: params.encode().to_vec(),
category: "fits/image",
})
}
}
#[must_use]
fn parse_fits(data: &[u8]) -> Option<FitsParams> {
if data.len() < FITS_MAGIC.len() + FITS_RECORD_LEN {
return None;
}
if &data[0..FITS_MAGIC.len()] != FITS_MAGIC {
return None;
}
let mut bitpix: Option<i32> = None;
let mut naxis: Option<u32> = None;
let mut naxis1: Option<u32> = None;
let mut naxis2: Option<u32> = None;
let header_block = data.len().min(2880);
let mut off = 0;
while off + FITS_RECORD_LEN <= header_block {
let record = &data[off..off + FITS_RECORD_LEN];
let key = &record[0..8];
let value = &record[10..30];
if key.starts_with(b"END ") {
break;
}
match key {
b"BITPIX " => bitpix = parse_int(value),
b"NAXIS " => naxis = parse_int(value).map(|i| i as u32),
b"NAXIS1 " => naxis1 = parse_int(value).map(|i| i as u32),
b"NAXIS2 " => naxis2 = parse_int(value).map(|i| i as u32),
_ => {}
}
off += FITS_RECORD_LEN;
}
Some(FitsParams {
bitpix: bitpix?,
naxis: naxis?,
naxis1: naxis1.unwrap_or(0),
naxis2: naxis2.unwrap_or(0),
})
}
fn parse_int(value: &[u8]) -> Option<i32> {
let s = std::str::from_utf8(value).ok()?;
s.trim().trim_end_matches('/').trim().parse().ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn make_fits_record(key: &str, value: &str) -> [u8; FITS_RECORD_LEN] {
let mut rec = [b' '; FITS_RECORD_LEN];
let key_bytes = key.as_bytes();
let copy_len = key_bytes.len().min(8);
rec[..copy_len].copy_from_slice(&key_bytes[..copy_len]);
rec[8] = b'=';
rec[9] = b' ';
let value_bytes = value.as_bytes();
let copy_len = value_bytes.len().min(20);
rec[10..10 + copy_len].copy_from_slice(&value_bytes[..copy_len]);
rec
}
fn make_fits_header() -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&make_fits_record("SIMPLE", "T"));
buf.extend_from_slice(&make_fits_record("BITPIX", "16"));
buf.extend_from_slice(&make_fits_record("NAXIS", "2"));
buf.extend_from_slice(&make_fits_record("NAXIS1", "512"));
buf.extend_from_slice(&make_fits_record("NAXIS2", "512"));
buf.extend_from_slice(&make_fits_record("END", ""));
while buf.len() < 2880 {
buf.push(0);
}
buf
}
#[test]
fn fits_routes_to_ricepp_when_enabled() {
let c = FitsCategorizer;
let fits = make_fits_header();
let cat = c
.categorize(Path::new("/x.fits"), &fits)
.expect("fits claims");
assert_eq!(cat.codec_id, limnifs_core::codec::CODEC_RICEPP);
}
#[test]
fn fits_header_parsed_correctly() {
let fits = make_fits_header();
let params = parse_fits(&fits).expect("fits parses");
assert_eq!(params.bitpix, 16);
assert_eq!(params.naxis, 2);
assert_eq!(params.naxis1, 512);
assert_eq!(params.naxis2, 512);
}
#[test]
fn rejects_non_fits_magic() {
let junk = vec![0u8; 4096];
assert!(parse_fits(&junk).is_none());
}
}