Add FITS read/write (astronomy images) (#2956)

* Add FITS read/write codec

FITS (the astronomy format) opens and saves as a flat image: the primary
HDU or first IMAGE extension; gray, or RGB when NAXIS3 = 3; BITPIX 8/16/
32/64/-32/-64 with BSCALE/BZERO/BLANK; rows follow ROWORDER (bottom-up by
default). Camera U16 data stays exact; float data is normalised to 0..1.
Header cards round-trip as text metadata (HIERARCH for long names).
Tile-compressed (fpack) files are refused as unsupported.

Wired into File > Open, Save As and the batch commands as .fits/.fit/.fts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Test FITS open and save through photocraft-io

Gray 16-bit, RGB 8-bit and RGB 32-bit float documents round-trip exactly,
and header cards travel as the document's text metadata.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* FITS: hostile-header regression test (absurd axes, bad BITPIX, no END, HDU chains, overflowing sizes)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
David Moulton
2026-10-11 13:15:22 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Brandon Thomas
parent f8af27d261
commit a35af4e279
15 changed files with 735 additions and 9 deletions
+9
View File
@@ -44,6 +44,7 @@ let out = encode(&img, Format::Tiff, &EncodeOptions::default())?;
| QOI | yes | yes | U8 | RGB, RGBA | yes | no | no | no | no | no | no | `image` |
| OpenEXR | yes | yes | F16, F32 | Y, YA, RGB, RGBA | yes | no | no | no | no | no | no | `exr` |
| Radiance HDR | yes | yes | F32 | RGB | no | no | no | no | no | no | yes (RGBE) | `image` |
| FITS | yes | yes | U8, U16, F32 | Gray, RGB | no | no | no | no | no | yes (header cards) | no | built in |
| AVIF | **no** | only with feature `avif` | U8 | RGB, RGBA | yes | no | no | no | no | no | yes | `image` + `ravif` |
| HEIF/HEIC | with feature `heif` | **no** | U8, U16 (10/12-bit decodes to U16) | RGB, RGBA | yes (auxiliary alpha) | yes (`colr` prof) | yes | yes | no | no | n/a | `heic-rs` |
@@ -62,6 +63,14 @@ the same.
## Documented asymmetries and limitations
* **FITS.** The image in the primary HDU, or the first `IMAGE` extension, is read; `NAXIS3 = 3`
opens as RGB and any other plane count opens the first plane with `DecodeWarning::MorePages`.
Rows follow `ROWORDER` (bottom-up by default, as Siril and DS9 do). Physical values that fit
stay exact as U8/U16 (a camera's `BITPIX = 16`, `BZERO = 32768`); the rest become F32, taken as
normalised when at most 2 and scaled by 65535 (or the maximum) otherwise. Header cards are
kept as text (`HIERARCH` for long names). Tile-compressed (fpack) files are refused with
`CodecError::Unsupported`; Bayer mosaics open undebayered.
* **Camera raw files** (DNG, CR2, NEF, ARW… which are TIFF-structured) are recognised and refused
with `CodecError::Unsupported`: they are sensor data, not flat images. `photocraft-raw` decodes
and develops them, and `photocraft-io` routes them there.