use std::io::Read;
use std::io::Seek;
use std::ops::Range;
use crate::ascii::AsciiTable;
use crate::bitpix::Bitpix;
use crate::block::BLOCK_SIZE;
use crate::block::CARD_SIZE;
use crate::block::padded_len;
use crate::checksum;
use crate::data::ImageView;
use crate::data::RawImage;
use crate::data::Scaling;
use crate::data::shape_product;
use crate::data::swap_into_words;
use crate::data::view_words;
use crate::error::FitsError;
use crate::error::Result;
use crate::groups::RandomGroups;
use crate::hdu::HduKind;
use crate::hdu::data_extent;
use crate::header::Header;
use crate::table::BinTable;
pub(crate) mod source;
use source::SliceSource;
use source::Source;
use source::StreamSource;
#[cfg(feature = "compression")]
use crate::compress::{decompress_image, uncompress_table};
#[cfg(feature = "compression")]
use crate::data::Image;
#[cfg(feature = "compression")]
use crate::data::copy_samples_into_words;
#[derive(Debug)]
pub struct Hdu {
pub header: Header,
pub kind: HduKind,
pub(crate) header_bytes: Vec<u8>,
pub(crate) data_offset: u64,
pub(crate) data_bytes: u64,
}
impl Hdu {
fn ensure_plain_image(&self) -> Result<()> {
if !matches!(self.kind, HduKind::Primary | HduKind::Image) {
return Err(FitsError::NotAnImage);
}
if self.header.get_integer("PCOUNT").unwrap_or(0) != 0
|| self.header.get_integer("GCOUNT").unwrap_or(1) != 1
{
return Err(FitsError::ImageHasGroups);
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct DataUnit {
pub bytes: Vec<u8>,
pub data_range: Range<usize>,
}
impl DataUnit {
pub fn data(&self) -> &[u8] {
&self.bytes[self.data_range.clone()]
}
}
#[derive(Debug)]
pub struct FitsReader<S> {
source: S,
pub(crate) hdus: Vec<Hdu>,
scratch: Vec<u8>,
}
pub type StreamReader<R> = FitsReader<StreamSource<R>>;
pub type SliceReader<'a> = FitsReader<SliceSource<'a>>;
#[cfg(feature = "mmap")]
pub type MmapReader = FitsReader<source::MmapSource>;
impl<R: Read + Seek> FitsReader<StreamSource<R>> {
pub fn open(source: R) -> Result<StreamReader<R>> {
FitsReader::from_source(StreamSource::new(source)?)
}
}
impl<'a> FitsReader<SliceSource<'a>> {
pub fn from_bytes(bytes: &'a [u8]) -> Result<SliceReader<'a>> {
FitsReader::from_source(SliceSource::new(bytes))
}
}
#[cfg(feature = "mmap")]
impl FitsReader<source::MmapSource> {
pub fn open_mmap(path: impl AsRef<std::path::Path>) -> Result<MmapReader> {
FitsReader::from_source(source::MmapSource::open(path.as_ref())?)
}
}
impl<S: Source> FitsReader<S> {
fn from_source(mut source: S) -> Result<FitsReader<S>> {
let mut scratch = Vec::new();
let mut hdus = Vec::new();
let mut offset = 0u64;
loop {
match scan_header_unit(&mut source, &mut offset, &mut scratch)? {
NextHeader::Found(header_bytes) => {
let header = Header::parse(&header_bytes)?;
let kind = HduKind::classify(&header);
let data_offset = offset;
let extent = data_extent(&header)?;
let next = data_offset
.checked_add(extent.padded_bytes)
.ok_or(FitsError::DataUnitOverflow)?;
hdus.push(Hdu {
header,
kind,
header_bytes,
data_offset,
data_bytes: extent.data_bytes,
});
offset = next.min(source.size());
}
NextHeader::End => break,
NextHeader::Trailing if hdus.is_empty() => return Err(FitsError::UnexpectedEof),
NextHeader::Trailing => break,
}
}
Ok(FitsReader {
source,
hdus,
scratch,
})
}
fn checked_hdu(&self, index: usize) -> Result<&Hdu> {
self.hdus.get(index).ok_or(FitsError::HduIndexOutOfBounds {
index,
len: self.hdus.len(),
})
}
pub fn hdus(&self) -> &[Hdu] {
&self.hdus
}
pub fn hdu_index(&self, name: &str, version: Option<i64>) -> Option<usize> {
self.hdus.iter().position(|h| {
h.header
.get_text("EXTNAME")
.is_some_and(|n| n.eq_ignore_ascii_case(name))
&& version.is_none_or(|v| h.header.get_integer("EXTVER").unwrap_or(1) == v)
})
}
pub fn image_indices(&self) -> Vec<usize> {
self.hdus
.iter()
.enumerate()
.filter(|(_, h)| match h.kind {
HduKind::Image | HduKind::CompressedImage => true,
HduKind::Primary => h.header.naxis().is_ok_and(|n| n > 0),
_ => false,
})
.map(|(i, _)| i)
.collect()
}
pub fn read_data_raw(&mut self, index: usize) -> Result<DataUnit> {
let hdu = self.checked_hdu(index)?;
let (data_offset, data_bytes) = (hdu.data_offset, hdu.data_bytes);
let bytes = self
.source
.read_owned(data_offset, padded_len(data_bytes) as usize)?;
Ok(DataUnit {
bytes,
data_range: 0..data_bytes as usize,
})
}
#[cfg(feature = "compression")]
fn decompress_at(&mut self, index: usize) -> Result<Image> {
let table = self.read_table(index)?;
decompress_image(&self.hdus[index].header, &table)
}
pub fn read_image(&mut self, index: usize) -> Result<RawImage<'_>> {
#[cfg(feature = "compression")]
if self.checked_hdu(index)?.kind == HduKind::CompressedImage {
let img = self.decompress_at(index)?;
return Ok(RawImage::decoded(img.samples, img.shape, img.scaling));
}
let hdu = self.checked_hdu(index)?;
hdu.ensure_plain_image()?;
let bitpix = hdu.header.bitpix()?;
let shape = hdu.header.axes()?;
let scaling = Scaling::from_header(&hdu.header);
let (data_offset, data_bytes) = (hdu.data_offset, hdu.data_bytes);
let unit = self.source.slice(
data_offset,
padded_len(data_bytes) as usize,
&mut self.scratch,
)?;
let bytes = &unit[..data_bytes as usize];
debug_assert_eq!(
bytes.len(),
shape_product(&shape) * bitpix.elem_size(),
"image data length must match the axis product"
);
Ok(RawImage::raw(shape, bitpix, scaling, bytes))
}
pub fn read_image_view<'a>(
&'a mut self,
index: usize,
scratch: &'a mut Vec<u64>,
) -> Result<ImageView<'a>> {
#[cfg(feature = "compression")]
if self.checked_hdu(index)?.kind == HduKind::CompressedImage {
let img = self.decompress_at(index)?;
let bitpix = img.samples.bitpix();
let nbytes = copy_samples_into_words(&img.samples, scratch);
return Ok(view_words(scratch, bitpix, nbytes));
}
let hdu = self.checked_hdu(index)?;
hdu.ensure_plain_image()?;
let bitpix = hdu.header.bitpix()?;
let data_bytes = hdu.data_bytes as usize;
let padded = padded_len(hdu.data_bytes) as usize;
let data_offset = hdu.data_offset;
let unit = self.source.slice(data_offset, padded, &mut self.scratch)?;
let be = &unit[..data_bytes];
if bitpix == Bitpix::U8 {
return Ok(ImageView::U8(be));
}
swap_into_words(be, bitpix, scratch);
Ok(view_words(scratch, bitpix, data_bytes))
}
pub fn read_table(&mut self, index: usize) -> Result<BinTable> {
let unit = self.read_data_raw(index)?; let hdu = &self.hdus[index];
if !matches!(
hdu.kind,
HduKind::BinTable | HduKind::CompressedImage | HduKind::CompressedTable
) {
return Err(FitsError::NotABinTable);
}
BinTable::from_data(&hdu.header, unit.bytes)
}
pub fn read_ascii_table(&mut self, index: usize) -> Result<AsciiTable> {
let unit = self.read_data_raw(index)?;
let hdu = &self.hdus[index];
if hdu.kind != HduKind::AsciiTable {
return Err(FitsError::NotAnAsciiTable);
}
AsciiTable::from_data(&hdu.header, unit.bytes)
}
pub fn read_groups(&mut self, index: usize) -> Result<RandomGroups> {
let hdu = self.checked_hdu(index)?;
if hdu.kind != HduKind::RandomGroups {
return Err(FitsError::NotRandomGroups);
}
let (data_offset, data_bytes) = (hdu.data_offset, hdu.data_bytes);
let unit = self.source.slice(
data_offset,
padded_len(data_bytes) as usize,
&mut self.scratch,
)?;
RandomGroups::from_data(&self.hdus[index].header, &unit[..data_bytes as usize])
}
#[cfg(feature = "compression")]
pub fn read_compressed_table(&mut self, index: usize) -> Result<BinTable> {
let table = self.read_table(index)?;
let header = self.hdus[index].header.clone();
let parts = uncompress_table(&header, &table)?;
BinTable::from_data(&parts.header, parts.data)
}
pub fn verify_checksum(&mut self, index: usize) -> Result<ChecksumReport> {
let hdu = self.checked_hdu(index)?;
let (data_offset, data_bytes) = (hdu.data_offset, hdu.data_bytes);
let unit = self.source.slice(
data_offset,
padded_len(data_bytes) as usize,
&mut self.scratch,
)?;
let data_sum = checksum::accumulate(unit, 0);
let hdu = &self.hdus[index];
let hdu_sum = checksum::accumulate(unit, checksum::accumulate(&hdu.header_bytes, 0));
Ok(ChecksumReport {
datasum_ok: hdu
.header
.get_text("DATASUM")
.map(|s| s.trim().parse::<u32>().ok() == Some(data_sum)),
checksum_ok: hdu
.header
.get_text("CHECKSUM")
.map(|_| hdu_sum == 0xFFFF_FFFF),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChecksumReport {
pub datasum_ok: Option<bool>,
pub checksum_ok: Option<bool>,
}
enum NextHeader {
Found(Vec<u8>),
End,
Trailing,
}
fn scan_header_unit<S: Source>(
source: &mut S,
offset: &mut u64,
scratch: &mut Vec<u8>,
) -> Result<NextHeader> {
let size = source.size();
let mut bytes = Vec::with_capacity(BLOCK_SIZE);
loop {
match size - *offset {
0 if bytes.is_empty() => return Ok(NextHeader::End),
0 => return Ok(NextHeader::Trailing),
avail if avail < BLOCK_SIZE as u64 => return Ok(NextHeader::Trailing),
_ => {}
}
let block = source.slice(*offset, BLOCK_SIZE, scratch)?;
*offset += BLOCK_SIZE as u64;
bytes.extend_from_slice(block);
if block_has_end(block) {
return Ok(NextHeader::Found(bytes));
}
}
}
fn block_has_end(block: &[u8]) -> bool {
block
.chunks_exact(CARD_SIZE)
.any(|card| &card[..3] == b"END" && card[3..].iter().all(|&b| b == b' '))
}
#[cfg(test)]
mod tests;