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 quickstartThis 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_filereads the file, locates the header by scanning forEND, 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::MissingEndif the file has noENDcard. -
Creating a new file — the rarer case where you already have pixel data and are writing it for the first time —
Header::write_to_filewrites the header block followed by your pixel bytes. It createspathand errors if it already exists, so it can never clobber an existing file’s data — useupdate_filefor 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
- README for the feature summary and install instructions.
- docs.rs/fits-header for the full
API reference, including number-formatting wrappers
(
Literal,Fixed,Sci), the number parsers (parse_f64,parse_i64), and the date/time helpers (parse_datetime,format_datetime). - Extending the typed read/write layer: implement
FromCardfor a new read type behindHeader::get, orIntoValuefor a new write type behindHeader::set.