{"id":17113011,"url":"https://github.com/emmt/fitsheaders.jl","last_synced_at":"2025-10-03T12:43:10.501Z","repository":{"id":71510214,"uuid":"588101156","full_name":"emmt/FITSHeaders.jl","owner":"emmt","description":"A pure Julia package for managing FITS header records.","archived":false,"fork":false,"pushed_at":"2025-01-08T14:42:25.000Z","size":284,"stargazers_count":2,"open_issues_count":1,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-23T22:34:39.306Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Julia","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/emmt.png","metadata":{"files":{"readme":"README.md","changelog":"NEWS.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-01-12T10:26:55.000Z","updated_at":"2025-01-08T14:42:29.000Z","dependencies_parsed_at":null,"dependency_job_id":"b4c8e88d-39f2-4fd3-9acb-2d401d514cd2","html_url":"https://github.com/emmt/FITSHeaders.jl","commit_stats":{"total_commits":140,"total_committers":3,"mean_commits":"46.666666666666664","dds":"0.014285714285714235","last_synced_commit":"a0577a2ba0d9b9069b16becac42a40ead1d38827"},"previous_names":["emmt/fitsheaders.jl"],"tags_count":17,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emmt%2FFITSHeaders.jl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emmt%2FFITSHeaders.jl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emmt%2FFITSHeaders.jl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emmt%2FFITSHeaders.jl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/emmt","download_url":"https://codeload.github.com/emmt/FITSHeaders.jl/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248107871,"owners_count":21049024,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-14T17:02:18.488Z","updated_at":"2025-10-03T12:43:05.479Z","avatar_url":"https://github.com/emmt.png","language":"Julia","funding_links":[],"categories":[],"sub_categories":[],"readme":"# FITSHeaders [![Build Status](https://github.com/emmt/FITSHeaders.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/emmt/FITSHeaders.jl/actions/workflows/CI.yml?query=branch%3Amain) [![Coverage](https://codecov.io/gh/emmt/FITSHeaders.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/emmt/FITSHeaders.jl)\n\n`FITSHeaders` is a pure [Julia](https://julialang.org/) package for managing basic FITS\nstructures such as FITS headers. [FITS (for *Flexible Image Transport\nSystem*)](https://fits.gsfc.nasa.gov/fits_standard.html) is a data file format widely used\nin astronomy. A FITS file is a concatenation of *Header Data Units* (HDUs) that consist in\na header part and a data part. The header of a HDU is a collection of so-called *FITS\ncards*. Each such card is stored in textual form and associates a keyword with a value\nand/or a comment.\n\nThe `FITSHeaders` package is intended to provide:\n\n- Methods for fast parsing of a FITS header or of a piece of a FITS header (that is a\n  single FITS header card).\n\n- An expressive API for creating FITS cards and accessing their components (keyword,\n  value, and comment), possibly, in a *type-stable* way.\n\n- Methods for easy and efficient access to the records of a FITS header.\n\n\n## Building FITS cards\n\nA FITS header card associates a keyword (or a name) with a value and a comment (both\noptional). A FITS header card can be efficiently stored as an instance of `FitsCard` built\nby:\n\n``` julia\ncard = FitsCard(key =\u003e (val, com))\n```\n\nwith `key` the card name, `val` its value, and `com` its comment. The value\n`val` may be:\n\n- a boolean to yield a card of type `FITS_LOGICAL`;\n- an integer to yield a card of type `FITS_INTEGER`;\n- a non-integer real to yield a card of type `FITS_FLOAT`;\n- a complex to yield a card of type `FITS_COMPLEX`;\n- a string to yield a card of type `FITS_STRING`;\n- `nothing` to yield a card of type `FITS_COMMENT`;\n- `undef` or `missing` to yield a card of type `FITS_UNDEFINED`.\n\nThe comment may be omitted for a normal FITS card and the value may be omitted for a\ncommentary FITS card:\n\n``` julia\ncard = FitsCard(key =\u003e val::Number)\ncard = FitsCard(key =\u003e str::AbstractString)\n```\n\nIn the 1st case, the comment is assumed to be empty. In the 2nd case, the string `str` is\nassumed to be the card comment if `key` is `\"COMMENT\"` or `\"HISTORY\"` and the card value\notherwise.\n\nConversely, `Pair(card)` yields the pair `key =\u003e (val, com)`. The `convert` method is\nextended by the `FITSHeaders` package to perform these conversions.\n\nIf the string value of a FITS card is too long, it shall be split across several\nconsecutive `CONTINUE` cards when writing a FITS file. Likewise, if the comment of a\ncommentary keyword (`\"COMMENT\"` or `\"HISTORY\"`) is too long, it shall be split across\nseveral consecutive cards with the same keyword when writing a FITS file.\n\n\n## FITS cards properties\n\nFITS cards have the following properties (among others):\n\n``` julia\ncard.type     # type of card: FITS_LOGICAL, FITS_INTEGER, etc.\ncard.key      # quick key of card: Fits\"BITPIX\", Fits\"HIERARCH\", etc.\ncard.name     # name of card\ncard.value    # callable object representing the card value\ncard.comment  # comment of card\ncard.units    # units of card value\ncard.unitless # comment of card without the units part if any\n```\n\nAs the values of FITS keywords have different types, `card.value` does not yield a Julia\nvalue but a callable object. Called without any argument, this object yields the actual\ncard value:\n\n``` julia\ncard.value() -\u003e val::Union{Bool,Int64,Float64,ComplexF64,String,Nothing,UndefInitializer}\n```\n\nbut such a call is not *type-stable* as indicated by the union `Union{...}` in the above\ntype assertion. For a type-stable result, the card value can be converted to a given data\ntype `T`:\n\n``` julia\ncard.value(T)\nconvert(T, card.value)\n```\n\nboth yield the value of `card` converted to type `T`. For readability, `T` may be an\nabstract type: `card.value(Integer)` yields the same result as `card.value(Int64)`,\n`card.value(Real)` or `card.value(AbstractFloat)` yield the same result as\n`card.value(Float64)`, `card.value(Complex)` yields the same result as\n`card.value(ComplexF64)`, and `card.value(AbstractString)` yields the same result as\n`card.value(String)`.\n\nTo make things easier, a few additional properties are aliases that yield the card value\nconverted to a specific type:\n\n``` julia\ncard.logical :: Bool       # alias for card.value(Bool)\ncard.integer :: Int64      # alias for card.value(Integer)\ncard.float   :: Float64    # alias for card.value(Real)\ncard.complex :: ComplexF64 # alias for card.value(Complex)\ncard.string  :: String     # alias for card.value(String)\n```\n\nWhen the actual card value is of a different type than the one requested, an error is\nthrown if the conversion is not possible or inexact.\n\n`valtype(card)` yields the Julia type of the value of `card` while `isassigned(card)`\nyields whether `card` has a value (that is whether it is neither a commentary card nor a\ncard with an undefined value).\n\n\n## FITS keywords\n\nThere are two kinds of FITS keywords:\n\n- Short FITS keywords are words with at most 8 ASCII characters from the restricted set of\n  upper case letters (bytes 0x41 to 0x5A), decimal digits (hexadecimal codes 0x30 to\n  0x39), hyphen (hexadecimal code 0x2D), or underscore (hexadecimal code 0x5F). In a FITS\n  file, keywords shorter than 8 characters are right-padded with ordinary spaces\n  (hexadecimal code 0x20).\n\n- `HIERARCH` FITS keywords start with the string `\"HIERARCH \"` (with a single trailing\n  space) followed by one or more words composed from the same restricted set of ASCII\n  characters as short keywords and separated by a single space.\n\nKeywords longer than 8 characters or composed of several words can only be represented as\n`HIERARCH` FITS keywords. To simplify the representation of FITS cards as pairs, the\n`FitsCard` constructor automatically converts long keywords or multi-word keywords into a\n`HIERARCH` FITS keyword by prefixing the keyword with the string `\"HIERARCH \"` for\nexample:\n\n``` julia\njulia\u003e card = FitsCard(\"VERY-LONG-NAME\" =\u003e (2, \"keyword is longer than 8 characters\"))\nFitsCard: HIERARCH VERY-LONG-NAME = 2 / keyword is longer than 8 characters\n\njulia\u003e card.name\n\"HIERARCH VERY-LONG-NAME\"\n\njulia\u003e FitsCard(\"SOME KEY\" =\u003e (3, \"keyword has 8 characters but 2 words\"))\nFitsCard: HIERARCH SOME KEY = 3 / keyword has 8 characters but 2 words\n\njulia\u003e card.name\n\"HIERARCH SOME KEY\"\n```\n\nThis rule is only applied to the construction of FITS cards from pairs. When parsing a\nFITS header card from a file, the `\"HIERARCH \"` prefix must be present.\n\nThe non-exported method `FITSHeaders.keyword` may be used to apply this rule:\n\n``` julia\njulia\u003e FITSHeaders.keyword(\"VERY-LONG-NAME\")\n\"HIERARCH VERY-LONG-NAME\"\n\njulia\u003e FITSHeaders.keyword(\"SOME KEY\")\n\"HIERARCH SOME KEY\"\n\njulia\u003e FITSHeaders.keyword(\"NAME\")\n\"NAME\"\n\njulia\u003e FITSHeaders.keyword(\"HIERARCH NAME\")\n\"HIERARCH NAME\"\n```\n\n\n## Quick FITS keys\n\nIn `FITSHeaders`, a key of type `FitsKey` is a 64-bit value computed from a FITS keyword.\nThe key of a short FITS keyword is unique and exactly matches the first 8 bytes of the\nkeyword as it is stored in a FITS file. Thus quick keys provide fast means to compare and\nsearch FITS keywords. The constructor `FitsKey(name)` yields the quick key of the string\n`name`. A quick key may be literally expressed by using the `@Fits_str` macro in Julia\ncode. For example:\n\n``` julia\ncard.key == Fits\"NAXIS\"\n```\n\nis faster than, say `card.name == \"NAXIS\"`, to check whether the name of the FITS header\ncard `card` is `\"NAXIS\"`. This is because, the comparison is performed on a single integer\n(not on several characters) and expression `Fits\"....\"` is a constant computed at compile\ntime with no run-time penalty. Compared to `FitsKey(name)`, `Fits\"....\"` checks the\nvalidity of the characters composing the literal short keyword (again this is done at\ncompile time so without run-time penalty) and, for readability, does not allow for\ntrailing spaces.\n\nFor a `HIERARCH` keyword, the quick key is equal to the constant `Fits\"HIERARCH\"` whatever\nthe other part of the keyword.\n\n\n## Parsing of FITS header cards\n\nEach FITS header card is stored in a FITS file as 80 consecutive bytes from the restricted\nset of ASCII characters from `' '` to `'~'` (hexadecimal codes 0x20 to 0x7E). Hence Julia\nstrings (whether they are encoded in ASCII or in UTF8) can be treated as vectors of bytes.\nThe parsing methods provided by the `FITSHeaders` package exploit this to deal with FITS\nheaders and cards stored as either vectors of bytes (of type `AbstractVector{UInt8}`) or\nas Julia strings (of type `String` or `SubString{String}`).\n\nA `FitsCard` object can be built by parsing a FITS header card as it is stored in a FITS\nfile:\n\n``` julia\ncard = FitsCard(buf; offset=0)\n```\n\nwhere `buf` is either a string or a vector of bytes. Keyword `offset` can be used to\nspecify the number of bytes to skip at the beginning of `buf`, so that it is possible to\nextract a specific FITS header card, not just the first one. At most, the 80 first bytes\nafter the offset are scanned to build the `FitsCard` object. The next FITS card to parse\nis then at `offset + 80` and so on.\n\nThe considered card may be shorter than 80 bytes, the result being exactly the same as if\nthe missing bytes were spaces. If there are no bytes left, a `FitsCard` object equivalent\nto the final `END` card of a FITS header is returned.\n\n\n## FITS headers\n\nThe `FITSHeaders` package provides objects of type `FitsHeader` to store, possibly\npartial, FITS headers.\n\n\n### Building a FITS header\n\nTo build a FITS header initialized with records `args..`, call:\n\n``` julia\nhdr = FitsHeader(args...)\n```\n\nwhere `args...` is a variable number of records in any form allowed by the `FitsCard`\nconstructor, it can also be a vector or a tuple of records. For example:\n\n``` julia\ndims = (384, 288)\nhdr = FitsHeader(\"SIMPLE\" =\u003e true,\n                 \"BITPIX\" =\u003e (-32, \"32-bit floats\"),\n                 \"NAXIS\" =\u003e (length(dims), \"number of dimensions\"),\n                 ntuple(i -\u003e \"NAXIS$i\" =\u003e dims[i], length(dims))...,\n                 \"COMMENT\" =\u003e \"A comment.\",\n                 \"COMMENT\" =\u003e \"Another comment.\",\n                 \"DATE\" =\u003e (\"2023-02-01\", \"1st of February, 2023\"),\n                 \"COMMENT\" =\u003e \"Yet another comment.\")\n```\n\nMethod `keys` can be applied to get the list of keywords in a FITS header:\n\n``` julia\njulia\u003e keys(hdr)\nKeySet for a Dict{String, Int64} with 7 entries. Keys:\n  \"COMMENT\"\n  \"BITPIX\"\n  \"SIMPLE\"\n  \"NAXIS2\"\n  \"NAXIS1\"\n  \"NAXIS\"\n  \"DATE\"\n```\n\n\n### Retrieving records from a FITS header\n\nA FITS header object behaves as a vector of `FitsCard` elements with integer or keyword\n(string) indices. When indexed by keywords, a FITS header object is similar to a\ndictionary except that the order of records is preserved and that commentary and\ncontinuation records (with keywords `\"COMMENT\"`, `\"HISTORY\"`, `\"\"`, or `\"CONTINUE\"`) may\nappear more than once.\n\nAn integer (linear) index `i` or a string index `key` can be used to retrieve a given\nrecord:\n\n``` julia\nhdr[i]   # i-th record\nhdr[key] # first record whose name matches `key`\n```\n\nFor example (with `hdr` as built above):\n\n``` julia\njulia\u003e hdr[2]\nFitsCard: BITPIX  = -32 / 32-bit floats\n\njulia\u003e hdr[\"NAXIS\"]\nFitsCard: NAXIS   = 2 / number of dimensions\n\njulia\u003e hdr[\"COMMENT\"]\nFitsCard: COMMENT A comment.\n```\n\nNote that, when indexing by name, the first matching record is returned. This may be a\nconcern for non-unique keywords as in the last above example. All matching records can be\ncollected into a vector of `FitsCard` elements by:\n\n``` julia\ncollect(key, hdr) # all records whose name matches `key`\n```\n\nFor example:\n\n``` julia\njulia\u003e collect(\"COMMENT\", hdr)\n3-element Vector{FitsCard}:\n FitsCard: COMMENT A comment.\n FitsCard: COMMENT Another comment.\n FitsCard: COMMENT Yet another comment.\n\njulia\u003e collect(rec -\u003e startswith(rec.name, \"NAXIS\"), hdr)\n3-element Vector{FitsCard}:\n FitsCard: NAXIS   = 2 / number of dimensions\n FitsCard: NAXIS1  = 384\n FitsCard: NAXIS2  = 288\n\njulia\u003e collect(r\"^NAXIS[0-9]+$\", hdr)\n2-element Array{FitsCard,1}:\n FitsCard(\"NAXIS1\" =\u003e 384)\n FitsCard(\"NAXIS2\" =\u003e 288)\n```\n\nThis behavior is different from that of `filter` which yields another FITS header\ninstance:\n\n``` julia\njulia\u003e filter(rec -\u003e startswith(rec.name, \"NAXIS\"), hdr)\n3-element FitsHeader:\n FitsCard: NAXIS   = 2 / number of dimensions\n FitsCard: NAXIS1  = 384\n FitsCard: NAXIS2  = 288\n```\n\nFor more control, searching for the index `i` of an existing record in FITS header object\n`hdr` can be done by the usual methods:\n\n``` julia\nfindfirst(what, hdr)\nfindlast(what, hdr)\nfindnext(what, hdr, start)\nfindprev(what, hdr, start)\n```\n\nwhich all return a valid integer index if a record matching `what` is found and `nothing`\notherwise. The matching pattern `what` can be a keyword (string), a FITS card (an instance\nof `FitsCard` whose name is used as a matching pattern), a regular expression, or a\npredicate function which takes a FITS card argument and shall return whether it matches.\nThe find methods just yield `nothing` for any unsupported kind of pattern.\n\nThe `eachmatch` method is a simple mean to iterate over matching records:\n\n``` julia\neachmatch(what, hdr)\n```\n\nyields an iterator over the records of `hdr` matching `what`. For example:\n\n``` julia\n@inbounds for rec in eachmatch(what, hdr)\n    ... # do something\nend\n```\n\nis a shortcut for:\n\n``` julia\ni = findfirst(what, hdr)\n@inbounds while i !== nothing\n    rec = hdr[i]\n    ... # do something\n    i = findnext(what, hdr, i+1)\nend\n```\n\nwhile:\n\n``` julia\n@inbounds for rec in reverse(eachmatch(what, hdr))\n    ... # do something\nend\n```\n\nis equivalent to:\n\n``` julia\ni = findlast(what, hdr)\n@inbounds while i !== nothing\n    rec = hdr[i]\n    ... # do something\n    i = findprev(what, hdr, i-1)\nend\n```\n\nIf it is not certain that a record exists or to avoid throwing a `KeyError` exception, use\nthe `get` method. For example:\n\n``` julia\njulia\u003e get(hdr, \"BITPIX\", nothing)\nFitsCard: BITPIX  = -32 / 32-bit floats\n\njulia\u003e get(hdr, \"GIZMO\", missing)\nmissing\n```\n\n\n### Modifying a FITS header\n\nA record `rec` may be pushed to a FITS header `hdr` to modify the header:\n\n``` julia\npush!(hdr, rec)\n```\n\nwhere `rec` may have any form allowed by the `FitsCard` constructor. If the keyword is\n`\"COMMENT\"`, `\"HISTORY\"`, `\"\"`, or `\"CONTINUE\"`, `rec` is appended to the end of the list\nof records stored by `hdr`. For other keywords which must be unique, if a record of the\nsame name exists in `hdr`, it is replaced by `rec`; otherwise, it is appended to the end\nof the list of records stored by `hdr`.\n\nThe `setindex!` method may be used with a keyword (string) index. For example, the two\nfollowing statements are equivalent:\n\n``` julia\nhdr[key] = (val, com)\npush!(hdr, key =\u003e (val, com))\n```\n\nThe `setindex!` method may also be used with a linear (integer) index. For example:\n\n``` julia\nhdr[i] = rec\n```\n\nreplaces the `i`-th record in `hdr` by `rec`. With an integer index, the rule for unique /\nnon-unique keywords is not applied, so this indexing should be restricted to editing the\nvalue and/or comment of an existing entry. The following example illustrates how this can\nbe used to modify the comment of the BITPIX record:\n\n``` julia\njulia\u003e if (i = findfirst(\"BITPIX\", hdr)) != nothing\n           hdr[i] = (\"BITPIX\" =\u003e (hdr[i].value(), \"A better comment.\"))\n       end\n\"BITPIX\" =\u003e (-32, \"A better comment.\")\n```\n\n\n## Timings\n\n`FITSHeaders` is ought to be fast. Below are times and memory allocations for parsing\n80-byte FITS cards measured with Julia 1.8.5 on a Linux laptop with an Intel Core i7-5500U\nCPU:\n\n- parsing logical FITS card:  114.588 ns (2 allocations:  64 bytes)\n- parsing integer FITS card:  118.519 ns (2 allocations:  72 bytes)\n- parsing HIERARCH FITS card: 142.462 ns (2 allocations:  88 bytes)\n- parsing float FITS card:    274.119 ns (4 allocations: 152 bytes)\n- parsing complex FITS card:  424.060 ns (6 allocations: 248 bytes)\n- parsing string FITS card:   155.694 ns (4 allocations: 144 bytes)\n- parsing string with quotes: 169.223 ns (4 allocations: 168 bytes)\n- parsing COMMENT FITS card:   90.615 ns (2 allocations: 112 bytes)\n- parsing HISTORY FITS card:  100.591 ns (2 allocations:  72 bytes)\n- parsing blank FITS card:     78.261 ns (0 allocations:   0 bytes)\n- parsing END FITS card:       82.286 ns (0 allocations:   0 bytes)\n\nThe benchmark code is in file [`test/benchmarks.jl`](test/benchmarks.jl). The HIERARCH\ncard has an integer value. The float and complex valued cards take more time to parse\nbecause parsing a floating-point value is more complex than parsing, say, an integer and\nbecause the string storing the floating-point value must be copied to replace letters `d`\nand `D`, allowed in FITS standard to indicate the exponent, by an `e`.\n\nFor comparison, just extracting the keyword, value, and comment parts from a 80-characters\nFITS card by calling the functions `fits_get_keyname` and `fits_parse_value` of CFITSIO\nlibrary takes about 150 ns on the same machine. This does not includes the allocation of\nthe buffers to store these 3 parts (about 120 ns for this) and the parsing of the value\nwhich are all included in the timings of the `FitsCard` constructor above.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Femmt%2Ffitsheaders.jl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Femmt%2Ffitsheaders.jl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Femmt%2Ffitsheaders.jl/lists"}