Task 226: .FITS File Format

Task 226: .FITS File Format

FITS File Format Specifications

The Flexible Image Transport System (FITS) is a standard file format primarily used in astronomy for storing scientific data, such as images, spectra, and tables. It was originally developed in the late 1970s and is maintained by the International Astronomical Union (IAU) FITS Working Group. The official specification is documented in the FITS Standard Version 4.0, published by NASA Goddard Space Flight Center. This version includes support for primary headers, extensions, binary tables, and specific data representations.

List of All Properties Intrinsic to the FITS File Format
Based on the official specifications, the following are the key properties intrinsic to the FITS file format's structure and handling within file systems. These include fixed structural elements, identification markers, and standard behaviors for parsing and storage:

  • File Extension: Typically .fits or .fit.
  • MIME Type: application/fits or image/fits.
  • Format Type: Binary file format with ASCII headers.
  • Byte Order: Big-endian for all multi-byte numerical data.
  • Block Size: Logical records are fixed at 2880 bytes (both headers and data are padded to multiples of 2880 bytes).
  • Header Card Size: Each header record (card) is exactly 80 bytes of ASCII text.
  • File Signature (Magic String): The file always starts with the ASCII string "SIMPLE  =                    T" (padded with spaces to 80 bytes), indicating conformance to the FITS standard.
  • Header Structure: Consists of 80-byte cards in 2880-byte blocks, ending with an "END" card padded with spaces. Cards follow the format: KEYWORD (bytes 1-8), '=' (byte 9), value (bytes 11-80, formatted by type), optional '/' comment.
  • Data Structure: Optional binary data follows the header in 2880-byte blocks, with size determined by BITPIX and NAXIS keywords. Data is stored in row-major order (Fortran-style).
  • Padding Rules: Headers are padded with ASCII spaces (decimal 32) after the END card; data is padded with zeros to complete the last block.
  • Mandatory Keywords for Primary HDU: SIMPLE (logical T), BITPIX (integer specifying bits per pixel: 8, 16, 32, 64, -32, -64), NAXIS (non-negative integer for number of axes), NAXISn (dimensions for each axis n=1 to NAXIS), END (marks header end).
  • Supported Data Types: Based on BITPIX: 8 (unsigned 8-bit integer), 16 (signed 16-bit integer), 32 (signed 32-bit integer), 64 (signed 64-bit integer), -32 (IEEE single-precision float), -64 (IEEE double-precision float).
  • Extensions Support: Optional conforming extensions (e.g., IMAGE, TABLE, BINTABLE) start with XTENSION keyword; primary HDU may have EXTEND=T to indicate possible extensions.
  • Reserved Keywords: 53 total, including templates with 'n' (e.g., NAXISn). Full list: (blank), AUTHOR, BITPIX, BLANK, BLOCKED, BSCALE, BUNIT, BZERO, CDELTn, COMMENT, CROTAn, CRPIXn, CRVALn, CTYPEn, DATAMAX, DATAMIN, DATE, DATE-OBS, END, EPOCH, EQUINOX, EXTEND, GROUPS, PCOUNT, GCOUNT, XTENSION, EXTLEVEL, EXTNAME, EXTVER, SIMPLE, NAXIS, NAXISn, OBJECT, OBSERVER, ORIGIN, REFERENC, INSTRUME, TELESCOP, HISTORY, TFIELDS, TTYPEn, TFORMn, TBCOLn, TDISPn, TDIMn, THEAP, TNULLn, TSCALn, TZEROn, TUNITn, PSCALn, PZEROn, PTYPEn.
  • Scaling Support: Optional BSCALE and BZERO for linear transformation of pixel values to physical units.
  • Special Values: BLANK for undefined integer pixels; IEEE NaN for floating-point undefined values.
  • Character Encoding: Headers use ASCII (7-bit); data is binary.
  • File Integrity: No built-in checksum, but conventions like CHECKSUM and DATASUM can be added.

Two Direct Download Links for .FITS Files

Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .FITS File Properties Dump
This is an embeddable HTML snippet with JavaScript that can be placed in a Ghost blog post. It creates a drop zone for a .FITS file, parses the header to extract all properties (header keywords and values from the list above, if present), and displays them on the screen. It handles only the primary HDU for simplicity and assumes the file is valid.

Drag and drop a .FITS file here

Python Class for .FITS File Handling
This Python class manually decodes a .FITS file, reads the header properties (all reserved keywords found), prints them to console, and can write a new .FITS file with the same or modified properties (simple primary HDU with no data for demonstration).

import struct
import sys

class FitsHandler:
    def __init__(self, filepath):
        self.filepath = filepath
        self.properties = {}
        self.data = None  # Optional data bytes
        self.read()

    def read(self):
        with open(self.filepath, 'rb') as f:
            header = b''
            while True:
                block = f.read(2880)
                if not block: break
                header += block
                if b'END     ' in header: break
            # Parse cards
            for i in range(0, len(header), 80):
                card = header[i:i+80].decode('ascii', errors='ignore')
                if card.startswith('END'): break
                key = card[0:8].strip()
                if '=' in card:
                    value_str = card[10:80].strip()
                    if value_str.startswith("'") and value_str.endswith("'"):
                        value = value_str[1:-1].strip()  # String
                    elif value_str in ('T', 'F'):
                        value = value_str == 'T'  # Logical
                    else:
                        try:
                            value = float(value_str) if '.' in value_str or 'E' in value_str.upper() else int(value_str)
                        except ValueError:
                            value = value_str
                    self.properties[key] = value
            # Read data if present
            bitpix = self.properties.get('BITPIX', 0)
            naxis = self.properties.get('NAXIS', 0)
            size = abs(bitpix) // 8
            for i in range(1, naxis + 1):
                size *= self.properties.get(f'NAXIS{i}', 1)
            self.data = f.read(size)
            # Pad read to block
            pad = (size % 2880)
            if pad: f.read(2880 - pad)

    def print_properties(self):
        for key, value in self.properties.items():
            print(f'{key}: {value}')

    def write(self, output_path):
        header = ''
        for key, value in self.properties.items():
            if isinstance(value, str):
                value_str = f"'{value.ljust(20)}'"
            elif isinstance(value, bool):
                value_str = 'T' if value else 'F'
            else:
                value_str = str(value).rjust(20)
            header += f'{key.ljust(8)}= {value_str} / \n'.ljust(80)
        header += 'END'.ljust(80)
        # Pad header to 2880
        pad = (len(header) % 2880)
        if pad: header += ' ' * (2880 - pad)
        with open(output_path, 'wb') as f:
            f.write(header.encode('ascii'))
            if self.data:
                f.write(self.data)
                data_pad = (len(self.data) % 2880)
                if data_pad: f.write(b'\x00' * (2880 - data_pad))

# Example usage:
# fits = FitsHandler('example.fits')
# fits.print_properties()
# fits.write('output.fits')

Java Class for .FITS File Handling
This Java class manually decodes a .FITS file, reads the header properties, prints them to console, and can write a new .FITS file.

import java.io.*;
import java.util.HashMap;
import java.util.Map;

public class FitsHandler {
    private String filepath;
    private Map<String, Object> properties = new HashMap<>();
    private byte[] data = null;

    public FitsHandler(String filepath) {
        this.filepath = filepath;
        read();
    }

    private void read() {
        try (FileInputStream fis = new FileInputStream(filepath);
             BufferedInputStream bis = new BufferedInputStream(fis)) {
            byte[] headerBlock = new byte[2880];
            ByteArrayOutputStream header = new ByteArrayOutputStream();
            while (true) {
                int len = bis.read(headerBlock);
                if (len <= 0) break;
                header.write(headerBlock, 0, len);
                if (new String(headerBlock).contains("END     ")) break;
            }
            String headerStr = header.toString("ASCII");
            for (int i = 0; i < headerStr.length(); i += 80) {
                String card = headerStr.substring(i, Math.min(i + 80, headerStr.length()));
                if (card.startsWith("END")) break;
                String key = card.substring(0, 8).trim();
                if (card.charAt(8) == '=') {
                    String valueStr = card.substring(10, 80).trim();
                    Object value;
                    if (valueStr.startsWith("'") && valueStr.endsWith("'")) {
                        value = valueStr.substring(1, valueStr.length() - 1).trim();
                    } else if (valueStr.equals("T") || valueStr.equals("F")) {
                        value = valueStr.equals("T");
                    } else {
                        try {
                            value = valueStr.contains(".") || valueStr.toUpperCase().contains("E") ? Double.parseDouble(valueStr) : Integer.parseInt(valueStr);
                        } catch (NumberFormatException e) {
                            value = valueStr;
                        }
                    }
                    properties.put(key, value);
                }
            }
            // Read data
            int bitpix = (int) properties.getOrDefault("BITPIX", 0);
            int naxis = (int) properties.getOrDefault("NAXIS", 0);
            long size = Math.abs(bitpix) / 8;
            for (int j = 1; j <= naxis; j++) {
                size *= (int) properties.getOrDefault("NAXIS" + j, 1);
            }
            data = new byte[(int) size];
            bis.read(data);
            long pad = size % 2880;
            if (pad != 0) bis.skip(2880 - pad);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void printProperties() {
        for (Map.Entry<String, Object> entry : properties.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }

    public void write(String outputPath) {
        try (FileOutputStream fos = new FileOutputStream(outputPath)) {
            StringBuilder header = new StringBuilder();
            for (Map.Entry<String, Object> entry : properties.entrySet()) {
                String key = entry.getKey().formatted("%-8s", entry.getKey());
                String valueStr;
                if (entry.getValue() instanceof String) {
                    valueStr = String.format("'%-20s'", (String) entry.getValue());
                } else if (entry.getValue() instanceof Boolean) {
                    valueStr = ((Boolean) entry.getValue()) ? "T" : "F";
                } else {
                    valueStr = String.format("%20s", entry.getValue().toString());
                }
                header.append(String.format("%s= %s / %n", key, valueStr).formatted("%-80s", ""));
            }
            header.append("END".formatted("%-80s", "END"));
            int pad = header.length() % 2880;
            if (pad != 0) header.append(" ".repeat(2880 - pad));
            fos.write(header.toString().getBytes("ASCII"));
            if (data != null) {
                fos.write(data);
                int dataPad = data.length % 2880;
                if (dataPad != 0) fos.write(new byte[2880 - dataPad]);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // Example usage:
    // public static void main(String[] args) {
    //     FitsHandler fits = new FitsHandler("example.fits");
    //     fits.printProperties();
    //     fits.write("output.fits");
    // }
}

JavaScript Class for .FITS File Handling
This JavaScript class (for Node.js) manually decodes a .FITS file, reads the header properties, prints them to console, and can write a new .FITS file using fs module.

const fs = require('fs');

class FitsHandler {
  constructor(filepath) {
    this.filepath = filepath;
    this.properties = {};
    this.data = null;
    this.read();
  }

  read() {
    const buffer = fs.readFileSync(this.filepath);
    let offset = 0;
    let header = '';
    while (true) {
      const block = buffer.slice(offset, offset + 2880).toString('ascii');
      header += block;
      offset += 2880;
      if (block.includes('END     ')) break;
    }
    const cards = header.match(/.{1,80}/g) || [];
    for (let card of cards) {
      if (card.startsWith('END')) break;
      const key = card.slice(0, 8).trim();
      if (card[8] === '=') {
        let valueStr = card.slice(10, 80).trim();
        let value;
        if (valueStr.startsWith("'") && valueStr.endsWith("'")) {
          value = valueStr.slice(1, -1).trim();
        } else if (valueStr === 'T' || valueStr === 'F') {
          value = valueStr === 'T';
        } else {
          value = isNaN(parseFloat(valueStr)) ? valueStr : parseFloat(valueStr);
        }
        this.properties[key] = value;
      }
    }
    // Read data
    const bitpix = this.properties.BITPIX || 0;
    const naxis = this.properties.NAXIS || 0;
    let size = Math.abs(bitpix) / 8;
    for (let i = 1; i <= naxis; i++) {
      size *= this.properties[`NAXIS${i}`] || 1;
    }
    this.data = buffer.slice(offset, offset + size);
    // Pad offset implied
  }

  printProperties() {
    console.log(this.properties);
  }

  write(outputPath) {
    let header = '';
    for (let [key, value] of Object.entries(this.properties)) {
      let valueStr;
      if (typeof value === 'string') {
        valueStr = `'${value.padEnd(20)}'`;
      } else if (typeof value === 'boolean') {
        valueStr = value ? 'T' : 'F';
      } else {
        valueStr = value.toString().padStart(20);
      }
      header += `${key.padEnd(8)}= ${valueStr} / \n`.padEnd(80);
    }
    header += 'END'.padEnd(80);
    const pad = 2880 - (header.length % 2880);
    if (pad < 2880) header += ' '.repeat(pad);
    let outputBuffer = Buffer.from(header, 'ascii');
    if (this.data) {
      outputBuffer = Buffer.concat([outputBuffer, this.data]);
      const dataPad = 2880 - (this.data.length % 2880);
      if (dataPad < 2880) outputBuffer = Buffer.concat([outputBuffer, Buffer.alloc(dataPad)]);
    }
    fs.writeFileSync(outputPath, outputBuffer);
  }
}

// Example usage:
// const fits = new FitsHandler('example.fits');
// fits.printProperties();
// fits.write('output.fits');

C++ Class for .FITS File Handling
This C++ class manually decodes a .FITS file, reads the header properties, prints them to console, and can write a new .FITS file.

#include <iostream>
#include <fstream>
#include <map>
#include <string>
#include <vector>
#include <iomanip>
#include <sstream>
#include <cmath>

class FitsHandler {
private:
    std::string filepath;
    std::map<std::string, std::string> properties; // Store as string for simplicity
    std::vector<char> data;

public:
    FitsHandler(const std::string& fp) : filepath(fp) {
        read();
    }

    void read() {
        std::ifstream file(filepath, std::ios::binary);
        if (!file) return;
        std::string header;
        char block[2880];
        while (true) {
            file.read(block, 2880);
            header.append(block, 2880);
            if (header.find("END     ") != std::string::npos) break;
        }
        for (size_t i = 0; i < header.size(); i += 80) {
            std::string card = header.substr(i, 80);
            if (card.substr(0, 3) == "END") break;
            std::string key = card.substr(0, 8);
            key.erase(key.find_last_not_of(" ") + 1);
            if (card[8] == '=') {
                std::string valueStr = card.substr(10, 70);
                valueStr.erase(0, valueStr.find_first_not_of(" "));
                valueStr.erase(valueStr.find_last_not_of(" ") + 1);
                // Simple parse to string
                properties[key] = valueStr;
            }
        }
        // Read data
        int bitpix = std::stoi(properties["BITPIX"]);
        int naxis = std::stoi(properties["NAXIS"]);
        size_t size = std::abs(bitpix) / 8;
        for (int i = 1; i <= naxis; ++i) {
            std::string axKey = "NAXIS" + std::to_string(i);
            size *= std::stoi(properties[axKey]);
        }
        data.resize(size);
        file.read(data.data(), size);
        size_t pad = size % 2880;
        if (pad) file.ignore(2880 - pad);
        file.close();
    }

    void printProperties() {
        for (const auto& pair : properties) {
            std::cout << pair.first << ": " << pair.second << std::endl;
        }
    }

    void write(const std::string& outputPath) {
        std::ofstream file(outputPath, std::ios::binary);
        std::string header;
        for (const auto& pair : properties) {
            std::ostringstream oss;
            oss << std::left << std::setw(8) << pair.first << "= " << std::setw(20) << std::right << pair.second << " / ";
            std::string card = oss.str().substr(0, 80);
            if (card.length() < 80) card += std::string(80 - card.length(), ' ');
            header += card;
        }
        header += std::string("END") + std::string(77, ' ');
        size_t pad = 2880 - (header.length() % 2880);
        if (pad < 2880) header += std::string(pad, ' ');
        file << header;
        if (!data.empty()) {
            file.write(data.data(), data.size());
            size_t dataPad = 2880 - (data.size() % 2880);
            if (dataPad < 2880) {
                std::vector<char> zeroPad(dataPad, 0);
                file.write(zeroPad.data(), dataPad);
            }
        }
        file.close();
    }
};

// Example usage:
// int main() {
//     FitsHandler fits("example.fits");
//     fits.printProperties();
//     fits.write("output.fits");
//     return 0;
// }