fitsjs-ng 1.0.0 → 1.0.2

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,17 +1,26 @@
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
11
20
  - **Compressed Images** — Rice (RICE_1) decompression with subtractive dithering
12
21
  - **Multiple HDUs** — Sequential parsing of all Header Data Units
13
22
  - **Modern API** — Async/await, TypeScript types, ES modules, tree-shakeable
14
- - **Universal** — Works in Node.js (18+) and modern browsers
23
+ - **Universal** — Works in Node.js (18+), modern browsers, and React Native (runtime-safe root import)
15
24
 
16
25
  ## Installation
17
26
 
@@ -21,32 +30,146 @@ npm install fitsjs-ng
21
30
  pnpm add fitsjs-ng
22
31
  ```
23
32
 
33
+ ## Runtime Compatibility Matrix
34
+
35
+ | Capability | Node.js | Browser | React Native |
36
+ | -------------------------------------------------- | ------- | ---------------------------- | ---------------------------- |
37
+ | `import { ... } from 'fitsjs-ng'` root import | ✅ | ✅ | ✅ |
38
+ | FITS/SER/XISF from `ArrayBuffer`/`Blob`/`URL` | ✅ | ✅ | ✅ |
39
+ | XISF detached signature verification (default on) | ✅ | ✅ (requires WebCrypto) | ✅ (requires WebCrypto) |
40
+ | `NodeFSTarget` | ✅ | ❌ (runtime error) | ❌ (runtime error) |
41
+ | `HiPS.open('/local/path')` | ✅ | ❌ (runtime error) | ❌ (runtime error) |
42
+ | `lintHiPS('/local/path')` | ✅ | ❌ (runtime error report) | ❌ (runtime error report) |
43
+ | distributed XISF `path(...)` with default resolver | ✅ | ❌ (provide custom resolver) | ❌ (provide custom resolver) |
44
+
45
+ Node-only APIs fail with actionable runtime messages in non-Node environments instead of failing at bundle-import time.
46
+
24
47
  ## Quick Start
25
48
 
26
49
  ```ts
27
- import { FITS, Image } from 'fitsjs-ng'
50
+ import {
51
+ FITS,
52
+ SER,
53
+ XISF,
54
+ XISFWriter,
55
+ parseSERBuffer,
56
+ parseSERBlob,
57
+ convertFitsToXisf,
58
+ convertXisfToFits,
59
+ convertSerToFits,
60
+ convertFitsToSer,
61
+ convertSerToXisf,
62
+ convertXisfToSer,
63
+ convertXisfToHiPS,
64
+ convertHiPSToXisf,
65
+ NodeFSTarget,
66
+ Image,
67
+ } from 'fitsjs-ng'
68
+ import fs from 'node:fs'
69
+
70
+ // FITS from ArrayBuffer / Blob / Node buffer-like / URL
71
+ const fits = FITS.fromArrayBuffer(
72
+ await fs.promises
73
+ .readFile('image.fits')
74
+ .then((b) => b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength)),
75
+ )
76
+ const fitsFromBlob = await FITS.fromBlob(new Blob([await fs.promises.readFile('image.fits')]))
77
+ const fitsFromNodeBuffer = FITS.fromNodeBuffer(await fs.promises.readFile('image.fits'))
78
+ const fitsFromUrl = await FITS.fromURL('https://example.com/image.fits')
79
+
80
+ // Access header + image
81
+ const header = fits.getHeader()
82
+ console.log(header?.get('BITPIX'))
83
+ const image = fits.getDataUnit() as Image
84
+ const pixels = await image.getFrame(0)
85
+ const [min, max] = image.getExtent(pixels)
28
86
 
29
- // From a URL
30
- const fits = await FITS.fromURL('https://example.com/image.fits')
87
+ // FITS <-> XISF
88
+ const xisfBytes = await convertFitsToXisf(
89
+ await fs.promises
90
+ .readFile('image.fits')
91
+ .then((b) => b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength)),
92
+ )
93
+ const xisf = await XISF.fromArrayBuffer(xisfBytes as ArrayBuffer)
94
+ const fitsBytes = await convertXisfToFits(xisf)
95
+
96
+ // SER parse + conversions
97
+ const serBytes = await fs.promises
98
+ .readFile('capture.ser')
99
+ .then((b) => b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength))
100
+ const ser = SER.fromArrayBuffer(serBytes)
101
+ const parsedSer = parseSERBuffer(serBytes)
102
+ const parsedSerBlob = await parseSERBlob(new Blob([serBytes]))
103
+ const fitsFromSer = await convertSerToFits(serBytes, { layout: 'cube' })
104
+ const serFromFits = await convertFitsToSer(fitsFromSer, { sourceLayout: 'auto' })
105
+ const xisfFromSer = await convertSerToXisf(serBytes)
106
+ const serFromXisf = await convertXisfToSer(xisfFromSer as ArrayBuffer, { imageIndex: 0 })
107
+
108
+ // XISF <-> HiPS (offline/local target)
109
+ const hipsTarget = new NodeFSTarget('./demo/.out/readme-quickstart-hips')
110
+ await convertXisfToHiPS(xisfBytes as ArrayBuffer, {
111
+ output: hipsTarget,
112
+ title: 'XISF Survey',
113
+ creatorDid: 'ivo://example/xisf',
114
+ hipsOrder: 4,
115
+ minOrder: 1,
116
+ tileWidth: 128,
117
+ formats: ['fits', 'png'],
118
+ })
119
+ const xisfCutout = await convertHiPSToXisf(hipsTarget, {
120
+ cutout: { width: 512, height: 512, ra: 83.63, dec: 22.01, fov: 1.2 },
121
+ })
122
+
123
+ // XISF writer outputs
124
+ const monolithic = await XISFWriter.toMonolithic(xisf.unit, { compression: 'zlib' })
125
+ const distributed = await XISFWriter.toDistributed(xisf.unit, { compression: 'zlib' })
126
+ // distributed.header => .xish bytes, distributed.blocks['blocks.xisb'] => .xisb bytes
127
+ ```
31
128
 
32
- // From an ArrayBuffer
33
- const fits = FITS.fromArrayBuffer(buffer)
129
+ ### HiPS Quick Start
34
130
 
35
- // From a File/Blob (browser)
36
- const fits = await FITS.fromBlob(file)
131
+ ```ts
132
+ import { NodeFSTarget, convertFitsToHiPS, convertHiPSToFITS, HiPS, lintHiPS } from 'fitsjs-ng'
133
+
134
+ const target = new NodeFSTarget('./out/my-hips')
135
+ await convertFitsToHiPS(fitsArrayBuffer, {
136
+ output: target,
137
+ title: 'My Survey',
138
+ creatorDid: 'ivo://example/my-survey',
139
+ hipsOrder: 6,
140
+ tileWidth: 512,
141
+ formats: ['fits', 'png'],
142
+ interpolation: 'bilinear',
143
+ })
144
+
145
+ const hips = await HiPS.open('./out/my-hips')
146
+ const tile = await hips.readTile({ order: 6, ipix: 12345, format: 'fits' })
147
+
148
+ const cutoutFits = await convertHiPSToFITS('./out/my-hips', {
149
+ cutout: { width: 1024, height: 1024, ra: 83.63, dec: 22.01, fov: 1.2 },
150
+ backend: 'auto', // local first, fallback to hips2fits if hipsId is set
151
+ hipsId: 'CDS/P/2MASS/K',
152
+ })
153
+
154
+ const lint = await lintHiPS('./out/my-hips')
155
+ console.log(lint.ok, lint.issues)
156
+ ```
37
157
 
38
- // From a Node.js Buffer
39
- const fits = FITS.fromNodeBuffer(fs.readFileSync('image.fits'))
158
+ ### React Native Notes
40
159
 
41
- // Access the primary header
42
- const header = fits.getHeader()
43
- console.log(header.get('BITPIX')) // e.g. -32
44
- console.log(header.get('NAXIS1')) // e.g. 1024
160
+ - Prefer `ArrayBuffer` / `Blob` / URL-based workflows.
161
+ - Use custom `HiPSExportTarget` implementations or browser-friendly targets (`BrowserZipTarget`) instead of `NodeFSTarget`.
162
+ - Avoid local filesystem path inputs (`HiPS.open('/path')`, `lintHiPS('/path')`) unless you provide your own storage abstraction.
163
+ - Detached XISF signature verification requires `crypto.subtle`; if unavailable, verification fails by default.
45
164
 
46
- // Read image pixels
47
- const image = fits.getDataUnit() as Image
48
- const pixels = await image.getFrame(0)
49
- const [min, max] = image.getExtent(pixels)
165
+ ```ts
166
+ import { XISF } from 'fitsjs-ng'
167
+
168
+ // If your RN runtime does not provide WebCrypto, disable signature verification explicitly.
169
+ const xisf = await XISF.fromArrayBuffer(bytes, {
170
+ verifySignatures: false,
171
+ signaturePolicy: 'ignore',
172
+ })
50
173
  ```
51
174
 
52
175
  ## API Reference
@@ -62,6 +185,80 @@ Static factory methods:
62
185
  | `FITS.fromURL(url)` | Fetch and parse remote file (async) |
63
186
  | `FITS.fromNodeBuffer(buffer)` | Parse from Node.js `Buffer` (sync) |
64
187
 
188
+ ### `XISF`
189
+
190
+ Static factory methods:
191
+
192
+ | Method | Description |
193
+ | ------------------------------ | ---------------------------------------- |
194
+ | `XISF.fromArrayBuffer(buffer)` | Parse from `ArrayBuffer` |
195
+ | `XISF.fromBlob(blob)` | Parse from `Blob`/`File` |
196
+ | `XISF.fromURL(url)` | Fetch and parse remote `.xisf`/`.xish` |
197
+ | `XISF.fromNodeBuffer(buffer)` | Parse from Node.js `Buffer`-like payload |
198
+
199
+ ### `SER`
200
+
201
+ Static factory methods:
202
+
203
+ | Method | Description |
204
+ | ----------------------------- | --------------------------------------------------- |
205
+ | `SER.fromArrayBuffer(buffer)` | Parse SER from `ArrayBuffer` |
206
+ | `SER.fromBlob(blob)` | Parse SER from `Blob`/`File` |
207
+ | `SER.fromURL(url)` | Fetch and parse remote `.ser` |
208
+ | `SER.fromNodeBuffer(buffer)` | Parse SER from Node.js `Buffer`-like payload |
209
+ | `parseSERBuffer(buffer)` | Parse SER buffer and return structured parse result |
210
+ | `parseSERBlob(blob)` | Parse SER blob and return structured parse result |
211
+ | `writeSER(input)` | Serialize SER header + frames (+ optional trailer) |
212
+
213
+ Instance helpers:
214
+
215
+ | Method | Description |
216
+ | -------------------------- | ---------------------------------------------- |
217
+ | `ser.getFrameCount()` | Total frame count |
218
+ | `ser.getFrameRGB(i)` | RGB helper decode for mono/Bayer/CMY/RGB/BGR |
219
+ | `ser.getDurationTicks()` | Duration from trailer timestamps (100ns ticks) |
220
+ | `ser.getDurationSeconds()` | Duration in seconds from trailer timestamps |
221
+ | `ser.getEstimatedFPS()` | Estimated FPS from timestamp spacing |
222
+
223
+ ### `XISFWriter`
224
+
225
+ | Method | Description |
226
+ | ---------------------------- | ------------------------------------------------ |
227
+ | `XISFWriter.toMonolithic()` | Serialize to monolithic `.xisf` bytes |
228
+ | `XISFWriter.toDistributed()` | Serialize to distributed `.xish` + `.xisb` bytes |
229
+
230
+ ### Conversion
231
+
232
+ | Method | Description |
233
+ | ----------------------------------- | -------------------------------------------------- |
234
+ | `convertXisfToFits(input)` | Convert XISF to FITS bytes |
235
+ | `convertFitsToXisf(input)` | Convert FITS to XISF bytes (or distributed object) |
236
+ | `convertSerToFits(input)` | Convert SER to FITS bytes |
237
+ | `convertFitsToSer(input)` | Convert FITS to SER bytes |
238
+ | `convertSerToXisf(input)` | Convert SER to XISF bytes |
239
+ | `convertXisfToSer(input)` | Convert XISF to SER bytes |
240
+ | `convertFitsToHiPS(input, options)` | Convert FITS to HiPS directory |
241
+ | `convertHiPSToFITS(input, options)` | Export HiPS to FITS tile/map/cutout |
242
+
243
+ SER conversion options:
244
+
245
+ - `convertSerToFits(input, { layout: 'cube' | 'multi-hdu' })` (default: `'cube'`)
246
+ - `convertFitsToSer(input, { sourceLayout: 'auto' | 'cube' | 'multi-hdu' })` (default: `'auto'`)
247
+ - `convertXisfToSer(input, { imageIndex })` for multi-image XISF units
248
+
249
+ ### HiPS
250
+
251
+ | Method / Class | Description |
252
+ | ---------------------------------------- | ------------------------------------------------- |
253
+ | `HiPS.open(source)` | Open HiPS from local path, URL, or storage target |
254
+ | `HiPS.getProperties()` | Load and parse `properties` |
255
+ | `HiPS.readTile({ order, ipix, format })` | Read/decode one tile |
256
+ | `NodeFSTarget` | Node filesystem output target |
257
+ | `BrowserZipTarget` | Browser ZIP output target |
258
+ | `BrowserOPFSTarget` | Browser OPFS output target |
259
+ | `HiPSProperties` | Parse/serialize/validate HiPS properties |
260
+ | `lintHiPS(source)` | Validate metadata and structure |
261
+
65
262
  Instance methods:
66
263
 
67
264
  | Method | Description |
@@ -84,13 +281,40 @@ Instance methods:
84
281
 
85
282
  ### `Image`
86
283
 
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 |
284
+ | Method | Description |
285
+ | -------------------------- | --------------------------------------------------------- |
286
+ | `getFrame(frame?)` | Read a single frame (async) |
287
+ | `getFrameAsNumber(frame?)` | Read frame as `Float64Array` (explicitly lossy for int64) |
288
+ | `getFrames(start, count)` | Read multiple frames (async) |
289
+ | `getExtent(pixels)` | Compute `[min, max]` (`number`/`bigint`) |
290
+ | `getPixel(pixels, x, y)` | Get pixel at (x, y) (`number`/`bigint`) |
291
+ | `isDataCube()` | Whether NAXIS > 2 |
292
+
293
+ `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.
294
+
295
+ ### XISF Signature Policy
296
+
297
+ `XISF.fromArrayBuffer()` accepts:
298
+
299
+ - `signaturePolicy: 'require' | 'warn' | 'ignore'` (default: `'require'`)
300
+ - `verifySignatures` (default: `true`)
301
+
302
+ Behavior:
303
+
304
+ - **`require`**: signed documents must verify; failures throw `XISFSignatureError`
305
+ - **`warn`**: signature failures are reported through warnings and `unit.signature`
306
+ - **`ignore`**: signature verification is skipped
307
+
308
+ When a detached signature is present and verification is enabled, checksum verification is forced for attachment/path/url data blocks.
309
+
310
+ ### FITS↔XISF Preservation Scope
311
+
312
+ `convertFitsToXisf()` / `convertXisfToFits()` preserve:
313
+
314
+ - FITS keyword values **and comments** (`Header.getCards()` based mapping)
315
+ - non-image HDUs through `FITS:PreservedHDULayout` metadata (reversible card+payload container)
316
+
317
+ For `BITPIX=64`, canonical unsigned encoding (`BSCALE=1`, `BZERO=9223372036854775808`) is detected with strict raw-card parsing (no tolerance heuristics).
94
318
 
95
319
  ### `Table` (ASCII)
96
320
 
@@ -169,8 +393,29 @@ pnpm test # Run tests
169
393
  pnpm build # Build library
170
394
  pnpm typecheck # Type check
171
395
  pnpm lint # Lint
396
+ pnpm demo:all # Run all Node demos in sequence
397
+ pnpm demo # FITS/XISF CLI demo
398
+ pnpm demo:hips # HiPS Node demo (FITS->HiPS->FITS)
399
+ pnpm demo:xisf # XISF Node demo (FITS<->XISF, monolithic/distributed)
400
+ pnpm demo:ser # SER Node demo (SER<->FITS<->XISF)
401
+ pnpm demo:web # Serve web demos (open /demo/web/index.html, /demo/web/hips.html, /demo/web/xisf.html)
172
402
  ```
173
403
 
404
+ Node demo artifacts are written under `demo/.out/*`.
405
+
406
+ ## Standards & Compatibility
407
+
408
+ - HiPS metadata and directory naming follow HiPS 1.0 conventions (`Norder*/Dir*/Npix*`, `Norder3/Allsky.*`, `properties`, `Moc.fits`).
409
+ - FITS writing follows FITS 4.0 card/block alignment rules (80-char cards, 2880-byte blocks).
410
+ - Output `properties` defaults to `hips_version=1.4` and also writes legacy compatibility fields (`coordsys`, `maxOrder`, `format`).
411
+ - XISF default codec provider supports `zlib`, `lz4`, and `lz4hc` for read/write and `zstd` for read; custom providers can extend encoding support.
412
+
413
+ ## Remote Backend Behavior
414
+
415
+ - `backend: 'local'`: all conversion is performed locally.
416
+ - `backend: 'remote'`: cutout export uses CDS hips2fits endpoint directly.
417
+ - `backend: 'auto'`: try local cutout first, then fallback to hips2fits when `hipsId` is provided.
418
+
174
419
  ## Credits
175
420
 
176
421
  Based on [astrojs/fitsjs](https://github.com/astrojs/fitsjs) by Amit Kapadia / Zooniverse.