Pure-Go FITS library for reading, writing, and processing astronomical data files.
This library is not trying to replace cfitsio, wcslib, healpy, or any other established C/C++/Python implementation. Those tools have decades of production mileage and are the right choice for many workflows. Our goal is to bring Go onto level footing — giving Go programs native access to the same FITS capabilities without cgo, while staying as compatible as possible with the files and conventions those reference implementations define.
Algorithms are ported from the reference source (cfitsio for I/O and
compression, wcslib 8.10 for WCS, healpix_cxx — healpy's backend — for
HEALPix, Siril 1.4.0 for statistics and stretches) and checked against
that reference. Where our output differs from the reference, we treat it
as a bug in our code, unless the difference is a deliberate, documented
choice (for example, refusing a header that wcslib would silently
misread). Every such difference is listed in
docs/VALIDATION.md.
This library is 100% scientific fidelity. It returns the bytes, values, and
types exactly as the file stores them. It does not auto-scale, auto-window,
auto-percentile, auto-display, silently coerce types, or silently map
NaN/BLANK to zero. Where it cannot represent something exactly, it returns
an error rather than a wrong value. Any interpretive helper that rescales,
reshapes, or transforms data for display belongs in a sibling package
(stretch, stats) — never in the root fits package.
One deliberate exception: fits.ReadFloat32, ReadFrame and
ReadFrameRGB are processing-pipeline helpers that normalise integer data
to [0, 1] float32 (the Siril convention). Their exact rules (BLANK to NaN,
rounding to the float32 nearest the exact value, errors for BSCALE = 0,
float32 overflow and a BSCALE / BZERO / BLANK card that is not a valid
value card; a duplicated scaling card is applied as cfitsio and astropy
apply it: the last card on a plain image, the first on a tile-compressed
one) are in the ReadFloat32 doc.
Every capability below is backed by a committed test that compares this
library against an independent truth source — libcfitsio 4.7.0,
fpack/funpack, fitsverify, astropy 8.0.2, wcslib 8.10, WCSTools 3.9.7,
healpy 1.20.1 / healpix_cxx 8.3.0, Siril 1.4.0 compiled from source,
OpenCV 5.0.0, numpy 2.4.6, mpmath — using committed samples and goldens
that are regenerated by committed harnesses.
docs/VALIDATION.md is the ledger: capability →
proving test → truth source and version → sample files → tolerance, plus
the list of what is not supported or not yet proven, and how to
regenerate every golden. TestValidationLedger fails if the ledger names
a test or sample that does not exist.
go test ./... # needs only Go
go test -tags interop ./... # needs the reference tools belowgo test ./... needs nothing but Go and the files committed in the
repository: no C compiler, no make, no cfitsio, fpack, funpack, fitsverify,
wcslib or WCSTools program, no Python, no environment variables, no
network. Every test compares the library with committed goldens, samples
and recorded tool readings. The proofs still hold there: where a test
judges a file the library writes by an external reader (cfitsio,
fitsverify, funpack, astropy, wcslib), the reader's reading of exactly
those bytes was recorded when the tools verified them (testdata/recorded),
so the test's assertions run on the real reading, and a writer that
produced different bytes fails. No skip is caused by a missing tool; the
few skips left are platform ones (editing on Windows, a file system
without POSIX ACLs).
go test -tags interop ./... is for golden regeneration and
development: it runs the reference programs live (and the *Reproduce*
tests regenerate every golden). It needs the C harnesses built
(make -C <dir> for each Makefile in the tree, against libcfitsio
4.7.0, wcslib 8.10, healpix_cxx, glib2 and OpenCV), fitsverify, fpack,
funpack, xy2sky/sky2xy (WCSTools 3.9.7), HPXcvt, go,
qemu-aarch64-static, and the environment variables
FITS_ASTROPY_PYTHON (a Python with numpy 2.4.6, astropy 8.0.2, healpy,
mpmath and opencv-python-headless; the repo venv),
FITS_SIRIL_SRC (Siril 1.4.0 source tree), FITS_HEALPIX_CXX
(healpix_cxx 8.3.0 source) and FITS_SIRIL_CLI (a siril-cli 1.4.0,
stretch/testdata/build_siril_cli.sh). A missing one is a failure that
names it. FITS_RECORD=1 re-records the readings of the tests that pass.
docs/VALIDATION.md ("Test tiers") has the details.
A missing committed golden or sample is a failure in both tiers.
- HDU framing matches cfitsio (
fits_get_hduaddrll) for every sample, including random groups (sized per FITS eq. 4, then refused withErrRandomGroups), strayNAXISnbeyondNAXIS, trailing zero/junk blocks and a missing final pad (File.TrailingBytesreports the ignored bytes), and an extension of a type the library does not decode (XTENSION = 'FOREIGN', framed like cfitsio, then refused withErrUnsupportedExtension). A truncated trailingXTENSIONheader is an error (stricter than cfitsio, like astropy). A malformed non-structural card does not make a file unopenable;HeaderOfreturns the parse error.HDUByNamereturns the first HDU (primary included) whose EXTNAME matches case-sensitively. Header lookups are case-insensitive and act on the first card of a duplicated keyword, as cfitsio's. The exception is image scaling, which follows one rule on every image path (ReadPixels,ReadPixelsMasked,CanConvert,ImageHDU.BSCALE/BZERO/BLANK,ReadFloat32,ReadFrame,AsImage,OverwritePixels,WriteImage's caller cards, tile-compressed images): of duplicated BSCALE/BZERO/BLANK cards the last applies on a plain image and the first on a tile-compressed header, as both cfitsio and astropy read them; a card the two references read differently isErrInvalidScaling(a value that is not a valid FITS number, a lowercase or misplaced-=card,HIERARCH BSCALE/BZERO/BLANK, a BLANK outside the BITPIX range or equal to 1234554321). - Image read (
ReadPixels,ReadPixelsMasked,ReadSubset,CanConvert) for every BITPIX: float targets equal cfitsio's TFLOAT/TDOUBLE reads bit for bit (beyond float32 range we return ±Inf, cfitsio ±FLT_MAX); integer targets are accepted only when they hold every possible physical value exactly (otherwiseErrTypeMismatch), and are computed in 128-bit integer arithmetic, so int64 and uint64 (BZERO 2^63) are exact. The BLANK/NaN mask equals cfitsio's null flags. - Image write (
WriteImage,AppendImage,OverwritePixels): the data unit is byte-identical to cfitsio's for every BITPIX and convention in a 53-case matrix (uint16/32/64 via BZERO 2^15/2^31/2^63, int8 via BZERO -128, caller BSCALE/BZERO including negative and non-integer, up to both edges of the stored range). One exception: BITPIX 64 with an integral BZERO other than 0 and 2^63 stores v - BZERO exactly, where cfitsio rounds it through a double (e.g. 2^62 + 1 with BZERO 1000). A value that cannot be stored is never clamped:WriteImage/OverwritePixelsreturnErrNumOverflowand write nothing, exactly where cfitsio reports NUM_OVERFLOW, and also where cfitsio stores a wrong value silently (BITPIX 64 scaled value of exactly 2^63, float scaled to ±Inf). Header bytes are not compared with cfitsio's. cfitsio, fitsverify and astropy read the written files back identically. - CHECKSUM/DATASUM:
ChecksumStatus/VerifyChecksumagree withfits_verify_chksum;WriteChecksumoutput verifies in cfitsio and fitsverify. - Header cards decode exactly as cfitsio reads them (key, value, type,
comment) for every card of every sample, including malformed cards (kept
as
TypeUnparsedwithRawValue), integers beyond int64 (TypeBigInt, read withHeader.BigInt;IntreturnsErrIntOverflow), CONTINUE long strings and HIERARCH. Unmodified headers re-encode byte for byte (Flush,CopyHDUWithHeader). Cards copied from a file into a new HDU keep their original bytes (Card.RawCards). Cards built in memory are read back with identical values by cfitsio and astropy, or the write returns an error. Trailing blanks of string values, value-card comments and commentary text are not significant in FITS and are not written (cfitsio, astropy and this package drop them on read). fitsverify reports no errors for headers built withSet/Add/AddCardand for cards copied from a file. A caller header with a decoded lowercase (non-HIERARCH) keyword is refused withheader.ErrInvalidKeywordbyWriteImage,AppendBinaryTableandAppendCompressedImage(readers disagree on such a card); upper-case the copy's Key to write it. - Header write rules: keywords longer than 8 characters need
HIERARCHand HIERARCH paths need single blanks (ErrInvalidKeyword); non-ASCII text is refused (ErrInvalidText);CONTINUEcannot be set; COMMENT/HISTORY take one text; a CONTINUE'd string ending in&is refused; NaN/±Inf are refused; aRawValueis accepted only when it reads back unchanged;Header.AddCardadds a card under the same rules; Encode refuses cards that would join or split on read. The writers return an error for an invalid in-memory card or non-ASCII column Name/Unit/Display; they never drop a card silently.
- Binary tables: every TFORM (
L X B I J K A E D C M, scalar and vector,P/Qvariable-length arrays of every element type), TNULL, TSCAL/TZERO including the unsigned conventions, read and written. Every numeric column of every sample is read as every Go target type and compared with the exact expected outcome: integer targets never truncate or wrap (ErrTypeMismatch) on the exact paths; integer TZERO literals are read exactly even where no double holds them. String arrays (TDIM(w, d2, ...)andrAwsubstring arrays) are read per element withReadStringArrayColumn; writing them, and layouts the references disagree on, are errors. Not supported: TSCAL/TZERO on complex columns (an error), a general TSCAL inverse on write (with TScal/TZero the caller writes stored values). Under TSCAL 1 and an integer TZERO, float targets get the exact value correctly rounded (float32 targets: the float32 nearest it); integer targets on the float64 scaling path get the exact value orErrTypeMismatch. - ASCII tables read (exact integers at any size, implied decimals, TNULL
per §7.2.2) and write (
AppendASCIITable; Fw.d/Ew.d/Dw.d, TNULL; no TSCAL/TZERO/TDISP).
OpenForEdit+Flush: a same-size header rewrite uses an undo journal (<name>.journal, pre-image + CRC; removing it is the commit point). A header resize, andEditFile, write<name>.fits-tmp, fsync it, rename it over the file and fsync the directory. After a process crash at any step, the nextOpenForEditorEditFilerecovers the file to exactly the original or exactly the result; until then a read-onlyOpenfails withErrPendingJournal(for any journal, including one from an older release).OpenForEditandEditFilereportErrLegacyJournalfor a journal written by an older release, which cannot be recovered automatically. Power-loss safety rests on the fsync order (proven by call order, not by a power cut).OverwritePixelsand appends in edit mode run under the same undo journal. Editing works on Linux and macOS (ErrEditUnsupportedelsewhere, e.g. Windows); on macOS every sync isF_FULLFSYNCand an ACL is not carried over by the temp-file rewrite (seedocs/VALIDATION.md). A secondOpenForEdit/EditFileof the same file getsErrLocked; an in-place write (Flushwithout a resize,OverwritePixels, an append in edit mode) while the file is open for reading through this package getsErrOpenForReadingand writes nothing.CreaterefusesErrPendingJournal,ErrLockedandErrOpenForReadingand holds the edit lock untilClose. The locks are advisory: programs that do not use this package are not stopped.Closedoes not flush; useFlushorCloseAndFlush.FlushandCopyHDUWithHeaderrefuse structural keyword changes, any card the encoder cannot represent exactly, and a HIERARCH card whose path is a standard keyword name (HIERARCH BSCALE: cfitsio reads it as BSCALE, astropy does not), leaving the file byte-identical; every writer refuses such a card too. A card named END that is not the END card (HIERARCH END = ...included) isErrMalformedEND.HDUByNameuses the first EXTNAME card of each HDU. It differs from cfitsio'sfits_movnam_hdu: the match is case-sensitive, there is no HDUNAME fallback, a non-string EXTNAME is no name, so it can return a different, later HDU than cfitsio.EditFileconverts HDUs that change position exactly like cfitsiofits_copy_hdu(including its EXTEND / standard-COMMENT rules; a converted header drops reserved blank cards). New images go throughWriteImage(out, ...)and become IMAGE extensions after position 0; aWritercannot create tables.CopyHDUWithHeaderrefuses a changed data size or HDU kind.EditFilerefuses empty output, keeps the file mode and follows symlinks. Tile-compressed HDUs can be copied.cmd/fitsdumpexits 1 when an HDU, a header or a table's columns cannot be decoded (reported in place).cmd/fitscopycopies byte for byte, exits 3 when the copy differs and 2 when input and output are the same file.
- Read with
ReadPixelsCompressed[T]/ReadPixelsCompressedMasked[T](ReadPixelstakes*ImageHDU): bit-exact against libcfitsio 4.7.0 over a 148-case fpack/libcfitsio corpus (plus 12 ZBITPIX 64 cases) with a sample for every algorithm x ZBITPIX x BZERO convention x dither cell cfitsio can write — RICE_1 (and itsRICE_ONEalias), GZIP_1, GZIP_2, HCOMPRESS_1 (including SMOOTH and lossy 32-bit), PLIO_1 (including the uint16 +32768 storage); BITPIX 8/16/32/-32/-64; BSCALE/BZERO incl. uint16/uint32/int8; SUBTRACTIVE_DITHER_1/2, NO_DITHER, ZQUANTIZ NONE; ZBLANK, BLANK and per-tile ZBLANK nulls; 3-D tiles; 4-D and 6-D images; GZIP_COMPRESSED_DATA and UNCOMPRESSED_DATA fallbacks (in fallback tiles of scaled images we follow funpack/astropy, wherefits_read_imgis inconsistent); tile integers wider or narrower than ZBITPIX, as old cfitsio wrote them (GZIP width from the inflated length, RICE_1 without ZVAL2 = BYTEPIX 4), converted to ZBITPIX as cfitsio does. ZBITPIX 64 is read with GZIP_1, GZIP_2, NOCOMPRESS, RICE_1, HCOMPRESS_1 and PLIO_1. The reader also decodes all 91 images cfitsio'sfits_img_compresswrote for the writer-parity goldens exactly as cfitsio does (incl. the dither random-sequence wrap in 10000-pixel tiles). Integer targets follow theReadPixelsrule (refused withErrTypeMismatchunless they hold every physical value). ZCMPTYPE is matched case-sensitively, as cfitsio does. A ZBITPIX that is no BITPIX, a ZNAXISn below 1, an HCOMPRESS_1 reconstruction outside int32 (cfitsio wraps it), HCOMPRESS_1/PLIO_1 values the ZBITPIX type cannot hold (except 16-bit lossy HCOMPRESS, which is clipped as funpack and cfitsio's TSHORT read do; cfitsio's TUSHORT read of a uint16 image refuses it with status 412), ZNAXIS outside 1..6, ZTILEn <= 0, a missing COMPRESSED_DATA column, a GZIP tile whose inflated length is not 1/2/4/8 x its pixels (or a tile width no sample proves), tile values the ZBITPIX type cannot hold, RICE_1 BYTEPIX 8 and PLIO_1 with BZERO 32768 at ZBITPIX 8/32 are refused, and so is tiled TABLE compression (ZTABLE = T,ErrTiledTableCompression). NOCOMPRESS is decoded but has no cfitsio truth (astropy only). Real files in the corpus: astropy's M13 (a 2006 SkyView int16 image) in GZIP_1 (4-byte tiles), RICE_1 (BYTEPIX 4), HCOMPRESS_1 and PLIO_1, and two other astropy test files. Open gap: no modern camera/mission.fzfile (DECam, HSC, ...) is committed yet; a row-tiled one can be cut by whole tiles, as the M13 files were. - Write (
AppendCompressedImage,AppendCompressedFloat32Image,AppendCompressedFloat64Image): RICE_1, GZIP_1, GZIP_2, HCOMPRESS_1 and PLIO_1 for uint8/int8/int16/uint16/int32/uint32, 1-D to 6-D (cfitsio's MAX_COMPRESS_DIM; more axes are refused), and float32 with quantization (SUBTRACTIVE_DITHER_1/2, NO_DITHER, ZDITHER0 1..10000; NaN and ±Inf stored as nulls, ±0/denormals as +0, as cfitsio does), and float64 the same way. Output is the same compressed image as cfitsio'sfits_img_compressfor 91 golden images incl. 4-D and 6-D (RICE/HCOMPRESS/PLIO byte-identical, GZIP inflated-identical) and 19 of the 20 float64 goldens (the 20th is a cfitsio overflow that this writer stores losslessly); 5 cfitsio refusals (incl. 7 axes) and 2 deliberate refusals are refused too. Where cfitsio's quantizer overflows (a tiny absolute QLevel, the float rounding of the centered zero point, a pixel landing on the null sentinel), cfitsio 4.7.0 writes wrong pixels with status 0; this writer stores those tiles losslessly instead. Nullcheck compares NullValue with the raw pixel value, asfits_write_imgnulldoes; the float64 writer takes NullValue64 only, and the other width's null value is refused. Shape errors (data length, empty shape, non-positive axes, tile rank, non-positive tile axes) are refused before anything is written (the length withErrShapeMismatch). On a freshCreate, an empty primary is written in front of the first extension. Every written variant passes fitsverify and decodes identically in cfitsio, funpack and astropy. Not supported: 64-bit integers (cfitsio cannot write them), float HCOMPRESS/PLIO, noise-relative HCOMPRESS scale, PLIO uint16, NOCOMPRESS write. PLIO takes values 0..2^24-1; HCOMPRESS needs 2-D tiles ≥ 4 px (edge tiles too).
- Projections: all 28 codes wcslib 8.10 implements (Paper II's 26
including AIR, plus HPX and XPH), plus the AIPS aliases NCP (→ SIN) and
GLS (→ SFL) translated as wcslib
wcssetdoes, ported from wcslibprj.c. The trigonometry is Go's math functions under wcslib's wcstrig structure (exact results at multiples of 90°, cancellation-free asin/acos), so values can differ from wcslib's (which calls glibc) in the last bits. Each projection is checked point by point against wcslib in both directions within the stated budget (1e-11 deg; a row the projection makes ill-conditioned, next to a pole, horizon or cube-face centre, within its own libm sensitivity), with every accept/reject boundary pinned to 1e-7 deg, through the full header pipeline, and XPH and HPX also against their defining papers. Accept/reject decisions are identical to wcslib's except at boundary-ambiguous points, where a few-ULP nudge of the input flips the answer. - Header pipeline (
wcs.Parse→transform.New): checked against wcslib 8.10 in both directions, including rejection status, on real HST ACS/WFC3, JWST NIRCam, Spitzer IRAC, astrometry.net, ZTF and PTF headers (SIP and TPV), ROSAT (GLON-AIT) and MSX (GLON-CAR) survey headers, and Calabretta's Paper II example maps, DSS plate solutions (CNPIX/PPO/PLT*/AMDX/AMDY) applied as wcslib 8.10 does (including a real STScI DSS header), SIP keywords applied with or without the-SIPsuffix, as wcslib does, except on aRA---TPVheader: there (the TPV + redundant SIP form of ATLAS images, proven on a real ATLAS header) the SIP keywords are ignored, as astropy does, where wcslib applies both and moves every position by 13-14 px;RA---TPV-SIPapplies both; plus synthetic cases for LONPOLE/LATPOLE/PVi_0..4, CROTA2 (precedence PC > CD > CROTA2), CUNIT (deg/arcmin/arcsec/mas/rad), PC+CD (PC wins), partial CD, swapped axes, cubes, WCSAXESa and alternate WCS, SLON/SLAT and HLON/HLAT pairs,-SIPon projections other than TAN, and the defunct PC00i00j / CD00i00j / PROJPn keywords (read as wcslib's WCSHDR_all and astropy do; a real Pan-STARRS skycell header among them). TPV in PC+CDELT form follows wcslib; a missing TPV PVi_1 defaults to 1 per the TPV registry (wcslib uses 0). SIP of orders 7 and 9, TPV with every PVi_0..PVi_39 set and ZPN of degree 12 and with PV2_29 are checked against wcslib too.PixelToSky(p1, p2)takes the celestial pixel axes in increasing axis order. Also checked against astropy throughwcs.Parse. wcs.Parseerrors on wrong-typed WCS keywords, invalid SIP orders (not an integer 0..9), partial SIP declarations, SIP coefficient cards with a multi-digit index or of degree > 9 (wcslib refuses the latter too), non-numeric or above-order non-zero SIP coefficients, and SIP keywords on a TNX/ZPX header.transform.Newrefuses what wcslib refuses too (PVi_m with m > 4 on the longitude axis, TPV PVi_40+, theta0 outside [-90, 90]) and, stricter than wcslib (which computes something): CPDIS/CQDIS/D2IM lookup tables,-SIPon alternate descriptions,-SIPon one celestial axis only, unknown CTYPE distortion codes, wcslib's TPU alias, matrices that couple non-celestial pixel axes into RA/DEC, TPV with non-degree CUNIT, a singular CD, and a defunct PC00i00j / CD00i00j / PROJPn card that disagrees with its modern twin. Accepted although wcslib 8.10 refuses: SIP without AP/BP (or with one of them empty) and a DSS header whose PLATEID card follows the plate keywords; both give wcslib's answer for the equivalent header.- TNX (IRAF; Chebyshev, Legendre and polynomial surfaces) through
transform.NewTNX(*header.Header), proven against WCSTools 3.9.7: Chebyshev and polynomial surfaces on real CTIO 4 m Mosaic II and KPNO 4 m Mosaic-1 headers, Legendre (type 2) on synthetic headers only (no public real Legendre header was found). Headers that WCSTools and wcslib read differently are refused with an error: SIP keywords on a TNX header (stricter than wcslib on purpose: wcslib would apply SIP + TAN and ignore the WAT correction, WCSTools applies TNX), CD together with PC, a partial CDELT/PC set (one CDELT alone, or a PC matrix without CDELT1/PC1_1, when there is no CD), CD00i00j without CDi_j, PVi_m that would move the fiducial point (longitude-axis PVi_m, latitude PVi_m with no correction surface), CUNIT other than deg, lowercase CTYPE, LONPOLE > 360, and WAT gaps, duplicates, CONTINUE or strings over 1999 characters. - Sky frames: ICRS and FK5 J2000 ↔ galactic and supergalactic, ICRS ↔ IAU 2006 mean ecliptic of date, FK5 precession (IAU 1976), FK4 B1950 (with E-terms) ↔ FK5 J2000, checked point by point against astropy 8.0.2/ERFA goldens to 1e-8 arcsec. Directions only (no proper motion); FK4 at equinox B1950 only.
RING (any nside) and NESTED (power of 2) up to nside 2^29, following
healpix_cxx, validated against healpy 1.20.1 and the real healpix_cxx 8.3.0,
including the cos/sin switch-over points. Ang2Pix/Vec2Pix use Go's
math.Cos/Sin/Atan2; fed the correctly rounded values the references were
made with, every pixel equals healpix_cxx's exactly, and with Go's libm
every pixel equals healpy's except at boundary-ambiguous directions
(within a few ULP of a pixel boundary, where a few-ULP nudge changes the
pixel), which the tests count and cap.
stats:MeanStdevSiril,Percentile/Median/MAD(float32 path) andFilterNonZeroare bit-identical to Siril 1.4.0 compiled from its source;SigmaClipfollowsastropy.stats.sigma_clip(maxIter <= 0iterates until stable with no cap, astropymaxiters=None;NIteris reported).BuildHistogramreturns(Histogram, error): its counts and edges are bit-identical tonumpy.histogram, including numpy's one-off index corrections at bin edges, and it returnsErrHistogramRangewherever numpy raises.Histogram.Percentileis the left-continuous inverse of the piecewise-linear CDF, cross-checked againstscipy.stats.rv_histogram.ppf.Percentile/Median/MADreturn NaN on ±Inf data or a max-min that overflows.MeanStdevis a float64 one-pass sample stdev (N−1) that agrees with numpystd(ddof=1)within the one-pass rounding bound and loses precision on large offsets (1e9 + {0,1} gives 95.83 on amd64 and 96.15 on arm64, not 0.50025; its low bits depend on the build's FMA fusion, each build pinned);MeanStdevTwoPassis the accurate generic version (its mean is correctly rounded).stretch: autostretch (linked/unlinked), MTF, asinh (incl.-human), linear and GHT are bit-exact against Siril 1.4.0 (GHT allows glibc's 1-ULP logf/powf/expf difference), including NaN, ±Inf and negative input; CLAHE matches Siril's OpenCV 5.0.0 pipeline, including OpenCV's x86-64 clip-limit conversion (clip*tilePixels/65536 >= 2^31 or +Inf acts as limit 1, NaN or clip <= 0 as no limit). Only float32 image paths are ported. Parameters:LinearreturnsErrLinearParams(BP outside [0,1] or NaN);Autostretch/AutostretchUnlinked/AutostretchLinkedreturnErrAutostretchParams(targetBG outside [0,1] or NaN, NaN shadows clip) and the*Paramsfunctions panic on the same arguments;GHTreturnsErrGHTParams(outside D in [0,10], B in [-5,15], 0 <= LP <= SP <= HP <= 1, or NaN;GHTParams.HPZeroasks for HP = 0, since HP: 0 means Siril's default 1);Asinh/AsinhRGB/AsinhRGBHumanreturnErrAsinhParams(NaN or infinite beta or offset, beta < 0, offset >= 1; beta in [0,1) is accepted as in Siril's GUI);MTFreturnsErrMTFParams(NaN or infinite parameter, midtones outside [0,1]; other finite shadows/highlights and midtones 0/1 are accepted as Siril's autostretch uses them).CLAHEhas no error result (it panics on a bad size or tile < 1).NormalizeChannelsleaves ±Inf out of its range and maps them to 1 / 0.
AsImage/image.Decode: 2-D BITPIX 8 (unscaled) → image.Gray, BITPIX 16
with BZERO 32768 → image.Gray16, float BITPIX → FloatGray when every
value is exactly representable in float32. BSCALE = 0, BLANK pixels, an
invalid or malformed last BSCALE/BZERO/BLANK card and everything else →
ErrNotImageCompatible. No rescaling.
Breaking changes against v1 master (d4c5067), as reported by apidiff.
They ship in a v1 minor release (the module path stays
github.com/dmarkham/fits); each one is fixed at the call site by handling
the added return value:
fits:(*ImageHDU).BSCALE,(*ImageHDU).BZERO,(*CompressedImageHDU).BSCALEand(*CompressedImageHDU).BZEROnow return(float64, error); the error wrapsErrInvalidScalingfor a card the scaling-card rule refuses.(*CompressedImageHDU).BSCALEof a header with BSCALE = 0 now returns 0 (it returned 1).fits:VarColumn[T Numeric]is nowVarColumn[T any](it also holds the bool results ofReadVarLogicalColumn/ReadVarBitColumnand complex elements).header:Cardis no longer comparable (it carries the CONTINUE cards it was decoded from); compare fields instead of using==.stats:BuildHistogram(data, nbins)now returns(Histogram, error)(ErrHistogramRangewhere numpy raises).stretch:Asinh,AsinhRGB,Autostretch,AutostretchLinked,GHT,LinearandMTFnow return anerror(ErrAsinhParams,ErrAutostretchParams,ErrGHTParams,ErrLinearParams,ErrMTFParams) for parameters outside the ranges listed above.
Additions: (*ImageHDU).BLANK() (int64, bool, error),
stretch.AsinhRGBHuman, stretch.AutostretchUnlinked. For valid inputs
the values are unchanged except where the old code was wrong (see
docs/VALIDATION.md).
- Go 1.24 or later.
- Tested on Linux: amd64 (GOAMD64=v1 and v3) and arm64 (under qemu-aarch64); every bit-exact claim holds on all three, because the library wraps float products in explicit conversions so the compiler cannot fuse them into FMA instructions. Other architectures are not tested.
- macOS (arm64 and amd64): supported, including editing, with the same
lock and crash-safety guarantees as Linux, to be proven by running the
whole suite on a real Mac (pending; see
docs/VALIDATION.md). Windows and other systems: reading and writing new files compile but are not tested; editing (OpenForEdit,EditFile) is refused withErrEditUnsupported. - New dependency for macOS:
golang.org/x/sys(extended-attribute calls, which the standard library lacks on darwin).
*File is not safe for concurrent use by multiple goroutines. Callers
that want parallelism must coordinate externally (one *File per goroutine,
or an external mutex).
See plan.md for the original design and scope.
MIT. See LICENSE.