Skip to main content

atfits_rs/
mem_header.rs

1//! Build a cube's FITS header without touching disk, so the data unit is never
2//! zero-filled by cfitsio.
3//!
4//! cfitsio writes a full pass of zeros over the data unit when an image HDU is
5//! flushed/closed (≈0.5 s for a 500 MB cube; it is NOT sparse on APFS). A
6//! streaming-write tool can sidestep this the way the Python reference tools do:
7//! write only the header, sparsely extend the file to its final length (so the
8//! OS backs the data unit with zero pages on demand), then write the real planes
9//! exactly once with plain `std::fs` I/O.
10//!
11//! To get the header bytes without any disk zero-fill, [`create_mem_cube`]
12//! assembles the HDU in an **in-memory** cfitsio file (`mem://`) and
13//! [`extract_header_layout`] serialises it with `ffhdr2str`. The caller owns the
14//! data-unit write (raw `write_all_at` of big-endian planes).
15use std::ffi::{CStr, CString};
16use std::os::raw::{c_char, c_int, c_void};
17use std::path::Path;
18use std::ptr;
19
20use fitsio::FitsFile;
21use fitsio::errors::check_status;
22use fitsio::sys::{LONGLONG, fitsfile};
23
24use crate::error::{AtfitsError, Result};
25use crate::image::is_structural_keyword;
26
27/// The byte layout of a freshly built cube: the complete primary header padded
28/// to a 2880-byte block, and the byte offset where the data unit begins.
29pub struct CubeLayout {
30    /// Full primary header, padded with spaces to `datastart` bytes.
31    pub header: Vec<u8>,
32    /// Byte offset of the data unit (== `header.len()`).
33    pub datastart: u64,
34}
35
36/// Create an in-memory image HDU of shape `dims` (FITS order, NAXIS1 first) and
37/// `bitpix`, copy every non-structural card from `template`'s primary header,
38/// and return the open (memory-backed) handle.
39///
40/// Nothing is written to disk; closing the returned handle just frees RAM. Pair
41/// with [`extract_header_layout`] to obtain the on-disk header bytes.
42pub fn create_mem_cube(template: &Path, bitpix: i64, dims: &[usize]) -> Result<FitsFile> {
43    let mut status = 0;
44    let memname = CString::new("mem://").expect("static name has no NUL");
45    let mut raw: *mut fitsfile = ptr::null_mut();
46    unsafe {
47        fitsio::sys::ffinit(&mut raw, memname.as_ptr(), &mut status);
48    }
49    check_status(status)?;
50
51    // `ffcrimll` takes naxes in FITS order (naxes[0] == NAXIS1).
52    let mut naxes: Vec<LONGLONG> = dims.iter().map(|&d| d as LONGLONG).collect();
53    unsafe {
54        fitsio::sys::ffcrimll(
55            raw,
56            bitpix as c_int,
57            naxes.len() as c_int,
58            naxes.as_mut_ptr(),
59            &mut status,
60        );
61    }
62    if let Err(e) = check_status(status) {
63        let mut close = 0;
64        unsafe { fitsio::sys::ffclos(raw, &mut close) };
65        return Err(e.into());
66    }
67
68    // Copy every non-structural card from the template's primary header.
69    let mut in_fptr = FitsFile::open(template.to_string_lossy().as_ref())?;
70    in_fptr.primary_hdu()?;
71    unsafe {
72        let mut nkeys: c_int = 0;
73        let mut morekeys: c_int = 0;
74        fitsio::sys::ffghsp(in_fptr.as_raw(), &mut nkeys, &mut morekeys, &mut status);
75        check_status(status)?;
76
77        // `c_char` signedness differs by platform; match cfitsio's `*mut c_char`.
78        let mut card = [0 as c_char; 81];
79        for i in 1..=nkeys {
80            card.fill(0);
81            fitsio::sys::ffgrec(in_fptr.as_raw(), i, card.as_mut_ptr(), &mut status);
82            if check_status(status).is_err() {
83                break;
84            }
85            let card_str = CStr::from_ptr(card.as_ptr()).to_string_lossy();
86            let name = card_str.split([' ', '=']).next().unwrap_or("").trim();
87            if is_structural_keyword(name) {
88                continue;
89            }
90            fitsio::sys::ffprec(raw, card.as_ptr(), &mut status);
91            check_status(status)?;
92        }
93    }
94
95    let handle = unsafe { FitsFile::from_raw(raw, fitsio::FileOpenMode::READWRITE)? };
96    Ok(handle)
97}
98
99/// Serialise the primary header of `fptr` to bytes and report where the data
100/// unit starts, so the caller can lay the header down with raw I/O.
101pub fn extract_header_layout(fptr: &mut FitsFile) -> Result<CubeLayout> {
102    fptr.primary_hdu()?; // position at the primary HDU
103    let raw = unsafe { fptr.as_raw() };
104    let mut status = 0;
105
106    let mut headstart: LONGLONG = 0;
107    let mut datastart: LONGLONG = 0;
108    let mut dataend: LONGLONG = 0;
109    unsafe {
110        fitsio::sys::ffghadll(
111            raw,
112            &mut headstart,
113            &mut datastart,
114            &mut dataend,
115            &mut status,
116        );
117    }
118    check_status(status)?;
119
120    // `ffhdr2str` concatenates every card (80 chars each) into one malloc'd
121    // string, EXCLUDING the END card and the trailing block padding.
122    let mut header_ptr: *mut c_char = ptr::null_mut();
123    let mut nkeys: c_int = 0;
124    unsafe {
125        fitsio::sys::ffhdr2str(
126            raw,
127            0, // keep COMMENT/HISTORY cards
128            ptr::null_mut(),
129            0,
130            &mut header_ptr,
131            &mut nkeys,
132            &mut status,
133        );
134    }
135    check_status(status)?;
136
137    let mut header = unsafe {
138        let bytes = CStr::from_ptr(header_ptr).to_bytes().to_vec();
139        let mut free_status = 0;
140        fitsio::sys::fffree(header_ptr as *mut c_void, &mut free_status);
141        bytes
142    };
143
144    let datastart = datastart as u64;
145    if header.len() as u64 + 3 > datastart {
146        return Err(AtfitsError::Other(format!(
147            "header ({} bytes) does not fit before data unit ({datastart} bytes)",
148            header.len()
149        )));
150    }
151    // Re-append the END card and pad to the data-unit boundary with spaces — the
152    // exact on-disk header cfitsio's own layout (datastart) accounts for.
153    header.extend_from_slice(b"END");
154    header.resize(datastart as usize, b' ');
155
156    Ok(CubeLayout { header, datastart })
157}