/**
* spc-parser - Thermo Galactic GRAMS SPC files parser
* @version v0.5.1
* @link https://github.com/cheminfo/spc-parser#readme
* @license MIT
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.SPCParser = {}));
}(this, (function (exports) { 'use strict';
// eslint-disable-next-line import/no-unassigned-import
const decoder = new TextDecoder('utf-8');
function decode(bytes) {
return decoder.decode(bytes);
}
const encoder = new TextEncoder();
function encode(str) {
return encoder.encode(str);
}
const defaultByteLength = 1024 * 8;
class IOBuffer {
/**
* @param data - The data to construct the IOBuffer with.
* If data is a number, it will be the new buffer's length
* If data is `undefined`, the buffer will be initialized with a default length of 8Kb
* If data is an ArrayBuffer, SharedArrayBuffer, an ArrayBufferView (Typed Array), an IOBuffer instance,
* or a Node.js Buffer, a view will be created over the underlying ArrayBuffer.
* @param options
*/
constructor(data = defaultByteLength, options = {}) {
let dataIsGiven = false;
if (typeof data === 'number') {
data = new ArrayBuffer(data);
} else {
dataIsGiven = true;
this.lastWrittenByte = data.byteLength;
}
const offset = options.offset ? options.offset >>> 0 : 0;
const byteLength = data.byteLength - offset;
let dvOffset = offset;
if (ArrayBuffer.isView(data) || data instanceof IOBuffer) {
if (data.byteLength !== data.buffer.byteLength) {
dvOffset = data.byteOffset + offset;
}
data = data.buffer;
}
if (dataIsGiven) {
this.lastWrittenByte = byteLength;
} else {
this.lastWrittenByte = 0;
}
this.buffer = data;
this.length = byteLength;
this.byteLength = byteLength;
this.byteOffset = dvOffset;
this.offset = 0;
this.littleEndian = true;
this._data = new DataView(this.buffer, dvOffset, byteLength);
this._mark = 0;
this._marks = [];
}
/**
* Checks if the memory allocated to the buffer is sufficient to store more
* bytes after the offset.
* @param byteLength - The needed memory in bytes.
* @returns `true` if there is sufficient space and `false` otherwise.
*/
available(byteLength = 1) {
return this.offset + byteLength <= this.length;
}
/**
* Check if little-endian mode is used for reading and writing multi-byte
* values.
* @returns `true` if little-endian mode is used, `false` otherwise.
*/
isLittleEndian() {
return this.littleEndian;
}
/**
* Set little-endian mode for reading and writing multi-byte values.
*/
setLittleEndian() {
this.littleEndian = true;
return this;
}
/**
* Check if big-endian mode is used for reading and writing multi-byte values.
* @returns `true` if big-endian mode is used, `false` otherwise.
*/
isBigEndian() {
return !this.littleEndian;
}
/**
* Switches to big-endian mode for reading and writing multi-byte values.
*/
setBigEndian() {
this.littleEndian = false;
return this;
}
/**
* Move the pointer n bytes forward.
* @param n - Number of bytes to skip.
*/
skip(n = 1) {
this.offset += n;
return this;
}
/**
* Move the pointer to the given offset.
* @param offset
*/
seek(offset) {
this.offset = offset;
return this;
}
/**
* Store the current pointer offset.
* @see {@link IOBuffer#reset}
*/
mark() {
this._mark = this.offset;
return this;
}
/**
* Move the pointer back to the last pointer offset set by mark.
* @see {@link IOBuffer#mark}
*/
reset() {
this.offset = this._mark;
return this;
}
/**
* Push the current pointer offset to the mark stack.
* @see {@link IOBuffer#popMark}
*/
pushMark() {
this._marks.push(this.offset);
return this;
}
/**
* Pop the last pointer offset from the mark stack, and set the current
* pointer offset to the popped value.
* @see {@link IOBuffer#pushMark}
*/
popMark() {
const offset = this._marks.pop();
if (offset === undefined) {
throw new Error('Mark stack empty');
}
this.seek(offset);
return this;
}
/**
* Move the pointer offset back to 0.
*/
rewind() {
this.offset = 0;
return this;
}
/**
* Make sure the buffer has sufficient memory to write a given byteLength at
* the current pointer offset.
* If the buffer's memory is insufficient, this method will create a new
* buffer (a copy) with a length that is twice (byteLength + current offset).
* @param byteLength
*/
ensureAvailable(byteLength = 1) {
if (!this.available(byteLength)) {
const lengthNeeded = this.offset + byteLength;
const newLength = lengthNeeded * 2;
const newArray = new Uint8Array(newLength);
newArray.set(new Uint8Array(this.buffer));
this.buffer = newArray.buffer;
this.length = this.byteLength = newLength;
this._data = new DataView(this.buffer);
}
return this;
}
/**
* Read a byte and return false if the byte's value is 0, or true otherwise.
* Moves pointer forward by one byte.
*/
readBoolean() {
return this.readUint8() !== 0;
}
/**
* Read a signed 8-bit integer and move pointer forward by 1 byte.
*/
readInt8() {
return this._data.getInt8(this.offset++);
}
/**
* Read an unsigned 8-bit integer and move pointer forward by 1 byte.
*/
readUint8() {
return this._data.getUint8(this.offset++);
}
/**
* Alias for {@link IOBuffer#readUint8}.
*/
readByte() {
return this.readUint8();
}
/**
* Read `n` bytes and move pointer forward by `n` bytes.
*/
readBytes(n = 1) {
const bytes = new Uint8Array(n);
for (let i = 0; i < n; i++) {
bytes[i] = this.readByte();
}
return bytes;
}
/**
* Read a 16-bit signed integer and move pointer forward by 2 bytes.
*/
readInt16() {
const value = this._data.getInt16(this.offset, this.littleEndian);
this.offset += 2;
return value;
}
/**
* Read a 16-bit unsigned integer and move pointer forward by 2 bytes.
*/
readUint16() {
const value = this._data.getUint16(this.offset, this.littleEndian);
this.offset += 2;
return value;
}
/**
* Read a 32-bit signed integer and move pointer forward by 4 bytes.
*/
readInt32() {
const value = this._data.getInt32(this.offset, this.littleEndian);
this.offset += 4;
return value;
}
/**
* Read a 32-bit unsigned integer and move pointer forward by 4 bytes.
*/
readUint32() {
const value = this._data.getUint32(this.offset, this.littleEndian);
this.offset += 4;
return value;
}
/**
* Read a 32-bit floating number and move pointer forward by 4 bytes.
*/
readFloat32() {
const value = this._data.getFloat32(this.offset, this.littleEndian);
this.offset += 4;
return value;
}
/**
* Read a 64-bit floating number and move pointer forward by 8 bytes.
*/
readFloat64() {
const value = this._data.getFloat64(this.offset, this.littleEndian);
this.offset += 8;
return value;
}
/**
* Read a 1-byte ASCII character and move pointer forward by 1 byte.
*/
readChar() {
return String.fromCharCode(this.readInt8());
}
/**
* Read `n` 1-byte ASCII characters and move pointer forward by `n` bytes.
*/
readChars(n = 1) {
let result = '';
for (let i = 0; i < n; i++) {
result += this.readChar();
}
return result;
}
/**
* Read the next `n` bytes, return a UTF-8 decoded string and move pointer
* forward by `n` bytes.
*/
readUtf8(n = 1) {
return decode(this.readBytes(n));
}
/**
* Write 0xff if the passed value is truthy, 0x00 otherwise and move pointer
* forward by 1 byte.
*/
writeBoolean(value) {
this.writeUint8(value ? 0xff : 0x00);
return this;
}
/**
* Write `value` as an 8-bit signed integer and move pointer forward by 1 byte.
*/
writeInt8(value) {
this.ensureAvailable(1);
this._data.setInt8(this.offset++, value);
this._updateLastWrittenByte();
return this;
}
/**
* Write `value` as an 8-bit unsigned integer and move pointer forward by 1
* byte.
*/
writeUint8(value) {
this.ensureAvailable(1);
this._data.setUint8(this.offset++, value);
this._updateLastWrittenByte();
return this;
}
/**
* An alias for {@link IOBuffer#writeUint8}.
*/
writeByte(value) {
return this.writeUint8(value);
}
/**
* Write all elements of `bytes` as uint8 values and move pointer forward by
* `bytes.length` bytes.
*/
writeBytes(bytes) {
this.ensureAvailable(bytes.length);
for (let i = 0; i < bytes.length; i++) {
this._data.setUint8(this.offset++, bytes[i]);
}
this._updateLastWrittenByte();
return this;
}
/**
* Write `value` as a 16-bit signed integer and move pointer forward by 2
* bytes.
*/
writeInt16(value) {
this.ensureAvailable(2);
this._data.setInt16(this.offset, value, this.littleEndian);
this.offset += 2;
this._updateLastWrittenByte();
return this;
}
/**
* Write `value` as a 16-bit unsigned integer and move pointer forward by 2
* bytes.
*/
writeUint16(value) {
this.ensureAvailable(2);
this._data.setUint16(this.offset, value, this.littleEndian);
this.offset += 2;
this._updateLastWrittenByte();
return this;
}
/**
* Write `value` as a 32-bit signed integer and move pointer forward by 4
* bytes.
*/
writeInt32(value) {
this.ensureAvailable(4);
this._data.setInt32(this.offset, value, this.littleEndian);
this.offset += 4;
this._updateLastWrittenByte();
return this;
}
/**
* Write `value` as a 32-bit unsigned integer and move pointer forward by 4
* bytes.
*/
writeUint32(value) {
this.ensureAvailable(4);
this._data.setUint32(this.offset, value, this.littleEndian);
this.offset += 4;
this._updateLastWrittenByte();
return this;
}
/**
* Write `value` as a 32-bit floating number and move pointer forward by 4
* bytes.
*/
writeFloat32(value) {
this.ensureAvailable(4);
this._data.setFloat32(this.offset, value, this.littleEndian);
this.offset += 4;
this._updateLastWrittenByte();
return this;
}
/**
* Write `value` as a 64-bit floating number and move pointer forward by 8
* bytes.
*/
writeFloat64(value) {
this.ensureAvailable(8);
this._data.setFloat64(this.offset, value, this.littleEndian);
this.offset += 8;
this._updateLastWrittenByte();
return this;
}
/**
* Write the charCode of `str`'s first character as an 8-bit unsigned integer
* and move pointer forward by 1 byte.
*/
writeChar(str) {
return this.writeUint8(str.charCodeAt(0));
}
/**
* Write the charCodes of all `str`'s characters as 8-bit unsigned integers
* and move pointer forward by `str.length` bytes.
*/
writeChars(str) {
for (let i = 0; i < str.length; i++) {
this.writeUint8(str.charCodeAt(i));
}
return this;
}
/**
* UTF-8 encode and write `str` to the current pointer offset and move pointer
* forward according to the encoded length.
*/
writeUtf8(str) {
return this.writeBytes(encode(str));
}
/**
* Export a Uint8Array view of the internal buffer.
* The view starts at the byte offset and its length
* is calculated to stop at the last written byte or the original length.
*/
toArray() {
return new Uint8Array(this.buffer, this.byteOffset, this.lastWrittenByte);
}
/**
* Update the last written byte offset
* @private
*/
_updateLastWrittenByte() {
if (this.offset > this.lastWrittenByte) {
this.lastWrittenByte = this.offset;
}
}
}
/**
* Gets the parameter in each bit of the flag
* @param {number} flag First byte of the main header
* @returns {object} The parameters
*/
function getFlagParameters(flag) {
const parameters = {}; //Z is time
parameters.y16BitPrecision = (flag & 1) !== 0; //Y values are 16 bits instead of 32
parameters.useExperimentExtension = (flag & 2) !== 0; //Enable experiment mode
parameters.multiFile = (flag & 4) !== 0; //Multiple spectra
parameters.zValuesRandom = (flag & 8) !== 0; //Z values in random order if multiFile
parameters.zValuesUneven = (flag & 16) !== 0; //Z values ordered but unevenly spaced if multi
parameters.customAxisLabels = (flag & 32) !== 0; //Custom labels
parameters.xyxy = (flag & 64) !== 0; //One X array per subfile, for discontinuous curves
parameters.xy = (flag & 128) !== 0; // Non-evenly spaced X, X before Y
return parameters;
}
/**
*
* Gets the Subfile flags
* @param {number} flag First byte of the subheader
* @return {object} The parameters
*/
function getSubFlagParameters(flag) {
const parameters = {};
parameters.changed = (flag & 1) !== 0;
parameters.noPeakTable = (flag & 8) !== 0;
parameters.modifiedArithmetic = (flag & 128) !== 0;
return parameters;
}
/**
* Generates an array of evenly spaced numbers
* @param {number} minimum Lower bound
* @param {number} maximum Upper bound
* @param {number} numberPoints Number of points
* @return {array} Evenly spaced numbers
*/
function equidistantArray(minimum, maximum, numberPoints) {
const equidistantArray = new Float64Array(numberPoints);
const step = (maximum - minimum) / (numberPoints - 1);
for (let i = 0; i < numberPoints; i++) {
equidistantArray[i] = minimum + i * step;
}
return equidistantArray;
}
/**
* Gets the date encoded in binary in a long number
* @param {number} long Binary date
* @return {string} Date formatted to ISO 8601:2019 convention
*/
function longToDate(long) {
if (long === 0) {
return '0000-00-00T00:00:00.00Z';
}
const date = new Date();
date.setUTCFullYear(long >> 20);
date.setUTCMonth((long >> 16 & 0x0f) - 1);
date.setUTCDate(long >> 11 & 0x1f);
date.setUTCHours(long >> 6 & 0x1f);
date.setUTCMinutes(long & 0x3f);
date.setUTCSeconds(0);
date.setUTCMilliseconds(0);
return date.toISOString();
}
/* eslint-disable no-control-regex */
/**
* Parses the subheader of the current subfile
*
* @export
* @param {object} buffer SPC buffer
* @return {object} Current subfile's subheader
*/
function subHeader(buffer) {
const subHeader = {};
subHeader.parameters = getSubFlagParameters(buffer.readUint8());
subHeader.exponentY = buffer.readInt8();
subHeader.indexNumber = buffer.readUint16();
subHeader.startingZ = buffer.readFloat32();
subHeader.endingZ = buffer.readFloat32();
subHeader.noiseValue = buffer.readFloat32();
subHeader.numberPoints = buffer.readUint32();
subHeader.numberCoAddedScans = buffer.readUint32();
subHeader.wAxisValue = buffer.readFloat32();
subHeader.reserved = buffer.readChars(4).trim().replace(/\x00/g, '');
return subHeader;
}
/**
* Reads the data block of the SPC file
*
* @export
* @param {object} buffer spc buffer
* @param {object} mainHeader main header
* @return {array} Array containing the spectra
*/
function readDataBlock(buffer, mainHeader) {
let x;
let y;
let spectra = [];
if (!mainHeader.parameters.xyxy && mainHeader.xy) {
x = new Float32Array(mainHeader.numberPoints);
for (let i = 0; i < mainHeader.numberPoints; i++) {
x[i] = buffer.readFloat32();
}
} else if (!mainHeader.parameters.xy) {
x = equidistantArray(mainHeader.startingX, mainHeader.endingX, mainHeader.numberPoints);
}
let spectrum;
for (let i = 0; i < mainHeader.spectra || mainHeader.fileVersion === 0x4d && buffer.offset + mainHeader.numberPoints < buffer.length; i++) {
spectrum = {};
spectrum.meta = subHeader(buffer);
if (mainHeader.parameters.xyxy) {
x = new Float32Array(spectrum.meta.numberPoints);
for (let j = 0; j < spectrum.meta.numberPoints; j++) {
x[j] = buffer.readFloat32();
}
}
if (spectrum.meta.exponentY === 0) {
spectrum.meta.exponentY = mainHeader.exponentY;
}
const yFactor = Math.pow(2, spectrum.meta.exponentY - (mainHeader.parameters.y16BitPrecision && spectrum.meta.exponentY !== 0x80 ? 16 : 32));
const nbPoints = spectrum.meta.numberPoints ? spectrum.meta.numberPoints : mainHeader.numberPoints;
if (mainHeader.parameters.y16BitPrecision) {
y = new Float32Array(nbPoints);
for (let j = 0; j < nbPoints; j++) {
y[j] = buffer.readInt16() * yFactor;
}
} else {
y = new Float32Array(nbPoints);
for (let j = 0; j < nbPoints; j++) {
if (mainHeader.fileVersion === 0x4d) {
y[j] = ((buffer.readUint8() << 16) + (buffer.readInt8() << 24) + (buffer.readUint8() << 0) + (buffer.readUint8() << 8)) * yFactor;
} else {
y[j] = buffer.readInt32() * yFactor;
}
}
}
const xAxis = mainHeader.xUnitsType.match(/(?