Spectral File Formats Explained: JCAMP-DX, SPC, OPUS, SPA, WDF & CSV
Spectroscopy instruments produce data — but the file format that data gets saved in varies widely from one manufacturer to another. Bruker saves FTIR spectra in its proprietary OPUS format. Thermo Fisher instruments produce SPC and SPA files. Renishaw Raman systems write WDF files. Academic databases favor the JCAMP-DX standard. And many researchers simply export to CSV when all else fails.
This format fragmentation creates real problems in analytical laboratories. You receive a collaborator's data and cannot open it. You upgrade instruments and your old files become inaccessible. You need to submit spectral data for publication but the journal requires a specific format. You want to compare spectra from two different instruments, but the files are incompatible.
This guide is the most comprehensive reference for spectroscopy file formats available online. It covers binary structure diagrams, parsing code examples in Python and JavaScript, format comparison tables, common pitfalls, and practical recommendations. Whether you're a developer building spectral software, a researcher trying to open legacy data, or a lab manager choosing an archival format, this page has the answers.
SpectralBench can open all of these formats directly — no software to install. Works on any device with a modern browser.
Format Comparison Table
A quick reference for every major spectroscopy file format, comparing openness, encoding, metadata support, and SpectralBench compatibility.
| Format | Extension | Vendor | Open Standard | Binary/Text | Multi-spectrum | Metadata | SpectralBench |
|---|---|---|---|---|---|---|---|
| JCAMP-DX | .dx, .jdx | IUPAC standard | Yes (open) | Text | Yes (compound files) | Rich | Full |
| SPC | .spc | Galactic / Thermo Fisher | Partially documented | Binary | Yes (sub-files) | Moderate | Full |
| OPUS | .0, .1, .2, ... | Bruker | Proprietary | Binary | Yes (data blocks) | Very rich | Full |
| SPA | .spa, .spg | Thermo Fisher | Proprietary | Binary | .spg only (groups) | Limited | Full |
| WDF | .wdf | Renishaw | Proprietary | Binary | Yes (maps/series) | Rich | Full |
| SP | .sp | PerkinElmer | Proprietary | Binary | No | Moderate | Full |
| JWS | .jws, .jwb | JASCO | Proprietary | Binary | No | Moderate | Full |
| EMSA/MSA | .msa, .emsa | ISO 22029 standard | Yes (open) | Text | No | Rich | Full |
| CSV/TSV | .csv, .tsv, .txt | Universal | Yes | Text | No | None | Full |
| Excel | .xlsx | Microsoft | Yes (Open XML) | ZIP/XML | Via sheets | Via headers | Full |
JCAMP-DX (.jdx, .dx) — Deep Dive
JCAMP-DX stands for Joint Committee on Atomic and Molecular Physical Data — Data Exchange. It is an IUPAC-endorsed, open standard designed specifically for exchanging spectroscopic data between different instruments and software platforms. The base standard for IR and Raman data is version 4.24 (1988). Later IUPAC extensions — version 5.01 (1993) for NMR data and compound file structures, and a separate JCAMP-MS extension for mass spectrometry — broadened the standard to additional techniques.
JCAMP-DX files are plain ASCII text, which means you can open them in any text editor and inspect the contents directly. The format uses Labeled Data Records (LDRs) — lines beginning with ## followed by a label and value.
File Structure
┌─────────────────────────────────────────────────────────────┐ │ JCAMP-DX File Structure (ASCII Text) │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ##TITLE= Polystyrene Film │ │ ##JCAMP-DX= 4.24 │ │ ##DATA TYPE= INFRARED SPECTRUM │ │ ##XUNITS= 1/CM │ │ ##YUNITS= ABSORBANCE │ │ ##FIRSTX= 399.225 │ │ ##LASTX= 3999.642 │ │ ##NPOINTS= 1868 │ │ ##XFACTOR= 1.0 │ │ ##YFACTOR= 0.000001 │ │ ##XYDATA= (X++(Y..Y)) │ │ 399.2 J1234 k567 L89 M012 ... │ │ 403.4 N345 o678 P901 Q234 ... │ │ ... │ │ ##END= │ │ │ └─────────────────────────────────────────────────────────────┘
Data Encoding Types (ASDF Compression)
The data section supports several compression forms to reduce file size while remaining ASCII-compatible:
- AFFN (ASCII Free Format Numeric): Plain numbers separated by spaces or commas. No compression. Largest files, easiest to read.
- SQZ (Squeezed): The first significant digit of each Y value is replaced by an alphabetic character: @=0, A–I = 1–9 (positive), a–i = 1–9 (negative). Each token is a standalone absolute Y value. Example: "C456" = 3456.
- PAC (Packed): Numbers are packed without delimiters using the same pseudo-digit character table as SQZ. "PAC" and "SQZ" use identical encoding; some implementations treat them as synonymous.
- DIF (Difference): Characters J-R (positive) and j-r (negative) encode differences from the previous Y value. "J0" = +10, "k5" = -25.
- DUP (Duplicate): Characters S-Z and s encode run-length repeats. "T" means repeat the previous value 2 more times.
Parsing — Python Example
import re
def parse_jcamp(filepath):
"""Extract metadata and data from a JCAMP-DX file."""
labels = {}
data_lines = []
in_data = False
with open(filepath, 'r') as f:
for line in f:
if line.startswith('##'):
match = re.match(r'^##(.+?)=\s*(.*)', line)
if match:
key = match.group(1).upper().replace(' ', '')
value = match.group(2).strip()
if key in ('XYDATA', 'XYPOINTS', 'PEAKTABLE'):
in_data = True
continue
if key == 'END':
break
labels[key] = value
in_data = False
elif in_data:
data_lines.append(line.strip())
first_x = float(labels.get('FIRSTX', 0))
last_x = float(labels.get('LASTX', 0))
npoints = int(labels.get('NPOINTS', 0))
y_factor = float(labels.get('YFACTOR', 1))
# For AFFN data (plain numbers), decode Y values:
y_values = []
for line in data_lines:
nums = line.split()
# First number is X checkpoint, rest are Y values
y_values.extend(float(n) for n in nums[1:])
# Generate evenly-spaced X axis
dx = (last_x - first_x) / (npoints - 1) if npoints > 1 else 0
points = [(first_x + i * dx, y * y_factor)
for i, y in enumerate(y_values[:npoints])]
return labels, pointsParsing — JavaScript Example
function parseJCAMPHeaders(text) {
const labels = {};
const lines = text.split(/\r?\n/);
for (const line of lines) {
if (!line.startsWith('##')) continue;
const match = line.match(/^##(.+?)=\s*(.*)/);
if (!match) continue;
const key = match[1].toUpperCase().replace(/\s/g, '');
if (key === 'END') break;
if (key === 'XYDATA' || key === 'XYPOINTS') continue;
labels[key] = match[2].trim();
}
return {
title: labels['TITLE'] || '',
xUnits: labels['XUNITS'] || '1/CM',
yUnits: labels['YUNITS'] || 'ABSORBANCE',
firstX: parseFloat(labels['FIRSTX'] || '0'),
lastX: parseFloat(labels['LASTX'] || '0'),
nPoints: parseInt(labels['NPOINTS'] || '0', 10),
xFactor: parseFloat(labels['XFACTOR'] || '1'),
yFactor: parseFloat(labels['YFACTOR'] || '1'),
};
}Common Gotchas
- Encoding detection: Many files mix SQZ and DIF encoding within the same data block. Your parser must handle per-character mode switching.
- Multi-block compound files: JCAMP-DX 5.x allows multiple spectra in one file via ##BLOCKS= and nested ##TITLE= / ##END= sections.
- Vendor extensions: Labels starting with ## followed by a dollar sign (e.g., ##$BRUKER PARAMETERS=) are vendor-specific and should be preserved but not required.
- Line-ending variance: Files may use CR+LF, LF only, or rarely CR only. Always split on /\r?\n/.
- YFACTOR precision: The Y-factor can be extremely small (1e-8). Always multiply Y values by YFACTOR after decoding — not before.
The great strength of JCAMP-DX is portability and longevity. Because the files are human-readable text and the format is an open standard, JCAMP files created in the 1990s can still be read today without any special software. This makes it the preferred format for spectral databases (NIST, SDBS, SpectraBase), academic publications, and long-term data archiving.
SPC File Format (.spc) — Deep Dive
The SPC file format (sometimes called GRAMS SPC) originated from Galactic Industries, the company behind the GRAMS spectroscopy software suite. Galactic was later acquired by Thermo Fisher Scientific, making the SPC file one of the most widely encountered spectral data formats in commercial analytical laboratories worldwide. If you have worked with Thermo Fisher instruments or received data from a GRAMS user, you have likely encountered an SPC file.
Unlike JCAMP-DX, the SPC file format is binary. Each SPC file begins with a fixed-length 512-byte header that contains all metadata: the experiment type, x-axis and y-axis units, the number of data points, the date and time of acquisition, instrument parameters, and a free-text memo field. To open an SPC file, you typically need GRAMS, OMNIC, or a compatible viewer — or you can use SpectralBench's free online viewer to view SPC files directly.
Binary Header Structure (New Format, 0x4B)
┌───────────────────────────────────────────────────────────────┐ │ SPC File Structure (New Format, version 0x4B) │ ├───────┬──────┬────────────────────────────────────────────────┤ │Offset │ Size │ Description │ ├───────┼──────┼────────────────────────────────────────────────┤ │ 0x00 │ 1 │ ftflgs — flags byte (see bit definitions) │ │ 0x01 │ 1 │ fversn — version (0x4B=new, 0x4D=old) │ │ 0x02 │ 1 │ fexper — experiment type code (IR, Raman, etc) │ │ 0x03 │ 1 │ fexp — Y exponent (128 = IEEE float32) │ │ 0x04 │ 4 │ fnpts — number of data points (int32 LE) │ │ 0x08 │ 8 │ ffirst — first X value (float64 LE) │ │ 0x10 │ 8 │ flast — last X value (float64 LE) │ │ 0x18 │ 4 │ fnsub — number of sub-files (int32 LE) │ │ 0x1C │ 1 │ fxtype — X-axis type code │ │ 0x1D │ 1 │ fytype — Y-axis type code │ │ 0x1E │ 1 │ fztype — Z-axis type code │ │ 0x1F │ 1 │ fpost — posting disposition │ │ 0x20 │ 4 │ fdate — date as packed 32-bit integer │ │ ... │ │ │ │ 0x58 │ 130 │ fcmnt — comment / memo text (null-terminated) │ │ ... │ │ │ │ 0x200 │ var │ [Optional X-values array if TXVALS flag set] │ │ │ │ Sub-file header (32 bytes each) │ │ │ │ Y-values array (float32 or scaled int32) │ └───────┴──────┴────────────────────────────────────────────────┘ Flags byte (ftflgs) bit definitions: Bit 0 (0x01): TSPREC — Y data is 16-bit (else 32-bit) Bit 2 (0x04): TMULTI — Multiple sub-files with individual headers Bit 6 (0x40): TXYXYS — Each sub-file has its own X array Bit 7 (0x80): TXVALS — Separate X-values array follows main header
Old vs New Format
SPC has two distinct versions. The "new" format (version byte 0x4B or 0x4C) uses a 512-byte header with float64 X-range values. The "old" format (version byte 0x4D) predates the Thermo acquisition and uses a 256-byte header with float32 values and word-swapped Y data encoding. SpectralBench handles both transparently.
Parsing — Python Example
import struct
import numpy as np
def parse_spc(filepath):
"""Parse an SPC file (new format 0x4B)."""
with open(filepath, 'rb') as f:
data = f.read()
# Read the 512-byte main header
ftflgs, fversn, fexper, fexp = struct.unpack_from('<BBBB', data, 0)
fnpts = struct.unpack_from('<i', data, 4)[0]
ffirst, flast = struct.unpack_from('<dd', data, 8)
fnsub = struct.unpack_from('<i', data, 24)[0]
fxtype, fytype = struct.unpack_from('<BB', data, 28)
# Generate evenly-spaced X values (unless TXVALS flag is set)
if ftflgs & 0x80: # TXVALS: explicit X array follows header
x = np.frombuffer(data, dtype='<f4', count=fnpts, offset=512)
y_offset = 512 + fnpts * 4
else:
x = np.linspace(ffirst, flast, fnpts)
y_offset = 512
# Skip 32-byte sub-file header
y_offset += 32
# Read Y values
if fexp == 128: # IEEE float32
y = np.frombuffer(data, dtype='<f4', count=fnpts, offset=y_offset)
else: # Scaled int32
raw = np.frombuffer(data, dtype='<i4', count=fnpts, offset=y_offset)
y = raw * (2.0 ** (fexp - 32))
return x, yParsing — JavaScript Example
function parseSPC(arrayBuffer) {
const view = new DataView(arrayBuffer);
// Read main header fields
const ftflgs = view.getUint8(0);
const fversn = view.getUint8(1); // 0x4B = new format
const fexp = view.getUint8(3); // 128 = float32 Y data
const fnpts = view.getInt32(4, true); // little-endian
const ffirst = view.getFloat64(8, true);
const flast = view.getFloat64(16, true);
// Generate X axis
const x = new Array(fnpts);
for (let i = 0; i < fnpts; i++) {
x[i] = ffirst + (i * (flast - ffirst)) / (fnpts - 1);
}
// Read Y values (skip 32-byte sub-file header at offset 512)
const yOffset = 512 + 32;
const y = new Array(fnpts);
if (fexp === 128) {
for (let i = 0; i < fnpts; i++) {
y[i] = view.getFloat32(yOffset + i * 4, true);
}
} else {
const scale = Math.pow(2, fexp - 32);
for (let i = 0; i < fnpts; i++) {
y[i] = view.getInt32(yOffset + i * 4, true) * scale;
}
}
return { x, y };
}Common Gotchas
- Endianness: All multi-byte values are little-endian. This is consistent across all SPC files regardless of the platform that created them.
- Y-exponent encoding: When fexp=128, Y data is stored as IEEE float32 directly. Otherwise, Y data is stored as int32 scaled by 2^(fexp-32). Check this first.
- Log data blocks: SPC files can contain a log block (audit trail) after the spectral data. Its presence doesn't affect spectrum parsing but may cause naive readers to read garbage if they don't check bounds.
- Old format word-swapping: Version 0x4D files store 32-bit Y integers with bytes 0-1 and 2-3 swapped relative to standard little-endian. Read bytes individually and reassemble.
- Multi-spectrum sub-files: Each sub-file has its own 32-byte header. The sub-file's fexp may differ from the main header's fexp — always use the sub-file value when TMULTI is set.
OPUS File Format (.0, .1, .2, ...) — Deep Dive
Bruker's OPUS format is used by all Bruker FTIR instruments, including the popular Alpha, Tensor, and Vertex series. It is immediately recognizable by its unusual file extensions: numbered sequentially as .0, .1, .2, .3, and so on. These numbered extensions confuse most file managers and operating systems.
Internally, OPUS uses a block-based binary structure. A directory at a fixed location in the file serves as an index, pointing to the locations of individual data blocks. Each block has a type identifier encoding what it contains: the result spectrum (absorbance, transmittance, or reflectance), single-channel spectra, interferograms, and comprehensive instrument parameters.
Block-Based Structure
┌────────────────────────────────────────────────────────────────┐ │ OPUS File Structure (Bruker Binary) │ ├────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────────────────────┐ │ │ │ File Header (24 bytes) │ │ │ │ Magic: 0x0A 0x0A 0xFE 0xFE │ │ │ │ ... │ │ │ │ Directory Pointer ──────────────┼─────┐ │ │ │ Max Directory Entries │ │ │ │ └──────────────────────────────────┘ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────┐ │ │ │ Block Directory (12 bytes per entry) │ │ │ ├──────────────────────────────────────────────┤ │ │ │ Entry: [blockType:u32] [length:u32] [ptr:u32] │──┐ │ │ │ Entry: [blockType:u32] [length:u32] [ptr:u32] │──┼─┐ │ │ │ Entry: [blockType:u32] [length:u32] [ptr:u32] │──┼─┼─┐ │ │ │ ... │ │ │ │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ │ │ ┌────────────────────────────┐◄───────────────────┘ │ │ │ │ │ Absorbance Data Block │ │ │ │ │ │ float32[nPoints] │ │ │ │ │ └────────────────────────────┘ │ │ │ │ │ │ │ │ ┌────────────────────────────┐◄─────────────────────┘ │ │ │ │ Parameter Block │ │ │ │ │ NPT, FXV, LXV, CSF, DXU │ │ │ │ └────────────────────────────┘ │ │ │ │ │ │ ┌────────────────────────────┐◄───────────────────────┘ │ │ │ Instrument Parameters │ │ │ │ RES, APT, VEL, SRC, DTC │ │ │ └────────────────────────────┘ │ │ │ └────────────────────────────────────────────────────────────────┘ Block Type Encoding (32-bit field): Bits 4-9: param (0=data, 1=data-status, 2=instrument, ...) Bits 10-16: data (4=absorbance, 5=transmittance, 1=single-ch) Bits 2-3: type (1=sample, 2=reference, 3=ratio) Bits 19-21: extend (extension flags)
Parameter Block Layout
Each parameter block contains a series of tagged entries. Each entry has a 3-character ASCII name, a type code, a reserved size field, and the value data. Key parameters for spectrum reconstruction:
- NPT — Number of data points (int32)
- FXV — First X value, i.e., start of wavenumber range (float64)
- LXV — Last X value, i.e., end of wavenumber range (float64)
- CSF — Scaling factor applied to all Y values (float64)
- DXU— X-axis unit string ("WN" = wavenumber, "MI" = microns)
Parsing — Python Example
import struct
import numpy as np
def parse_opus(filepath):
"""Read the first absorbance spectrum from a Bruker OPUS file."""
with open(filepath, 'rb') as f:
data = f.read()
# Verify magic bytes
if data[:4] != b'\x0a\x0a\xfe\xfe':
raise ValueError("Not a valid OPUS file")
# Read directory pointer and max entries from header
dir_ptr = struct.unpack_from('<I', data, 12)[0]
max_entries = struct.unpack_from('<I', data, 16)[0]
# Scan block directory (12 bytes per entry)
blocks = []
for i in range(max_entries):
off = dir_ptr + i * 12
btype, blength, bptr = struct.unpack_from('<III', data, off)
if bptr == 0:
break
param = (btype >> 4) & 0x3F
data_code = (btype >> 10) & 0x7F
blocks.append((param, data_code, blength, bptr))
# Find absorbance data block (param=0, data=4)
data_block = next((b for b in blocks if b[0] == 0 and b[1] == 4), None)
param_block = next((b for b in blocks if b[0] == 1 and b[1] == 4), None)
if not data_block:
raise ValueError("No absorbance data block found")
# Read parameters (NPT, FXV, LXV) from the param block
# (simplified — full implementation parses tagged entries)
npt = data_block[2] # block length = number of float32 values
fxv, lxv = 400.0, 4000.0 # defaults, override from param block
# Read float32 spectral data
y = np.frombuffer(data, dtype='<f4', count=npt, offset=data_block[3])
x = np.linspace(fxv, lxv, npt)
return x, yParsing — JavaScript Example
function parseOPUS(arrayBuffer) {
const view = new DataView(arrayBuffer);
const bytes = new Uint8Array(arrayBuffer);
// Verify magic: 0x0A 0x0A 0xFE 0xFE
if (bytes[0] !== 0x0A || bytes[1] !== 0x0A ||
bytes[2] !== 0xFE || bytes[3] !== 0xFE) {
throw new Error('Not a valid OPUS file');
}
// Read directory location from header
const dirPtr = view.getUint32(12, true);
const maxEntries = view.getUint32(16, true);
// Scan the block directory
const blocks = [];
for (let i = 0; i < maxEntries; i++) {
const off = dirPtr + i * 12;
const blockType = view.getUint32(off, true);
const blockLen = view.getUint32(off + 4, true);
const blockPtr = view.getUint32(off + 8, true);
if (blockPtr === 0) break;
blocks.push({
param: (blockType >>> 4) & 0x3F,
data: (blockType >>> 10) & 0x7F,
length: blockLen,
ptr: blockPtr,
});
}
// Find absorbance data (param=0, data=4)
const dataBlock = blocks.find(b => b.param === 0 && b.data === 4);
if (!dataBlock) throw new Error('No absorbance block');
// Read float32 Y values
const npts = dataBlock.length;
const y = new Float32Array(npts);
for (let i = 0; i < npts; i++) {
y[i] = view.getFloat32(dataBlock.ptr + i * 4, true);
}
return y;
}Common Gotchas
- Multiple data blocks: A single OPUS file often contains absorbance, transmittance, single-channel sample, single-channel reference, AND the interferogram. Your parser must select the right one based on the block type code.
- Parameter block matching: Each data block has a corresponding parameter block with matching data/type codes. Use the matched pair — mismatching parameters with data produces incorrect X-axis scaling.
- Version differences: OPUS files from different software versions (6.x, 7.x, 8.x) may have slightly different block layouts. Always validate the directory pointer, don't hardcode offsets.
- CSF scaling factor: Some OPUS files use a non-unity CSF value that must be applied to all Y values. Missing this produces spectra with incorrect amplitudes.
SPA File Format (.spa, .spg) — Thermo Fisher OMNIC
SPA is Thermo Fisher's OMNIC binary format, produced by the most widely used FTIR software suite in the world. Unlike SPC (which Thermo inherited through the Galactic acquisition), SPA is a Thermo-native format tightly integrated with OMNIC and OMNIC Paradigm software.
SPA files have no universal magic number, making format detection rely on structural validation. The file contains a title at offset 0x1E, a key-based directory starting at offset 0x120, and spectral data pointed to by directory entries. Key 2 points to the spectral header (containing the point count and wavenumber range), and key 3 points to the float32 intensity data.
┌────────────────────────────────────────────────────────┐ │ SPA / SPG File Structure (Thermo OMNIC) │ ├────────┬───────────────────────────────────────────────┤ │ 0x001E │ Title / sample description (null-terminated) │ ├────────┼───────────────────────────────────────────────┤ │ 0x0120 │ Key Directory (16-byte entries): │ │ │ [key:u8] [pad:u8] [offset:u32] [len:u32] │ │ │ Key 2 → Spectral Header │ │ │ Key 3 → Intensity Data (float32[]) │ │ │ Key 4 → Single Channel │ │ │ Key 7 → Interferogram │ ├────────┼───────────────────────────────────────────────┤ │ Header │ +0x04: uint32 numPoints │ │ (key2) │ +0x10: float32 firstX (max wavenumber) │ │ │ +0x14: float32 lastX (min wavenumber) │ ├────────┼───────────────────────────────────────────────┤ │ Data │ float32[numPoints] — intensity values │ │ (key3) │ stored high→low wavenumber │ └────────┴───────────────────────────────────────────────┘ .spg "group" files: multiple spectra concatenated. SpectralBench reads the first spectrum from group files.
SPA is being gradually replaced by newer Thermo formats in their latest instruments, but millions of legacy SPA files exist in analytical laboratories worldwide. If your lab has a Nicolet iS50, iS20, or any Nicolet/Avatar-series FTIR, your data is likely in SPA format.
SpectralBench parses both single-spectrum .spa files and multi-spectrum .spg group files directly, using the key-directory approach with fixed-offset fallback for maximum compatibility with files from different OMNIC versions.
Renishaw WDF (.wdf)
WDF is the native binary format of Renishaw's WiRE software, used with all Renishaw Raman spectrometers (inVia, RA100, Virsa). Renishaw dominates the research-grade confocal Raman market, making WDF one of the most important formats for Raman spectroscopy data.
WDF uses a block/chunk container architecture. The file starts with a 512-byte "WDF1" header, followed by a sequence of typed blocks. Each block has a 16-byte header containing a 4-character ID, a UID, and the total block size (including its own header).
┌────────────────────────────────────────────────────────────┐ │ WDF File Structure (Renishaw WiRE) │ ├────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────┐ │ │ │ WDF1 Header (512 bytes) │ │ │ │ 0x00: "WDF1" magic signature │ │ │ │ 0x04: uint32 version │ │ │ │ 0x3C: uint32 npoints (per spectrum) │ │ │ │ 0x40: uint64 nspectra (capacity) │ │ │ │ 0x48: uint64 ncollected │ │ │ │ 0x84: uint32 measurement type │ │ │ │ 0x9C: float32 laser wavenumber │ │ │ │ 0xF0: char[160] title │ │ │ └─────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────┐ │ │ │ Block: XLST (X-axis list) │ │ │ │ Header: [id:4][uid:4][size:8] │ │ │ │ Data: [type:u32][unit:u32] │ │ │ │ float32[npoints] — Raman shifts │ │ │ └─────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────┐ │ │ │ Block: DATA (Intensities) │ │ │ │ Header: [id:4][uid:4][size:8] │ │ │ │ Data: float32[npoints × nspectra] │ │ │ └─────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────┐ │ │ │ Block: ORGN (Origin metadata) │ │ │ │ Block: WMAP (Mapping parameters) │ │ │ │ Block: TEXT (Comments) │ │ │ │ ... │ │ │ └─────────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────────────────────┘ Measurement types: 1=Single, 2=Series, 3=Map For maps/series: DATA contains npoints×nspectra floats; SpectralBench reads the first spectrum.
WDF files can be extremely large for Raman maps (hundreds of MB for large area scans with thousands of spectra). SpectralBench efficiently reads only the first spectrum and the X-axis list without loading the entire file into memory.
CSV & TXT (.csv, .txt, .tsv)
CSV (comma-separated values) and plain text files are the universal fallback for spectral data exchange. When no other format works, when you need to get data into Excel or Python, or when a journal asks for "raw data in a standard format," CSV is the answer.
There is no standardized structure for spectral CSV files. Typically, the first column contains x-axis values (wavenumber in cm⁻¹ for FTIR, wavelength in nm for UV-Vis) and the second column contains y-axis values (absorbance, transmittance, or intensity). Some files include a header row with column labels; others do not. Delimiters may be commas, tabs, semicolons, or spaces. Decimal separators may be periods or commas (depending on the locale settings of the exporting instrument).
Auto-Detection Heuristics
Because spectral CSV has no formal spec, robust parsers use heuristics to identify the structure:
- Delimiter detection: Count occurrences of comma, tab, and semicolon in the first 10 lines. The most frequent delimiter wins.
- Header detection: If the first row contains any non-numeric field that isn't parseable as a float, it's a header row.
- X-axis direction: If X values decrease from first to last row, the data is stored high → low (common for FTIR wavenumber). Sort ascending for display.
- Unit inference: X range 200–4500 → likely wavenumber (cm⁻¹). X range 100–1100 → likely wavelength (nm).
- Locale handling: European instruments export with semicolon delimiters and comma decimal points. Detect this when semicolons outnumber commas in numeric rows.
Common Column Arrangements
# Layout 1: Wavenumber-first, no header (most common for FTIR)
4000.0, 0.0234
3998.1, 0.0256
3996.2, 0.0241
...
# Layout 2: Wavelength-first with header (UV-Vis)
Wavelength (nm), Absorbance
200, 2.341
201, 2.298
202, 2.255
...
# Layout 3: Tab-separated, Raman shift
# Raman Shift (cm-1) Intensity
100.5 1234.5
101.2 1298.3
102.0 1156.7
...
# Layout 4: European locale (semicolons, comma decimals)
Wellenzahl;Absorbanz
4000,0;0,0234
3998,1;0,0256
...The strength of CSV is universality: every programming language, every spreadsheet application, and every plotting tool can read CSV. The weakness is the complete absence of metadata — a CSV file tells you nothing about the instrument, measurement conditions, sample preparation, or even which units the columns use.
Which Format Should I Use?
The best format depends on your use case. Here are practical recommendations for common scenarios:
| Use Case | Recommended Format | Reason |
|---|---|---|
| Long-term archival | JCAMP-DX | Open standard, text-based, self-documenting. Readable in 30 years without special software. |
| Day-to-day analysis | Native format | OPUS, SPC, or WDF — preserves all metadata, instrument parameters, and processing history. |
| Sharing with collaborators | CSV or JCAMP-DX | CSV for Python/R/Excel users. JCAMP-DX for spectroscopy software users who want metadata. |
| Journal publication | CSV + JCAMP-DX | CSV for supplementary data. JCAMP-DX for spectral databases. Most journals accept both. |
| Maximum metadata preservation | Native + JCAMP-DX backup | Keep the original instrument file for all parameters. Add a JCAMP-DX copy for accessibility. |
| Data processing pipeline | CSV or NumPy (.npy) | Fastest to read programmatically. No parsing overhead. Simple array format. |
As a general rule: always keep a copy in the original instrument format (it contains the most information). Then export to JCAMP-DX for long-term archival and to CSV for maximum interoperability. SpectralBench makes all of these conversions trivial — drag in any format and export to any other.
Converting Between Formats
SpectralBench converts between all supported formats with no installation required — just open the tool in any browser and drop your files. Batch conversion lets you process multiple files at once and download them as a ZIP archive — ideal for converting entire folders of legacy instrument data.
Metadata is preserved where the target format supports it. Converting from OPUS to JCAMP-DX carries over the spectral data, units, and basic measurement parameters. Converting to CSV strips all metadata, leaving only the x-y data pairs. For the richest possible conversion, always keep a copy in the original instrument format.
Common conversion workflows:
- OPUS → CSV: Extract Bruker FTIR data for Python/MATLAB analysis
- SPC → JCAMP-DX: Convert legacy Thermo data to an open, archival format
- WDF → CSV: Get Renishaw Raman data into R or Origin for plotting
- Any → SPC: Write native SPC binary files from any input for GRAMS software
- Batch: folder of .0/.1/.2 → CSV: Convert an entire directory of OPUS measurements at once
Instrument-Specific Guides
Each vendor page has tailored information for users of that specific instrument brand:
- Bruker OPUS File Viewer — open .0, .1, .2 files without OPUS software
- Thermo Fisher File Viewer — open SPC and SPA files without OMNIC
- Shimadzu File Viewer — open SPC files without LabSolutions
- Horiba & Renishaw Raman Viewer — natively reads WDF, NGS, and L6S files from WiRE and LabSpec
- JCAMP-DX Viewer & Converter — open, visualize, and convert .jdx and .dx files
- All Supported Formats — complete list of every file format SpectralBench can read
Related Resources
- Spectral File Viewer — open any spectral format in your browser
- Spectral File Converter — convert between JCAMP-DX, SPC, OPUS, SPA, WDF, and CSV
- FTIR Spectrum Interpretation Guide — learn to read FTIR spectra after opening your files
- FTIR Functional Group Table — identify peaks and assign functional groups