xisf-header 0.4.3

Read and write XISF image-file headers: extract FITS keywords and CRUD the XISF header container. Header-only (never touches pixel data).
Documentation
# Quickstart guide

A task-oriented walkthrough for building, editing, and round-tripping an XISF
header. The snippets below mirror
[`examples/quickstart.rs`](https://github.com/nightwatch-astro/xisf-header/blob/main/examples/quickstart.rs) (`cargo run --example
quickstart`), which runs the same steps end to end against a temporary file.

The walkthrough builds one header for a master dark calibration frame and
carries it through every capability in this crate.

## Create a header and set FITS keywords

[`Header::new`](https://docs.rs/xisf-header/latest/xisf_header/struct.Header.html#method.new)
starts empty.
[`Header::set`](https://docs.rs/xisf-header/latest/xisf_header/struct.Header.html#method.set)
upserts a keyword: it appends when the name is absent and updates in place
when it is unique. The Rust type of the value chooses its on-disk form —
strings are quoted, numbers are bare literals; wrap a float in
[`Fixed`](https://docs.rs/xisf-header/latest/xisf_header/struct.Fixed.html)
for controlled fixed-point formatting.

```rust
use xisf_header::{Fixed, Header, StructuralHints};

let mut header = Header::new();
header.set("IMAGETYP", "Master Dark")?;
header.set_comment("IMAGETYP", "Type of image")?;
header.set("EXPTIME", Fixed(300.0, 2))?; // fixed-point, 2 decimals
header.set("GAIN", 100_i64)?;
# Ok::<(), xisf_header::Error>(())
```

## Track repeated keywords

A bare name must be unique to read or write directly; `HISTORY`-style
keywords that repeat are built with
[`Header::append`](https://docs.rs/xisf-header/latest/xisf_header/struct.Header.html#method.append)
and read back with `get_all`/`count`. An `("HISTORY", n)` key addresses one
occurrence — to update it in place or remove it.

```rust
# use xisf_header::Header;
# let mut header = Header::new();
header.append("HISTORY", "reduced with siril")?;
header.append("HISTORY", "stacked 20x300s")?;

// Read them all, then revise the first line and drop it.
let steps: Vec<String> = header.get_all("HISTORY");
assert_eq!(steps.len(), 2);
header.set(("HISTORY", 0), "reduced with siril v2")?; // update one occurrence
header.remove(("HISTORY", 0))?;                        // remove one occurrence
assert_eq!(header.count("HISTORY"), 1);
# Ok::<(), xisf_header::Error>(())
```

## Attach XISF properties

XISF `<Property>` elements are a separate namespace from FITS keywords: FITS
keywords (`set`/`get`/`append`) use ≤ 8-character uppercase names and can
repeat, while properties are keyed by a colon-delimited hierarchical id (e.g.
`Observation:Object:Name`), carry an explicit XISF type, and are unique per
id. Reach for FITS keywords when other FITS-aware tools need to read the
metadata; reach for properties for XISF-native structured metadata.
[`Header::set_property`](https://docs.rs/xisf-header/latest/xisf_header/struct.Header.html#method.set_property)
creates a `String`-typed property;
[`Header::set_property_with_type`](https://docs.rs/xisf-header/latest/xisf_header/struct.Header.html#method.set_property_with_type)
records an explicit XISF type (e.g. `Float32`), which round-trips verbatim.

```rust
# use xisf_header::Header;
# let mut header = Header::new();
header.set_property("Observation:Object:Name", "NGC 7000")?;
header.set_property_with_type("Instrument:Telescope:FocalLength", "0.53", "Float32")?;
# Ok::<(), xisf_header::Error>(())
```

## Serialize a header block

[`Header::to_header_bytes`](https://docs.rs/xisf-header/latest/xisf_header/struct.Header.html#method.to_header_bytes)
emits the preamble plus XML header from
[`StructuralHints`](https://docs.rs/xisf-header/latest/xisf_header/struct.StructuralHints.html),
with the `<Image location>` attachment offset already sized to fit data
matching those hints;
[`Header::parse`](https://docs.rs/xisf-header/latest/xisf_header/struct.Header.html#method.parse)
reads one back. This is an in-memory building block — nothing is written to
disk until you call `write_to_file` or `update_file` (below).

```rust
# use xisf_header::{Header, StructuralHints};
# let header = Header::new();
let hints = StructuralHints::default(); // 1x1x1 8-bit grayscale = 1 byte
let mut container = header.to_header_bytes(&hints);
container.push(0); // the caller's own pixel data
assert_eq!(Header::parse(&container)?, header);
# Ok::<(), xisf_header::Error>(())
```

## Edit a file's header in place

The common case: change a keyword or property on an existing XISF file
without touching its pixel data or unmodeled XML.

[`Header::update_file`](https://docs.rs/xisf-header/latest/xisf_header/struct.Header.html#method.update_file)
reads a file's header, applies an edit closure, and splices the result back
into the file — byte-exact and data-preserving. Everything outside the
edited `<FITSKeyword>`/`<Property>` elements (unmodeled XML, whitespace, the
attached pixel data) survives untouched, and a no-op edit reproduces the
file byte-for-byte. If the edit changes the header's length, the `<Image
location>` offset is recomputed and the original data moves (unchanged) to
the new offset — `SIZE` never changes.

```rust
# use xisf_header::{Header, StructuralHints};
# let path = std::env::temp_dir().join("master-dark-update.xisf");
# let header = Header::new();
# let mut container = header.to_header_bytes(&StructuralHints::default());
# container.push(0);
# std::fs::write(&path, &container)?;
Header::update_file(&path, |h| {
    h.set("OBJECT", "NGC 7000")?;
    Ok(())
})?;
# std::fs::remove_file(&path).ok();
# Ok::<(), xisf_header::Error>(())
```

`update_file` targets the common single-image layout: exactly one `<Image
location="attachment:…">` element. A file with zero or multiple attachments
(e.g. a `Thumbnail` alongside the `Image`) is rejected with
[`Error::Unsupported`](https://docs.rs/xisf-header/latest/xisf_header/enum.Error.html#variant.Unsupported)
rather than risking data loss.

## Create a new file

Reach for this only when there's no existing file to edit — you're
assembling a fresh XISF file from pixel data you already have.
[`Header::write_to_file`](https://docs.rs/xisf-header/latest/xisf_header/struct.Header.html#method.write_to_file)
writes the preamble, the XML header (`<Image>` filled in from `hints`), and
your `data` bytes to a **new** file in one call — the `location` `SIZE`
attribute is set from `data.len()`. It errors rather than overwriting an
existing path, and never fabricates pixel data: `data` is always the
caller's own (`&[]` for a header-only file).

```rust
# use xisf_header::{Header, StructuralHints};
# let header = Header::new();
# let hints = StructuralHints::default();
let path = std::env::temp_dir().join("master-dark.xisf");
let data = [0u8]; // the caller's own pixel data, matching `hints`
header.write_to_file(&path, &hints, &data)?;

let reloaded = Header::read_from_file(&path)?;
assert_eq!(reloaded, header);
# std::fs::remove_file(&path).ok();
# Ok::<(), xisf_header::Error>(())
```

## Handling errors

Keyword accessors return
[`Result`](https://docs.rs/xisf-header/latest/xisf_header/type.Result.html):
[`Error::Ambiguous`](https://docs.rs/xisf-header/latest/xisf_header/enum.Error.html)
signals a bare name that matches more than one keyword — select an occurrence
with an `(name, n)` key instead.

```rust
# use xisf_header::{Error, Header};
# let mut header = Header::new();
# header.append("HISTORY", "reduced with siril").unwrap();
# header.append("HISTORY", "stacked 20x300s").unwrap();
assert!(matches!(
    header.get_str("HISTORY"),
    Err(Error::Ambiguous { count: 2, .. })
));
assert_eq!(header.get_str(("HISTORY", 1))?, Some("stacked 20x300s"));
# Ok::<(), xisf_header::Error>(())
```