xisf-header 0.3.0

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

xisf-header

CI Crates.io Docs.rs License

Rust crate that reads and writes XISF (Extensible Image Serialization Format) image-file headers: it extracts the embedded FITS keywords and XISF <Property> elements, supports create/read/update/delete on both, and serializes a header back into an XISF container.

The crate is header-only: it parses and emits the 16-byte XISF preamble plus the UTF-8 XML header, and never reads image/pixel data.

Features

  • Parse an XISF header from bytes or a file with Header::parse / Header::read_from_file. The XISF0100 signature, the little-endian XML-length field (capped at 8 MiB), and UTF-8 encoding are validated.
  • Strict keyword access. A bare name must be unique or the accessor returns Error::Ambiguous; repeated keywords (e.g. HISTORY) are addressed with an (name, n) key or the get_all/count helpers.
  • Typed reads and writes. One generic get::<T> over the open FromField trait (String, f64, i64, u32, bool, and a date/time), with get_str/get_f64/… wrappers; writes take impl IntoValue, so the Rust type chooses string vs. bare-literal formatting.
  • <Property> round-trip. XISF properties keep their type, comment, and format attributes verbatim. A String property written as child text (<Property id=…>text</Property>) is read the same as the attribute form, and writes normalize it to a value= attribute. Values are stored raw (XISF properties are not FITS-quoted).
  • Two write paths. Assemble a new file with to_header_bytes(&hints) plus your own data, or edit an existing file in place with update_file, which splices only the changed keywords/properties into the file's raw bytes — byte-exact and data-preserving.
  • Enumerate and bulk-edit. Read keywords in document order with keywords/iter; apply atomic batches with set_many/remove_many, and clear every occurrence of a repeated name with remove_all.
  • No unsafe. Dependencies are pure Rust (no C/sys crates): quick-xml, thiserror, time, and optional serde. MSRV 1.82.

Install

[dependencies]
xisf-header = "0.2"

Optional features

  • serde — derive Serialize/Deserialize on Header, FitsKeyword, Property, and the value types:

    xisf-header = { version = "0.2", features = ["serde"] }
    

Usage

Parse a header and read keywords

Header::parse reads a byte buffer into a Header.

use xisf_header::Header;

let bytes = std::fs::read("frame.xisf")?;
let header = Header::parse(&bytes)?;

// Typed reads are strict: `?` surfaces a duplicate-keyword ambiguity; the inner
// `Option` is `None` when the keyword is absent or unreadable as that type.
let exposure = header.get_f64("EXPTIME")?;
let image_type = header.get_str("IMAGETYP")?;

// XISF <Property> access.
let focal_length_m = header.property_get::<f64>("Instrument:Telescope:FocalLength");
# Ok::<(), xisf_header::Error>(())

Create, read, update, delete

Header::new starts empty; set, set_comment, set_with_comment, and remove edit it in place.

use xisf_header::Header;

let mut header = Header::new();

// `set` upserts: update a unique keyword in place, or append when absent. The
// Rust type of the value chooses its on-disk form — strings are quoted,
// numbers and logicals are bare literals.
header.set("IMAGETYP", "Master Dark")?;
header.set_comment("IMAGETYP", "Type of image")?;
header.set("EXPTIME", 300.0)?;
header.set("GAIN", 100_i64)?;

assert_eq!(header.get_str("IMAGETYP")?, Some("Master Dark"));
assert_eq!(header.get_i64("GAIN")?, Some(100));

header.set("EXPTIME", 600.0)?; // update
header.remove("GAIN")?; // delete
# Ok::<(), xisf_header::Error>(())

Repeated keywords

append adds an occurrence unconditionally; select one back with an (name, n) key.

use xisf_header::Header;

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

// A bare name is ambiguous once it repeats — select an occurrence instead.
assert!(header.get_str("HISTORY").is_err());
assert_eq!(header.get_str(("HISTORY", 1))?, Some("stacked 20x300s"));
assert_eq!(header.count("HISTORY"), 2);
# Ok::<(), xisf_header::Error>(())

XISF properties

set_property and set_property_with_type write a Property entry; remove_property deletes one by id.

use xisf_header::Header;

let mut header = Header::new();

// Plain `set_property` creates a `String` property; an explicit XISF type is
// kept on the property and survives round-trips.
header.set_property("Observation:Object:Name", "NGC 7000")?;
header.set_property_with_type("Instrument:Telescope:FocalLength", "0.53", "Float32")?;

assert_eq!(header.property("Observation:Object:Name"), Some("NGC 7000"));
assert_eq!(header.property_get::<f64>("Instrument:Telescope:FocalLength"), Some(0.53));
assert_eq!(header.properties()["Instrument:Telescope:FocalLength"].type_, "Float32");
# Ok::<(), xisf_header::Error>(())

Controlled numeric formatting

Fixed and Sci wrap an f64 for fixed-point or scientific-notation output; both implement IntoValue.

use xisf_header::{Fixed, Header};

let mut header = Header::new();
header.set("EXPTIME", Fixed(300.0, 2))?; // fixed-point, 2 decimals
assert_eq!(header.get_str("EXPTIME")?, Some("300.00"));
# Ok::<(), xisf_header::Error>(())

Assemble a new file

to_header_bytes(&hints) emits the preamble plus XML header, using StructuralHints to fill in the <Image> geometry and to size the location attribute's attachment offset; append your own pixel data (sized to match hints) to complete the container.

use xisf_header::{Header, StructuralHints};

let mut header = Header::new();
header.set("IMAGETYP", "Master Dark")?;

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
std::fs::write("out.xisf", &container)?;

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

Edit a file's header in place

Header::update_file reads a file's header, applies an edit closure, and splices the result back into the file. It is byte-exact and data-preserving: everything outside the edited <FITSKeyword>/<Property> elements — unmodeled XML (Metadata, Resolution, thumbnails, …), whitespace, and the attached pixel data — survives untouched, and a no-op edit reproduces the file byte-for-byte. If an edit changes the header's length, the <Image location> offset is recomputed and the original data is moved (unchanged) to the new offset.

use xisf_header::Header;

Header::update_file("out.xisf", |h| {
    h.set("OBJECT", "NGC 7000")?;
    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 rather than risking data loss.

Documentation

  • Quickstart guide — a task-oriented walkthrough backed by examples/quickstart.rs.
  • docs.rs/xisf-header — full API documentation generated from the source doc comments, published for every release (all features enabled). Build it locally with cargo doc --no-deps --all-features --open. Every public item is documented; CI fails the build on missing or broken documentation.

License

Licensed under the Apache License, Version 2.0.