npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

zigfitsio

v0.2.0

Published

TypeScript/JavaScript bindings for zigfitsio — a pure-Zig FITS 4.0 I/O library, shipped as a single WebAssembly module (Bun + Node + browser)

Readme

zigfitsio

TypeScript bindings npm node License: MIT

Astropy-style FITS I/O for Node, Bun, and the browser — one WebAssembly package, no native addons, no runtime dependencies.

TypeScript bindings for zigfitsio, a pure-Zig FITS 4.0 I/O library, in two layers: a high-level astropy-style API (open, HDUList, HDU classes, Header, Column, verify) and a low-level 1:1 mapping of the C ABI (zigfitsio/lowlevel).

Install

npm install zigfitsio     # or: bun add zigfitsio

On Node and Bun the WebAssembly module loads synchronously at import, so the API is fully synchronous and ready immediately. In the browser, call await zf.ready() once first:

import * as zf from "zigfitsio";

await zf.ready();  // browser: fetches + compiles zigfitsio.wasm (no-op on Node/Bun)
// ...then every call below is synchronous.

Quickstart

Images

import * as zf from "zigfitsio";

// Shape is C-order, [NAXIS2, NAXIS1] — same layout as numpy/astropy.
const pixels = new zf.FitsArray(Float32Array.from({ length: 24 }, (_, i) => i * 0.25), [4, 6]);
zf.writeTo("image.fits", pixels, { overwrite: true });

using hdul = zf.open("image.fits"); // `using` closes at scope exit (or call hdul.close())
const img = hdul.image(0).data;     // FitsArray | null — typed, no `as` cast
console.log(img?.shape, img?.dtype, img?.get(2, 3));

// Read a strided cutout without materializing the whole image.
const cut = hdul.image(0).section({ window: [[1, 3], [2, 5]], step: [1, 1] });
console.log(cut.shape); // [2, 3]

Tables

// `tableFromArrays` infers each TFORM from the array type.
const table = zf.tableFromArrays(
  {
    INDEX: Int32Array.from([10, 20, 30]),
    FLUX: Float32Array.from([1.5, 2.5, 3.5]),
    NAME: ["alpha", "beta", "gamma"],
  },
  { name: "EVENTS", units: { FLUX: "Jy" } },
);
// (Explicit TFORMs — complex, VLA, hand-tuned widths: `BinTableHDU.fromColumns` + `new Column(...)`.)
new zf.HDUList([new zf.PrimaryHDU(), table]).writeTo("table.fits", { overwrite: true });

using hdul = zf.open("table.fits");
const rec = hdul.table("EVENTS").data; // TableData | null (columnar)
console.log(rec?.names, rec?.numRows, rec?.numeric("FLUX"));
for (const row of rec ?? []) console.log(row.NAME, row.FLUX); // ...or iterate rows

Compression & checksums

// Tile compression: RICE/GZIP/PLIO/HCOMPRESS, incl. lossy + quantized floats.
const ramp = new zf.FitsArray(Int32Array.from({ length: 256 }, (_, i) => i), [16, 16]);
new zf.HDUList([new zf.PrimaryHDU(), new zf.CompImageHDU({ data: ramp, compression: "RICE_1" })])
  .writeTo("compressed.fits", { overwrite: true });

zf.writeTo("check.fits", new zf.FitsArray(new Float32Array(24), [4, 6]), { overwrite: true, checksum: true });
console.log(zf.verify("check.fits")); // [] when clean

Atomic header edits

Header setters remain eager by default. Group related changes in transaction() to validate and serialize them with one Wasm call and one header commit. The callback is synchronous; callback, validation, and revision-conflict failures leave the file unchanged. Device I/O failures restore the JavaScript header and trigger the core's best-effort disk rollback, but this is not a crash-safe/on-disk journaling transaction.

using hdul = zf.open("image.fits", "update");
hdul.image(0).header.transaction((header) => {
  header.set("OBSERVER", "Hubble", "who");
  header.set("ESO DET CHIP ID", 42); // HIERARCH is serialized by the Zig core
  header.addHistory("calibrated");
});

WCS

Pixel pairs are one-based and ordered (longitude-axis pixel, latitude-axis pixel); the celestial axes need not be FITS Axes 1 and 2.

using hdul = zf.open("wcs.fits");
const [lon, lat] = hdul.image(0).pix2world(40.0, 30.0); // 1-based FITS pixel convention
const [px, py] = hdul.image(0).world2pix(lon, lat);

Typed access without casts

using hdul = zf.open("table.fits");

// 1) Typed accessors assert the HDU flavor (throw FitsTypeError on mismatch).
const img = hdul.image(0).data;        // FitsArray | null
const rec = hdul.table("EVENTS").data; // TableData | null

// 2) Narrow the AnyHDU union on the `kind` discriminant.
for (const hdu of hdul) {
  if (hdu.kind === "bintable" || hdu.kind === "asciitable") console.log(hdu.data?.numRows);
  else console.log(hdu.data?.shape); // image kinds
}

// 3) Name the column shape for fully-typed column/row reads.
const t = hdul.table<{ INDEX: Int32Array; NAME: string[] }>("EVENTS").data!;
const idx = t.get("INDEX"); // Int32Array
const first = t.row(0);     // { INDEX: …, NAME: string }

Browser usage

There is no filesystem in the browser, so the path-based open()/writeTo() are unavailable there — use fromBytes/toBytes with your own fetch/File I/O:

import * as zf from "zigfitsio";

await zf.ready(); // or await zf.ready({ wasm: myBytesOrModule }) to host the .wasm yourself

const bytes = new Uint8Array(await (await fetch("/data/image.fits")).arrayBuffer());
const hdul = zf.fromBytes(bytes);
const img = hdul.image(0).data;
hdul.close();

Bundlers (Vite, webpack 5, esbuild, Rollup) resolve the .wasm asset automatically via the package's new URL(..., import.meta.url) reference and browser export condition — nothing to mark external, in browsers or Electron. Gzipped .fits.gz is a Node/Bun convenience; in the browser, inflate with DecompressionStream("gzip") and pass the result to fromBytes.

Conventions

  • Arrays are native-endian TypedArrays. Images are a flat TypedArray + C-order shape (FitsArray); tables are columnar (TableData, iterable by row); complex values are interleaved float pairs (dtype c8/c16); Header iterates like a Map.
  • 64-bit integers use BigInt64Array/BigUint64Array (values are bigint). Header integers parse to number when exactly representable as a double, else bigint.
  • Scaling is automatic. BSCALE/BZERO and TSCAL/TZEROn are applied on read (scaled data reads as floats); the unsigned convention maps to/from u2/u4/u8 exactly.
  • Close your handles. hdul.close() (flushes first in update/append modes), or using hdul = zf.open(...) — TypeScript transpiles using everywhere; plain JavaScript needs Bun or Node ≥24. There is no GC-based freeing.
  • Errors are typed FitsError subclasses carrying the CFITSIO-compatible status (e.g. KeywordNotFound 202). Unlike Python, KeywordNotFound is not a KeyError — catch the class.

Performance

The FFI calls are synchronous and blocking by design (like better-sqlite3) — simpler and faster for CPU-bound work, at the cost of blocking the event loop during a large read/write. Read only what you need with ImageHDU.section() (bounded memory and time; not available on tile-compressed images), and offload multi-hundred-MB jobs to a Worker, transferring the resulting TypedArray's ArrayBuffer back zero-copy.

Low-level API

zigfitsio/lowlevel exposes every zf_* symbol (see zigfitsio.h) with typed marshalling, check(status), and the struct codecs:

import { lowlevel as ll } from "zigfitsio";
const out = ll.outU64();
ll.check(ll.lib.zf_create_memory(null, out));
// ... raw zf_* calls ...
ll.lib.zf_close(out[0]);

Known limitations

  • Not a CFITSIO drop-in (zf_*, not fits_*).
  • Runs in WebAssembly: the compute-heavy codecs are ~1.5–3× slower than a native build; the module is single-threaded and its heap never shrinks while loaded.
  • In-place update of compressed images, VLA or scaled columns, or a changed row count throws NotSupportedError — use writeTo() to a new file.
  • Tables with duplicate effective column names can be inspected as metadata or copied verbatim, but high-level data access/reconstruction throws FitsTableError (status 219); use low-level indexed column reads when duplicates must be addressed.
  • Integer BLANK/TNULLn values are not auto-masked; float nulls surface as NaN.
  • table<T>() column shapes are compile-time only — the runtime-checked reads are numeric()/strings()/vla()/complex() (throw on kind mismatch).

The full list, including every TS-native-surface boundary, lives in CAVEATS.md §3.

Development

zig build wasm          # build zigfitsio.wasm into zig-out/bin/ (small)
cd bindings/typescript
npm ci
bun test tests          # Bun lane
npx vitest run          # Node lane
npm run build           # clean + tsc -> dist/ + build & copy zigfitsio.wasm

The dev loader honors ZIGFITSIO_WASM and falls back to a zig-out/bin build.

tsc uses TypeScript 7 through the @typescript/native npm alias. The typescript alias supplies the TypeScript 6 API required by TypeDoc, following Microsoft's compatibility setup. Remove the compatibility alias when TypeDoc supports the TypeScript 7 API.

License

MIT — see LICENSE.