use alloc::vec;
use alloc::vec::Vec;
use bytemuck::pod_collect_to_vec;
use crate::block::padded_byte_len;
use crate::endian::{
buf_f32_native_to_be, buf_f64_native_to_be, buf_i16_native_to_be, buf_i32_native_to_be,
buf_i64_native_to_be,
};
use crate::error::{Error, Result};
use crate::hdu::{Hdu, HduInfo};
use crate::header::{serialize_header, Card};
use crate::primary::build_primary_header;
use crate::value::Value;
#[derive(Debug, Clone, PartialEq)]
pub enum ImageData {
U8(Vec<u8>),
I16(Vec<i16>),
I32(Vec<i32>),
I64(Vec<i64>),
F32(Vec<f32>),
F64(Vec<f64>),
}
pub fn image_dimensions(hdu: &Hdu) -> Result<Vec<usize>> {
match &hdu.info {
HduInfo::Primary { naxes, .. } => Ok(naxes.clone()),
HduInfo::Image { naxes, .. } => Ok(naxes.clone()),
HduInfo::CompressedImage { znaxes, .. } => Ok(znaxes.clone()),
_ => Err(Error::InvalidHeader("not an image HDU")),
}
}
fn hdu_bitpix(hdu: &Hdu) -> Result<i64> {
match &hdu.info {
HduInfo::Primary { bitpix, .. } | HduInfo::Image { bitpix, .. } => Ok(*bitpix),
HduInfo::CompressedImage { zbitpix, .. } => Ok(*zbitpix),
_ => Err(Error::InvalidHeader("not an image HDU")),
}
}
pub fn read_image_data(fits_data: &[u8], hdu: &Hdu) -> Result<ImageData> {
if matches!(&hdu.info, HduInfo::CompressedImage { .. }) {
return crate::tiled::read_tiled_image(fits_data, hdu);
}
let bitpix = hdu_bitpix(hdu)?;
let data_len = hdu.data_len;
if data_len == 0 {
return match bitpix {
8 => Ok(ImageData::U8(Vec::new())),
16 => Ok(ImageData::I16(Vec::new())),
32 => Ok(ImageData::I32(Vec::new())),
64 => Ok(ImageData::I64(Vec::new())),
-32 => Ok(ImageData::F32(Vec::new())),
-64 => Ok(ImageData::F64(Vec::new())),
other => Err(Error::InvalidBitpix(other)),
};
}
let end = hdu.data_start + data_len;
if end > fits_data.len() {
return Err(Error::UnexpectedEof);
}
let raw = &fits_data[hdu.data_start..end];
match bitpix {
8 => Ok(ImageData::U8(raw.to_vec())),
16 => {
let mut pixels: Vec<i16> = pod_collect_to_vec(raw);
for v in &mut pixels {
*v = i16::from_be(*v);
}
Ok(ImageData::I16(pixels))
}
32 => {
let mut pixels: Vec<i32> = pod_collect_to_vec(raw);
for v in &mut pixels {
*v = i32::from_be(*v);
}
Ok(ImageData::I32(pixels))
}
64 => {
let mut pixels: Vec<i64> = pod_collect_to_vec(raw);
for v in &mut pixels {
*v = i64::from_be(*v);
}
Ok(ImageData::I64(pixels))
}
-32 => {
let mut pixels: Vec<f32> = pod_collect_to_vec(raw);
for v in &mut pixels {
*v = f32::from_bits(u32::from_be(v.to_bits()));
}
Ok(ImageData::F32(pixels))
}
-64 => {
let mut pixels: Vec<f64> = pod_collect_to_vec(raw);
for v in &mut pixels {
*v = f64::from_bits(u64::from_be(v.to_bits()));
}
Ok(ImageData::F64(pixels))
}
other => Err(Error::InvalidBitpix(other)),
}
}
pub fn read_image_data_into_f32(fits_data: &[u8], hdu: &Hdu, buf: &mut [f32]) -> Result<()> {
let bitpix = hdu_bitpix(hdu)?;
let bpp = bytes_per_pixel(bitpix)?;
let data_len = hdu.data_len;
let npixels = data_len.checked_div(bpp).unwrap_or(0);
if buf.len() != npixels {
return Err(Error::InvalidValue);
}
if npixels == 0 {
return Ok(());
}
let end = hdu.data_start + data_len;
if end > fits_data.len() {
return Err(Error::UnexpectedEof);
}
let raw = &fits_data[hdu.data_start..end];
match bitpix {
8 => {
for (i, &b) in raw.iter().enumerate() {
buf[i] = b as f32;
}
}
16 => {
for i in 0..npixels {
buf[i] = crate::endian::read_i16_be(&raw[i * 2..]) as f32;
}
}
32 => {
for i in 0..npixels {
buf[i] = crate::endian::read_i32_be(&raw[i * 4..]) as f32;
}
}
64 => {
for i in 0..npixels {
buf[i] = crate::endian::read_i64_be(&raw[i * 8..]) as f32;
}
}
-32 => {
for i in 0..npixels {
buf[i] = crate::endian::read_f32_be(&raw[i * 4..]);
}
}
-64 => {
for i in 0..npixels {
buf[i] = crate::endian::read_f64_be(&raw[i * 8..]) as f32;
}
}
other => return Err(Error::InvalidBitpix(other)),
}
Ok(())
}
pub fn read_image_data_into_f64(fits_data: &[u8], hdu: &Hdu, buf: &mut [f64]) -> Result<()> {
let bitpix = hdu_bitpix(hdu)?;
let bpp = bytes_per_pixel(bitpix)?;
let data_len = hdu.data_len;
let npixels = data_len.checked_div(bpp).unwrap_or(0);
if buf.len() != npixels {
return Err(Error::InvalidValue);
}
if npixels == 0 {
return Ok(());
}
let end = hdu.data_start + data_len;
if end > fits_data.len() {
return Err(Error::UnexpectedEof);
}
let raw = &fits_data[hdu.data_start..end];
match bitpix {
8 => {
for (i, &b) in raw.iter().enumerate() {
buf[i] = b as f64;
}
}
16 => {
for i in 0..npixels {
buf[i] = crate::endian::read_i16_be(&raw[i * 2..]) as f64;
}
}
32 => {
for i in 0..npixels {
buf[i] = crate::endian::read_i32_be(&raw[i * 4..]) as f64;
}
}
64 => {
for i in 0..npixels {
buf[i] = crate::endian::read_i64_be(&raw[i * 8..]) as f64;
}
}
-32 => {
for i in 0..npixels {
buf[i] = crate::endian::read_f32_be(&raw[i * 4..]) as f64;
}
}
-64 => {
for i in 0..npixels {
buf[i] = crate::endian::read_f64_be(&raw[i * 8..]);
}
}
other => return Err(Error::InvalidBitpix(other)),
}
Ok(())
}
pub fn apply_bscale_bzero(data: &ImageData, bscale: f64, bzero: f64) -> Vec<f64> {
match data {
ImageData::U8(v) => v.iter().map(|&p| bzero + bscale * (p as f64)).collect(),
ImageData::I16(v) => v.iter().map(|&p| bzero + bscale * (p as f64)).collect(),
ImageData::I32(v) => v.iter().map(|&p| bzero + bscale * (p as f64)).collect(),
ImageData::I64(v) => v.iter().map(|&p| bzero + bscale * (p as f64)).collect(),
ImageData::F32(v) => v.iter().map(|&p| bzero + bscale * (p as f64)).collect(),
ImageData::F64(v) => v.iter().map(|&p| bzero + bscale * p).collect(),
}
}
pub fn extract_bscale_bzero(cards: &[Card]) -> (f64, f64) {
let bscale = find_float_keyword(cards, "BSCALE").unwrap_or(1.0);
let bzero = find_float_keyword(cards, "BZERO").unwrap_or(0.0);
(bscale, bzero)
}
pub fn extract_blank(cards: &[Card]) -> Option<i64> {
find_integer_keyword(cards, "BLANK")
}
fn find_integer_keyword(cards: &[Card], keyword: &str) -> Option<i64> {
cards.iter().find_map(|c| {
if c.keyword_str() == keyword {
match &c.value {
Some(Value::Integer(n)) => Some(*n),
_ => None,
}
} else {
None
}
})
}
pub fn blank_mask(data: &ImageData, blank: Option<i64>) -> Option<Vec<bool>> {
let mask: Vec<bool> = match data {
ImageData::U8(v) => match blank {
Some(b) => {
let bv = b as u8;
v.iter().map(|&p| p == bv).collect()
}
None => return None,
},
ImageData::I16(v) => match blank {
Some(b) => {
let bv = b as i16;
v.iter().map(|&p| p == bv).collect()
}
None => return None,
},
ImageData::I32(v) => match blank {
Some(b) => {
let bv = b as i32;
v.iter().map(|&p| p == bv).collect()
}
None => return None,
},
ImageData::I64(v) => match blank {
Some(b) => v.iter().map(|&p| p == b).collect(),
None => return None,
},
ImageData::F32(v) => v.iter().map(|p| p.is_nan()).collect(),
ImageData::F64(v) => v.iter().map(|p| p.is_nan()).collect(),
};
if mask.iter().any(|&b| b) {
Some(mask)
} else {
None
}
}
fn find_float_keyword(cards: &[Card], keyword: &str) -> Option<f64> {
cards.iter().find_map(|c| {
if c.keyword_str() == keyword {
match &c.value {
Some(Value::Float(f)) => Some(*f),
Some(Value::Integer(n)) => Some(*n as f64),
_ => None,
}
} else {
None
}
})
}
pub fn read_image_physical(fits_data: &[u8], hdu: &Hdu) -> Result<Vec<f64>> {
let raw = read_image_data(fits_data, hdu)?;
let (bscale, bzero) = extract_bscale_bzero(&hdu.cards);
let blank = extract_blank(&hdu.cards);
let mut physical = apply_bscale_bzero(&raw, bscale, bzero);
if let Some(mask) = blank_mask(&raw, blank) {
for (val, is_blank) in physical.iter_mut().zip(mask.iter()) {
if *is_blank {
*val = f64::NAN;
}
}
}
Ok(physical)
}
pub fn serialize_image_u8(pixels: &[u8]) -> Vec<u8> {
let raw_len = pixels.len();
let padded_len = padded_byte_len(raw_len);
let mut buf = vec![0u8; padded_len];
buf[..raw_len].copy_from_slice(pixels);
buf
}
pub fn serialize_image_i16(pixels: &[i16]) -> Vec<u8> {
let raw_len = pixels.len() * 2;
let padded_len = padded_byte_len(raw_len);
let mut buf: Vec<u8> = pod_collect_to_vec(pixels);
buf_i16_native_to_be(&mut buf);
buf.resize(padded_len, 0);
buf
}
pub fn serialize_image_i32(pixels: &[i32]) -> Vec<u8> {
let raw_len = pixels.len() * 4;
let padded_len = padded_byte_len(raw_len);
let mut buf: Vec<u8> = pod_collect_to_vec(pixels);
buf_i32_native_to_be(&mut buf);
buf.resize(padded_len, 0);
buf
}
pub fn serialize_image_i64(pixels: &[i64]) -> Vec<u8> {
let raw_len = pixels.len() * 8;
let padded_len = padded_byte_len(raw_len);
let mut buf: Vec<u8> = pod_collect_to_vec(pixels);
buf_i64_native_to_be(&mut buf);
buf.resize(padded_len, 0);
buf
}
pub fn serialize_image_f32(pixels: &[f32]) -> Vec<u8> {
let raw_len = pixels.len() * 4;
let padded_len = padded_byte_len(raw_len);
let mut buf: Vec<u8> = pod_collect_to_vec(pixels);
buf_f32_native_to_be(&mut buf);
buf.resize(padded_len, 0);
buf
}
pub fn serialize_image_f64(pixels: &[f64]) -> Vec<u8> {
let raw_len = pixels.len() * 8;
let padded_len = padded_byte_len(raw_len);
let mut buf: Vec<u8> = pod_collect_to_vec(pixels);
buf_f64_native_to_be(&mut buf);
buf.resize(padded_len, 0);
buf
}
pub fn serialize_image(data: &ImageData) -> Vec<u8> {
match data {
ImageData::U8(v) => serialize_image_u8(v),
ImageData::I16(v) => serialize_image_i16(v),
ImageData::I32(v) => serialize_image_i32(v),
ImageData::I64(v) => serialize_image_i64(v),
ImageData::F32(v) => serialize_image_f32(v),
ImageData::F64(v) => serialize_image_f64(v),
}
}
pub fn build_image_hdu(bitpix: i64, naxes: &[usize], data: &ImageData) -> Result<Vec<u8>> {
let cards = build_primary_header(bitpix, naxes)?;
let header_bytes = serialize_header(&cards)?;
let data_bytes = serialize_image(data);
let mut hdu = Vec::with_capacity(header_bytes.len() + data_bytes.len());
hdu.extend_from_slice(&header_bytes);
hdu.extend_from_slice(&data_bytes);
Ok(hdu)
}
pub fn reverse_bscale_bzero(
physical: &[f64],
bscale: f64,
bzero: f64,
bitpix: i64,
) -> Result<ImageData> {
let inv = |v: f64| (v - bzero) / bscale;
let round_clamp = |v: f64, lo: f64, hi: f64| -> f64 {
let r = libm::round(v);
if r < lo {
lo
} else if r > hi {
hi
} else {
r
}
};
match bitpix {
8 => {
let pixels: Vec<u8> = physical
.iter()
.map(|&v| round_clamp(inv(v), 0.0, 255.0) as u8)
.collect();
Ok(ImageData::U8(pixels))
}
16 => {
let pixels: Vec<i16> = physical
.iter()
.map(|&v| round_clamp(inv(v), i16::MIN as f64, i16::MAX as f64) as i16)
.collect();
Ok(ImageData::I16(pixels))
}
32 => {
let pixels: Vec<i32> = physical
.iter()
.map(|&v| round_clamp(inv(v), i32::MIN as f64, i32::MAX as f64) as i32)
.collect();
Ok(ImageData::I32(pixels))
}
64 => {
let pixels: Vec<i64> = physical
.iter()
.map(|&v| round_clamp(inv(v), i64::MIN as f64, i64::MAX as f64) as i64)
.collect();
Ok(ImageData::I64(pixels))
}
-32 => {
let pixels: Vec<f32> = physical.iter().map(|&v| inv(v) as f32).collect();
Ok(ImageData::F32(pixels))
}
-64 => {
let pixels: Vec<f64> = physical.iter().map(|&v| inv(v)).collect();
Ok(ImageData::F64(pixels))
}
other => Err(Error::InvalidBitpix(other)),
}
}
pub fn build_image_hdu_with_scaling(
bitpix: i64,
naxes: &[usize],
physical: &[f64],
bscale: f64,
bzero: f64,
) -> Result<Vec<u8>> {
let raw = reverse_bscale_bzero(physical, bscale, bzero, bitpix)?;
let mut cards = build_primary_header(bitpix, naxes)?;
let is_non_default = bscale != 1.0 || bzero != 0.0;
if is_non_default {
fn make_card(keyword: &str, value: Value) -> Card {
let mut kw = [b' '; 8];
let bytes = keyword.as_bytes();
let len = bytes.len().min(8);
kw[..len].copy_from_slice(&bytes[..len]);
Card {
keyword: kw,
value: Some(value),
comment: None,
}
}
cards.push(make_card("BSCALE", Value::Float(bscale)));
cards.push(make_card("BZERO", Value::Float(bzero)));
}
let header_bytes = serialize_header(&cards)?;
let data_bytes = serialize_image(&raw);
let mut hdu = Vec::with_capacity(header_bytes.len() + data_bytes.len());
hdu.extend_from_slice(&header_bytes);
hdu.extend_from_slice(&data_bytes);
Ok(hdu)
}
pub fn bytes_per_pixel(bitpix: i64) -> Result<usize> {
match bitpix {
8 | 16 | 32 | 64 | -32 | -64 => Ok((bitpix.unsigned_abs() / 8) as usize),
_ => Err(Error::InvalidBitpix(bitpix)),
}
}
fn hdu_bitpix_naxes(hdu: &Hdu) -> Result<(i64, &[usize])> {
match &hdu.info {
HduInfo::Primary { bitpix, naxes } | HduInfo::Image { bitpix, naxes } => {
Ok((*bitpix, naxes))
}
HduInfo::CompressedImage {
zbitpix, znaxes, ..
} => Ok((*zbitpix, znaxes)),
_ => Err(Error::InvalidHeader("not an image HDU")),
}
}
fn decode_pixels(raw: &[u8], bitpix: i64) -> Result<ImageData> {
bytes_per_pixel(bitpix)?; match bitpix {
8 => Ok(ImageData::U8(raw.to_vec())),
16 => {
let mut pixels: Vec<i16> = pod_collect_to_vec(raw);
for v in &mut pixels {
*v = i16::from_be(*v);
}
Ok(ImageData::I16(pixels))
}
32 => {
let mut pixels: Vec<i32> = pod_collect_to_vec(raw);
for v in &mut pixels {
*v = i32::from_be(*v);
}
Ok(ImageData::I32(pixels))
}
64 => {
let mut pixels: Vec<i64> = pod_collect_to_vec(raw);
for v in &mut pixels {
*v = i64::from_be(*v);
}
Ok(ImageData::I64(pixels))
}
-32 => {
let mut pixels: Vec<f32> = pod_collect_to_vec(raw);
for v in &mut pixels {
*v = f32::from_bits(u32::from_be(v.to_bits()));
}
Ok(ImageData::F32(pixels))
}
-64 => {
let mut pixels: Vec<f64> = pod_collect_to_vec(raw);
for v in &mut pixels {
*v = f64::from_bits(u64::from_be(v.to_bits()));
}
Ok(ImageData::F64(pixels))
}
_ => Err(Error::InvalidBitpix(bitpix)),
}
}
pub fn read_image_section(
fits_data: &[u8],
hdu: &Hdu,
start_pixel: usize,
count: usize,
) -> Result<ImageData> {
let (bitpix, naxes) = hdu_bitpix_naxes(hdu)?;
let bpp = bytes_per_pixel(bitpix)?;
let total_pixels: usize = if naxes.is_empty() {
0
} else {
naxes.iter().copied().product()
};
let end_pixel = start_pixel.checked_add(count).ok_or(Error::InvalidValue)?;
if end_pixel > total_pixels {
return Err(Error::UnexpectedEof);
}
let byte_offset = hdu.data_start + start_pixel * bpp;
let byte_end = byte_offset + count * bpp;
if byte_end > fits_data.len() {
return Err(Error::UnexpectedEof);
}
decode_pixels(&fits_data[byte_offset..byte_end], bitpix)
}
pub fn read_image_rows(
fits_data: &[u8],
hdu: &Hdu,
start_row: usize,
num_rows: usize,
) -> Result<ImageData> {
let (_, naxes) = hdu_bitpix_naxes(hdu)?;
if naxes.len() < 2 {
return Err(Error::InvalidHeader(
"image needs at least 2 axes for row slicing",
));
}
let row_len = naxes[0];
let total_rows: usize = naxes[1..].iter().copied().product();
let end_row = start_row.checked_add(num_rows).ok_or(Error::InvalidValue)?;
if end_row > total_rows {
return Err(Error::UnexpectedEof);
}
let start_pixel = start_row * row_len;
let pixel_count = num_rows * row_len;
read_image_section(fits_data, hdu, start_pixel, pixel_count)
}
pub fn read_image_region(
fits_data: &[u8],
hdu: &Hdu,
ranges: &[(usize, usize)],
) -> Result<ImageData> {
let (bitpix, naxes) = hdu_bitpix_naxes(hdu)?;
let bpp = bytes_per_pixel(bitpix)?;
let ndim = naxes.len();
if ranges.len() != ndim {
return Err(Error::InvalidValue);
}
let mut sub_dims = Vec::with_capacity(ndim);
for (i, &(start, end)) in ranges.iter().enumerate() {
if start > end || end > naxes[i] {
return Err(Error::InvalidValue);
}
sub_dims.push(end - start);
}
let total_out: usize = if sub_dims.is_empty() {
0
} else {
sub_dims.iter().copied().product()
};
if total_out == 0 {
return decode_pixels(&[], bitpix);
}
let mut strides = Vec::with_capacity(ndim);
let mut s: usize = 1;
for &dim in naxes.iter() {
strides.push(s);
s *= dim;
}
let mut raw = Vec::with_capacity(total_out * bpp);
let mut idx: Vec<usize> = vec![0; ndim];
for _ in 0..total_out {
let mut flat = 0;
for d in 0..ndim {
flat += (ranges[d].0 + idx[d]) * strides[d];
}
let byte_offset = hdu.data_start + flat * bpp;
let byte_end = byte_offset + bpp;
if byte_end > fits_data.len() {
return Err(Error::UnexpectedEof);
}
raw.extend_from_slice(&fits_data[byte_offset..byte_end]);
let mut carry = true;
for d in 0..ndim {
if carry {
idx[d] += 1;
if idx[d] < sub_dims[d] {
carry = false;
} else {
idx[d] = 0;
}
}
}
}
decode_pixels(&raw, bitpix)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::endian::{
read_f32_be, read_f64_be, read_i16_be, read_i32_be, write_f32_be, write_f64_be,
write_i16_be, write_i32_be, write_i64_be,
};
use crate::header::Card;
use crate::value::Value;
fn kw(name: &[u8]) -> [u8; 8] {
let mut buf = [b' '; 8];
let len = name.len().min(8);
buf[..len].copy_from_slice(&name[..len]);
buf
}
fn card(keyword: &str, value: Value) -> Card {
Card {
keyword: kw(keyword.as_bytes()),
value: Some(value),
comment: None,
}
}
fn primary_header_image(bitpix: i64, dims: &[usize]) -> Vec<Card> {
let mut cards = vec![
card("SIMPLE", Value::Logical(true)),
card("BITPIX", Value::Integer(bitpix)),
card("NAXIS", Value::Integer(dims.len() as i64)),
];
for (i, &d) in dims.iter().enumerate() {
let name = alloc::format!("NAXIS{}", i + 1);
cards.push(card(&name, Value::Integer(d as i64)));
}
cards
}
fn primary_header_with_bscale(
bitpix: i64,
dims: &[usize],
bscale: f64,
bzero: f64,
) -> Vec<Card> {
let mut cards = primary_header_image(bitpix, dims);
cards.push(card("BSCALE", Value::Float(bscale)));
cards.push(card("BZERO", Value::Float(bzero)));
cards
}
fn build_fits(header_cards: &[Card], data: &[u8]) -> Vec<u8> {
let header = serialize_header(header_cards).unwrap();
let padded_data_len = padded_byte_len(data.len());
let mut result = Vec::with_capacity(header.len() + padded_data_len);
result.extend_from_slice(&header);
result.resize(header.len() + padded_data_len, 0u8);
result[header.len()..header.len() + data.len()].copy_from_slice(data);
result
}
fn parse_primary(data: &[u8]) -> Hdu {
crate::hdu::parse_fits(data)
.unwrap()
.hdus
.into_iter()
.next()
.unwrap()
}
#[test]
fn read_u8_image() {
let pixels: Vec<u8> = vec![0, 1, 127, 255];
let cards = primary_header_image(8, &[4]);
let fits = build_fits(&cards, &pixels);
let hdu = parse_primary(&fits);
let data = read_image_data(&fits, &hdu).unwrap();
assert_eq!(data, ImageData::U8(vec![0, 1, 127, 255]));
}
#[test]
fn read_i16_image() {
let values: [i16; 4] = [0, 1, -1, i16::MAX];
let mut raw = vec![0u8; 8];
for (i, &v) in values.iter().enumerate() {
write_i16_be(&mut raw[i * 2..], v);
}
let cards = primary_header_image(16, &[4]);
let fits = build_fits(&cards, &raw);
let hdu = parse_primary(&fits);
let data = read_image_data(&fits, &hdu).unwrap();
assert_eq!(data, ImageData::I16(vec![0, 1, -1, i16::MAX]));
}
#[test]
fn read_i32_image() {
let values: [i32; 3] = [0, -42, i32::MAX];
let mut raw = vec![0u8; 12];
for (i, &v) in values.iter().enumerate() {
write_i32_be(&mut raw[i * 4..], v);
}
let cards = primary_header_image(32, &[3]);
let fits = build_fits(&cards, &raw);
let hdu = parse_primary(&fits);
let data = read_image_data(&fits, &hdu).unwrap();
assert_eq!(data, ImageData::I32(vec![0, -42, i32::MAX]));
}
#[test]
fn read_i64_image() {
let values: [i64; 2] = [i64::MIN, i64::MAX];
let mut raw = vec![0u8; 16];
for (i, &v) in values.iter().enumerate() {
write_i64_be(&mut raw[i * 8..], v);
}
let cards = primary_header_image(64, &[2]);
let fits = build_fits(&cards, &raw);
let hdu = parse_primary(&fits);
let data = read_image_data(&fits, &hdu).unwrap();
assert_eq!(data, ImageData::I64(vec![i64::MIN, i64::MAX]));
}
#[test]
fn read_f32_image() {
let values: [f32; 3] = [0.0, 1.5, -42.25];
let mut raw = vec![0u8; 12];
for (i, &v) in values.iter().enumerate() {
write_f32_be(&mut raw[i * 4..], v);
}
let cards = primary_header_image(-32, &[3]);
let fits = build_fits(&cards, &raw);
let hdu = parse_primary(&fits);
let data = read_image_data(&fits, &hdu).unwrap();
assert_eq!(data, ImageData::F32(vec![0.0, 1.5, -42.25]));
}
#[test]
fn read_f64_image() {
let values: [f64; 2] = [core::f64::consts::FRAC_1_SQRT_2, -1e100];
let mut raw = vec![0u8; 16];
for (i, &v) in values.iter().enumerate() {
write_f64_be(&mut raw[i * 8..], v);
}
let cards = primary_header_image(-64, &[2]);
let fits = build_fits(&cards, &raw);
let hdu = parse_primary(&fits);
let data = read_image_data(&fits, &hdu).unwrap();
assert_eq!(
data,
ImageData::F64(vec![core::f64::consts::FRAC_1_SQRT_2, -1e100])
);
}
#[test]
fn read_zero_size_image() {
let cards = primary_header_image(16, &[]);
let fits = build_fits(&cards, &[]);
let hdu = parse_primary(&fits);
let data = read_image_data(&fits, &hdu).unwrap();
assert_eq!(data, ImageData::I16(Vec::new()));
}
#[test]
fn read_single_pixel_u8() {
let cards = primary_header_image(8, &[1]);
let fits = build_fits(&cards, &[42]);
let hdu = parse_primary(&fits);
let data = read_image_data(&fits, &hdu).unwrap();
assert_eq!(data, ImageData::U8(vec![42]));
}
#[test]
fn read_single_pixel_f32() {
let mut raw = vec![0u8; 4];
write_f32_be(&mut raw, 2.5);
let cards = primary_header_image(-32, &[1]);
let fits = build_fits(&cards, &raw);
let hdu = parse_primary(&fits);
let data = read_image_data(&fits, &hdu).unwrap();
assert_eq!(data, ImageData::F32(vec![2.5]));
}
#[test]
fn read_2d_i16_image() {
let width = 3;
let height = 2;
let pixel_count = width * height;
let mut raw = vec![0u8; pixel_count * 2];
for i in 0..pixel_count {
write_i16_be(&mut raw[i * 2..], (i + 1) as i16);
}
let cards = primary_header_image(16, &[width, height]);
let fits = build_fits(&cards, &raw);
let hdu = parse_primary(&fits);
let data = read_image_data(&fits, &hdu).unwrap();
assert_eq!(data, ImageData::I16(vec![1, 2, 3, 4, 5, 6]));
}
#[test]
fn apply_bscale_bzero_identity() {
let data = ImageData::U8(vec![10, 20, 30]);
let result = apply_bscale_bzero(&data, 1.0, 0.0);
assert_eq!(result, vec![10.0, 20.0, 30.0]);
}
#[test]
fn apply_bscale_bzero_scale_and_offset() {
let data = ImageData::I16(vec![0, 1, 2, 3]);
let result = apply_bscale_bzero(&data, 2.0, 100.0);
assert_eq!(result, vec![100.0, 102.0, 104.0, 106.0]);
}
#[test]
fn apply_bscale_bzero_unsigned_16bit() {
let data = ImageData::I16(vec![0, -32768, 32767]);
let result = apply_bscale_bzero(&data, 1.0, 32768.0);
assert_eq!(result, vec![32768.0, 0.0, 65535.0]);
}
#[test]
fn apply_bscale_bzero_f64() {
let data = ImageData::F64(vec![1.0, 2.0]);
let result = apply_bscale_bzero(&data, 0.5, 10.0);
assert_eq!(result, vec![10.5, 11.0]);
}
#[test]
fn extract_bscale_bzero_defaults() {
let cards: Vec<Card> = vec![card("BITPIX", Value::Integer(16))];
let (bscale, bzero) = extract_bscale_bzero(&cards);
assert_eq!(bscale, 1.0);
assert_eq!(bzero, 0.0);
}
#[test]
fn extract_bscale_bzero_present() {
let cards = vec![
card("BSCALE", Value::Float(2.5)),
card("BZERO", Value::Float(100.0)),
];
let (bscale, bzero) = extract_bscale_bzero(&cards);
assert_eq!(bscale, 2.5);
assert_eq!(bzero, 100.0);
}
#[test]
fn extract_bscale_bzero_integer_values() {
let cards = vec![
card("BSCALE", Value::Integer(3)),
card("BZERO", Value::Integer(32768)),
];
let (bscale, bzero) = extract_bscale_bzero(&cards);
assert_eq!(bscale, 3.0);
assert_eq!(bzero, 32768.0);
}
#[test]
fn read_image_physical_with_calibration() {
let values: [i16; 3] = [0, 1, 2];
let mut raw = vec![0u8; 6];
for (i, &v) in values.iter().enumerate() {
write_i16_be(&mut raw[i * 2..], v);
}
let cards = primary_header_with_bscale(16, &[3], 2.0, 100.0);
let fits = build_fits(&cards, &raw);
let hdu = parse_primary(&fits);
let physical = read_image_physical(&fits, &hdu).unwrap();
assert_eq!(physical, vec![100.0, 102.0, 104.0]);
}
#[test]
fn read_image_physical_no_calibration() {
let pixels: Vec<u8> = vec![10, 20];
let cards = primary_header_image(8, &[2]);
let fits = build_fits(&cards, &pixels);
let hdu = parse_primary(&fits);
let physical = read_image_physical(&fits, &hdu).unwrap();
assert_eq!(physical, vec![10.0, 20.0]);
}
#[test]
fn image_dimensions_2d() {
let cards = primary_header_image(16, &[100, 200]);
let fits = build_fits(&cards, &vec![0u8; 100 * 200 * 2]);
let hdu = parse_primary(&fits);
let dims = image_dimensions(&hdu).unwrap();
assert_eq!(dims, vec![100, 200]);
}
#[test]
fn image_dimensions_0d() {
let cards = primary_header_image(8, &[]);
let fits = build_fits(&cards, &[]);
let hdu = parse_primary(&fits);
let dims = image_dimensions(&hdu).unwrap();
assert!(dims.is_empty());
}
#[test]
fn read_image_data_invalid_bitpix() {
let hdu = Hdu {
info: HduInfo::Primary {
bitpix: 7,
naxes: vec![10],
},
header_start: 0,
data_start: 2880,
data_len: 10,
cards: vec![],
};
let fits = vec![0u8; 5760];
let result = read_image_data(&fits, &hdu);
assert!(result.is_err());
}
#[test]
fn read_image_data_truncated() {
let hdu = Hdu {
info: HduInfo::Primary {
bitpix: 8,
naxes: vec![100],
},
header_start: 0,
data_start: 2880,
data_len: 100,
cards: vec![],
};
let fits = vec![0u8; 2900];
let result = read_image_data(&fits, &hdu);
assert!(result.is_err());
}
#[test]
fn image_dimensions_non_image_hdu() {
let hdu = Hdu {
info: HduInfo::AsciiTable {
naxis1: 100,
naxis2: 50,
tfields: 5,
},
header_start: 0,
data_start: 2880,
data_len: 5000,
cards: vec![],
};
assert!(image_dimensions(&hdu).is_err());
}
#[test]
fn read_3d_f32_cube() {
let dims = [2, 2, 2];
let pixel_count: usize = dims.iter().product();
let mut raw = vec![0u8; pixel_count * 4];
for i in 0..pixel_count {
write_f32_be(&mut raw[i * 4..], (i as f32) + 0.5);
}
let cards = primary_header_image(-32, &dims);
let fits = build_fits(&cards, &raw);
let hdu = parse_primary(&fits);
let data = read_image_data(&fits, &hdu).unwrap();
let expected: Vec<f32> = (0..pixel_count).map(|i| (i as f32) + 0.5).collect();
assert_eq!(data, ImageData::F32(expected));
}
#[test]
fn zero_length_all_bitpix_types() {
for &bitpix in &[8i64, 16, 32, 64, -32, -64] {
let cards = primary_header_image(bitpix, &[]);
let fits = build_fits(&cards, &[]);
let hdu = parse_primary(&fits);
let data = read_image_data(&fits, &hdu).unwrap();
match (bitpix, &data) {
(8, ImageData::U8(v)) => assert!(v.is_empty()),
(16, ImageData::I16(v)) => assert!(v.is_empty()),
(32, ImageData::I32(v)) => assert!(v.is_empty()),
(64, ImageData::I64(v)) => assert!(v.is_empty()),
(-32, ImageData::F32(v)) => assert!(v.is_empty()),
(-64, ImageData::F64(v)) => assert!(v.is_empty()),
_ => panic!("Unexpected variant for bitpix={}", bitpix),
}
}
}
#[test]
fn serialize_u8_roundtrip() {
let pixels: Vec<u8> = (0..=255).collect();
let bytes = serialize_image_u8(&pixels);
assert_eq!(&bytes[..256], &pixels[..]);
}
#[test]
fn serialize_u8_padding() {
let pixels = vec![42u8; 100];
let bytes = serialize_image_u8(&pixels);
assert_eq!(bytes.len(), crate::block::BLOCK_SIZE);
assert_eq!(&bytes[..100], &pixels[..]);
for &b in &bytes[100..] {
assert_eq!(b, 0);
}
}
#[test]
fn serialize_i16_roundtrip() {
let pixels: Vec<i16> = vec![0, 1, -1, i16::MIN, i16::MAX, 256, -256];
let bytes = serialize_image_i16(&pixels);
for (i, &expected) in pixels.iter().enumerate() {
let actual = read_i16_be(&bytes[i * 2..]);
assert_eq!(actual, expected);
}
}
#[test]
fn serialize_i32_roundtrip() {
let pixels: Vec<i32> = vec![0, 1, -1, i32::MIN, i32::MAX];
let bytes = serialize_image_i32(&pixels);
for (i, &expected) in pixels.iter().enumerate() {
let actual = read_i32_be(&bytes[i * 4..]);
assert_eq!(actual, expected);
}
}
#[test]
fn serialize_f32_roundtrip() {
let pixels: Vec<f32> = vec![0.0, 1.0, -1.0, f32::MAX, core::f32::consts::PI];
let bytes = serialize_image_f32(&pixels);
for (i, &expected) in pixels.iter().enumerate() {
let actual = read_f32_be(&bytes[i * 4..]);
assert_eq!(actual, expected);
}
}
#[test]
fn serialize_f64_roundtrip() {
let pixels: Vec<f64> = vec![0.0, 1.0, -1.0, f64::MAX, core::f64::consts::PI];
let bytes = serialize_image_f64(&pixels);
for (i, &expected) in pixels.iter().enumerate() {
let actual = read_f64_be(&bytes[i * 8..]);
assert_eq!(actual, expected);
}
}
#[test]
fn serialize_empty_images() {
assert!(serialize_image_u8(&[]).is_empty());
assert!(serialize_image_i16(&[]).is_empty());
assert!(serialize_image_i32(&[]).is_empty());
assert!(serialize_image_i64(&[]).is_empty());
assert!(serialize_image_f32(&[]).is_empty());
assert!(serialize_image_f64(&[]).is_empty());
}
#[test]
fn all_serializers_block_aligned() {
assert_eq!(
serialize_image_u8(&[1; 100]).len() % crate::block::BLOCK_SIZE,
0
);
assert_eq!(
serialize_image_i16(&[1; 100]).len() % crate::block::BLOCK_SIZE,
0
);
assert_eq!(
serialize_image_i32(&[1; 100]).len() % crate::block::BLOCK_SIZE,
0
);
assert_eq!(
serialize_image_i64(&[1; 100]).len() % crate::block::BLOCK_SIZE,
0
);
assert_eq!(
serialize_image_f32(&[1.0; 100]).len() % crate::block::BLOCK_SIZE,
0
);
assert_eq!(
serialize_image_f64(&[1.0; 100]).len() % crate::block::BLOCK_SIZE,
0
);
}
#[test]
fn build_image_hdu_block_aligned() {
let data = ImageData::U8(vec![1; 100]);
let hdu = build_image_hdu(8, &[100], &data).unwrap();
assert_eq!(hdu.len() % crate::block::BLOCK_SIZE, 0);
}
#[test]
fn build_image_hdu_invalid_bitpix() {
let data = ImageData::U8(vec![1]);
assert!(build_image_hdu(12, &[1], &data).is_err());
}
fn build_i16_image_fits(cols: usize, rows: usize) -> (Vec<u8>, Vec<i16>) {
let cards = crate::primary::build_primary_header(16, &[cols, rows]).unwrap();
let n = cols * rows;
let mut raw = vec![0u8; n * 2];
let mut expected = Vec::with_capacity(n);
for i in 0..n {
let val = i as i16;
write_i16_be(&mut raw[i * 2..], val);
expected.push(val);
}
(build_fits(&cards, &raw), expected)
}
fn build_i16_cube_fits(nx: usize, ny: usize, nz: usize) -> (Vec<u8>, Vec<i16>) {
let cards = crate::primary::build_primary_header(16, &[nx, ny, nz]).unwrap();
let n = nx * ny * nz;
let mut raw = vec![0u8; n * 2];
let mut expected = Vec::with_capacity(n);
for i in 0..n {
let val = i as i16;
write_i16_be(&mut raw[i * 2..], val);
expected.push(val);
}
(build_fits(&cards, &raw), expected)
}
#[test]
fn bpp_valid_values() {
assert_eq!(bytes_per_pixel(8).unwrap(), 1);
assert_eq!(bytes_per_pixel(16).unwrap(), 2);
assert_eq!(bytes_per_pixel(32).unwrap(), 4);
assert_eq!(bytes_per_pixel(64).unwrap(), 8);
assert_eq!(bytes_per_pixel(-32).unwrap(), 4);
assert_eq!(bytes_per_pixel(-64).unwrap(), 8);
}
#[test]
fn bpp_invalid() {
assert!(bytes_per_pixel(0).is_err());
assert!(bytes_per_pixel(7).is_err());
}
#[test]
fn section_full_image() {
let (fits, expected) = build_i16_image_fits(10, 10);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.primary();
let data = read_image_section(&fits, hdu, 0, 100).unwrap();
match data {
ImageData::I16(v) => assert_eq!(v, expected),
other => panic!("Expected I16, got {:?}", other),
}
}
#[test]
fn section_partial() {
let (fits, expected) = build_i16_image_fits(10, 10);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.primary();
let data = read_image_section(&fits, hdu, 5, 10).unwrap();
match data {
ImageData::I16(v) => assert_eq!(v, expected[5..15]),
other => panic!("Expected I16, got {:?}", other),
}
}
#[test]
fn section_out_of_bounds() {
let (fits, _) = build_i16_image_fits(10, 10);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.primary();
assert!(read_image_section(&fits, hdu, 95, 10).is_err());
}
#[test]
fn rows_single() {
let (fits, expected) = build_i16_image_fits(10, 5);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.primary();
let data = read_image_rows(&fits, hdu, 2, 1).unwrap();
match data {
ImageData::I16(v) => assert_eq!(v, expected[20..30]),
other => panic!("Expected I16, got {:?}", other),
}
}
#[test]
fn rows_1d_errors() {
let cards = crate::primary::build_primary_header(16, &[100]).unwrap();
let raw = vec![0u8; 200];
let fits = build_fits(&cards, &raw);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.primary();
assert!(read_image_rows(&fits, hdu, 0, 1).is_err());
}
#[test]
fn region_2d_subregion() {
let (fits, _) = build_i16_image_fits(6, 5);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.primary();
let data = read_image_region(&fits, hdu, &[(1, 4), (2, 4)]).unwrap();
match data {
ImageData::I16(v) => {
assert_eq!(v.len(), 6);
assert_eq!(v, vec![13, 14, 15, 19, 20, 21]);
}
other => panic!("Expected I16, got {:?}", other),
}
}
#[test]
fn region_3d_subregion() {
let (fits, _) = build_i16_cube_fits(4, 3, 2);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.primary();
let data = read_image_region(&fits, hdu, &[(1, 3), (0, 2), (0, 2)]).unwrap();
match data {
ImageData::I16(v) => {
assert_eq!(v.len(), 8);
assert_eq!(v, vec![1, 2, 5, 6, 13, 14, 17, 18]);
}
other => panic!("Expected I16, got {:?}", other),
}
}
#[test]
fn region_wrong_dim_count() {
let (fits, _) = build_i16_image_fits(10, 10);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.primary();
assert!(read_image_region(&fits, hdu, &[(0, 10), (0, 10), (0, 1)]).is_err());
}
#[test]
fn region_empty_range() {
let (fits, _) = build_i16_image_fits(10, 10);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.primary();
let data = read_image_region(&fits, hdu, &[(5, 5), (0, 10)]).unwrap();
match data {
ImageData::I16(v) => assert!(v.is_empty()),
other => panic!("Expected I16, got {:?}", other),
}
}
#[test]
fn extract_blank_present() {
let cards = vec![
card("SIMPLE", Value::Logical(true)),
card("BITPIX", Value::Integer(16)),
card("NAXIS", Value::Integer(1)),
card("NAXIS1", Value::Integer(4)),
card("BLANK", Value::Integer(-32768)),
];
assert_eq!(extract_blank(&cards), Some(-32768));
}
#[test]
fn extract_blank_absent() {
let cards = vec![
card("SIMPLE", Value::Logical(true)),
card("BITPIX", Value::Integer(16)),
card("NAXIS", Value::Integer(0)),
];
assert_eq!(extract_blank(&cards), None);
}
#[test]
fn blank_mask_i16() {
let data = ImageData::I16(vec![1, -32768, 3, -32768]);
let mask = blank_mask(&data, Some(-32768));
assert_eq!(mask, Some(vec![false, true, false, true]));
}
#[test]
fn blank_mask_no_blank_keyword() {
let data = ImageData::I16(vec![1, 2, 3]);
assert!(blank_mask(&data, None).is_none());
}
#[test]
fn blank_mask_no_matches() {
let data = ImageData::I16(vec![1, 2, 3]);
assert!(blank_mask(&data, Some(-32768)).is_none());
}
#[test]
fn blank_mask_f32_nan() {
let data = ImageData::F32(vec![1.0, f32::NAN, 3.0]);
let mask = blank_mask(&data, None);
assert_eq!(mask, Some(vec![false, true, false]));
}
#[test]
fn read_physical_with_blank() {
let blank_val: i16 = -32768;
let values: [i16; 4] = [100, blank_val, 200, blank_val];
let mut raw = vec![0u8; 8];
for (i, &v) in values.iter().enumerate() {
write_i16_be(&mut raw[i * 2..], v);
}
let mut cards = primary_header_image(16, &[4]);
cards.push(card("BLANK", Value::Integer(blank_val as i64)));
let fits = build_fits(&cards, &raw);
let hdu = parse_primary(&fits);
let physical = read_image_physical(&fits, &hdu).unwrap();
assert_eq!(physical[0], 100.0);
assert!(physical[1].is_nan());
assert_eq!(physical[2], 200.0);
assert!(physical[3].is_nan());
}
#[test]
fn reverse_bscale_bzero_i16() {
let physical = vec![32768.0, 0.0, 65535.0];
let raw = reverse_bscale_bzero(&physical, 1.0, 32768.0, 16).unwrap();
assert_eq!(raw, ImageData::I16(vec![0, -32768, 32767]));
}
#[test]
fn reverse_bscale_bzero_u8() {
let physical = vec![0.0, 127.5, 255.0];
let raw = reverse_bscale_bzero(&physical, 1.0, 0.0, 8).unwrap();
assert_eq!(raw, ImageData::U8(vec![0, 128, 255]));
}
#[test]
fn reverse_bscale_bzero_with_scale() {
let physical = vec![100.0, 102.0, 104.0];
let raw = reverse_bscale_bzero(&physical, 2.0, 100.0, 16).unwrap();
assert_eq!(raw, ImageData::I16(vec![0, 1, 2]));
}
#[test]
fn reverse_bscale_bzero_clamps() {
let physical = vec![-1.0, 256.0];
let raw = reverse_bscale_bzero(&physical, 1.0, 0.0, 8).unwrap();
assert_eq!(raw, ImageData::U8(vec![0, 255]));
}
#[test]
fn reverse_bscale_bzero_invalid_bitpix() {
assert!(reverse_bscale_bzero(&[1.0], 1.0, 0.0, 7).is_err());
}
#[test]
fn build_hdu_with_scaling_roundtrip() {
let physical = vec![32768.0, 0.0, 65535.0];
let hdu_bytes = build_image_hdu_with_scaling(16, &[3], &physical, 1.0, 32768.0).unwrap();
let fits = hdu_bytes;
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.primary();
let (bscale, bzero) = extract_bscale_bzero(&hdu.cards);
assert_eq!(bscale, 1.0);
assert_eq!(bzero, 32768.0);
let result = read_image_physical(&fits, hdu).unwrap();
assert_eq!(result, vec![32768.0, 0.0, 65535.0]);
}
#[test]
fn build_hdu_with_scaling_no_keywords_when_default() {
let physical = vec![1.0, 2.0, 3.0];
let hdu_bytes = build_image_hdu_with_scaling(-64, &[3], &physical, 1.0, 0.0).unwrap();
let parsed = crate::hdu::parse_fits(&hdu_bytes).unwrap();
let hdu = parsed.primary();
let has_bscale = hdu.cards.iter().any(|c| c.keyword_str() == "BSCALE");
assert!(!has_bscale);
}
#[test]
fn read_into_f32_from_f32_image() {
let pixels: Vec<f32> = vec![1.0, 2.5, 3.125];
let fits = build_image_hdu(-32, &[3], &ImageData::F32(pixels.clone())).unwrap();
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.primary();
let mut buf = vec![0.0f32; 3];
read_image_data_into_f32(&fits, hdu, &mut buf).unwrap();
assert_eq!(buf, pixels);
}
#[test]
fn read_into_f64_from_i16_image() {
let pixels: Vec<i16> = vec![100, -200, 300];
let fits = build_image_hdu(16, &[3], &ImageData::I16(pixels)).unwrap();
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.primary();
let mut buf = vec![0.0f64; 3];
read_image_data_into_f64(&fits, hdu, &mut buf).unwrap();
assert_eq!(buf, vec![100.0, -200.0, 300.0]);
}
#[test]
fn read_into_wrong_size_errors() {
let pixels: Vec<f32> = vec![1.0, 2.0, 3.0];
let fits = build_image_hdu(-32, &[3], &ImageData::F32(pixels)).unwrap();
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.primary();
let mut buf = vec![0.0f32; 2]; assert!(read_image_data_into_f32(&fits, hdu, &mut buf).is_err());
}
}