Skip to main content

Crate fits_io

Crate fits_io 

Source
Expand description

Β§FitsIo

A safe, ergonomic, and pure-Rust library for reading and writing FITS (Flexible Image Transport System) files, inspired by CFITSIO.

This crate offers optional async I/O with Tokio and structured access to FITS headers, images, and tables β€” without any C dependencies.

Designed for astronomy, astrophotography, and scientific pipelines where portability and safety matter.

Β§Features

  • πŸ“¦ Pure Rust implementation (no CFITSIO, no C bindings)
  • ⚑ Async I/O with Tokio (enabled by default)
  • 🧩 Support for Primary HDUs and extensions
  • πŸ–ΌοΈ Image HDUs of any dimensionality, cubes and hypercubes included
  • πŸ“Š Binary and ASCII tables alike, with optional serde support in both directions
  • πŸ—œοΈ Tile-compressed images, read and written as images like any other
  • 🌍 World coordinate helpers: pixels to sky positions and back, with SIP and TPV distortions
  • 🧠 Typed access to FITS header keywords, and any keyword at all by name
  • πŸš€ Streaming and memory-efficient reads
  • πŸ›‘οΈ Idiomatic error handling with Result
  • πŸ” CFITSIO-inspired API, redesigned for Rust

Β§Installation

Add the crate to your Cargo.toml:

[dependencies]
fits-io = "0.2"

Β§Reading a file

use fits_io::Fits;
use fits_io::fs::FsFits;
use fits_io::hdu::ImageHDU;

let fits = FsFits::open("observation.fits".as_ref())?;

let hdu = fits.primary_hdu();
println!("{} x {}", hdu.images_width(), hdu.images_height());

if let Some(image) = hdu.read_image(0)? {
    let normalised = image.normalized();
    println!("first pixel: {}", normalised.get_pixel(0, 0)[0]);
}

Β§Reading table rows into your own structs

With the serde feature, a table’s rows deserialize straight into a struct, matching columns to fields by their TTYPEn names. The same works for an ASCII table through read_rows, from_ascii_table and to_ascii_table.

use fits_io::Fits;
use fits_io::fs::FsFits;
use fits_io::hdu::{BinTableHDU, ExtensionHDU};
use serde::Deserialize;

#[derive(Deserialize)]
struct Source {
    #[serde(rename = "RA")]
    right_ascension: f64,
    #[serde(rename = "DEC")]
    declination: f64,
    // A column with a TNULLn card may leave entries undefined.
    #[serde(rename = "MAG")]
    magnitude: Option<f32>,
}

let fits = FsFits::open("catalogue.fits".as_ref())?;

if let Some(ExtensionHDU::BinTable(hdu)) = fits.extension_hdu(0) {
    let sources: Vec<Source> = hdu.read_rows()?;
    println!("{} sources", sources.len());
}

Β§Writing

Setting data also brings the header into line with it, and saving fills in the mandatory cards, puts them in the order the standard requires, and writes CHECKSUM and DATASUM.

use fits_io::Fits;
use fits_io::fs::FsFits;
use fits_io::hdu::ImageHDU;

let mut fits = FsFits::open("observation.fits".as_ref())?;

fits.primary_hdu_mut()
    .set_raw_images_i16(2, 2, &[&[1, 2, 3, 4]])?;

// `to_vec` returns the bytes; `save` writes them back over the file.
let bytes = fits.to_vec()?;
fits.save()?;

Β§Editing the header

Every keyword this crate knows has an accessor of its own, and any keyword at all can be set by name. A keyword too long for the eight columns a card gives it becomes a HIERARCH card, and a value too long for one card is written across CONTINUE cards.

use fits_io::header::{Header, Value};

let mut header = Header::default();

header.set_card("OBJECT", "NGC 7000")?;
header.set_card("EXPTIME", Value::from(300.0).with_comment("seconds"))?;
header.set_card("ESO INS FILT1 NAME", "Halpha")?;
header.add_history("stacked from 42 subframes");

// The typed accessors see what was set by name.
assert_eq!(header.object(), Some("NGC 7000"));

Β§Compressing an image

An image can be stored tile-compressed, the way fpack writes one: cut into tiles, each tile compressed on its own, and the result written as a binary table that a reader treats as the image it stands for. Everything that reads an image goes on working.

use fits_io::hdu::ImageHDU;
use fits_io::image::compression::{Compression, CompressionOptions, Quantize};
use fits_io::{Fits, FitsSlice};

let mut fits = FitsSlice::new();
fits.primary_hdu_mut()
    .set_raw_images_i16(2, 2, &[&[1, 2, 3, 4]])?;

fits.primary_hdu_mut()
    .compress(&CompressionOptions::new(Compression::Rice))?;

let bytes = fits.to_vec()?;

// A floating point image has to be quantised before an integer coder can take
// it, which loses the low bits of every pixel; `Compression::Gzip` compresses
// one as it stands and loses nothing.
let lossy = CompressionOptions::new(Compression::Rice)
    .with_quantization(Quantize::NoiseLevel(4.0));

Β§Building a file from nothing

use fits_io::bin_table::to_bin_table;
use fits_io::hdu::{BinTableHDU, ExtensionHDU, ImageHDU};
use fits_io::{Fits, FitsSlice, SliceBinTableHDU};
use serde::Serialize;

#[derive(Serialize)]
struct Star {
    #[serde(rename = "NAME")]
    name: String,
    #[serde(rename = "MAG")]
    magnitude: f64,
}

let mut fits = FitsSlice::new();

fits.primary_hdu_mut()
    .set_raw_images_u8(2, 2, &[&[1, 2, 3, 4]])?;

// Column types and widths are worked out from the rows themselves.
let stars = vec![Star {
    name: "Vega".into(),
    magnitude: 0.03,
}];
let table = SliceBinTableHDU::from_table(&to_bin_table(&stars)?)?;
fits.push_extension(ExtensionHDU::BinTable(table));

let bytes = fits.to_vec()?;

Β§Working without a filesystem

FitsSlice reads a file that is already in memory β€” one arriving over a network, say, or a build with the fs feature turned off. It reads, writes and streams the same things FsFits does, and from_vec takes over your buffer rather than copying it. A gzipped buffer is decompressed transparently.

use fits_io::{Fits, FitsSlice};
use fits_io::hdu::ImageHDU;

let fits = FitsSlice::from_slice(&bytes)?;

assert_eq!(fits.primary_hdu().image_count(), 1);

Β§Feature flags

default-features = false gives you header, image and table parsing over in-memory data through FitsSlice, with no filesystem, async or threading support.

FeatureDefaultEffect
fsβœ…Read and write FITS files on the filesystem via FsFits
gzipβœ…Transparently decompress gzipped files and buffers
tokioβœ…Async open and streaming reads
rayonβœ…Parallel table row decoding, worth about 4x on a big table
serdeConvert table rows to and from your own structs

Β§Benchmarks

cargo bench --features fs,serde,rayon times opening a file, reading a table and decoding its rows, against the Gaia fixture under tests/. It reports the fastest of several runs and skips when the fixture is absent.

Β§Changelog

What has changed between releases is in CHANGELOG.md.

Β§Documentation

Every public item is documented, and #![deny(missing_docs)] keeps it that way. The API docs are on docs.rs, built with every feature enabled so the serde, tokio and gzip parts are visible.

Β§Design Goals

  • Safety β€” eliminate undefined behavior and unsafe FFI
  • Portability β€” run anywhere Rust runs
  • Ergonomics β€” minimal boilerplate
  • Performance β€” streaming-friendly, low overhead
  • Familiarity β€” CFITSIO-inspired, Rust-native

Β§Supported FITS Features

FeatureStatusNotes
Primary HDUβœ…
Extension HDUsβœ…
Image HDUβœ…Any number of axes; planes beyond the second are indexed
Binary tablesβœ…serde converts rows to and from your structs
ASCII tablesβœ…Read, written, streamed and serde-mapped like binary ones
Variable-length array columnsβœ…TFORMn P and Q, read and written through the heap
Complex columnsβœ…TFORMn C and M
Column scalingβœ…TSCALn, TZEROn and TNULLn applied both ways
Unsigned columnsβœ…The TZEROn convention, at all four integer widths
Multidimensional columnsβœ…TDIMn read and written, nesting as deep as it says
Undefined image pixelsβœ…BLANK reads as NaN rather than as black
Header readβœ…CONTINUE long values and HIERARCH keywords included
Header writeβœ…Mandatory cards filled in and ordered as the standard asks
Header editingβœ…set_card and remove_card by keyword, writing HIERARCH and CONTINUE as needed
Image writeβœ…Any number of axes, then save or to_vec
Table writeβœ…set_table / set_rows, for both table kinds
Building filesβœ…push_extension and remove_extension
Gzip decompressionβœ….fits.gz files and gzipped buffers alike
Streaming image readsβœ…stream_normalised_image, via the tokio feature
Streaming table rowsβœ…stream_table_rows, via the tokio feature
WCS conventionsβœ…CDi_j, PCi_j and CDELTn/CROTAn, with LONPOLE and LATPOLE
WCS projectionsβœ…TAN, SIN, ARC, STG, ZEA, CAR, MER, CEA, AIT, MOL
WCS distortionsβœ…SIP and TPV, both applied and inverted
Non-celestial axesβœ…A cube’s third axis and beyond, linear or -LOG
CHECKSUM and DATASUMβœ…Written on save; checksum::verify checks an HDU
Random groupsβœ…group_count and read_group, with PSCALn and PZEROn
Compressed image readβœ…RICE_1, HCOMPRESS_1, PLIO_1, GZIP_1/2, NOCOMPRESS, any number of axes
Compressed image writeβœ…RICE_1, HCOMPRESS_1, PLIO_1, GZIP_1/2 and NOCOMPRESS, through ImageHDU::compress
Quantised floating pointβœ…ZQUANTIZ dithering read and written, ZBLANK included
HCOMPRESS smoothingβœ…SMOOTH honoured, matching the reference implementation
Conic WCS projections🚧COE, COD and the rest say so rather than guessing

Β§License

Licensed under either of:

  • Apache License, Version 2.0
  • MIT License

at your option.

Β§Contributing

Issues, discussions, and pull requests are welcome. Please open an issue for large changes or new features.

Β§Acknowledgements

Inspired by CFITSIO and the FITS standard maintained by NASA/HEASARC.

Β§License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

ModulesΒ§

ascii_table
Structs for working with FITS ASCII tables.
bin_table
Structs for working with FITS Bin Tables
checksum
The FITS checksum convention.
fsfs
Reading and writing FITS files on the filesystem.
hdu
The header and data units a FITS file is made of.
header
FITS Header representations
image
Struct for working with fits images
wcs
Turning pixel positions into sky coordinates, and back.

StructsΒ§

FitsSlice
A FITS file read from a buffer rather than from the filesystem.
SliceAsciiTableHDU
An ASCII table HDU backed by a buffer rather than a file.
SliceBinTableHDU
A binary table HDU backed by a buffer rather than a file.
SliceImageHDU
An image HDU backed by a buffer rather than a file.

EnumsΒ§

Error
What can go wrong converting between FITS values and your own types.

TraitsΒ§

Fits
This is a representation of a FITS file (Flexible Image Transport System).

Type AliasesΒ§

Result
A result whose error is this crate’s Error.