fitskit 0.2.0

Pure Rust FITS v4.0 reader/writer with tile-compression read and write
Documentation

fitskit

Pure-Rust, zero-dependency reader and writer for FITS v4.0 — the Flexible Image Transport System format used throughout astronomy.

License: MIT

fitskit reads and writes the full FITS v4.0 standard — primary and extension HDUs, images, ASCII tables, and binary tables (including variable-length arrays) — with no external dependencies in the default build and no C toolchain required. It decodes every tile-compressed image type in the standard (RICE, GZIP, PLIO, HCOMPRESS) and — uniquely among pure-Rust FITS crates — writes RICE- and GZIP-compressed images that cfitsio's funpack reads back byte-for-byte.

Features

  • All standard HDU types — Primary, IMAGE, ASCII TABLE, BINTABLE
  • Full read and write — round-trip files, bytes, or any Read/Write
  • All BITPIX types — 8, 16, 32, 64, -32, -64
  • BSCALE/BZERO — raw and scaled access, with the unsigned-integer convention
  • Variable-length arraysP and Q heap descriptors in binary tables
  • CHECKSUM/DATASUM — ones-complement integrity computation and verification
  • Tile-compressed images (read) — RICE_1, GZIP_1, GZIP_2, PLIO_1, and HCOMPRESS_1, including float quantization with subtractive dithering; decoded bit-exactly against cfitsio's funpack
  • Tile-compressed images (write) — encode RICE_1 and GZIP_1/GZIP_2 (integer and float); output verified byte-exact through funpack
  • Zero dependencies by default — optional image and gzip features pull in pure-Rust crates only

Installation

[dependencies]
fitskit = "0.2"

Usage

Reading a FITS file

use fitskit::{FitsFile, HduData, PixelData};

let fits = FitsFile::from_file("image.fits")?;

let primary = fits.primary();
println!("BITPIX = {}", primary.header.get_int("BITPIX").unwrap());

if let HduData::Image(img) = &primary.data {
    println!("{}x{}", img.width().unwrap(), img.height().unwrap());
    if let PixelData::F32(pixels) = &img.pixels {
        println!("first pixel = {}", pixels[0]);
    }
}

for hdu in fits.extensions() {
    match &hdu.data {
        HduData::Image(img) => println!("IMAGE: {:?}", img.axes),
        HduData::BinTable(t) => println!("BINTABLE: {} rows, {} cols", t.nrows, t.columns.len()),
        HduData::AsciiTable(t) => println!("TABLE: {} rows", t.nrows),
        HduData::Empty => {}
    }
}
# Ok::<(), fitskit::Error>(())

Writing a FITS file

use fitskit::{FitsFile, ImageData, PixelData, HeaderValue};

let pixels: Vec<i16> = (0..10000).map(|i| (i % 1000) as i16).collect();
let img = ImageData::new(vec![100, 100], PixelData::I16(pixels));

let mut fits = FitsFile::with_primary_image(img);
fits.primary_mut().header.set("OBJECT", HeaderValue::String("M31".into()), None);

fits.to_file("output.fits")?;
# Ok::<(), fitskit::Error>(())

Building a binary table

use fitskit::{FitsFile, Hdu, BinTableBuilder, BinColumnType};

let table = BinTableBuilder::new()
    .add_column("RA", BinColumnType::D64(1))
    .add_column("DEC", BinColumnType::D64(1))
    .add_column("MAG", BinColumnType::E32(1))
    .push_row(|row| {
        row.write_f64(180.0);
        row.write_f64(45.0);
        row.write_f32(12.5);
    })
    .push_row(|row| {
        row.write_f64(90.0);
        row.write_f64(-30.0);
        row.write_f32(8.2);
    })
    .build();

let mut fits = FitsFile::with_empty_primary();
fits.push_extension(Hdu::bintable_extension(table));
# let _ = fits.to_bytes();

Reading a tile-compressed image

Tile-compressed images are stored on disk as a BINTABLE extension (the FITS Tiled Image Compression convention). Hdu::as_compressed_image cheaply detects them; decompress reassembles the full image lazily.

use fitskit::FitsFile;

let fits = FitsFile::from_file("compressed.fits")?;

for hdu in fits.extensions() {
    if let Some(cimg) = hdu.as_compressed_image() {
        println!("compression = {:?}", cimg.compression());
        let image = cimg.decompress()?;
        println!("decompressed to {:?}", image.axes);
    }
}
# Ok::<(), fitskit::Error>(())

Writing a tile-compressed image

ImageData::compress produces a compressed-image BINTABLE HDU ready to push into a file. The output is read back byte-for-byte by cfitsio's funpack.

use fitskit::{FitsFile, ImageData, PixelData, CompressOptions};

let pixels: Vec<i16> = (0..10000).map(|i| (i % 1000) as i16).collect();
let img = ImageData::new(vec![100, 100], PixelData::I16(pixels));

// Default options: RICE_1, one tile per row, lossless for integers.
let hdu = img.compress(&CompressOptions::default())?;

let mut fits = FitsFile::with_empty_primary();
fits.push_extension(hdu);
fits.to_file("compressed.fits")?;
# Ok::<(), fitskit::Error>(())

gzip feature: the GZIP_1/GZIP_2 algorithms (e.g. CompressOptions { algorithm: CompressionType::Gzip1, .. } when writing, or decoding a GZIP-compressed tile on read) require the gzip feature; RICE_1 works without it. Build with --features gzip or fitskit = { version = "0.2", features = ["gzip"] }.

Converting to/from the image crate (feature image)

With the image feature, ImageData converts to and from the image crate's DynamicImage — e.g. to save a FITS image as a PNG, or to ingest a raster as FITS. Build with fitskit = { version = "0.2", features = ["image"] }.

use fitskit::{FitsFile, HduData, ImageData};

let fits = FitsFile::from_file("image.fits")?;
if let HduData::Image(img) = &fits.primary().data {
    // FITS → image crate (BSCALE/BZERO applied; 1.0/0.0 = identity)
    let dynamic = img.to_dynamic_image(1.0, 0.0)?;
    dynamic.save("image.png").unwrap();

    // image crate → FITS, returning (ImageData, bscale, bzero)
    let (restored, _bscale, _bzero) = ImageData::from_dynamic_image(&dynamic)?;
    assert_eq!(restored.axes, img.axes);
}
# Ok::<(), fitskit::Error>(())

Checksums

use fitskit::{FitsFile, ImageData, PixelData};

let img = ImageData::new(vec![4], PixelData::U8(vec![1, 2, 3, 4]));
let fits = FitsFile::with_primary_image(img);

// Write with CHECKSUM/DATASUM keywords
let bytes = fits.to_bytes_with_checksum()?;

// Verify on read
let fits2 = FitsFile::from_bytes(&bytes)?;
fits2.primary().verify_datasum()?;
# Ok::<(), fitskit::Error>(())

World Coordinate System (WCS)

With the wcs feature, parse a two-axis celestial WCS straight from a header and map between 1-based FITS pixel coordinates and world coordinates in degrees. The spherical-projection math is backed by the zero-dependency mapproj crate.

use fitskit::FitsFile;

let fits = FitsFile::from_file("image.fits")?;
let wcs = fits.primary().header.wcs()?;

// Pixel (1-based) -> world (lon, lat) in degrees
let (ra, dec) = wcs.pixel_to_world(256.5, 256.5).unwrap();

// ...and back to pixel
let (x, y) = wcs.world_to_pixel(ra, dec).unwrap();
# Ok::<(), fitskit::Error>(())

Supported: the common 2-axis celestial case — CTYPEn = xxx--CCC for any projection mapproj implements (TAN, SIN, ARC, ZEA, STG, AIT, MER, CAR, CEA, SFL, MOL, conics, …), with the linear transform from CDi_j or PCi_j + CDELTi (CD takes precedence). SIP distortions and 3+-axis / spectral WCS are out of scope.

Core types

A FITS file is an ordered sequence of Header-Data Units (HDUs). fitskit mirrors that structure directly:

Type Role
FitsFile Top-level container — an ordered Vec<Hdu>. Reads/writes files, byte slices, or any Read/Write; builder methods (with_primary_image, with_empty_primary, push_extension, to_file, to_bytes, to_bytes_with_checksum).
Hdu One Header-Data Unit: a Header plus an HduData payload. as_compressed_image() exposes a tile-compressed image stored in a BINTABLE.
HduData The payload enum: Empty, Image(ImageData), AsciiTable(AsciiTable), BinTable(BinTable).
Header Ordered list of Keywords with typed accessors (get_int, get_float, get_string, get_bool) and set.
Keyword / HeaderValue An 80-byte header card and its typed value (with CONTINUE long-string handling).
ImageData / PixelData Image axes plus a typed pixel buffer (U8/I16/I32/I64/F32/F64); raw access and BSCALE/BZERO-scaled access (scaled_values).
BinTable / BinColumn / BinColumnType / BinCellValue Binary-table model — columns, rows, and heap (variable-length arrays); built with BinTableBuilder.
AsciiTable ASCII TABLE model with TFORMn column parsing and TSCALn/TZEROn scaling.
CompressedImage / CompressionType / TileGeometry / Quantize Read-side view over a tile-compressed image: detect the algorithm and geometry, then decompress() to an ImageData.
CompressOptions Write-side encode options (algorithm, tiling, quantization, dithering) for ImageData::compress / compress_image.
Bitpix The BITPIX data type (8/16/32/64/-32/-64).
Error / Result Crate error type and result alias.

Decoding is done eagerly at read time (big-endian → native), except tile-compressed images, which are decoded lazily on decompress() so the original compressed tiles are preserved for a lossless round-trip write.

Feature flags

Flag Default Description
(none) Core read/write, all HDU types, RICE_1 / PLIO_1 / HCOMPRESS_1 decompression, and RICE_1 compression — zero dependencies
image Conversion between ImageData and the image crate's DynamicImage
gzip GZIP_1/GZIP_2 tile compression and decompression via the pure-Rust miniz_oxide crate
wcs Two-axis celestial World Coordinate System pixel ⇄ world transforms (Wcs, Header::wcs) via the zero-dependency mapproj crate

The default build stays dependency-free; RICE_1, PLIO_1, and HCOMPRESS_1 decompression and RICE_1 compression all work without any feature (only GZIP_1/GZIP_2 need the gzip feature).

Why fitskit?

Among Rust FITS crates, fitskit fills a specific niche — pure Rust, zero default dependencies, full read + write including tables, complete compressed-image reading, and compressed-image writing, with no C toolchain to install. No other pure-Rust crate writes compressed FITS.

Crate Pure Rust Write Tables Compressed read Compressed write Notes
fitsio Wraps the cfitsio C library; needs a C toolchain
fitsrs partial Read-only
fitrs Dormant; no table support
fitskit ✓ (all types) ✓ (RICE/GZIP) Zero default deps; no C dependency

Supported / not supported

Supported

  • Primary + extension HDUs; images, ASCII tables, binary tables (read + write)
  • All BITPIX types; BSCALE/BZERO scaling and the unsigned-integer convention
  • Variable-length arrays (P/Q descriptors) in binary tables
  • CHECKSUM/DATASUM computation and verification
  • Tile-compressed image reading: RICE_1, GZIP_1, GZIP_2, PLIO_1, and HCOMPRESS_1 — for integer and floating-point images, including quantization with subtractive dithering (NO_DITHER / SUBTRACTIVE_DITHER_1 / SUBTRACTIVE_DITHER_2)
  • Tile-compressed image writing: RICE_1 and GZIP_1/GZIP_2 — integer (lossless) and float (lossless via GZIP, or lossy quantized + dithered); output verified byte-exact through cfitsio's funpack
  • Two-axis celestial WCS pixel ⇄ world transforms (wcs feature) for the common CTYPEn = xxx--CCC case, linear transform from CDi_j or PCi_j + CDELTi

Not supported

  • Encoding PLIO_1 or HCOMPRESS_1 (these decode only)
  • HCOMPRESS image smoothing (SMOOTH ≠ 0) on decode
  • Random groups (deprecated in FITS v4.0)
  • WCS: SIP distortions, 3+-axis / spectral WCS, PVi_m projection parameters, and non-degree CUNIT (the wcs feature handles the 2-axis celestial linear
    • projection case only)

License

MIT