Skip to main content

Module guide

Module guide 

Source
Expand description

§Quickstart

A task-oriented walkthrough of fits-header. The snippets below are adapted from examples/quickstart.rs, which packages the same steps into one runnable file — run it yourself with:

cargo run --example quickstart

This page also renders at fits_header::guide; every code block below compiles and runs as a doctest, so the guide cannot drift from the API. Each block rebuilds the fixture from scratch (hidden lines in the rendered doctest) so it stands alone. Full API reference: docs.rs/fits-header.

§The fixture

One header, reused for every step: a CCD image of M31. One string per 80-byte card, space-padded, in appearance order.

const SAMPLE_CARDS: &[&str] = &[
    "SIMPLE  =                    T / conforms to FITS standard",
    "BITPIX  =                  -32 / IEEE single-precision float",
    "NAXIS   =                    2 / number of data axes",
    "NAXIS1  =                 1024 / axis 1 length",
    "NAXIS2  =                 1024 / axis 2 length",
    "OBJECT  = 'M31     '           / target name",
    "EXPTIME =                120.0 / exposure time in seconds",
    "DATE-OBS= '2026-07-11T22:15:03' / UTC start of exposure",
    "GAIN    =                  1.0 / e-/ADU",
    "FILTER  = 'Ha      '           / filter name",
    "TELESCOP= 'EdgeHD 8'           / telescope",
    "HISTORY dark subtracted",
];

Pack it into a valid header unit — CARD_LEN-byte cards, an END card, padded to a BLOCK_LEN multiple — and Header::parse it into a Header:

use fits_header::Header;

let mut bytes = Vec::new();
for card in SAMPLE_CARDS.iter().chain(["END"].iter()) {
    let mut c = card.as_bytes().to_vec();
    c.resize(fits_header::CARD_LEN, b' ');
    bytes.extend(c);
}
while bytes.len() % fits_header::BLOCK_LEN != 0 {
    bytes.push(b' ');
}

let mut header: Header = Header::parse(&bytes).unwrap();

The same bytes read from disk instead of memory: Header::read_from_file reads the file and parses it the same way; parsing already stops at END, so the data unit is read but never interpreted.

Every card is retained, including ones this guide never touches — they re-serialize byte-for-byte at the end.

§Read

Header::get is one generic accessor for every value type; string keywords also have a borrowing shortcut, Header::get_str:

let object: Option<&str> = header.get_str("OBJECT").unwrap();
let exptime: Option<f64> = header.get("EXPTIME").unwrap();
assert_eq!(object, Some("M31"));
assert_eq!(exptime, Some(120.0));

COMMENT, HISTORY, and blank-keyword cards are free-text RecordKind::Commentary records rather than addressable RecordKind::Value cards, so they repeat. Count occurrences with Header::count and read them all with Header::get_all:

assert_eq!(header.count("HISTORY"), 1);
assert_eq!(
    header.get_all::<String>("HISTORY"),
    vec!["dark subtracted".to_string()]
);

Value cards are read by bare name, and that access is strict: nothing stops a keyword like GAIN from appearing more than once, so if it does, header.get::<f64>("GAIN") returns FitsError::AmbiguousKeyword instead of guessing. Select one occurrence with a Key pair, e.g. header.get::<f64>(("GAIN", 1)) for the second occurrence.

HIERARCH cards and other non-standard or malformed cards parse as opaque RecordKind::Opaque records. They pass through unmodified on re-serialization, but they carry no addressable keyword — get, set, and remove never see them, and Header::count reports them as absent.

§Mutate

Header::set updates the addressed card in place, or appends one when the (unique) keyword is absent. Header::append always adds a card, which is how repeatable keywords like HISTORY grow. Header::set_comment and Header::remove round out single-card CRUD:

header.set("OBJECT", "NGC 7000").unwrap(); // updates in place
header.append("HISTORY", "flat fielded").unwrap(); // HISTORY repeats, so this adds a second card
header.set(("HISTORY", 0), "dark subtracted (master dark v2)").unwrap(); // update one occurrence in place
header.set_comment("EXPTIME", "seconds, revised").unwrap();
header.remove("GAIN").unwrap();

These calls change only the in-memory Header; nothing is written to disk until you persist it — with update_file to edit an existing file in place (the common case), or write_to_file/to_header_bytes to create a new one (see Serialize below).

§Atomic batches

Header::set_many and Header::remove_many validate every entry before applying any of them — a rejected batch leaves the header untouched:

header
    .set_many([("FILTER", "OIII"), ("TELESCOP", "EdgeHD 11")])
    .unwrap();

§Serialize

Header::to_header_bytes writes the header block alone — cards plus END, padded to a BLOCK_LEN multiple:

let block: Vec<u8> = header.to_header_bytes();
assert_eq!(block.len() % fits_header::BLOCK_LEN, 0);

BITPIX, NAXIS*, and DATE-OBS were never touched above, so they come back byte-for-byte identical to the input.

This crate is header-only: it never owns, inspects, or fabricates pixel data. That shapes the two ways real files get written:

  • Editing an existing file — the common case — Header::update_file reads the file, locates the header by scanning for END, hands you the parsed header to mutate, then writes the new header back followed by everything that came after the original one (the data unit, and any later HDUs), untouched:

    use fits_header::Header;
    
    Header::update_file(&path, |h| {
        h.set("OBJECT", "NGC 7000")?;
        Ok(())
    })
    .unwrap();

    The write is atomic (temp file in the same directory, then rename), so a crash cannot leave a truncated file. It errors with FitsError::MissingEnd if the file has no END card.

  • Creating a new file — the rarer case where you already have pixel data and are writing it for the first time — Header::write_to_file writes the header block followed by your pixel bytes. It creates path and errors if it already exists, so it can never clobber an existing file’s data — use update_file for that:

    use fits_header::Header;
    
    let mut header = Header::new();
    header.set("OBJECT", "M31").unwrap();
    let pixel_data = [0u8; 4]; // caller-owned data, e.g. from an image buffer
    
    let path = std::env::temp_dir().join("fits-header-guide-doctest-write_to_file.fits");
    header.write_to_file(&path, &pixel_data).unwrap();
    
    let bytes = std::fs::read(&path).unwrap();
    assert_eq!(&bytes[bytes.len() - pixel_data.len()..], &pixel_data);
    
    // Writing to the same path again errors instead of overwriting it.
    assert!(header.write_to_file(&path, &pixel_data).is_err());

§Next