UNPKG

jsfitsio

Version:

FITS I/O javascript library.

198 lines 8.91 kB
// import { FITSHeader } from "./model/FITSHeader.js" import * as fs from 'fs'; export class FITSWriter { static createFITS(fitsParsed) { const headerBytes = this.createHeader(fitsParsed.header); const dataBytes = this.createData(fitsParsed.data, fitsParsed.header); const fitsFile = new Uint8Array(headerBytes.length + dataBytes.length); fitsFile.set(headerBytes, 0); fitsFile.set(dataBytes, headerBytes.length); return fitsFile; } static createHeader(header) { const BLOCK = 2880; const CARD = 80; const MUST_INT = new Set(["BITPIX", "NAXIS", "PCOUNT", "GCOUNT"]); const IS_LOGICAL = new Set(["SIMPLE", "EXTEND"]); const items = header.getItems(); function kw(s) { return (s ?? "").toUpperCase().padEnd(8, " ").slice(0, 8); } function card80(s) { return s.length >= CARD ? s.slice(0, CARD) : s.padEnd(CARD, " "); } // Emit COMMENT/HISTORY as multiple 72-char lines function makeCommentCards(kind, text) { const prefix = kw(kind); // "COMMENT " or "HISTORY " const width = CARD - prefix.length; // 72 const t = (text ?? "").toString(); if (!t.length) return [card80(prefix)]; // allow empty COMMENT/HISTORY line const out = []; for (let i = 0; i < t.length; i += width) { out.push(card80(prefix + t.slice(i, i + width))); } return out; } function quoteFitsString(s) { const unquoted = s.replace(/^'+|'+$/g, ""); const escaped = unquoted.replace(/'/g, "''"); return `'${escaped}'`; } // "= " + 20-char value field (or proper string) function valueField20(key, val) { let v = ""; const K = key.toUpperCase(); if (IS_LOGICAL.has(K)) { const tf = (val === true || val === "T" || val === "t") ? "T" : "F"; return `= ${tf.padStart(20, " ")}`; } if (MUST_INT.has(K) || /^NAXIS\d+$/.test(K)) { const n = Number(val); if (!Number.isFinite(n) || !Number.isInteger(n)) { throw new Error(`FITS header: ${K} must be an integer, got ${val}`); } return `= ${String(n).padStart(20, " ")}`; } if (typeof val === "number") { let s = Number.isInteger(val) ? String(val) : val.toExponential(10).replace("e", "E"); if (s.length > 20) s = val.toExponential(8).replace("e", "E"); return `= ${s.padStart(20, " ")}`; } if (typeof val === "string") { return `= ${quoteFitsString(val)}`; // strings can exceed 20-char field } return ""; } // Build one keyword card, and (if needed) emit overflow as COMMENT cards function makeKeywordWithComment(key, value, comment) { const K = key.toUpperCase(); if (K === "END") return [card80("END")]; if (K === "COMMENT" || K === "HISTORY") { const text = (value ?? comment ?? "").toString(); return makeCommentCards(K, text); } // Normal keyword let base = kw(K) + valueField20(K, value); // Attach trailing comment inside the same card if it fits if (comment && comment.length > 0) { const add = ` / ${comment}`; const spaceLeft = CARD - base.length; if (spaceLeft > 0) { const inCard = add.slice(0, spaceLeft); base = (base + inCard); // spill any overflow into COMMENT cards (strip a leading " / " if it didn't fit) const overflow = add.slice(spaceLeft).replace(/^\s*\/\s*/, ""); if (overflow.length > 0) { return [card80(base), ...makeCommentCards("COMMENT", overflow)]; } } else { // no room at all; put the whole comment in COMMENT lines return [card80(base), ...makeCommentCards("COMMENT", comment)]; } } return [card80(base)]; } // Build all cards with mandatory order first const map = new Map(items.map(it => [it.key.toUpperCase(), it])); const cards = []; const simple = map.get("SIMPLE"); if (!simple) throw new Error("Missing mandatory SIMPLE card"); cards.push(...makeKeywordWithComment("SIMPLE", simple.value, simple.comment)); const bitpix = map.get("BITPIX"); if (!bitpix) throw new Error("Missing mandatory BITPIX card"); cards.push(...makeKeywordWithComment("BITPIX", bitpix.value, bitpix.comment)); const naxis = map.get("NAXIS"); if (!naxis) throw new Error("Missing mandatory NAXIS card"); const nAxes = Number(naxis.value) || 0; cards.push(...makeKeywordWithComment("NAXIS", nAxes, naxis.comment)); for (let i = 1; i <= nAxes; i++) { const ki = `NAXIS${i}`; const it = map.get(ki); if (!it) throw new Error(`Missing mandatory ${ki} card`); cards.push(...makeKeywordWithComment(ki, it.value, it.comment)); } const extend = map.get("EXTEND"); if (extend) cards.push(...makeKeywordWithComment("EXTEND", extend.value, extend.comment)); for (const it of items) { const K = it.key.toUpperCase(); if (K === "SIMPLE" || K === "BITPIX" || K === "NAXIS" || /^NAXIS\d+$/.test(K) || K === "EXTEND" || K === "END") continue; cards.push(...makeKeywordWithComment(it.key, it.value, it.comment)); } // END + pad to 2880 cards.push(card80("END")); let headerString = cards.join(""); const pad = headerString.length % BLOCK ? BLOCK - (headerString.length % BLOCK) : 0; if (pad) headerString += " ".repeat(pad); return new TextEncoder().encode(headerString); } static createData(data, header) { // concat const totalLength = data.reduce((s, c) => s + c.length, 0); // OPTIONAL: verify size from BITPIX/NAXIS const bitpix = Math.abs(Number(header.findById("BITPIX")?.value ?? 0)); const naxis = Number(header.findById("NAXIS")?.value ?? 0); let elems = 1; for (let k = 1; k <= naxis; k++) { elems *= Number(header.findById(`NAXIS${k}`)?.value ?? 0); } const bytesPerElem = bitpix / 8; const expectedUnpadded = naxis > 0 ? elems * bytesPerElem : 0; if (expectedUnpadded && expectedUnpadded !== totalLength) { throw new Error(`Data length ${totalLength} does not match header expectation ${expectedUnpadded} (BITPIX=${bitpix}, NAXIS=${naxis})`); } // build and pad let dataBytes = new Uint8Array(totalLength); let off = 0; for (const chunk of data) { dataBytes.set(chunk, off); off += chunk.length; } const BLOCK = 2880; const remainder = dataBytes.length % BLOCK; if (remainder) { const pad = BLOCK - remainder; const padded = new Uint8Array(dataBytes.length + pad); padded.set(dataBytes); dataBytes = padded; // zeros already in new space } return dataBytes; } // static typedArrayToURL(fitsParsed: FITSParsed): string { // const fitsFile = FITSWriter.createFITS(fitsParsed) as Uint8Array; // const blob = new Blob([fitsFile], { type: "application/fits" }); // // console.log(`<html><body><img src='${URL.createObjectURL(b)}'</body></html>`); // const url = URL.createObjectURL(blob); // console.log(`Generated FITS file URL: ${url}`); // return url; // } static writeFITSFile(fitsParsed, filePath) { const fitsFile = this.createFITS(fitsParsed); try { fs.writeFileSync(filePath, fitsFile); console.log(`FITS file written successfully to: ${filePath}`); } catch (error) { console.error(`Error writing FITS file: ${error}`); } } } // const fitsParsed: FITSParsed = { // header: new FITSHeader(), // data: [new Uint8Array([1, 2, 3]), new Uint8Array([4, 5, 6])] // }; // // Specify the file path // const filePath = "/Users/fabriziogiordano/Desktop/PhD/code/new/FITSParser/output.fits"; // // Write the FITS file to the filesystem // FITSWriter.writeFITSFile(fitsParsed, filePath); //# sourceMappingURL=FITSWriter.js.map