π API documentation
A small, reusable Swift package that wraps CFITSIO (the standard C library for FITS files) with an idiomatic, modern-Swift API: open a FITS file, navigate its HDUs, and read table columns and image data.
It is a generic FITS bridge β it knows nothing about any specific survey or instrument. Domain logic (e.g. assembling a DESI spectrum) belongs in the consumer, built on top of these primitives.
-
macOS 13+ on Apple Silicon, Swift 6 toolchain.
-
CFITSIO via Homebrew:
brew install cfitsio
The
CFITSIOsystem-library target finds the headers and library throughpkg-config. Ifpkg-configcan't locate it, point it at the keg:export PKG_CONFIG_PATH=/opt/homebrew/opt/cfitsio/lib/pkgconfig
FITSFile is an actor: a CFITSIO handle is not thread-safe, so every call
on one file is serialized. Different files are independent actors and run in
parallel. Call sites use try await, and the whole surface uses Swift 6 typed
throws (throws(FITSError)).
import SwiftFITS
let file = try FITSFile(path: "spectrum.fits")
// Navigate HDUs.
let count = try await file.hduCount
try await file.selectHDU(named: "SPECTRUM") // by EXTNAME
// or: try await file.selectHDU(at: 2) // 1-based index
// Read a table column. The element type is inferred from the result β
// no `Type.self` argument. CFITSIO converts the on-disk type as needed.
let channels: [Int32] = try await file.readColumn("CHANNEL")
let rate: [Float] = try await file.readColumn("RATE")
print("rows:", try await file.rowCount, "first channel:", channels.first ?? -1)Reading image data:
try await file.selectHDU(at: 1) // an IMAGE HDU
let dims = try await file.imageDimensions // [fastAxis, rows]
let everything: [Float] = try await file.readImage() // row-major, fast axis contiguous
let oneRow: [Float] = try await file.readImageRow(0) // 0-based row indexFor data you already hold (e.g. an HTTP range-read), open without touching disk:
let bytes: Data = /* fetched however you like */
let file = try FITSFile(data: bytes)The bytes are copied into a buffer the handle owns for its lifetime. (CFITSIO's
in-memory driver keeps internal pointers to that buffer, so SwiftFITS manages
its lifetime for you β see the note in FITSFile.swift.)
For a large file on a server that supports HTTP Range (most do), open it
lazily: CFITSIO seeks to exactly the bytes it needs β the header blocks, the
column you read, the one image row β and SwiftFITS fetches only those spans.
A single object can be pulled from a multi-gigabyte coadd while transferring a
few megabytes.
let url = URL(string: "https://example.org/big-coadd.fits")!
let file = try FITSFile(remoteURL: url) // nothing downloaded yet
try await file.selectHDU(named: "FIBERMAP")
let ids: [Int64] = try await file.readColumn("TARGETID") // fetches ~that column
try await file.selectHDU(named: "B_FLUX")
let row: [Float] = try await file.readImageRow(784) // fetches ~one rowMeasured: reading one DESI spectrum out of the public 1.1 GB DR1 coadd transfers β14 MB (~1.2% of the file).
The transport is pluggable via FITSRangeReader (synchronous random-access byte
source). HTTPRangeReader is the built-in default; supply your own to read from
a CDN, an authenticated endpoint, or a cache. blockSize: (default 1 MiB) tunes
how reads are coalesced into range requests.
Range reads block the calling task while a request is in flight (CFITSIO's I/O is synchronous). Because
FITSFileis an actor, calls on one file are serialized; use separateFITSFiles (each on its own task) to fetch spectra concurrently.
public enum HDUType: Sendable { case image, asciiTable, binaryTable }
public enum FITSError: Error, Sendable, CustomStringConvertible {
case status(code: Int32, message: String) // CFITSIO status + message
case columnNotFound(String)
case typeMismatch
}
public protocol FITSScalar: Sendable { static var fitsTypeCode: Int32 { get } }
// Conformances: Int32, Int64, Float, Double.
/// Synchronous random-access byte source for a remote/custom FITS file.
public protocol FITSRangeReader: Sendable {
func totalLength() throws -> Int
func readRange(offset: Int, count: Int) throws -> Data
}
public struct HTTPRangeReader: FITSRangeReader { /* URLSession + Range requests */ }
public actor FITSFile {
public init(path: String) throws(FITSError)
public init(data: Data) throws(FITSError)
public init(rangeReader: FITSRangeReader, blockSize: Int = 1 << 20) throws(FITSError)
public init(remoteURL: URL, blockSize: Int = 1 << 20) throws(FITSError)
public var hduCount: Int { get throws(FITSError) }
public func selectHDU(named name: String) throws(FITSError)
public func selectHDU(at index: Int) throws(FITSError) // 1-based
public var currentHDUType: HDUType { get throws(FITSError) }
public var rowCount: Int { get throws(FITSError) }
public func columnNumber(named name: String) throws(FITSError) -> Int
public func readColumn<T: FITSScalar>(_ name: String) throws(FITSError) -> [T]
public var imageDimensions: [Int] { get throws(FITSError) }
public func readImage<T: FITSScalar>() throws(FITSError) -> [T]
public func readImageRow<T: FITSScalar>(_ rowIndex: Int) throws(FITSError) -> [T]
}swift testThe default suite runs offline against a bundled fixture
(Tests/SwiftFITSTests/Fixtures/sample.fits) covering HDU navigation, table
column reads (with type conversion), image reads, in-memory open, and error
paths.
An opt-in integration test reads one spectrum out of the real ~1.1 GB DESI DR1 coadd over HTTP range reads (no full download β it transfers β14 MB). It needs network access and is skipped unless enabled:
RUN_DESI_INTEGRATION=1 swift testFull API reference is published at
mnmly.github.io/SwiftFITS, built from
DocC and deployed by .github/workflows/docs.yml on every push to main.
Build the site locally with:
Scripts/build_docs.sh # static site into ./docs
Scripts/build_docs.sh preview # live-reloading local server- Built in Swift 6 language mode with strict concurrency: no warnings, no
@unchecked Sendable, no data-race suppressions. - SwiftFITS calls CFITSIO's canonical
ff*C functions directly. Thefits_*names are C macros that do not import into Swift (some are function-like, e.g.fits_open_file); each call documents itsfits_*equivalent in a comment. - Unmanaged CFITSIO resources are owned by a small RAII class and released in its
deinit, which keeps cleanup portable (no macOS 15.4+isolated deinit) and race-free. - Range reads are implemented by registering a custom CFITSIO I/O driver
(
fits_register_driver) whoseseek/readcallbacks are served by aFITSRangeReader, with block-aligned caching. This reuses all of CFITSIO's parsing β only the bytes it actually reads cross the network.