fits

package module
v1.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 14, 2026 License: MIT Imports: 24 Imported by: 0

README

fits

Pure-Go FITS library for reading, writing, and processing astronomical data files.

Philosophy

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.

Every algorithm is ported from the reference source (cfitsio, wcslib, astrometry.net, Siril) and cross-validated against it. Where our output differs from the reference, we treat that as a bug in our code, not a difference of opinion.

Guiding principle

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. Any interpretive helper that rescales, reshapes, or transforms data for display belongs in a sibling package or a separate module — never in the root fits package.

Goals

  • Pure Go, no cgo, standard library only.
  • Idiomatic Go: io.ReadSeeker, error returns, method-based API, generics for typed pixel and column access.
  • image.Image compatibility for the narrow cases where it is lossless.
  • Context support on blocking I/O.

Capabilities

  • Read/write primary images and IMAGE extensions with every standard BITPIX (±8/16/32/64 int, -32/-64 float). BSCALE/BZERO scaling applied transparently.
  • Binary tables with every scalar TFORM type, vector (repeat>1) columns, and variable-length arrays (P/Q descriptors + heap).
  • ASCII tables (read).
  • Header with full FITS v4 keyword compliance: fixed/free format, CONTINUE long strings, HIERARCH non-standard, all value types.
  • In-place edit surface: header mutation with journaled tail-shift crash safety, same-shape pixel overwrite, HDU append at EOF.
  • Streaming rebuild edit surface (EditFile): structural changes via temp file + atomic rename.
  • image.Image adapter for 2D integer BITPIX + FloatGray for float BITPIX (no auto-scaling — see guiding principle).
  • World Coordinate System (fits/wcs + fits/wcs/transform): all 27 projections from Paper II + HEALPix, SIP/TPV/TNX distortion, sky-frame conversions (ICRS/FK5/FK4/galactic/ecliptic/supergalactic), cross-validated against wcslib/astropy.
  • Tile compression (fits/compress): both read AND write for every algorithm in the Pence et al. 2010 FITS tile compression convention — RICE_1, GZIP_1, GZIP_2, HCOMPRESS_1, PLIO_1, and NOCOMPRESS. Read-side cross-validated byte-exact against astropy-generated fixtures; write- side cross-validated by having astropy read back Go-written files (all 6 algorithms pass).
  • Float tile compression with per-tile quantization (RICE_1, GZIP_1, GZIP_2) via the cfitsio fits_quantize_float algorithm ported bit-exactly to Go and validated against live libcfitsio on 11 golden fixtures. Supports NO_DITHER, SUBTRACTIVE_DITHER_1, and SUBTRACTIVE_DITHER_2 via the IRAF Park-Miller PRNG. NaN handling (ZBLANK) and GZIP_COMPRESSED_DATA fallback for constant tiles both cross-validated against astropy. Covers JWST, HST, Gaia, Planck, WMAP, and every other mission using tile-compressed FITS.

Requirements

  • Go 1.23 or later (generics + iter.Seq2).
  • Linux, macOS, and Windows. WASM is not supported (the edit surface requires a real filesystem; read-only via OpenReader / OpenReadAll may work but is untested).

Concurrency

*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).

Cross-validation

The library is validated against two independent reference implementations:

  • cfitsio (C, HEASARC). Byte-for-byte round-trip against every fixture in cfitsio's own test suite. Float quantization ported with a C reference harness (compress/testdata/cref/) that links libcfitsio and diffs results against the Go port; 11/11 fixtures agree bit-exactly on noise estimation, quantized int32 output, and bscale/bzero.
  • astropy / wcslib (Python). Every one of the 27 WCS projections and all 6 sky frames round-trip within numerical tolerance against live astropy.wcs. All 6 tile compression algorithms survive a Go → astropy round trip, including float quantization with SUBTRACTIVE_DITHER_2 exact-zero preservation and the GZIP_COMPRESSED_DATA fallback column.

See plan.md for the full design and scope.

License

MIT. See LICENSE.

Documentation

Overview

Package fits implements a pure-Go reader/writer for the FITS (Flexible Image Transport System) file format as defined by the FITS Standard v4.0.

This file defines the public error sentinels and typed errors.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrEmptyFile is returned when an input stream is zero bytes.
	ErrEmptyFile = errors.New("fits: empty file")

	// ErrNotFITS is returned when the first block does not begin with
	// "SIMPLE  =".
	ErrNotFITS = errors.New("fits: not a FITS file")

	// ErrKeyNotFound is returned by typed keyword getters when the key is
	// absent from a header.
	ErrKeyNotFound = errors.New("fits: key not found")

	// ErrNotImageHDU is returned when an image operation is attempted on a
	// non-image HDU.
	ErrNotImageHDU = errors.New("fits: not an image HDU")

	// ErrNotImageCompatible is returned by AsImage() for HDUs that cannot be
	// losslessly represented as an image.Image (multi-dim, complex BITPIX,
	// etc.).
	ErrNotImageCompatible = errors.New("fits: not image.Image-compatible")

	// ErrTypeMismatch is returned when a generic read is asked for a target
	// type that cannot losslessly represent the on-disk type.
	ErrTypeMismatch = errors.New("fits: type mismatch")

	// ErrReadOnly is returned when a write or edit operation is attempted on
	// a read-only *File.
	ErrReadOnly = errors.New("fits: file is read-only")

	// ErrShapeMismatch is returned when OverwritePixels or a similar
	// shape-preserving call receives data of the wrong length or dimension.
	ErrShapeMismatch = errors.New("fits: shape mismatch")

	// ErrRandomGroups is returned for random-groups (§6) HDUs, which
	// this library does not support by design — random groups are a
	// legacy radio-interferometry format superseded by binary tables.
	ErrRandomGroups = errors.New("fits: random-groups HDUs not supported")
)

Sentinel errors — match via errors.Is.

View Source
var FloatGrayModel color.Model = floatGrayModel{}

FloatGrayModel is the color model for FloatGray images.

Functions

func CanConvert

func CanConvert[T Numeric](h *ImageHDU) bool

CanConvert reports whether ReadPixels[T] on h would succeed without returning ErrTypeMismatch. It does not touch the file content.

func Decode

func Decode(r io.Reader) (image.Image, error)

Decode implements image.Decode for FITS files. Returns ErrNotImageCompatible if the primary HDU (or first HDU) is not losslessly representable as an image.Image.

func DecodeConfig

func DecodeConfig(r io.Reader) (image.Config, error)

DecodeConfig implements image.DecodeConfig for FITS files.

func EditFile

func EditFile(path string, fn func(in *File, out *Writer) error) error

EditFile provides a streaming-rebuild surface for structural changes (image resize, row/column insert/delete, HDU insert-in-middle, HDU delete, VLA heap reorganization).

It opens src read-only, creates a temp file in the same directory, passes the caller an (in, writer) pair, fsyncs on success, and atomically renames the temp file over src. On any error (callback returns err or panic) the temp file is removed and src is untouched.

Crash-safe by construction (temp + rename on same filesystem).

See plan decision 5.8.

func EditFileContext

func EditFileContext(ctx context.Context, path string, fn func(in *File, out *Writer) error) error

EditFileContext is EditFile with an explicit context.

func OverwritePixels

func OverwritePixels[T Numeric](h *ImageHDU, data []T) error

OverwritePixels writes new pixel values over the existing data allocation of an ImageHDU. It requires ModeEdit; ModeRead returns ErrReadOnly.

The call fails with:

  • ErrReadOnly if the parent file is not in ModeEdit.
  • ErrShapeMismatch if len(data) != NAXIS1 × NAXIS2 × ... for the HDU.
  • A wrapped ErrTypeMismatch if T does not correspond exactly to the HDU's BITPIX. Writing requires an exact-width match — we do not round or clip.

OverwritePixels does NOT resize the HDU; resize operations must go through EditFile.

func ReadASCIIColumn

func ReadASCIIColumn[T ColumnValue](t *ASCIITableHDU, col int) ([]T, error)

ReadASCIIColumn reads a scalar column from an ASCII table. For string columns (TFORM 'A') T must be ~string; for integer and float columns T must be a Numeric type.

func ReadColumn

func ReadColumn[T ColumnValue](t *BinaryTableHDU, col int) ([]T, error)

ReadColumn reads a scalar column (Repeat==1) across all rows and returns one value per row as a []T. For Char columns (TFORM "A") use T=string. For Logical columns use T=bool. For numeric columns use any Numeric T that can hold the column type (same rules as CanConvert on images).

TSCAL/TZERO are applied to numeric columns (BSCALE/BZERO analogue).

func ReadFloat32 added in v1.2.0

func ReadFloat32(h HDU) ([]float32, error)

ReadFloat32 reads the image pixels as float32 normalized to [0, 1]. Accepts both *ImageHDU and *CompressedImageHDU — callers do not need to type-switch.

For integer BITPIX (8, 16, 32, 64), the normalization maps the full physical range (after BSCALE/BZERO) into [0, 1]:

normalized = (physical - physMin) / (physMax - physMin)

The raw range per BITPIX:

BITPIX=8:  [0, 255]        → phys via BSCALE/BZERO
BITPIX=16: [-32768, 32767] → commonly [0, 65535] with BZERO=32768
BITPIX=32: [-2^31, 2^31-1] → phys via BSCALE/BZERO
BITPIX=64: [-2^63, 2^63-1] → phys via BSCALE/BZERO

For float BITPIX (-32, -64), values are returned as-is after BSCALE/BZERO application — assumed already in [0, 1] per the Siril convention used by astrophotography processing pipelines.

This is the standard entry point for processing pipelines (stacking, debayering, calibration, background extraction, etc.) that operate on normalized float32 data.

func ReadFrame added in v1.2.1

func ReadFrame(path string) (pix []float32, width, height int, err error)

ReadFrame reads a 2D mono image from a FITS file path and returns the pixels normalized to [0, 1] via ReadFloat32.

Returned dims follow the FITS convention: width == NAXIS1 (columns, the fastest-varying axis), height == NAXIS2 (rows). Pixels are row-major: index pixel (x, y) as pix[y*width + x].

Accepts:

  • NAXIS == 2
  • NAXIS == 3 with NAXIS3 == 1 (degenerate single-plane cube)

The image is taken from the primary HDU if it has image data, otherwise from HDU 1 (the standard tile-compressed layout). Tile-compressed images are supported transparently. Integer BITPIX is normalized to [0, 1]; float BITPIX passes through unchanged. NaN and BLANK pixels are not masked — callers who need that should use ReadPixelsMasked directly.

Returns an error for NAXIS != 2 (or NAXIS == 3 with NAXIS3 != 1), with a hint pointing at ReadFrameRGB for 3-plane color cubes.

func ReadFrameRGB added in v1.2.1

func ReadFrameRGB(path string) (r, g, b []float32, width, height int, err error)

ReadFrameRGB reads a 3D planar RGB image (NAXIS=3, NAXIS3=3) from a FITS file path and returns the three color planes normalized to [0, 1] via ReadFloat32.

Returned dims follow the FITS convention: width == NAXIS1 (columns, the fastest-varying axis), height == NAXIS2 (rows). Each plane is row-major: index pixel (x, y) as r[y*width + x] (and likewise for g, b).

Layout on disk is R plane, then G plane, then B plane (NAXIS1 fastest, NAXIS3=3 slowest) — the standard FITS/Siril/astropy convention.

HDU selection, normalization, and masking semantics match ReadFrame. Returns an error for NAXIS != 3 or NAXIS3 != 3.

func ReadPixels

func ReadPixels[T Numeric](h *ImageHDU) ([]T, error)

ReadPixels reads every pixel of h into a single flat slice of type T, applying BSCALE/BZERO. Returns an error (wrapping ErrTypeMismatch) if the requested type cannot losslessly hold the file's data. Row-major order matches the FITS on-disk layout: the first axis varies fastest.

func ReadPixelsCompressed

func ReadPixelsCompressed[T Numeric](h *CompressedImageHDU) ([]T, error)

ReadPixelsCompressed reads and decompresses every tile of h, returning a flat slice of pixels in row-major order matching the logical image shape (Shape()). BSCALE/BZERO from the Z-prefixed keywords are applied as with a regular ImageHDU.

This is the compressed counterpart to ReadPixels[T] for regular *ImageHDU. Users who open a file via fits.Open do not need to call this directly — they can continue to use the uniform ReadPixels[T] form and the library dispatches to the compressed path automatically.

func ReadPixelsCompressedContext

func ReadPixelsCompressedContext[T Numeric](ctx context.Context, h *CompressedImageHDU) ([]T, error)

ReadPixelsCompressedContext is ReadPixelsCompressed with an explicit context.

func ReadPixelsContext

func ReadPixelsContext[T Numeric](ctx context.Context, h *ImageHDU) ([]T, error)

ReadPixelsContext is ReadPixels with an explicit context. The context is consulted before the read and can be used to abort long reads.

func ReadPixelsMasked

func ReadPixelsMasked[T Numeric](h *ImageHDU) ([]T, []bool, error)

ReadPixelsMasked is like ReadPixels but also returns a per-pixel validity mask. A pixel is invalid (mask[i] == false) if the underlying raw value equals BLANK (integer BITPIX only) or is IEEE NaN (float BITPIX). BSCALE/BZERO are applied to valid pixels only; invalid pixels are left at the zero value of T.

func ReadSubset

func ReadSubset[T Numeric](h *ImageHDU, lower, upper, stride []int64) ([]T, error)

ReadSubset reads an n-dimensional hyperslab from h.

lower, upper, and stride each must have length equal to h.NAXIS(). The slab selected is the half-open interval [lower[i], upper[i]) along each axis, stepping by stride[i] (stride==1 means contiguous). Coordinates are 0-based and match the order of h.Shape() (axis 0 varies fastest on disk).

The output slice has length Π ceil((upper[i]-lower[i]) / stride[i]) and is laid out row-major matching the same axis order as Shape() — i.e. the first axis is the fastest-varying dimension, as on disk.

Unlike ReadPixels, ReadSubset does not issue one big read. It computes a per-row byte range along axis 0 and reads each row independently, seeking between them. For stride[0]==1 and contiguous axis-0 runs this is near-optimal; for large strided axes it issues more seeks than an all-at-once read would.

BSCALE/BZERO are applied as in ReadPixels.

func ReadVectorColumn

func ReadVectorColumn[T Numeric](t *BinaryTableHDU, col int) ([]T, error)

ReadVectorColumn reads a fixed-width vector column (Repeat>1) and returns a flat slice of length nrows*repeat in row-major order.

func VerifyChecksum

func VerifyChecksum(h HDU) error

VerifyChecksum verifies the CHECKSUM and DATASUM keywords on an HDU if they are present. It returns nil if both verify (or if they are absent). If one or both are present but wrong, it returns an error identifying which failed.

Algorithm (FITS Appendix J):

  • DATASUM is the 32-bit 1's-complement checksum of the data section only, encoded as an ASCII integer.
  • CHECKSUM is the 16-char ASCII-encoded 1's-complement checksum such that the combined (header + data) checksum sums to all-ones.

func WriteChecksum

func WriteChecksum(h HDU) (dataSum, checkSum uint32, err error)

WriteChecksum updates (or creates) the CHECKSUM and DATASUM cards on an HDU in edit mode. It must be called BEFORE Flush so that the re-serialized header contains the correct cards. The computed values are returned for inspection.

Checksums are not computed automatically on Close — callers must opt in via WriteChecksum per HDU. This keeps the strict-fidelity promise: users get byte-exact round-trip unless they explicitly ask for mutation.

func WriteMono added in v1.2.1

func WriteMono(path string, pix []float32, width, height int) error

WriteMono writes a 2D mono float32 image to a FITS file at path.

The output is a BITPIX=-32 primary HDU with shape [width, height] (NAXIS1=width, NAXIS2=height). Input pixels are row-major — pixel (x, y) at pix[y*width + x] — matching the layout ReadFrame returns. Values are written verbatim (no scaling, no clamping). Any existing file at path is truncated.

func WriteRGB added in v1.2.1

func WriteRGB(path string, r, g, b []float32, width, height int) error

WriteRGB writes a 3D planar RGB float32 image to a FITS file at path.

The output is a BITPIX=-32 primary HDU with shape [width, height, 3] (NAXIS1=width, NAXIS2=height, NAXIS3=3). The three planes are concatenated in R, G, B order (NAXIS1 fastest, NAXIS3=3 slowest) — the standard FITS/Siril/astropy convention. Each input plane is row-major — pixel (x, y) at r[y*width + x] (and likewise for g, b) — matching the layout ReadFrameRGB returns. Values are written verbatim (no scaling, no clamping). Any existing file at path is truncated.

Types

type ASCIITableHDU

type ASCIITableHDU struct {
	// contains filtered or unexported fields
}

ASCIITableHDU represents an ASCII TABLE extension (§7.2).

func (*ASCIITableHDU) ColumnByName

func (h *ASCIITableHDU) ColumnByName(name string) (Column, bool)

ColumnByName returns the column whose TTYPE matches name.

func (*ASCIITableHDU) Columns

func (h *ASCIITableHDU) Columns() ([]Column, error)

Columns returns the column metadata for the ASCII table.

func (*ASCIITableHDU) Compressed

func (*ASCIITableHDU) Compressed() bool

Compressed returns false — ASCII tables are never compressed.

func (*ASCIITableHDU) CompressionType

func (*ASCIITableHDU) CompressionType() string

CompressionType returns the empty string for ASCII tables.

func (*ASCIITableHDU) Header

func (h *ASCIITableHDU) Header() *header.Header

Header returns the parsed header (lazy).

func (*ASCIITableHDU) Index

func (h *ASCIITableHDU) Index() int

Index returns the 0-based HDU index.

func (*ASCIITableHDU) NumRows

func (h *ASCIITableHDU) NumRows() int64

NumRows returns the number of rows from NAXIS2.

func (*ASCIITableHDU) Type

func (h *ASCIITableHDU) Type() HDUType

Type returns TypeASCIITable.

type BinaryTableHDU

type BinaryTableHDU struct {
	// contains filtered or unexported fields
}

BinaryTableHDU represents a BINTABLE extension (§7.3).

func AppendBinaryTable

func AppendBinaryTable(f *File, hdr *header.Header, cols []ColumnData) (*BinaryTableHDU, error)

AppendBinaryTable appends a new BINTABLE HDU to the file. All columns must have the same row count. The TFORM code is inferred from the DataXxx slice that is populated (or from an explicit .TForm if set).

func (*BinaryTableHDU) ColumnByName

func (h *BinaryTableHDU) ColumnByName(name string) (Column, bool)

ColumnByName returns the column whose TTYPE matches name, or ok=false. The comparison is case-sensitive.

func (*BinaryTableHDU) ColumnIndex

func (h *BinaryTableHDU) ColumnIndex(name string) int

ColumnIndex returns the 1-based column index for name, or 0 if absent.

func (*BinaryTableHDU) Columns

func (h *BinaryTableHDU) Columns() ([]Column, error)

Columns returns the ordered column metadata for the binary table. The result is parsed lazily on first call and cached.

func (*BinaryTableHDU) Compressed

func (*BinaryTableHDU) Compressed() bool

Compressed returns false — plain binary tables are not compressed. (CompressedImageHDU is a separate type backed by a binary table with ZIMAGE=T, not a BinaryTableHDU.)

func (*BinaryTableHDU) CompressionType

func (*BinaryTableHDU) CompressionType() string

CompressionType returns the empty string for plain binary tables.

func (*BinaryTableHDU) Header

func (h *BinaryTableHDU) Header() *header.Header

Header returns the parsed header (lazy).

func (*BinaryTableHDU) Index

func (h *BinaryTableHDU) Index() int

Index returns the 0-based HDU index.

func (*BinaryTableHDU) NumRows

func (h *BinaryTableHDU) NumRows() int64

NumRows returns the number of rows from NAXIS2.

func (*BinaryTableHDU) RowBytes

func (h *BinaryTableHDU) RowBytes(row int64) ([]byte, error)

RowBytes returns the raw bytes of one row from the table data area. The returned slice is a copy safe to retain.

func (*BinaryTableHDU) Type

func (h *BinaryTableHDU) Type() HDUType

Type returns TypeBinaryTable.

type Column

type Column struct {
	Index   int    // 1-based per FITS convention
	Name    string // TTYPEn
	Unit    string // TUNITn
	TForm   string // raw TFORMn
	Repeat  int64
	Type    ColumnType
	Scale   float64 // TSCALn, default 1
	Zero    float64 // TZEROn, default 0
	Null    any     // TNULLn if set
	Display string  // TDISPn
	Dim     []int64 // TDIMn if set
	// contains filtered or unexported fields
}

Column describes a single table column.

type ColumnData

type ColumnData struct {
	Name    string
	Unit    string
	Display string
	TForm   string // optional override; if empty, inferred from Data*
	Dim     []int64

	// One of the following (mutually exclusive). Fixed-width columns take a
	// flat slice; variable-length array (VLA) columns take a slice of
	// slices, one row per outer element.
	DataUint8   []uint8
	DataInt16   []int16
	DataInt32   []int32
	DataInt64   []int64
	DataFloat32 []float32
	DataFloat64 []float64
	DataString  []string // for Char columns; all entries must be the same length
	DataBool    []bool

	// Variable-length arrays (§7.3.5). Each outer element is one row; the
	// inner slice length may differ per row. The writer emits a 1P<type>
	// descriptor column and appends payloads to the heap area.
	DataVarUint8   [][]uint8
	DataVarInt16   [][]int16
	DataVarInt32   [][]int32
	DataVarInt64   [][]int64
	DataVarFloat32 [][]float32
	DataVarFloat64 [][]float64
}

ColumnData describes a single column to write into a new binary table. Exactly one of Data* fields may be set; the type selects the TFORM code.

type ColumnType

type ColumnType int

ColumnType enumerates the supported table column types.

const (
	ColByte ColumnType = iota
	ColInt16
	ColInt32
	ColInt64
	ColFloat32
	ColFloat64
	ColString
	ColLogical
	ColBit
	ColComplex64
	ColComplex128
	ColVarArray
)

type ColumnValue

type ColumnValue interface {
	~uint8 | ~int8 | ~int16 | ~uint16 | ~int32 | ~uint32 |
		~int64 | ~uint64 | ~float32 | ~float64 |
		~string | ~bool
}

ColumnValue is the constraint for the typed ReadColumn API. It accepts every Numeric type plus string and bool for Char/Logical columns.

type CompressFloatOptions

type CompressFloatOptions struct {
	// Algorithm is the compression method. Must be one of RICE_1,
	// GZIP_1, GZIP_2, or NoCompress — HCOMPRESS_1 and PLIO_1 do not
	// support float input in the compressed image convention.
	// Default: RICE_1.
	Algorithm compress.Algorithm

	// TileShape picks per-tile dimensions, same convention as
	// CompressOptions.TileShape. Default: whole-row tiles.
	TileShape []int64

	// BlockSize is the RICE_1 block size. Default 32.
	BlockSize int

	// QLevel is the quantization level. Positive values set the
	// step as sigma/QLevel; negative values use an absolute step of
	// -QLevel. Default 4.0.
	QLevel float32

	// DitherMethod selects SUBTRACTIVE_DITHER_1 (default),
	// SUBTRACTIVE_DITHER_2 (zero-preserving), or NoDither.
	DitherMethod compress.DitherMethod

	// ZDither0 is the per-image dither seed written as the ZDITHER0
	// keyword. Default 1.
	ZDither0 int

	// NullValue is the float sentinel for null pixels. Default: -1e30.
	// The writer substitutes NaN inputs with this value before
	// quantization, and the quantizer emits NullValueInt32 at those
	// positions. The sentinel is also written as the ZBLANK header.
	NullValue float32

	// Nullcheck, if true, enables null detection. If false, NaN in
	// the input will produce undefined quantized output — the caller
	// is asserting there are no nulls. Default false.
	Nullcheck bool
}

CompressFloatOptions configures a tile-compressed float image write. Fields have sensible defaults so the zero value is usable for the common case (RICE_1, whole-row tiles, qlevel=4, SUBTRACTIVE_DITHER_1).

type CompressOptions

type CompressOptions struct {
	// Algorithm is the compression method. Defaults to RICE_1.
	Algorithm compress.Algorithm

	// TileShape is the per-tile dimensions in the same order as the
	// image's logical shape (first axis varies fastest). A zero or nil
	// value picks the cfitsio default: ZTILE1 = NAXIS1, ZTILE2..n = 1
	// (whole-row tiles).
	TileShape []int64

	// BlockSize is the RICE_1 block size. Default 32.
	BlockSize int

	// HCompressScale is the HCOMPRESS_1 quantization scale. 0 = lossless.
	HCompressScale int

	// ZDither0 is the dither seed (for float images compressed via
	// RICE_1 or GZIP with quantize_level > 0). Default 1.
	ZDither0 int
}

CompressOptions configures a tile-compressed image write. Fields have sensible defaults so the zero value is usable for the common case (RICE_1, whole-row tiles, lossless).

type CompressedImageHDU

type CompressedImageHDU struct {
	// contains filtered or unexported fields
}

CompressedImageHDU represents a tile-compressed image stored as a binary table HDU with ZIMAGE=T per the "Compressed Images" FITS convention (Pence et al. 2010).

From the user's point of view, a CompressedImageHDU behaves exactly like an ImageHDU: the same ReadPixels[T] / ReadPixelsContext[T] / ReadPixelsMasked[T] / BITPIX / NAXIS / Shape / Header methods are available, and the decompression pipeline runs transparently under the covers. Callers who care can ask Compressed() to tell them it's backed by tiles rather than raw bytes.

The physical backing is a binary table with:

  • COMPRESSED_DATA VLA column — one row per tile, carrying the compressed byte stream for that tile.
  • UNCOMPRESSED_DATA VLA column (optional) — fallback raw bytes for tiles that could not be compressed better than raw.
  • GZIP_COMPRESSED_DATA VLA column (optional) — secondary fallback when the primary algorithm fails and gzip is used instead.
  • ZSCALE, ZZERO columns (for float data) — per-tile quantization parameters.
  • ZBLANK column (optional) — per-tile null sentinel.

func AppendCompressedFloat32Image

func AppendCompressedFloat32Image(f *File, hdr *header.Header, shape []int64, data []float32, opts CompressFloatOptions) (*CompressedImageHDU, error)

AppendCompressedFloat32Image writes a float32 image HDU with tile compression. The pixel values are quantized to int32 per tile (using cfitsio's fits_quantize_float algorithm, ported to Go), then the integer tiles are fed through the chosen integer compression algorithm (RICE_1, GZIP_1, GZIP_2, or NoCompress).

This is lossy compression: the precision of reconstructed pixels is approximately sigma/QLevel, where sigma is the per-tile noise estimate. Tiles that cannot be quantized (e.g. constant-value regions) automatically fall back to gzip compression of the raw float bytes via the GZIP_COMPRESSED_DATA column, recovering those pixels bit-exactly on read.

The resulting HDU is a BINTABLE with ZIMAGE=T, ZQUANTIZ set to the dither method name, plus per-tile ZSCALE / ZZERO columns. Open() transparently decompresses it back to float32 on read.

func AppendCompressedImage

func AppendCompressedImage[T Numeric](f *File, hdr *header.Header, shape []int64, data []T, opts CompressOptions) (*CompressedImageHDU, error)

AppendCompressedImage writes a new tile-compressed image HDU to the destination file. For integer BITPIX the compression is lossless. For float BITPIX callers should use AppendCompressedFloat32Image / AppendCompressedFloat64Image which handle quantization explicitly.

This is the write counterpart of the read-side CompressedImageHDU. The resulting HDU is a binary table with ZIMAGE=T and is automatically recognized by Open() when re-reading the file.

func (*CompressedImageHDU) BITPIX

func (h *CompressedImageHDU) BITPIX() int

BITPIX returns the logical BITPIX of the uncompressed image (from ZBITPIX), NOT the BITPIX=8 of the wrapping binary table.

func (*CompressedImageHDU) BSCALE

func (h *CompressedImageHDU) BSCALE() float64

BSCALE returns the BSCALE from the compressed image's Z-prefixed keyword (ZBSCALE) or falls back to BSCALE. Defaults to 1.0.

func (*CompressedImageHDU) BZERO

func (h *CompressedImageHDU) BZERO() float64

BZERO returns the BZERO for the uncompressed image. Defaults to 0.

func (*CompressedImageHDU) Compressed

func (h *CompressedImageHDU) Compressed() bool

Compressed always returns true for a CompressedImageHDU.

func (*CompressedImageHDU) CompressionType

func (h *CompressedImageHDU) CompressionType() string

CompressionType returns the ZCMPTYPE value (e.g. "RICE_1").

func (*CompressedImageHDU) Header

func (h *CompressedImageHDU) Header() *header.Header

Header returns the parsed header of the underlying binary table. Callers see every keyword on the HDU, including the Z* compression keywords. To access the "logical" image header (what the uncompressed image would have looked like), reconstruct it from the Z-prefixed keywords via the compressedMetadata fields below.

func (*CompressedImageHDU) Index

func (h *CompressedImageHDU) Index() int

Index returns the 0-based HDU index within the parent file.

func (*CompressedImageHDU) NAXIS

func (h *CompressedImageHDU) NAXIS() int

NAXIS returns the logical NAXIS of the uncompressed image (ZNAXIS).

func (*CompressedImageHDU) Shape

func (h *CompressedImageHDU) Shape() []int64

Shape returns the logical image dimensions (ZNAXIS1..ZNAXISn).

func (*CompressedImageHDU) Type

func (h *CompressedImageHDU) Type() HDUType

Type returns TypeImage — the compressed HDU presents an image-shaped view to callers regardless of its physical binary-table backing.

type ErrMissingRequiredKeyword

type ErrMissingRequiredKeyword struct {
	HDU     int
	Keyword string
}

ErrMissingRequiredKeyword describes a missing mandatory keyword.

func (*ErrMissingRequiredKeyword) Error

func (e *ErrMissingRequiredKeyword) Error() string

type ErrTruncatedData

type ErrTruncatedData struct {
	HDU      int
	Expected int64
	Got      int64
}

ErrTruncatedData describes a data section that ends before the declared data size.

func (*ErrTruncatedData) Error

func (e *ErrTruncatedData) Error() string

type ErrUnterminatedHeader

type ErrUnterminatedHeader struct {
	HDU          int
	BytesScanned int64
}

ErrUnterminatedHeader describes a header that does not contain an END card within a generous block limit.

func (*ErrUnterminatedHeader) Error

func (e *ErrUnterminatedHeader) Error() string

type File

type File struct {
	// contains filtered or unexported fields
}

File is a handle to an open FITS file. Not safe for concurrent use by multiple goroutines; see the package doc for the concurrency model.

func Create

func Create(name string) (*File, error)

Create opens a new file for writing, truncating any existing contents. The returned *File is in ModeCreate and accepts HDU appends through the Append* family of functions. Callers must call Close() to finalize the trailing block pad.

func CreateContext

func CreateContext(ctx context.Context, name string) (*File, error)

CreateContext is Create with an explicit context.

func CreateWriter

func CreateWriter(rws io.ReadWriteSeeker) (*File, error)

CreateWriter opens a FITS writer over an existing io.ReadWriteSeeker. The caller retains ownership of rws and must not close it via *File.Close.

func Open

func Open(name string) (*File, error)

Open opens a FITS file by path for read-only access.

func OpenContext

func OpenContext(ctx context.Context, name string) (*File, error)

OpenContext is Open with explicit context.Context. The context is consulted during the initial HDU scan; if it is cancelled, the open fails early.

func OpenForEdit

func OpenForEdit(name string) (*File, error)

OpenForEdit opens a FITS file for in-place mutation.

The returned *File supports header keyword update/add/delete (via the Header() surface), same-shape pixel overwrite (OverwritePixels), and HDU append at EOF (AppendImage/AppendBinaryTable). Structural mutations (resize, insert-in-middle, delete) must be done through EditFile.

On Open we check for a stale journal file from a crashed previous edit; if present it is either replayed or rolled back before returning. See plan decision 5.8 and implementation step 27.

func OpenForEditContext

func OpenForEditContext(ctx context.Context, name string) (*File, error)

OpenForEditContext is OpenForEdit with an explicit context.

func OpenReadAll

func OpenReadAll(r io.Reader) (*File, error)

OpenReadAll slurps the contents of an io.Reader into memory and returns a regular *File. This is the helper for stream sources that do not already provide random access (see plan decision 5.3).

func OpenReader

func OpenReader(r io.ReadSeeker) (*File, error)

OpenReader opens a FITS file over an existing io.ReadSeeker. The caller retains ownership of r — Close() on the returned *File will NOT close r.

func (*File) Close

func (f *File) Close() error

Close releases any resources owned by the *File. It is safe to call on a *File returned from OpenReader; in that case only internal state is released.

func (*File) CloseAndFlush

func (f *File) CloseAndFlush() error

Close finalizes the file. In write modes it flushes pending header mutations and syncs. In all modes it releases any owned file handle.

func (*File) CopyTo

func (f *File) CopyTo(w io.Writer) error

CopyTo walks every HDU in f and writes its raw header + data + pad bytes to w in file order. When f was opened from a valid FITS file, the output is byte-for-byte identical to the input — this is the heart of the regression tool cmd/fitscopy.

CopyTo does not re-encode or modify anything: headers pass through from rec.rawHeader (which still holds the on-disk bytes unless the caller has mutated the parsed header and called Flush), data passes through via ReadRange on the block reader. Callers that want a structural rebuild must use EditFile instead.

func (*File) Flush

func (f *File) Flush() error

Flush persists any pending header mutations. Only meaningful in ModeEdit and ModeCreate. Returns ErrReadOnly for read-only files.

Implementation strategy (plan decision 5.8):

  1. For each dirty HDU, re-serialize the full header into bytes.
  2. If new length == old length (block count unchanged), overwrite in place and fsync. O(KB) per HDU.
  3. If new length differs, walk the dirty HDUs in reverse order and shift the file tail using a journal protocol (step 27). For v1 we conservatively refuse any flush that would require a tail shift and point the caller at EditFile. Journaled shift is implemented below but gated behind explicit opt-in while the crash-safety tests mature.

func (*File) HDU

func (f *File) HDU(i int) (HDU, error)

HDU returns the i-th HDU (zero-indexed; HDU(0) is the primary). An out-of- range index returns an error.

func (*File) HDUByName

func (f *File) HDUByName(name string) (HDU, error)

HDUByName returns the first extension HDU whose EXTNAME matches name. EXTNAME comparison is case-sensitive per convention. Returns an error if no matching HDU is found.

func (*File) Mode

func (f *File) Mode() Mode

Mode returns the access mode of the *File.

func (*File) Name

func (f *File) Name() string

Name returns the file name supplied at Open, or "" for OpenReader/OpenReadAll.

func (*File) NumHDU

func (f *File) NumHDU() int

NumHDU returns the total number of HDUs in the file.

func (*File) Primary

func (f *File) Primary() (*ImageHDU, error)

Primary returns the primary HDU as an *ImageHDU. The primary HDU is always an image (though it may be a zero-axis placeholder, i.e. NAXIS=0).

type FloatColor

type FloatColor struct {
	V float32
}

FloatColor wraps a float32 as a color.Color. RGBA returns the float value scaled to the 16-bit RGBA space via a lossy saturating cast. This is the only place the library does anything that could be called "scaling", and it exists solely to satisfy the color.Color interface contract. Callers that need faithful display MUST NOT rely on this mapping — they must build their own transform from the raw Pix[] slab.

func (FloatColor) RGBA

func (c FloatColor) RGBA() (r, g, b, a uint32)

RGBA implements color.Color. The float value is saturating-cast to the 0..0xFFFF range — a cheap placeholder that exists only to satisfy the interface. For real rendering, read Pix[] directly and transform.

type FloatGray

type FloatGray struct {
	Rect   image.Rectangle
	Stride int
	Pix    []float32
}

FloatGray is an image.Image over raw float32 pixels. It does NOT rescale — rendering through png.Encode without a caller-provided transform produces garbage, which is intentional. The purpose of this type is to let float FITS data participate in the image.Image ecosystem with lossless fidelity.

func (*FloatGray) At

func (f *FloatGray) At(x, y int) color.Color

At returns the raw float32 at (x, y), boxed in a FloatColor.

func (*FloatGray) Bounds

func (f *FloatGray) Bounds() image.Rectangle

Bounds returns the rectangle.

func (*FloatGray) ColorModel

func (f *FloatGray) ColorModel() color.Model

ColorModel returns a pass-through float color model.

type HDU

type HDU interface {
	Type() HDUType
	// Header returns the full parsed header. Mutations to the returned
	// *header.Header persist in memory but are only written to disk when
	// (*File).Flush is called on a ModeEdit *File. See plan decision 5.8.
	Header() *header.Header
	// Index returns the 0-based HDU index in the parent file.
	Index() int
	// Compressed reports whether this HDU is a tile-compressed image
	// backed by a binary table with ZIMAGE=T. Always false for plain
	// ImageHDU / BinaryTableHDU / ASCIITableHDU; true only for
	// CompressedImageHDU.
	Compressed() bool
	// CompressionType returns the ZCMPTYPE value ("RICE_1", "GZIP_1",
	// etc.) for compressed HDUs, or "" for uncompressed HDUs.
	CompressionType() string
}

HDU is the common interface implemented by every HDU kind.

Concrete types are *ImageHDU, *ASCIITableHDU, *BinaryTableHDU, and *CompressedImageHDU. Narrow to the concrete type via type assertion.

type HDUType

type HDUType int

HDUType enumerates the broad HDU categories exposed by the library.

const (
	// TypeImage identifies a primary HDU or an IMAGE extension.
	TypeImage HDUType = iota
	// TypeASCIITable identifies an ASCII TABLE extension (§7.2).
	TypeASCIITable
	// TypeBinaryTable identifies a BINTABLE extension (§7.3).
	TypeBinaryTable
)

func (HDUType) String

func (t HDUType) String() string

type ImageHDU

type ImageHDU struct {
	// contains filtered or unexported fields
}

ImageHDU represents a FITS primary HDU or an IMAGE extension.

func AppendImage

func AppendImage[T Numeric](f *File, hdr *header.Header, shape []int64, data []T) (*ImageHDU, error)

AppendImage appends a new image HDU to a ModeEdit or ModeCreate file. It is a thin wrapper over WriteImage. hdr may be nil.

func WriteImage

func WriteImage[T Numeric](dst WriteTarget, hdr *header.Header, shape []int64, data []T) (*ImageHDU, error)

WriteImage appends a new image HDU to the destination. The destination is either a *File in ModeCreate / ModeEdit (image is placed at EOF) or a *Writer returned from EditFile.

The HDU's mandatory structural keywords (SIMPLE/XTENSION, BITPIX, NAXIS, NAXISn, END) are synthesized from T, shape, and the destination position; callers must NOT add these to hdr themselves. Any keywords in hdr other than the mandatory structural ones are emitted after the structural block.

BSCALE/BZERO, if present in hdr, are honored on the write side: the library applies the inverse scaling to each value before encoding (so that reading the file back through ReadPixels[T] with the same BSCALE/BZERO returns the original values exactly).

func (*ImageHDU) AsImage

func (h *ImageHDU) AsImage() (image.Image, error)

AsImage returns a native image.Image for HDUs that can be represented losslessly:

  • 2D BITPIX=8 → *image.Gray (uint8)
  • 2D BITPIX=16 → *image.Gray16 (uint16, with TZERO=32768 unsigned handling)
  • 2D BITPIX=-32 / -64 → *FloatGray with raw float values (not rescaled)

Returns ErrNotImageCompatible for HDUs that cannot be represented without loss (multi-dim, complex, NAXIS < 2, signed int32/int64, etc.). Callers that want lower-dim views must Slice() first. No auto-rescaling — see plan guiding principle.

func (*ImageHDU) BITPIX

func (h *ImageHDU) BITPIX() int

BITPIX returns the integer BITPIX value from the HDU's structural metadata.

func (*ImageHDU) BSCALE

func (h *ImageHDU) BSCALE() float64

BSCALE returns the BSCALE keyword value, or 1.0 if absent (§4.4.2.5).

func (*ImageHDU) BZERO

func (h *ImageHDU) BZERO() float64

BZERO returns the BZERO keyword value, or 0.0 if absent (§4.4.2.5).

func (*ImageHDU) Compressed

func (*ImageHDU) Compressed() bool

Compressed returns false — plain ImageHDUs are raw, not tile-compressed.

func (*ImageHDU) CompressionType

func (*ImageHDU) CompressionType() string

CompressionType returns the empty string for uncompressed images.

func (*ImageHDU) Header

func (h *ImageHDU) Header() *header.Header

Header returns the parsed header, loading it lazily on first call. Errors during parse panic here; callers that need to handle parse errors explicitly must call (*File).HDU followed by direct access via the header package — in practice the scan pass already validated structure, so a lazy parse failure indicates programmer error or disk corruption between open and access.

func (*ImageHDU) Index

func (h *ImageHDU) Index() int

Index returns the 0-based HDU index.

func (*ImageHDU) NAXIS

func (h *ImageHDU) NAXIS() int

NAXIS returns the number of axes.

func (*ImageHDU) Shape

func (h *ImageHDU) Shape() []int64

Shape returns the axis lengths: [NAXIS1, NAXIS2, ...]. The returned slice is safe to hold — a copy is made.

func (*ImageHDU) Type

func (h *ImageHDU) Type() HDUType

Type returns TypeImage.

type KeyNotFoundError

type KeyNotFoundError struct {
	Key string
	HDU int
}

KeyNotFoundError carries detail about an ErrKeyNotFound failure.

func (*KeyNotFoundError) Error

func (e *KeyNotFoundError) Error() string

func (*KeyNotFoundError) Is

func (e *KeyNotFoundError) Is(target error) bool

type Mode

type Mode int

Mode is the access mode of an open *File.

const (
	// ModeRead opens the file read-only. Set/Add/Delete on returned headers
	// may succeed in memory but (*File).Flush returns ErrReadOnly.
	ModeRead Mode = iota
	// ModeEdit opens the file read-write for in-place mutations and
	// end-of-file HDU appends. Structural changes require EditFile.
	ModeEdit
	// ModeCreate opens a new file for writing. Existing contents are
	// truncated.
	ModeCreate
)

type Numeric

type Numeric interface {
	~uint8 | ~int8 | ~int16 | ~uint16 | ~int32 | ~uint32 |
		~int64 | ~uint64 | ~float32 | ~float64
}

Numeric is the constraint for generic pixel and column access. The library does NOT auto-convert across BITPIX widths — if the target type cannot losslessly hold the file's on-disk values, ReadPixels returns ErrTypeMismatch. See plan decision 5.1.

type TypeMismatchError

type TypeMismatchError struct {
	Requested string
	BITPIX    int
	Lossy     bool
}

TypeMismatchError carries detail about an ErrTypeMismatch failure.

func (*TypeMismatchError) Error

func (e *TypeMismatchError) Error() string

func (*TypeMismatchError) Is

func (e *TypeMismatchError) Is(target error) bool

type VarColumn

type VarColumn[T Numeric] struct {
	// contains filtered or unexported fields
}

VarColumn stores all rows' variable-length data in a single contiguous slab plus an offsets table. This layout mirrors the FITS on-disk heap and avoids one allocation per row.

See plan decision 5.5.

func ReadVarColumn

func ReadVarColumn[T Numeric](t *BinaryTableHDU, col int) (*VarColumn[T], error)

ReadVarColumn reads a variable-length (P/Q) binary-table column into a VarColumn[T]. The declared element type in the TFORM (e.g. "1PE(256)") must match T after scale/zero application.

func (*VarColumn[T]) At

func (v *VarColumn[T]) At(row int) []T

At returns a zero-copy view into the backing Values slab for row r. The returned slice aliases the backing buffer; if the caller needs an independent copy, they must clone it.

func (*VarColumn[T]) Len

func (v *VarColumn[T]) Len() int

Len returns the number of rows (not elements).

func (*VarColumn[T]) Raw

func (v *VarColumn[T]) Raw() (values []T, offsets []int64)

Raw returns the backing slab and offsets table directly. The caller may pass them to GPU, SIMD, or bulk-transfer code without an intermediate copy. Mutating either slice invalidates the VarColumn.

func (*VarColumn[T]) Rows

func (v *VarColumn[T]) Rows() iter.Seq2[int, []T]

Rows returns an iter.Seq2[int, []T] that yields (rowIndex, zeroCopySlice) for each row in insertion order.

type WriteTarget

type WriteTarget interface {
	// contains filtered or unexported methods
}

WriteTarget abstracts the two sinks WriteImage can emit into: a *File (ModeCreate or ModeEdit) and a *Writer produced by EditFile. The interface is unexported to keep the write surface closed at v1.

type Writer

type Writer struct {
	// contains filtered or unexported fields
}

Writer is the streaming output side of EditFile. Users emit HDUs to it using CopyHDU (pass-through), CopyHDUWithHeader (new header, same data bytes), SkipHDU (omit from output), or AppendImage/AppendBinaryTable to add new HDUs.

Writer tracks whether the caller has emitted a primary HDU and rejects SkipHDU for the primary (every valid FITS file must have a primary).

func (*Writer) CopyHDU

func (w *Writer) CopyHDU(h HDU) error

CopyHDU writes h to the output verbatim — both header bytes and data bytes are passed through with no re-parse.

func (*Writer) CopyHDUWithHeader

func (w *Writer) CopyHDUWithHeader(h HDU, hdr *header.Header) error

CopyHDUWithHeader writes h to the output, replacing its header with hdr but passing the data bytes through verbatim. The caller's hdr must not include any mandatory structural keyword that would conflict with the data shape on disk — the library does not validate this; the output file's integrity is the caller's responsibility.

func (*Writer) SkipHDU

func (w *Writer) SkipHDU(h HDU) error

SkipHDU omits h from the output. Rejects skipping the primary (HDU 0).

Directories

Path Synopsis
cmd
fitscopy command
fitscopy reads an input FITS file through the fits library, walks every HDU, and writes them byte-for-byte to an output file via the library's public CopyTo API.
fitscopy reads an input FITS file through the fits library, walks every HDU, and writes them byte-for-byte to an output file via the library's public CopyTo API.
fitsdump command
fitsdump prints a human-readable summary of every HDU in a FITS file, including the full header keyword list.
fitsdump prints a human-readable summary of every HDU in a FITS file, including the full header keyword list.
Package compress implements decoders for the FITS tile-compression algorithms defined by the "Compressed Images" FITS convention (Pence et al.
Package compress implements decoders for the FITS tile-compression algorithms defined by the "Compressed Images" FITS convention (Pence et al.
Package header implements FITS header-card parsing and serialization.
Package header implements FITS header-card parsing and serialization.
Package healpix implements HEALPix pixel indexing for the Hierarchical Equal-Area isoLatitude Pixelisation of the sphere (Gorski et al.
Package healpix implements HEALPix pixel indexing for the Hierarchical Equal-Area isoLatitude Pixelisation of the sphere (Gorski et al.
internal
bigendian
Package bigendian provides typed big-endian read/write helpers and bulk in-place byte-swapping for FITS data sections.
Package bigendian provides typed big-endian read/write helpers and bulk in-place byte-swapping for FITS data sections.
bitpix
Package bitpix defines the BITPIX enumeration and helpers.
Package bitpix defines the BITPIX enumeration and helpers.
block
Package block provides 2880-byte block I/O over io.ReadSeeker and io.WriteSeeker.
Package block provides 2880-byte block I/O over io.ReadSeeker and io.WriteSeeker.
checksum
Package checksum implements the FITS checksum algorithm defined in Appendix J of the FITS Standard v4.0.
Package checksum implements the FITS checksum algorithm defined in Appendix J of the FITS Standard v4.0.
tform
Package tform parses the FITS TFORMn and TDIMn column-format keyword values for ASCII and binary tables.
Package tform parses the FITS TFORMn and TDIMn column-format keyword values for ASCII and binary tables.
Package stats provides generic, NaN-aware statistics functions for astronomical image data.
Package stats provides generic, NaN-aware statistics functions for astronomical image data.
Package stretch implements image stretching operations for FITS astronomical images.
Package stretch implements image stretching operations for FITS astronomical images.
wcs
Package wcs parses the FITS World Coordinate System keyword set defined in Greisen & Calabretta 2002 ("Representations of world coordinates in FITS", Paper I) into a typed in-memory struct.
Package wcs parses the FITS World Coordinate System keyword set defined in Greisen & Calabretta 2002 ("Representations of world coordinates in FITS", Paper I) into a typed in-memory struct.
transform
Package transform computes forward and inverse FITS World Coordinate System mappings between image pixel coordinates and celestial spherical coordinates.
Package transform computes forward and inverse FITS World Coordinate System mappings between image pixel coordinates and celestial spherical coordinates.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL