use std::ffi::{CStr, CString};
use std::io::Write;
use std::os::raw::{c_char, c_int, c_long, c_void};
use std::path::Path;
use fitsio::FitsFile;
use fitsio::images::{ImageDescription, ImageType};
use fitsio::sys::{self, LONGLONG};
use crate::error::{AtfitsError, Result, ffcall};
use crate::keys::cstr;
use crate::path::utf8;
const RESERVED_CARDS: usize = 36;
const SIMPLE_CARD: &[u8; 80] =
b"SIMPLE = T / file does conform to FITS standard ";
const END_CARD: &[u8; 80] =
b"END ";
pub fn resize_image(fptr: &mut FitsFile, bitpix: i64, dims: &[usize]) -> Result<()> {
let mut naxes: Vec<c_long> = dims.iter().map(|&d| d as c_long).collect();
ffcall(|s| unsafe {
sys::ffrsim(
fptr.as_raw(),
bitpix as c_int,
naxes.len() as c_int,
naxes.as_mut_ptr(),
s,
)
})
}
pub fn is_structural_keyword(name: &str) -> bool {
matches!(
name,
"SIMPLE" | "BITPIX" | "NAXIS" | "EXTEND" | "PCOUNT" | "GCOUNT" | "END"
) || name
.strip_prefix("NAXIS")
.is_some_and(|n| !n.is_empty() && n.bytes().all(|c| c.is_ascii_digit()))
}
pub fn is_scaling_keyword(name: &str) -> bool {
matches!(name, "BSCALE" | "BZERO" | "BLANK")
}
pub fn bitpix_to_image_type(bitpix: i64) -> ImageType {
match bitpix {
8 => ImageType::UnsignedByte,
16 => ImageType::Short,
32 => ImageType::Long,
64 => ImageType::LongLong,
-64 => ImageType::Double,
_ => ImageType::Float,
}
}
pub fn copy_header_only(input: &Path, output: &Path) -> Result<()> {
let mut src = FitsFile::open(utf8(input)?)?;
src.primary_hdu()?;
write_header_file(&mut src, output)
}
pub fn copy_header_only_open(input: &Path, output: &Path) -> Result<FitsFile> {
copy_header_only(input, output)?;
Ok(FitsFile::edit(utf8(output)?)?)
}
pub fn copy_hdu_header(src: &mut FitsFile, output: &Path) -> Result<FitsFile> {
write_header_file(src, output)?;
Ok(FitsFile::edit(utf8(output)?)?)
}
fn write_header_file(src: &mut FitsFile, output: &Path) -> Result<()> {
refuse_input_clobber(src.file_path(), output)?;
let mut hdutype: c_int = 0;
ffcall(|s| unsafe { sys::ffghdt(src.as_raw(), &mut hdutype, s) })?;
if hdutype != sys::IMAGE_HDU as c_int {
return Err(AtfitsError::Other("current HDU is not an image".into()));
}
let data_len = data_unit_len(src)?;
let mut cards = header_cards(src, &["CHECKSUM", "DATASUM"])?;
if cards.starts_with(b"XTENSION") {
cards.splice(..80, *SIMPLE_CARD);
let mut kept = Vec::with_capacity(cards.len());
for card in cards.chunks(80) {
if !matches!(card_name(card), "PCOUNT" | "GCOUNT") {
kept.extend_from_slice(card);
}
}
cards = kept;
}
let header = finish_header(cards, RESERVED_CARDS);
remove_existing(output)?;
let mut file = std::fs::File::create_new(output)?;
file.write_all(&header)?;
file.set_len(header.len() as u64 + data_len)?;
Ok(())
}
pub fn create_cube_open(
input: &Path,
output: &Path,
bitpix: i64,
dims: &[usize],
) -> Result<FitsFile> {
refuse_input_clobber(input, output)?;
remove_existing(output)?;
let c_order: Vec<usize> = dims.iter().rev().copied().collect();
let desc = ImageDescription {
data_type: bitpix_to_image_type(bitpix),
dimensions: &c_order,
};
let mut out = FitsFile::create(utf8(output)?)
.with_custom_primary(&desc)
.open()?;
out.primary_hdu()?;
let mut src = FitsFile::open(utf8(input)?)?;
src.primary_hdu()?;
copy_template_cards(&mut src, &mut out, bitpix)?;
ffcall(|s| unsafe { sys::ffrdef(out.as_raw(), s) })?;
Ok(out)
}
fn keep_template_card(name: &str, bitpix: i64) -> bool {
!(is_structural_keyword(name)
|| matches!(name, "CHECKSUM" | "DATASUM")
|| (bitpix < 0 && is_scaling_keyword(name)))
}
pub(crate) fn copy_template_cards(
src: &mut FitsFile,
dst: &mut FitsFile,
bitpix: i64,
) -> Result<()> {
let mut nkeys: c_int = 0;
ffcall(|s| unsafe { sys::ffghsp(src.as_raw(), &mut nkeys, std::ptr::null_mut(), s) })?;
let mut card = [0 as c_char; 81];
for i in 1..=nkeys {
ffcall(|s| unsafe { sys::ffgrec(src.as_raw(), i, card.as_mut_ptr(), s) })?;
let name = card_name(unsafe { CStr::from_ptr(card.as_ptr()) }.to_bytes());
if keep_template_card(name, bitpix) {
ffcall(|s| unsafe { sys::ffprec(dst.as_raw(), card.as_ptr(), s) })?;
}
}
Ok(())
}
fn card_name(card: &[u8]) -> &str {
let end = card
.iter()
.position(|&b| b == b' ' || b == b'=')
.unwrap_or(card.len());
std::str::from_utf8(&card[..end]).unwrap_or("")
}
fn data_unit_len(fptr: &mut FitsFile) -> Result<u64> {
let (mut head, mut data, mut end): (LONGLONG, LONGLONG, LONGLONG) = (0, 0, 0);
ffcall(|s| unsafe { sys::ffghadll(fptr.as_raw(), &mut head, &mut data, &mut end, s) })?;
Ok((end - data) as u64)
}
pub(crate) fn serialise_header(
fptr: &mut FitsFile,
exclude: &[&str],
reserve: usize,
) -> Result<Vec<u8>> {
Ok(finish_header(header_cards(fptr, exclude)?, reserve))
}
fn header_cards(fptr: &mut FitsFile, exclude: &[&str]) -> Result<Vec<u8>> {
let excl: Vec<CString> = exclude.iter().map(|k| cstr(k)).collect::<Result<_>>()?;
let mut excl_ptrs: Vec<*mut c_char> = excl.iter().map(|c| c.as_ptr() as *mut c_char).collect();
let mut text: *mut c_char = std::ptr::null_mut();
let mut nkeys: c_int = 0;
ffcall(|s| unsafe {
sys::ffhdr2str(
fptr.as_raw(),
0,
excl_ptrs.as_mut_ptr(),
excl_ptrs.len() as c_int,
&mut text,
&mut nkeys,
s,
)
})?;
let mut header = unsafe { CStr::from_ptr(text) }.to_bytes().to_vec();
ffcall(|s| unsafe { sys::fffree(text as *mut c_void, s) })?;
if !header.ends_with(END_CARD) {
return Err(AtfitsError::Other(
"serialised header has no END card".into(),
));
}
header.truncate(header.len() - END_CARD.len());
Ok(header)
}
fn finish_header(mut cards: Vec<u8>, reserve: usize) -> Vec<u8> {
cards.resize(cards.len() + reserve * 80, b' ');
cards.extend_from_slice(END_CARD);
cards.resize(cards.len().div_ceil(2880) * 2880, b' ');
cards
}
fn refuse_input_clobber(input: &Path, output: &Path) -> Result<()> {
if same_file(input, output) {
return Err(AtfitsError::Other(format!(
"output {} is the input image",
output.display()
)));
}
Ok(())
}
#[cfg(unix)]
fn same_file(a: &Path, b: &Path) -> bool {
use std::os::unix::fs::MetadataExt;
match (std::fs::metadata(a), std::fs::metadata(b)) {
(Ok(x), Ok(y)) => x.dev() == y.dev() && x.ino() == y.ino(),
_ => false,
}
}
#[cfg(not(unix))]
fn same_file(a: &Path, b: &Path) -> bool {
match (a.canonicalize(), b.canonicalize()) {
(Ok(x), Ok(y)) => x == y,
_ => false,
}
}
fn remove_existing(path: &Path) -> Result<()> {
match std::fs::remove_file(path) {
Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(e.into()),
_ => Ok(()),
}
}