Parser for .spc spectroscopy files: Thermo Galactic GRAMS SPC and Shimadzu UVProbe (OLE2 and flat), and for PerkinElmer .sp files (binary and ASCII). The format is auto-detected.
$ npm i spc-parser
This package is ESM-only. It requires Node.js ≥ 20.19, ≥ 22.12, or any 24.x or later, or a bundler. CommonJS consumers must import it (no require).
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { parse } from 'spc-parser';
const arrayBuffer = readFileSync(join(import.meta.dirname, 'spectrum.spc'));
const result = parse(arrayBuffer);
// result is a JSON object containing everything that was parsed
Version 3 changes the output where 2.x was wrong or incomplete. Y values and the number of spectra can both change, so re-read anything derived from a 2.x result.
logs.data is removed. Despite the name it was never the log's data: it held the binary area only, decoded as a string, and dropped the disk area — wplanes.spc, whose log is 5884 disk bytes and nothing else, came back empty. The two areas are now logs.binary and logs.disk, both Uint8Array (see The log block). Old-format files have logs: null.timeZone to turn them into real instants (see Dates). Only Renishaw WiRE MS-DOS and Unix dates keep Z. date, dateLayout and zpdIndex are omitted when absent.m_evenz.spc and DEMO_3D.SPC were 2x too small, and subfile 39 of LC_DIODE_ARRAY.SPC 64x too small. meta.exponent is the exponent used, meta.exponentY the raw subexp.spectra[0] and its Y values can differ; the Y label follows the measuring mode (Absorbance, Transmittance or Reflectance).Unknown (code N).encoding option.sortX: true, logs.imaginary is reversed together with a decreasing X.parse throws SpcParserError (not a bare Error or RangeError) and returns warnings, which is always an array.RangeError in 2.x now parse: a log area past the end of the file, a log after an XYXY directory, and a gap before flogoff.timeZone, a file whose date is a wall-clock reading adds one warning saying so and how to resolve it. Passing timeZone silences it.| Option | Default | Description |
|---|---|---|
encoding |
'windows-1252' |
Encoding of text that is not valid UTF-8 (header memo and labels, log text, UVProbe text), e.g. 'gbk' or 'shift_jis'. Valid UTF-8 is always decoded as such. |
strict |
false |
Throw a SpcParserError with code 'STRICT' on the first anomaly instead of adding it to warnings. |
sortX |
true |
Return every spectrum sorted by increasing X. With false, the file order is kept (for Galactic files, meta.xDirection tells which it is). |
timeZone |
(none) | Zone the instrument's clock was set to, as an IANA name ('Europe/Zurich'), a fixed offset ('+01:00') or 'UTC'. Makes wall-clock dates real instants — see Dates. |
logger |
(none) | A cheminfo-types LightLogger (console, a FifoLogger, …) that receives every anomaly as it is found, in addition to warnings. |
| Export | Description |
|---|---|
parse(buffer, options) |
Parses an .spc or .sp file, auto-detecting the format. Accepts an ArrayBuffer, a typed array / DataView, a Node.js Buffer, or an IOBuffer. Returns a ParseResult. |
SpcParserError |
Error class thrown by parse, with a code of 'UNSUPPORTED_FORMAT', 'TRUNCATED' or 'STRICT'. |
getLogValue(logs, key) |
Value of a Galactic log key (case-insensitive, first match), or undefined. |
guessSpectraType(meta) |
Classifies result.meta as 'ir' | 'uv' | 'raman' | 'mass' | 'other' from the raw Galactic codes; UVProbe results are 'uv'; PerkinElmer results follow their X units (nm is 'uv', cm-1 is 'ir'). |
| types | ParseOptions (encoding, strict, sortX, timeZone, logger), ParseResult, Header, TheNewHeader, TheOldHeader, GalacticSpectrumMeta, UvProbeMeta, UvProbeDatasetMeta, ShimadzuFlatHeader, PerkinElmerMeta, PerkinElmerSpectrumMeta, PerkinElmerAsciiSpectrumMeta, PerkinElmerHistoryRecord, Axes, Axis, HeaderCodes, CustomLabels, DateLayout, XDirection, ZSpacing, SpectraType, LogBlock, LogMetadata, LogEntry, LogHistoryEntry, SpcParserErrorCode, … |
ParseResult| Property | Description |
|---|---|
meta |
The Galactic Header (kind 'new' or 'old'), the Shimadzu UvProbeMeta (kind 'ole' or 'flat') or the PerkinElmerMeta (kind 'pepe' or 'peds'). |
spectra |
Array of MeasurementXY (cheminfo-types). Each spectrum owns its x and y Float64Arrays, and spectrum.meta holds its own metadata: cast it to GalacticSpectrumMeta for Galactic files to UvProbeDatasetMeta (OLE) or UvProbeMeta (flat) for UVProbe files, and to PerkinElmerSpectrumMeta (binary) or PerkinElmerAsciiSpectrumMeta (ASCII) for PerkinElmer files. |
logs |
Galactic log block, or null when the file has none (always null for the old format; absent for UVProbe and PerkinElmer). |
warnings |
Non-fatal anomalies found while parsing, always an array. Each message is written for a human; the data that could be read is still returned. |
parse throws a SpcParserError for files it cannot read, never a raw RangeError. An unsupported encoding or timeZone option throws a TypeError before the file is read.
code |
When |
|---|---|
UNSUPPORTED_FORMAT |
The buffer is not a Galactic SPC, a UVProbe OLE2 document or a Shimadzu flat file. The message starts with Unsupported SPC file and names the detected format when it is known (see Rejected formats). |
TRUNCATED |
A Galactic file shorter than its header (512 bytes, 256 for the old format) or without one complete subfile. A buffer under 32 bytes is too short to be recognised and gets UNSUPPORTED_FORMAT. |
STRICT |
Any warning, when strict: true. |
Warnings cover, among others:
Subfile k of n is truncated; m complete subfiles returned, trailing bytes after the last old-format subfile;fnsub without the multifile flag or fnsub 0 in a multifile, TXYXYS without TXVALS, fwplanes not dividing the number of subfiles, an XYXY directory pointing outside the file, a Wasatch subnpts twice too large;fdate 0x… is not a valid date; date ignored, fdate 0x… decoded as a Unix timestamp;flogoff is 0;timeZone was given (once per file, see Dates).An SPC file records the wall clock the operator saw, and never its offset. The instrument writes 08:54 because that is what the lab machine's clock read; nothing in the file says which zone that was. So a decoded date cannot name an instant on its own, and by default meta.date carries no offset:
parse(buffer).meta.date; // '1995-10-28T08:54:00.000'
That string is not an instant. new Date() reads it as the reader's local time, so a lab PC in Zurich and a UTC server disagree about what it means — and a Tokyo CI runner lands on the previous day. Never pass it to new Date().
Because that is easy to miss, such a file adds one warning naming the fix, and strict: true turns it into a SpcParserError:
A date of this file is a wall-clock reading with no time zone, so it names no instant: reading it
with `new Date()` applies the reader's own zone, which differs between a lab machine and a UTC
server. Pass the `timeZone` option set to the zone the instrument clock was on, such as
{ timeZone: 'Europe/Zurich' }, or { timeZone: 'UTC' } to keep the strings version 2 produced.
`meta.dateLayout` says which layout the date came from.
It is reported once per file, never for a date that already names an instant, and not at all once timeZone is set.
Give timeZone the zone the instrument's clock was set to and every date becomes an instant that is the same everywhere:
parse(buffer, { timeZone: 'Europe/Zurich' }).meta.date;
// '1995-10-28T08:54:00.000+01:00' -> 1995-10-28T07:54:00.000Z
The wall-clock digits never change; only the offset is added. Daylight saving is resolved for the date in question, so a summer and a winter file in the same zone get different offsets. timeZone: 'UTC' reproduces the strings 2.x emitted.
Two layouts are already real instants and are never shifted by timeZone:
meta.dateLayout |
Meaning | Ends with Z |
|---|---|---|
spec, labspec, old |
wall clock, offset unknown | only with timeZone |
msdos |
Renishaw WiRE writes this field in UTC | always |
unix |
a time_t, which is UTC |
always |
meta.dateLayout is the way to tell the two apart. The same applies to the per-spectrum dates from the SUBFILEx log keys, the Shimadzu flat date and the UVProbe OLE dates, which are all wall-clock readings.
meta keeps every raw header field (exponentY, numberPoints, startingX, memo, logOffset, …) and adds:
codes {experiment, x, y, z, w, post, processing}: the raw enumerated codes;axes {x, y, z, w}: {label, units, code, custom}: the label and units of each axis, from the code tables and, when the file enables them (TALABS/ZTEXTL) or the code is 0, the custom labels. xUnitsType, yUnitsType, zUnitsType and wAxisUnits are the code-table texts; unknown codes give Unknown (code N);customLabels {x, y, z} and customLabelsApplied; memoFragments (readable text found after the memo's NUL);date, dateLayout ('spec', 'labspec', 'msdos', 'unix' or 'old') and rawDate. Dates without a time zone are local ISO strings (2016-02-19T16:46:00.000); only MS-DOS (Renishaw WiRE) and Unix dates end with Z. date is undefined when absent or invalid;modifications [{letter, description}], postingLabel, processingLabels, invertedPeaks, drawAsSticks, isChromatogram, zpdIndex;xDirection ('increasing', 'decreasing' or 'unordered', as stored in the file), zSpacing ('even', 'ordered' or 'random', multifiles), planeSize (4D files), directoryOffset and directory (XYXY files with a valid subfile directory);spectra (computed from the file size) and trailingText.Each spectrum.meta keeps the raw subheader (exponentY is the raw subexp, startingZ, wAxisValue, …) and adds exponent (the exponent actually used), isFloat, index, flags, and when defined z, zEnd, w, plane, noise, plus date and title from the SUBFILEx log keys.
A log block is four consecutive regions, each sized by a field of its 64-byte LOGSTC header. The two byte areas are separate fields of the format, not two views of the same bytes: GRAMS loaded one into memory and left the other on disk, which is where their names come from.
| Region | Size field | What it holds |
|---|---|---|
| header | — | the LOGSTC fields, as logs.meta |
| binary | logbins |
logs.binary. For NMR, the imaginary part as float32 |
| disk | logdsks |
logs.disk. Never loaded by GRAMS |
| text | at logtxto |
logs.text, and the entries parsed from it |
A file normally uses one area or the other: NMR_FID.SPC has 65536 binary bytes (16384 float32 values, one per Y point) and no disk area, while wplanes.spc has no binary area and 5884 disk bytes.
So logs holds meta (the LOGSTC fields), text (decoded and cut at its first NUL), lines, entries [{key, value, section?}], history [{date, letter?, description, program?, user?}], binary and disk (Uint8Array copies clamped to the file) and, for NMR files, imaginary (a Float64Array decoded from binary, in the order of y, reversed with it when a decreasing X is sorted).
meta describes the first dataset (numberPoints, title, date, properties) and lists all of them in datasets. Each spectrum.meta holds its dataset's storageName, datasetName, measuringModeCode, operator, organization, software, softwareVersion, instrumentName, series, model, serialNumber, date, title and properties. The Y label follows the measuring mode (Absorbance, Transmittance or Reflectance).meta.header holds the raw codes familyFlags, modeCode, speedCode, samplingCode, displayMax and displayMin. The Y label stays Absorbance.PEPE, kind 'pepe') return one spectrum per dataset block. meta.description is the file description (e.g. 2D constant interval DataSet file). Each spectrum.meta holds the dataset name, fileType, alias, samplingMethod, dataType, the unit type codes xUnitType and yUnitType, and the raw history records.PE … ASCII PEDS <version>, kind 'peds', saved as .sp or .asc) return one spectrum. meta.description is that first line; spectrum.meta holds name, creationDate, modificationDate and analyst. The dates are kept as written: the order of day and month follows the locale of the instrument computer, so meta.date is never set.meta.xUnits is the X axis code of the first spectrum. The axis codes nm, cm-1, A, %T and %R become Wavelength, Wavenumber, Absorbance, Transmittance and Reflectance; any other code is used as label and units.| Variant | Notes |
|---|---|
Galactic new format, little-endian (0x4B) |
Y, XY, XYY and XYXY (with or without the subfile directory), 16-bit, 32-bit and float Y |
Galactic new format, big-endian (0x4C) |
same layouts |
Galactic old format (0x4D, Spectra Calc / Lab Calc) |
single and multifile, 16- and 32-bit Y |
| Multifiles with even, ordered or random Z, and 4D W planes | z, w and plane per spectrum |
| Galactic log block | text, key/value entries, audit history, binary and disk areas, NMR imaginary data |
| Shimadzu UVProbe OLE2 documents | every dataset, localized (e.g. GBK) text with the encoding option |
Shimadzu UVProbe flat .spc |
fixed 120-byte header |
PerkinElmer binary .sp (PEPE) |
every dataset block, UV and IR |
PerkinElmer ASCII .sp / .asc (PEDS) |
versions 1.60 and 4.00 seen |
These files also use the .spc extension. They are recognized and rejected with UNSUPPORTED_FORMAT and a message naming the format: EDAX/AMETEK EDS or XRF spectra, ORTEC/EG&G gamma spectra, ASD FieldSpec spectra, Becker & Hickl TCSPC photon streams, Bruker EPR .par parameter files, Shimadzu RF-5300 fluorescence spectra, UVProbe Kinetics (.kin) documents, gzip / R RDS objects, an unidentified encrypted family, and plain text (XY data). Headerless Bruker EPR .spc data cannot be identified and gets the generic message.
Every fixture is listed with its source, license and SHA-256 in the PROVENANCE.md files next to it (src/galactic/__tests__/data/corpus/, src/uvProbe/__tests__/data/corpus/ and src/__tests__/data/). src/__tests__/regression.test.ts parses all of them. Set SPC_CORPUS_DIR to also run src/__tests__/corpus.test.ts over a local collection of files.
SPC is a file format used in the GRAMS Software Suite for storing all kinds of spectroscopic data, including infrared spectra, Raman spectra and UV/VIS spectra. The data is stored in binary with some parts encoded in ASCII. As such, the file must be viewed in either a hex editor or any compatible spectroscopy software.
An SPC file is organized in three blocks:
The File Header contains information about the whole file, such as how values are read, the type of format (Old, New LSB, New MSB), the structure of the data block (XY, XYY or XYXY) and the number of spectra.
The Data block contains the spectrum data and is composed of a subheader for each spectrum, the X values before the subheader if XY or XYY, else after each subheader if XYXY. After the subheader and X values come the Y values, which are read according to the method determined in the Main Header.
The Log block contains miscellaneous information that varies for each file, with a part written in ASCII and another one in binary.
The format details this parser implements are written up in docs/galactic-spec.md and docs/non-galactic-formats.md. Other useful files are in the docs folder.
The test fixtures are third-party instrument files under their own licences
(MIT, CC-BY-4.0, GPL-3.0, LGPL-3.0, NIST public domain). They are excluded from
the published package; see NOTICE.md for the attributions and the
PROVENANCE.md of each fixture folder for the per-file URL, licence and
SHA-256.