# fits-well
[](https://crates.io/crates/fits-well)
[](https://docs.rs/fits-well)
[](https://github.com/xorza/fits#license)
A blazing-fast Rust reader and writer for **FITS** (Flexible Image Transport
System) files — the standard data format of astronomy — targeting the full
**FITS 4.0** standard.
Two goals shape every decision:
1. **Fast** — zero-copy where the format allows, lazy seeking HDU access, reused
scratch buffers, and tile-parallel (de)compression across a rayon pool.
2. **Whole-standard coverage** — images, ASCII tables, binary tables with a heap
and variable-length arrays, random groups (read), a typed WCS layer (23
projections), time coordinates, and tiled compression.
## Install
```toml
[dependencies]
fits-well = "0.1"
```
The default build pulls in tiled compression (`flate2`) and tile parallelism
(`rayon`). For the dependency-light pure-Rust core, use
`default-features = false` (see the Feature flags section below).
## Usage
### Inspect a file
`open` scans every HDU boundary from the headers alone — no pixel data is read.
```rust
use std::fs::File;
use fits_well::FitsReader;
let reader = FitsReader::open(File::open("image.fits")?)?;
println!("{} HDU(s)", reader.hdus().len());
for (i, hdu) in reader.hdus().iter().enumerate() {
println!("HDU {i}: {:?}", hdu.kind);
if let Some(object) = hdu.header.get_text("OBJECT") {
println!(" OBJECT = {object}");
}
}
# Ok::<(), fits_well::FitsError>(())
```
### Write and read an image
`shape` is fastest-axis-first (`NAXIS1` first); `samples` is the flat buffer.
```rust
use std::fs::File;
use fits_well::{FitsReader, FitsWriter, Image, ImageData, Scaling};
let image = Image {
shape: vec![4, 3],
samples: ImageData::I16(vec![0, 1, 2, 3, 10, 11, 12, 13, 20, 21, 22, 23]),
scaling: Scaling { bscale: 1.0, bzero: 0.0, blank: None },
};
let mut writer = FitsWriter::new(File::create("out.fits")?);
writer.write_image(&image)?;
writer.into_inner().sync_all()?;
let mut reader = FitsReader::open(File::open("out.fits")?)?;
// `image_indices` lists the image-bearing HDUs, so you pick one rather than
// hard-coding it. `read_image` borrows the data unit in place (zero-copy).
let raw = reader.read_image(reader.image_indices()[0])?;
// `bitpix` is the stored width; `sample_type()` is the *effective* type, resolving
// the unsigned / signed-byte BZERO conventions (cfitsio's "equivalent type").
println!("shape {:?}, {:?}", raw.shape, raw.sample_type());
// `decode()` byte-swaps into an owned host-endian buffer; `physical()` applies
// BSCALE/BZERO and maps any BLANK value to NaN.
if let ImageData::I16(pixels) = raw.decode() {
println!("pixels {pixels:?}");
}
# Ok::<(), fits_well::FitsError>(())
```
A **tile-compressed** image (FITS §10) reads back through the *same* `read_image`
call — it detects `ZIMAGE` and decompresses transparently. To write one:
```rust
# use std::fs::File;
# use fits_well::{FitsWriter, CompressOptions, Image, ImageData, Scaling};
# let image = Image { shape: vec![16, 16], samples: ImageData::I16(vec![0; 256]), scaling: Scaling { bscale: 1.0, bzero: 0.0, blank: None } };
let options = CompressOptions::tiled([8, 8]); // 8×8 tiles
let mut writer = FitsWriter::new(File::create("compressed.fits")?);
writer.write_compressed_image(&image, "RICE_1", &options)?;
# writer.into_inner().sync_all()?;
# Ok::<(), fits_well::FitsError>(())
```
### Binary tables
Address a column by index or by its `TTYPEn` name; the handle decodes on demand.
```rust
use std::fs::File;
use fits_well::{ColumnData, FitsReader, FitsWriter, WriteColumn};
let columns = [
WriteColumn::fixed("ID", ColumnData::I32(vec![1, 2, 3]), 1),
WriteColumn::fixed("MAG", ColumnData::F64(vec![0.03, -1.46, 0.13]), 1).with_unit("mag"),
];
let mut writer = FitsWriter::new(File::create("table.fits")?);
writer.write_table(3, &columns)?; // 3 rows
writer.into_inner().sync_all()?;
let mut reader = FitsReader::open(File::open("table.fits")?)?;
let table = reader.read_table(1)?; // the table is HDU 1 (HDU 0 is the empty primary)
println!("{} rows, {} columns", table.nrows, table.columns.len());
// `.raw()` is the stored, typed plane; `.physical()` applies TZEROn/TSCALn and
// maps TNULLn to NaN, widening to f64. `.unsigned()`, `.complex()`, `.bits()`,
// and `.vla()` cover the other column kinds the same way.
println!("ID = {:?}", table.column_by_idx(0)?.raw()?);
println!("MAG = {:?}", table.column_by_name("MAG")?.physical()?);
# Ok::<(), fits_well::FitsError>(())
```
### World Coordinate System
`Header::wcs` parses the `CTYPEn`/`CRPIXn`/`CRVALn`/… keywords into a transform
that converts between pixel and sky coordinates in the file's declared frame.
```rust
use std::fs::File;
use fits_well::FitsReader;
let reader = FitsReader::open(File::open("wcs_tan.fits")?)?;
let wcs = reader.hdus()[0].header.wcs(None)?; // None = the primary WCS
let sky = wcs.pixel_to_world(&[256.0, 256.0]); // RA/Dec at the reference pixel
let pixel = wcs.world_to_pixel(&sky); // and back again
println!("{sky:?} -> {pixel:?}");
# Ok::<(), fits_well::FitsError>(())
```
The typed **time** layer (`Header::time`, `Datetime`, `TimeScale`) handles
ISO-8601/JD/MJD, epochs, and `UTC`…`TCB`/`GPS`/UT1 scale conversions.
## Examples
Runnable end-to-end programs live in [`examples/`](examples/):
```sh
cargo run --example image # write + read an image
cargo run --example table # binary-table columns
cargo run --example inspect -- f.fits # describe a file's HDUs and headers
cargo run --example wcs # pixel ↔ sky coordinates
cargo run --example time # observation times and scale conversions
cargo run --example compression # tile-compressed image round-trip
cargo run --example ndarray --features ndarray # read an image as an n-D array
```
## Feature flags
| `compression` | ✅ | Tiled image + table (de)compression — `GZIP_1/2`, `RICE_1`, `PLIO_1`, `HCOMPRESS_1` (pulls in `flate2`). |
| `parallel` | ✅ | Tile-parallel (de)compression across a rayon pool (implies `compression`). |
| `mmap` | — | `FitsReader::open_mmap` — zero-copy reads straight off memory-mapped pages (`memmap2`). |
| `ndarray` | — | `RawImage`/`Image` → typed `ImageArray` or a physical `ArrayD<f64>` in FITS axis order. |
`--no-default-features` gives the pure-Rust core (block / header / HDU / reader /
writer / WCS / time); its only unconditional dependencies are `bitvec` (packed
`X` bit-array columns) and `num-complex` (`C`/`M` complex columns). WCS (§8) and
time (§9) are dependency-free pure math and are always compiled.
## License
Licensed under either of
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE))
- MIT license ([LICENSE-MIT](LICENSE-MIT))
at your option.