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();

§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:

  • Creating a new file — write to_header_bytes(), then append your own pixel data after it. The caller owns the data; this crate never invents it.

  • Editing an existing fileHeader::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.

§Next