fitsjs-ng 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,19 @@
1
1
  # fitsjs-ng
2
2
 
3
- Modern TypeScript library for reading [FITS](https://fits.gsfc.nasa.gov/) (Flexible Image Transport System) astronomical files. A complete rewrite of [astrojs/fitsjs](https://github.com/astrojs/fitsjs) with Promise-based APIs, full type safety, and Node.js/browser dual support.
3
+ Modern TypeScript library for reading and writing [FITS](https://fits.gsfc.nasa.gov/), [SER](https://grischa-hahn.hier-im-netz.de/astro/ser/), and [XISF](https://pixinsight.com/xisf/) astronomical files. A complete rewrite of [astrojs/fitsjs](https://github.com/astrojs/fitsjs) with Promise-based APIs, full type safety, and Node.js/browser dual support.
4
4
 
5
5
  ## Features
6
6
 
7
- - **FITS Image Reading** — BITPIX 8, 16, 32, -32, -64 with BZERO/BSCALE scaling
7
+ - **FITS Image Reading** — BITPIX 8, 16, 32, 64, -32, -64 with BZERO/BSCALE scaling
8
+ - **FITS Image Writing** — build FITS HDUs and export complete FITS buffers
9
+ - **SER Read/Write** — full SER v3 parsing/writing, timestamps, Bayer/CMY + RGB/BGR support
10
+ - **XISF Read/Write** — monolithic (`.xisf`) and distributed (`.xish` + `.xisb`) workflows
11
+ - **XISF Signature Verification** — XML-DSig `SignedInfo`/digest/signature verification with policy control
12
+ - **XISF↔FITS Conversion** — strict conversion with metadata preservation
13
+ - **XISF↔HiPS Conversion** — direct conversion APIs via standards-preserving FITS bridge
14
+ - **SER↔FITS / SER↔XISF Conversion** — reversible metadata/time-stamp aware conversion pipelines
15
+ - **HiPS Image + HiPS3D** — read/write HiPS properties, tiles, Allsky, and lint checks
16
+ - **FITS↔HiPS Conversion** — build HiPS directories and export tile/map/cutout FITS
8
17
  - **Data Cubes** — Frame-by-frame reading of 3D+ image data
9
18
  - **ASCII Tables** — Fixed-width text table parsing (A/I/F/E/D format codes)
10
19
  - **Binary Tables** — All standard types (L/B/I/J/K/A/E/D/C/M/X), bit arrays, heap access
@@ -24,29 +33,112 @@ pnpm add fitsjs-ng
24
33
  ## Quick Start
25
34
 
26
35
  ```ts
27
- import { FITS, Image } from 'fitsjs-ng'
28
-
29
- // From a URL
30
- const fits = await FITS.fromURL('https://example.com/image.fits')
31
-
32
- // From an ArrayBuffer
33
- const fits = FITS.fromArrayBuffer(buffer)
34
-
35
- // From a File/Blob (browser)
36
- const fits = await FITS.fromBlob(file)
37
-
38
- // From a Node.js Buffer
39
- const fits = FITS.fromNodeBuffer(fs.readFileSync('image.fits'))
40
-
41
- // Access the primary header
36
+ import {
37
+ FITS,
38
+ SER,
39
+ XISF,
40
+ XISFWriter,
41
+ parseSERBuffer,
42
+ parseSERBlob,
43
+ convertFitsToXisf,
44
+ convertXisfToFits,
45
+ convertSerToFits,
46
+ convertFitsToSer,
47
+ convertSerToXisf,
48
+ convertXisfToSer,
49
+ convertXisfToHiPS,
50
+ convertHiPSToXisf,
51
+ NodeFSTarget,
52
+ Image,
53
+ } from 'fitsjs-ng'
54
+ import fs from 'node:fs'
55
+
56
+ // FITS from ArrayBuffer / Blob / Node buffer-like / URL
57
+ const fits = FITS.fromArrayBuffer(
58
+ await fs.promises
59
+ .readFile('image.fits')
60
+ .then((b) => b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength)),
61
+ )
62
+ const fitsFromBlob = await FITS.fromBlob(new Blob([await fs.promises.readFile('image.fits')]))
63
+ const fitsFromNodeBuffer = FITS.fromNodeBuffer(await fs.promises.readFile('image.fits'))
64
+ const fitsFromUrl = await FITS.fromURL('https://example.com/image.fits')
65
+
66
+ // Access header + image
42
67
  const header = fits.getHeader()
43
- console.log(header.get('BITPIX')) // e.g. -32
44
- console.log(header.get('NAXIS1')) // e.g. 1024
45
-
46
- // Read image pixels
68
+ console.log(header?.get('BITPIX'))
47
69
  const image = fits.getDataUnit() as Image
48
70
  const pixels = await image.getFrame(0)
49
71
  const [min, max] = image.getExtent(pixels)
72
+
73
+ // FITS <-> XISF
74
+ const xisfBytes = await convertFitsToXisf(
75
+ await fs.promises
76
+ .readFile('image.fits')
77
+ .then((b) => b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength)),
78
+ )
79
+ const xisf = await XISF.fromArrayBuffer(xisfBytes as ArrayBuffer)
80
+ const fitsBytes = await convertXisfToFits(xisf)
81
+
82
+ // SER parse + conversions
83
+ const serBytes = await fs.promises
84
+ .readFile('capture.ser')
85
+ .then((b) => b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength))
86
+ const ser = SER.fromArrayBuffer(serBytes)
87
+ const parsedSer = parseSERBuffer(serBytes)
88
+ const parsedSerBlob = await parseSERBlob(new Blob([serBytes]))
89
+ const fitsFromSer = await convertSerToFits(serBytes, { layout: 'cube' })
90
+ const serFromFits = await convertFitsToSer(fitsFromSer, { sourceLayout: 'auto' })
91
+ const xisfFromSer = await convertSerToXisf(serBytes)
92
+ const serFromXisf = await convertXisfToSer(xisfFromSer as ArrayBuffer, { imageIndex: 0 })
93
+
94
+ // XISF <-> HiPS (offline/local target)
95
+ const hipsTarget = new NodeFSTarget('./demo/.out/readme-quickstart-hips')
96
+ await convertXisfToHiPS(xisfBytes as ArrayBuffer, {
97
+ output: hipsTarget,
98
+ title: 'XISF Survey',
99
+ creatorDid: 'ivo://example/xisf',
100
+ hipsOrder: 4,
101
+ minOrder: 1,
102
+ tileWidth: 128,
103
+ formats: ['fits', 'png'],
104
+ })
105
+ const xisfCutout = await convertHiPSToXisf(hipsTarget, {
106
+ cutout: { width: 512, height: 512, ra: 83.63, dec: 22.01, fov: 1.2 },
107
+ })
108
+
109
+ // XISF writer outputs
110
+ const monolithic = await XISFWriter.toMonolithic(xisf.unit, { compression: 'zlib' })
111
+ const distributed = await XISFWriter.toDistributed(xisf.unit, { compression: 'zlib' })
112
+ // distributed.header => .xish bytes, distributed.blocks['blocks.xisb'] => .xisb bytes
113
+ ```
114
+
115
+ ### HiPS Quick Start
116
+
117
+ ```ts
118
+ import { NodeFSTarget, convertFitsToHiPS, convertHiPSToFITS, HiPS, lintHiPS } from 'fitsjs-ng'
119
+
120
+ const target = new NodeFSTarget('./out/my-hips')
121
+ await convertFitsToHiPS(fitsArrayBuffer, {
122
+ output: target,
123
+ title: 'My Survey',
124
+ creatorDid: 'ivo://example/my-survey',
125
+ hipsOrder: 6,
126
+ tileWidth: 512,
127
+ formats: ['fits', 'png'],
128
+ interpolation: 'bilinear',
129
+ })
130
+
131
+ const hips = await HiPS.open('./out/my-hips')
132
+ const tile = await hips.readTile({ order: 6, ipix: 12345, format: 'fits' })
133
+
134
+ const cutoutFits = await convertHiPSToFITS('./out/my-hips', {
135
+ cutout: { width: 1024, height: 1024, ra: 83.63, dec: 22.01, fov: 1.2 },
136
+ backend: 'auto', // local first, fallback to hips2fits if hipsId is set
137
+ hipsId: 'CDS/P/2MASS/K',
138
+ })
139
+
140
+ const lint = await lintHiPS('./out/my-hips')
141
+ console.log(lint.ok, lint.issues)
50
142
  ```
51
143
 
52
144
  ## API Reference
@@ -62,6 +154,80 @@ Static factory methods:
62
154
  | `FITS.fromURL(url)` | Fetch and parse remote file (async) |
63
155
  | `FITS.fromNodeBuffer(buffer)` | Parse from Node.js `Buffer` (sync) |
64
156
 
157
+ ### `XISF`
158
+
159
+ Static factory methods:
160
+
161
+ | Method | Description |
162
+ | ------------------------------ | ---------------------------------------- |
163
+ | `XISF.fromArrayBuffer(buffer)` | Parse from `ArrayBuffer` |
164
+ | `XISF.fromBlob(blob)` | Parse from `Blob`/`File` |
165
+ | `XISF.fromURL(url)` | Fetch and parse remote `.xisf`/`.xish` |
166
+ | `XISF.fromNodeBuffer(buffer)` | Parse from Node.js `Buffer`-like payload |
167
+
168
+ ### `SER`
169
+
170
+ Static factory methods:
171
+
172
+ | Method | Description |
173
+ | ----------------------------- | --------------------------------------------------- |
174
+ | `SER.fromArrayBuffer(buffer)` | Parse SER from `ArrayBuffer` |
175
+ | `SER.fromBlob(blob)` | Parse SER from `Blob`/`File` |
176
+ | `SER.fromURL(url)` | Fetch and parse remote `.ser` |
177
+ | `SER.fromNodeBuffer(buffer)` | Parse SER from Node.js `Buffer`-like payload |
178
+ | `parseSERBuffer(buffer)` | Parse SER buffer and return structured parse result |
179
+ | `parseSERBlob(blob)` | Parse SER blob and return structured parse result |
180
+ | `writeSER(input)` | Serialize SER header + frames (+ optional trailer) |
181
+
182
+ Instance helpers:
183
+
184
+ | Method | Description |
185
+ | -------------------------- | ---------------------------------------------- |
186
+ | `ser.getFrameCount()` | Total frame count |
187
+ | `ser.getFrameRGB(i)` | RGB helper decode for mono/Bayer/CMY/RGB/BGR |
188
+ | `ser.getDurationTicks()` | Duration from trailer timestamps (100ns ticks) |
189
+ | `ser.getDurationSeconds()` | Duration in seconds from trailer timestamps |
190
+ | `ser.getEstimatedFPS()` | Estimated FPS from timestamp spacing |
191
+
192
+ ### `XISFWriter`
193
+
194
+ | Method | Description |
195
+ | ---------------------------- | ------------------------------------------------ |
196
+ | `XISFWriter.toMonolithic()` | Serialize to monolithic `.xisf` bytes |
197
+ | `XISFWriter.toDistributed()` | Serialize to distributed `.xish` + `.xisb` bytes |
198
+
199
+ ### Conversion
200
+
201
+ | Method | Description |
202
+ | ----------------------------------- | -------------------------------------------------- |
203
+ | `convertXisfToFits(input)` | Convert XISF to FITS bytes |
204
+ | `convertFitsToXisf(input)` | Convert FITS to XISF bytes (or distributed object) |
205
+ | `convertSerToFits(input)` | Convert SER to FITS bytes |
206
+ | `convertFitsToSer(input)` | Convert FITS to SER bytes |
207
+ | `convertSerToXisf(input)` | Convert SER to XISF bytes |
208
+ | `convertXisfToSer(input)` | Convert XISF to SER bytes |
209
+ | `convertFitsToHiPS(input, options)` | Convert FITS to HiPS directory |
210
+ | `convertHiPSToFITS(input, options)` | Export HiPS to FITS tile/map/cutout |
211
+
212
+ SER conversion options:
213
+
214
+ - `convertSerToFits(input, { layout: 'cube' | 'multi-hdu' })` (default: `'cube'`)
215
+ - `convertFitsToSer(input, { sourceLayout: 'auto' | 'cube' | 'multi-hdu' })` (default: `'auto'`)
216
+ - `convertXisfToSer(input, { imageIndex })` for multi-image XISF units
217
+
218
+ ### HiPS
219
+
220
+ | Method / Class | Description |
221
+ | ---------------------------------------- | ------------------------------------------------- |
222
+ | `HiPS.open(source)` | Open HiPS from local path, URL, or storage target |
223
+ | `HiPS.getProperties()` | Load and parse `properties` |
224
+ | `HiPS.readTile({ order, ipix, format })` | Read/decode one tile |
225
+ | `NodeFSTarget` | Node filesystem output target |
226
+ | `BrowserZipTarget` | Browser ZIP output target |
227
+ | `BrowserOPFSTarget` | Browser OPFS output target |
228
+ | `HiPSProperties` | Parse/serialize/validate HiPS properties |
229
+ | `lintHiPS(source)` | Validate metadata and structure |
230
+
65
231
  Instance methods:
66
232
 
67
233
  | Method | Description |
@@ -84,13 +250,40 @@ Instance methods:
84
250
 
85
251
  ### `Image`
86
252
 
87
- | Method | Description |
88
- | ------------------------- | --------------------------------- |
89
- | `getFrame(frame?)` | Read a single frame (async) |
90
- | `getFrames(start, count)` | Read multiple frames (async) |
91
- | `getExtent(pixels)` | Compute `[min, max]` ignoring NaN |
92
- | `getPixel(pixels, x, y)` | Get pixel at (x, y) |
93
- | `isDataCube()` | Whether NAXIS > 2 |
253
+ | Method | Description |
254
+ | -------------------------- | --------------------------------------------------------- |
255
+ | `getFrame(frame?)` | Read a single frame (async) |
256
+ | `getFrameAsNumber(frame?)` | Read frame as `Float64Array` (explicitly lossy for int64) |
257
+ | `getFrames(start, count)` | Read multiple frames (async) |
258
+ | `getExtent(pixels)` | Compute `[min, max]` (`number`/`bigint`) |
259
+ | `getPixel(pixels, x, y)` | Get pixel at (x, y) (`number`/`bigint`) |
260
+ | `isDataCube()` | Whether NAXIS > 2 |
261
+
262
+ `BITPIX=64` reads use lossless `BigInt64Array` on the primary path when linear scaling is exact (`BSCALE=1`, safe-integer `BZERO`). Use `getFrameAsNumber()` only when you intentionally accept precision loss.
263
+
264
+ ### XISF Signature Policy
265
+
266
+ `XISF.fromArrayBuffer()` accepts:
267
+
268
+ - `signaturePolicy: 'require' | 'warn' | 'ignore'` (default: `'require'`)
269
+ - `verifySignatures` (default: `true`)
270
+
271
+ Behavior:
272
+
273
+ - **`require`**: signed documents must verify; failures throw `XISFSignatureError`
274
+ - **`warn`**: signature failures are reported through warnings and `unit.signature`
275
+ - **`ignore`**: signature verification is skipped
276
+
277
+ When a detached signature is present and verification is enabled, checksum verification is forced for attachment/path/url data blocks.
278
+
279
+ ### FITS↔XISF Preservation Scope
280
+
281
+ `convertFitsToXisf()` / `convertXisfToFits()` preserve:
282
+
283
+ - FITS keyword values **and comments** (`Header.getCards()` based mapping)
284
+ - non-image HDUs through `FITS:PreservedHDULayout` metadata (reversible card+payload container)
285
+
286
+ For `BITPIX=64`, canonical unsigned encoding (`BSCALE=1`, `BZERO=9223372036854775808`) is detected with strict raw-card parsing (no tolerance heuristics).
94
287
 
95
288
  ### `Table` (ASCII)
96
289
 
@@ -169,8 +362,29 @@ pnpm test # Run tests
169
362
  pnpm build # Build library
170
363
  pnpm typecheck # Type check
171
364
  pnpm lint # Lint
365
+ pnpm demo:all # Run all Node demos in sequence
366
+ pnpm demo # FITS/XISF CLI demo
367
+ pnpm demo:hips # HiPS Node demo (FITS->HiPS->FITS)
368
+ pnpm demo:xisf # XISF Node demo (FITS<->XISF, monolithic/distributed)
369
+ pnpm demo:ser # SER Node demo (SER<->FITS<->XISF)
370
+ pnpm demo:web # Serve web demos (open /demo/web/index.html, /demo/web/hips.html, /demo/web/xisf.html)
172
371
  ```
173
372
 
373
+ Node demo artifacts are written under `demo/.out/*`.
374
+
375
+ ## Standards & Compatibility
376
+
377
+ - HiPS metadata and directory naming follow HiPS 1.0 conventions (`Norder*/Dir*/Npix*`, `Norder3/Allsky.*`, `properties`, `Moc.fits`).
378
+ - FITS writing follows FITS 4.0 card/block alignment rules (80-char cards, 2880-byte blocks).
379
+ - Output `properties` defaults to `hips_version=1.4` and also writes legacy compatibility fields (`coordsys`, `maxOrder`, `format`).
380
+ - XISF default codec provider supports `zlib`, `lz4`, and `lz4hc` for read/write and `zstd` for read; custom providers can extend encoding support.
381
+
382
+ ## Remote Backend Behavior
383
+
384
+ - `backend: 'local'`: all conversion is performed locally.
385
+ - `backend: 'remote'`: cutout export uses CDS hips2fits endpoint directly.
386
+ - `backend: 'auto'`: try local cutout first, then fallback to hips2fits when `hipsId` is provided.
387
+
174
388
  ## Credits
175
389
 
176
390
  Based on [astrojs/fitsjs](https://github.com/astrojs/fitsjs) by Amit Kapadia / Zooniverse.