FITS Image Library for Swift.
This library provides a simple interface to read and write FITS files in Swift, based on the FITS 4.0 standard.
SwiftFITS supports both reading and writing. It parses existing FITS files into their header/data structure and serializes them back to standards-compliant bytes. A compliant file round-trips byte-for-byte; you can also build new files from scratch, and edit parsed files in place — only the sections you modify are re-rendered, while every untouched section keeps its original bytes.
SwiftFITS targets the base FITS 4.0 standard. The following properties are intentional, not latent surprises:
- Keyword names may contain only
A–Z,0–9,_and-, and must be left-justified in the 8-byte keyword field (a leading space is rejected). - Long-keyword conventions are out of scope:
HIERARCHand similar ESO conventions are treated as ordinary 8-byte keywords, not expanded. OnlyCONTINUE/HISTORY/COMMENTmulti-record merging is supported. - Mandatory keywords (
SIMPLE/XTENSION,BITPIX,NAXIS,NAXISn, andPCOUNT/GCOUNTfor extensions) must appear in their exact standard order and index — this is enforced even in lenient mode. - Section layout is geometry-driven: each header's declared geometry
(
|BITPIX|/8 × GCOUNT × (PCOUNT + ∏ NAXISn), with random groups handled) determines exactly how many data blocks follow, rather than guessing from byte content. Trailing all-blank padding is preserved. ENDis excluded from a section'spropertiesbut is retained in the raw bytes, so a compliant file round-trips byte-for-byte throughFITSFile.data.- Strict vs. lenient:
.strictrejects technically-noncompliant input, while.lenient(the default) tolerates common real-world deviations (unknown value types, trailing characters after a string's closing quote, non-printable header text, data-length mismatches, a missing space after the=value indicator, lowercasee/dfloat exponents, NUL-padded headers, a truncated trailing partial block, non-blank records after theENDmarker, NUL padding in value/comment fields, and orphanedCONTINUErecords). - Serialization is symmetric:
FITSSerializationOptionsmirrors the parsing options, with.strictand.lenientpresets. On write, mandatory keywords,BITPIX/NAXISgeometry and each data segment's size are validated —.strictrejects any mismatch, while.lenienttolerates a data-size mismatch and coerces keyword case. The library manages theENDmarker and 2880-byte padding; the caller owns the mandatory keywords and their order. - Not thread-safe:
FITSFile,FITSSectionandFITSBlockcarry mutable state and cache structural facts lazily on read, so they are notSendableand must not be shared across threads without external synchronization.
SwiftFITS is written in pure Swift and depends only on Foundation — no third-party dependencies. It is developed, built and tested on macOS (see the CI badge above). Being Foundation-only, the library is expected to be portable to Linux; Linux CI and SwiftPM-on-Linux verification are not yet in place and are tracked as future work.
SwiftFITS ships a Package.swift and can be consumed as a Swift package. Add it to your
dependencies:
.package( url: "https://github.com/macmade/SwiftFITS.git", branch: "main" )The Xcode project (SwiftFITS.xcodeproj) is also provided for development.
This project uses submodules.
To clone it, use the following command:
git clone --recursive https://github.com/macmade/SwiftFITS.gitimport Foundation
import SwiftFITS
do
{
let file = try FITSFile( url: URL( fileURLWithPath: "/path/to/file.fits" ), options: .lenient )
if let header = file.header
{
print( header.properties )
}
}
catch // SwiftFITS.FITSError
{
print( error )
}Build a file from scratch — a primary image HDU plus an optional extension — and write it out. The
mandatory keywords (SIMPLE/XTENSION, BITPIX, NAXIS, NAXISn, …) are populated for you:
import Foundation
import SwiftFITS
// A 3x2 8-bit image: six bytes of pixel data.
let file = try FITSFile( bitpix: 8, axes: [ 3, 2 ], data: Data( [ 1, 2, 3, 4, 5, 6 ] ) )
// Append a 4x4 16-bit image extension (4 * 4 * 2 = 32 bytes of data).
try file.appendExtension( type: "IMAGE", bitpix: 16, axes: [ 4, 4 ], data: Data( count: 32 ) )
try file.write( to: URL( fileURLWithPath: "/path/to/out.fits" ), options: .strict )Edit a parsed file and write it back. Only the sections you touch are re-rendered; everything else is preserved byte-for-byte:
let file = try FITSFile( url: URL( fileURLWithPath: "/path/to/file.fits" ), options: .lenient )
// Set or replace a keyword on the primary header.
try file.header?.setProperty( FITSProperty( name: "OBJECT", string: "M42", options: .strict ) )
// Reshape the primary data (512x512 16-bit = 512 * 512 * 2 bytes), keeping its
// geometry keywords in sync.
let pixels = Data( count: 512 * 512 * 2 )
try file.setPrimaryData( bitpix: 16, axes: [ 512, 512 ], data: pixels )
// Serialize to bytes, or write straight to disk.
let bytes = try file.serializedData( options: .lenient )
try file.write( to: URL( fileURLWithPath: "/path/to/edited.fits" ), options: .lenient )Project is released under the terms of the MIT License.
Owner: Jean-David Gadina - XS-Labs
Web: www.xs-labs.com
Blog: www.noxeos.com
Twitter: @macmade
GitHub: github.com/macmade
LinkedIn: ch.linkedin.com/in/macmade/
StackOverflow: stackoverflow.com/users/182676/macmade