Task 441: .NC File Format
Task 441: .NC File Format
File Format Specifications for .NC (NetCDF)
The .NC file extension is commonly associated with the NetCDF (Network Common Data Form) format, a self-describing, machine-independent binary format for storing array-oriented scientific data, such as multidimensional arrays in climate, oceanography, and atmospheric sciences. It supports dimensions, variables, and attributes. There are several variants: Classic (original, 32-bit offsets), 64-bit offset (for larger files), and HDF5-based (NetCDF-4). Based on the specifications, I'll focus on the NetCDF Classic format (version 1), as it's the foundational one and matches the typical .NC usage without additional qualifiers in the query. The format is defined in big-endian byte order, with 4-byte alignment padding using null bytes in headers.
The specifications are sourced from the official Unidata documentation, which provides a BNF grammar for the structure.
- List of All Properties Intrinsic to This File Format
The properties refer to the structural and metadata elements defined in the NetCDF Classic format specification. These are intrinsic to the format's structure (header and data layout) and include:
- Magic String: The file signature ('CDF' followed by the version byte, e.g., \x01 for Classic).
- Version: The format version (1 for Classic).
- Number of Records: The current length of the record (unlimited) dimension, if present (a 32-bit non-negative integer or a special streaming value).
- Dimensions: A list of dimensions, each with:
- Name (UTF-8 string, padded to 4 bytes).
- Length (32-bit non-negative integer; 0 indicates the unlimited/record dimension; at most one per file).
- Global Attributes: A list of file-level attributes, each with:
- Name (UTF-8 string, padded).
- Type (one of: NC_BYTE, NC_CHAR, NC_SHORT, NC_INT, NC_FLOAT, NC_DOUBLE).
- Number of elements (32-bit non-negative integer).
- Values (array of the specified type, padded to 4 bytes).
- Variables: A list of variables, each with:
- Name (UTF-8 string, padded).
- Rank (number of dimensions, 32-bit integer).
- Dimension IDs (list of 32-bit indices referencing the dimensions list; first ID=0 for record variables).
- Variable Attributes (similar to global attributes: name, type, number of elements, values).
- Type (same as attribute types).
- VSize (32-bit size in bytes for the variable's data per record or total, padded to 4 bytes).
- Begin Offset (32-bit offset from file start to the variable's data).
- Fill Values: Default or overridden (_FillValue attribute) values for unwritten data (type-specific defaults, e.g., -127 for NC_BYTE).
- Data Layout: Non-record variables stored contiguously in row-major order; record variables interleaved at the end. (Note: Actual data values are not "properties" but can be read; properties focus on metadata.)
These properties define the self-describing nature of the format, allowing decoding without external schema.
- Two Direct Download Links for .NC Files
- https://www.image.ucar.edu/GSP/Software/Netcdf/example.nc (Simple 2D example NetCDF file from UCAR).
- https://people.sc.fsu.edu/~jburkardt/data/netcdf/sresa1b_ncar_ccsm3_example.nc (Sample climate model data from Florida State University data directory).
- Ghost Blog Embedded HTML JavaScript for Drag and Drop
Here's a complete HTML page with embedded JavaScript that can be embedded in a blog (e.g., Ghost platform). It creates a drop zone where users can drag and drop a .NC file. The script uses FileReader to read the file as an ArrayBuffer, parses the NetCDF Classic header, extracts the properties, and dumps them to the screen in a element.
This script parses the header for properties and displays them in JSON format. It handles padding, big-endian, and basic types. For write functionality, it's not included as drag-and-drop is read-only; use the JavaScript class below for node-based write.
- Python Class for .NC Files
Here's a Python class that can open, decode (read), write, and print the properties to console. It uses struct for binary parsing/packing, assuming Classic format. Write creates a new file with the parsed structure (or modified).
import struct
import sys
class NetCDFHandler:
def __init__(self, filepath):
self.filepath = filepath
self.properties = None
def read(self):
with open(self.filepath, 'rb') as f:
data = f.read()
self.properties = self._parse(data)
return self.properties
def _parse(self, data):
offset = 0
magic = data[offset:offset+3].decode('ascii')
if magic != 'CDF':
raise ValueError('Not a NetCDF file')
offset += 3
version, = struct.unpack('>B', data[offset:offset+1])
offset += 1
if version != 1:
raise ValueError('Only Classic format supported')
numrecs, = struct.unpack('>I', data[offset:offset+4])
offset += 4
# Dimensions
dim_tag, = struct.unpack('>I', data[offset:offset+4])
offset += 4
dims = []
if dim_tag == 10:
nelems, = struct.unpack('>I', data[offset:offset+4])
offset += 4
for _ in range(nelems):
name_len, = struct.unpack('>I', data[offset:offset+4])
offset += 4
name = data[offset:offset+name_len].decode('utf-8')
offset += name_len
offset += self._pad(name_len)
length, = struct.unpack('>I', data[offset:offset+4])
offset += 4
dims.append({'name': name, 'length': length})
# Global attributes
gatt_tag, = struct.unpack('>I', data[offset:offset+4])
offset += 4
global_attrs = []
if gatt_tag == 12:
nelems, = struct.unpack('>I', data[offset:offset+4])
offset += 4
for _ in range(nelems):
attr, new_offset = self._parse_attr(data, offset)
global_attrs.append(attr)
offset = new_offset
# Variables
var_tag, = struct.unpack('>I', data[offset:offset+4])
offset += 4
vars = []
if var_tag == 11:
nelems, = struct.unpack('>I', data[offset:offset+4])
offset += 4
for _ in range(nelems):
name_len, = struct.unpack('>I', data[offset:offset+4])
offset += 4
name = data[offset:offset+name_len].decode('utf-8')
offset += name_len
offset += self._pad(name_len)
rank, = struct.unpack('>I', data[offset:offset+4])
offset += 4
dimids = []
for _ in range(rank):
dimid, = struct.unpack('>I', data[offset:offset+4])
offset += 4
dimids.append(dimid)
vatt_tag, = struct.unpack('>I', data[offset:offset+4])
offset += 4
var_attrs = []
if vatt_tag == 12:
nelems_attr, = struct.unpack('>I', data[offset:offset+4])
offset += 4
for _ in range(nelems_attr):
attr, new_offset = self._parse_attr(data, offset)
var_attrs.append(attr)
offset = new_offset
type, = struct.unpack('>I', data[offset:offset+4])
offset += 4
vsize, = struct.unpack('>I', data[offset:offset+4])
offset += 4
begin, = struct.unpack('>I', data[offset:offset+4])
offset += 4
vars.append({'name': name, 'rank': rank, 'dimids': dimids, 'attributes': var_attrs, 'type': self._type_str(type), 'vsize': vsize, 'begin': begin})
return {
'magic': 'CDF',
'version': version,
'numrecs': numrecs,
'dimensions': dims,
'global_attributes': global_attrs,
'variables': vars
}
def _pad(self, len_):
return (4 - (len_ % 4)) % 4
def _type_str(self, type_):
types = {1: 'NC_BYTE', 2: 'NC_CHAR', 3: 'NC_SHORT', 4: 'NC_INT', 5: 'NC_FLOAT', 6: 'NC_DOUBLE'}
return types.get(type_, 'Unknown')
def _get_type_size(self, type_):
sizes = {1: 1, 2: 1, 3: 2, 4: 4, 5: 4, 6: 8}
return sizes.get(type_, 0)
def _parse_attr(self, data, start_offset):
offset = start_offset
name_len, = struct.unpack('>I', data[offset:offset+4])
offset += 4
name = data[offset:offset+name_len].decode('utf-8')
offset += name_len
offset += self._pad(name_len)
type_, = struct.unpack('>I', data[offset:offset+4])
offset += 4
nelems, = struct.unpack('>I', data[offset:offset+4])
offset += 4
values = []
fmt = self._get_fmt(type_)
size = self._get_type_size(type_)
for _ in range(nelems):
val, = struct.unpack(fmt, data[offset:offset+size])
values.append(val)
offset += size
offset += self._pad(nelems * size)
return {'name': name, 'type': self._type_str(type_), 'values': values}, offset
def _get_fmt(self, type_):
fmts = {1: '>B', 2: '>c', 3: '>h', 4: '>i', 5: '>f', 6: '>d'}
return fmts.get(type_, '>i')
def print_properties(self):
if not self.properties:
self.read()
print(self.properties)
def write(self, new_filepath, properties=None):
if properties is None:
properties = self.properties or self.read()
with open(new_filepath, 'wb') as f:
f.write(b'CDF')
f.write(struct.pack('>B', properties['version']))
f.write(struct.pack('>I', properties['numrecs']))
# Dimensions
if properties['dimensions']:
f.write(struct.pack('>I', 10)) # NC_DIMENSION
f.write(struct.pack('>I', len(properties['dimensions'])))
for dim in properties['dimensions']:
name_b = dim['name'].encode('utf-8')
f.write(struct.pack('>I', len(name_b)))
f.write(name_b)
f.write(b'\x00' * self._pad(len(name_b)))
f.write(struct.pack('>I', dim['length']))
else:
f.write(struct.pack('>II', 0, 0))
# Global attributes
if properties['global_attributes']:
f.write(struct.pack('>I', 12))
f.write(struct.pack('>I', len(properties['global_attributes'])))
for attr in properties['global_attributes']:
self._write_attr(f, attr)
else:
f.write(struct.pack('>II', 0, 0))
# Variables
if properties['variables']:
f.write(struct.pack('>I', 11))
f.write(struct.pack('>I', len(properties['variables'])))
for var in properties['variables']:
name_b = var['name'].encode('utf-8')
f.write(struct.pack('>I', len(name_b)))
f.write(name_b)
f.write(b'\x00' * self._pad(len(name_b)))
f.write(struct.pack('>I', var['rank']))
for dimid in var['dimids']:
f.write(struct.pack('>I', dimid))
if var['attributes']:
f.write(struct.pack('>I', 12))
f.write(struct.pack('>I', len(var['attributes'])))
for attr in var['attributes']:
self._write_attr(f, attr)
else:
f.write(struct.pack('>II', 0, 0))
type_num = self._str_to_type(var['type'])
f.write(struct.pack('>I', type_num))
f.write(struct.pack('>I', var['vsize']))
f.write(struct.pack('>I', var['begin']))
else:
f.write(struct.pack('>II', 0, 0))
# Note: Data writing not implemented here; this writes header only.
def _str_to_type(self, type_str):
types = {'NC_BYTE': 1, 'NC_CHAR': 2, 'NC_SHORT': 3, 'NC_INT': 4, 'NC_FLOAT': 5, 'NC_DOUBLE': 6}
return types.get(type_str, 4)
def _write_attr(self, f, attr):
name_b = attr['name'].encode('utf-8')
f.write(struct.pack('>I', len(name_b)))
f.write(name_b)
f.write(b'\x00' * self._pad(len(name_b)))
type_num = self._str_to_type(attr['type'])
f.write(struct.pack('>I', type_num))
f.write(struct.pack('>I', len(attr['values'])))
fmt = self._get_fmt(type_num)
size = self._get_type_size(type_num)
for val in attr['values']:
f.write(struct.pack(fmt, val))
f.write(b'\x00' * self._pad(len(attr['values']) * size))
# Example usage:
# handler = NetCDFHandler('example.nc')
# handler.print_properties()
# handler.write('new.nc')
This class reads the file, parses properties, prints them as a dict, and can write a new header-based file.
- Java Class for .NC Files
Here's a Java class using ByteBuffer for big-endian parsing. It opens, reads, writes, and prints properties to console.
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class NetCDFHandler {
private String filepath;
private Map<String, Object> properties;
public NetCDFHandler(String filepath) {
this.filepath = filepath;
}
public Map<String, Object> read() throws IOException {
try (RandomAccessFile raf = new RandomAccessFile(filepath, "r")) {
FileChannel channel = raf.getChannel();
long size = channel.size();
ByteBuffer buffer = ByteBuffer.allocate((int) size).order(ByteOrder.BIG_ENDIAN);
channel.read(buffer);
buffer.flip();
properties = parse(buffer);
}
return properties;
}
private Map<String, Object> parse(ByteBuffer bb) {
Map<String, Object> props = new HashMap<>();
byte[] magicBytes = new byte[3];
bb.get(magicBytes);
String magic = new String(magicBytes, StandardCharsets.US_ASCII);
if (!magic.equals("CDF")) throw new RuntimeException("Not a NetCDF file");
props.put("magic", "CDF");
byte version = bb.get();
if (version != 1) throw new RuntimeException("Only Classic format supported");
props.put("version", version);
int numrecs = bb.getInt();
props.put("numrecs", numrecs);
// Dimensions
int dimTag = bb.getInt();
List<Map<String, Object>> dims = new ArrayList<>();
if (dimTag == 10) {
int nelems = bb.getInt();
for (int i = 0; i < nelems; i++) {
int nameLen = bb.getInt();
byte[] nameBytes = new byte[nameLen];
bb.get(nameBytes);
String name = new String(nameBytes, StandardCharsets.UTF_8);
bb.position(bb.position() + pad(nameLen));
int length = bb.getInt();
Map<String, Object> dim = new HashMap<>();
dim.put("name", name);
dim.put("length", length);
dims.add(dim);
}
} else if (dimTag != 0) throw new RuntimeException("Invalid dim tag");
props.put("dimensions", dims);
// Global attributes
int gattTag = bb.getInt();
List<Map<String, Object>> globalAttrs = new ArrayList<>();
if (gattTag == 12) {
int nelems = bb.getInt();
for (int i = 0; i < nelems; i++) {
Map<String, Object> attr = parseAttr(bb);
globalAttrs.add(attr);
}
} else if (gattTag != 0) throw new RuntimeException("Invalid gatt tag");
props.put("global_attributes", globalAttrs);
// Variables
int varTag = bb.getInt();
List<Map<String, Object>> vars = new ArrayList<>();
if (varTag == 11) {
int nelems = bb.getInt();
for (int i = 0; i < nelems; i++) {
int nameLen = bb.getInt();
byte[] nameBytes = new byte[nameLen];
bb.get(nameBytes);
String name = new String(nameBytes, StandardCharsets.UTF_8);
bb.position(bb.position() + pad(nameLen));
int rank = bb.getInt();
List<Integer> dimids = new ArrayList<>();
for (int j = 0; j < rank; j++) {
dimids.add(bb.getInt());
}
int vattTag = bb.getInt();
List<Map<String, Object>> varAttrs = new ArrayList<>();
if (vattTag == 12) {
int nelemsAttr = bb.getInt();
for (int j = 0; j < nelemsAttr; j++) {
Map<String, Object> attr = parseAttr(bb);
varAttrs.add(attr);
}
} else if (vattTag != 0) throw new RuntimeException("Invalid vatt tag");
int type = bb.getInt();
int vsize = bb.getInt();
int begin = bb.getInt();
Map<String, Object> var = new HashMap<>();
var.put("name", name);
var.put("rank", rank);
var.put("dimids", dimids);
var.put("attributes", varAttrs);
var.put("type", typeToString(type));
var.put("vsize", vsize);
var.put("begin", begin);
vars.add(var);
}
} else if (varTag != 0) throw new RuntimeException("Invalid var tag");
props.put("variables", vars);
return props;
}
private int pad(int len) {
return (4 - (len % 4)) % 4;
}
private String typeToString(int type) {
switch (type) {
case 1: return "NC_BYTE";
case 2: return "NC_CHAR";
case 3: return "NC_SHORT";
case 4: return "NC_INT";
case 5: return "NC_FLOAT";
case 6: return "NC_DOUBLE";
default: return "Unknown";
}
}
private int getTypeSize(int type) {
switch (type) {
case 1: case 2: return 1;
case 3: return 2;
case 4: case 5: return 4;
case 6: return 8;
default: return 0;
}
}
private Map<String, Object> parseAttr(ByteBuffer bb) {
Map<String, Object> attr = new HashMap<>();
int nameLen = bb.getInt();
byte[] nameBytes = new byte[nameLen];
bb.get(nameBytes);
String name = new String(nameBytes, StandardCharsets.UTF_8);
bb.position(bb.position() + pad(nameLen));
int type = bb.getInt();
int nelems = bb.getInt();
List<Object> values = new ArrayList<>();
int size = getTypeSize(type);
for (int i = 0; i < nelems; i++) {
values.add(readValue(bb, type));
bb.position(bb.position() + size - getReadSize(type)); // Adjust for read
}
bb.position(bb.position() + pad(nelems * size));
attr.put("name", name);
attr.put("type", typeToString(type));
attr.put("values", values);
return attr;
}
private int getReadSize(int type) {
switch (type) {
case 1: case 2: return 1;
case 3: return 2;
case 4: case 5: return 4;
case 6: return 8;
default: return 0;
}
}
private Object readValue(ByteBuffer bb, int type) {
switch (type) {
case 1: return bb.get();
case 2: return (char) bb.get();
case 3: return bb.getShort();
case 4: return bb.getInt();
case 5: return bb.getFloat();
case 6: return bb.getDouble();
default: return 0;
}
}
public void printProperties() throws IOException {
if (properties == null) read();
System.out.println(properties);
}
public void write(String newFilepath, Map<String, Object> props) throws IOException {
if (props == null) props = properties != null ? properties : read();
try (RandomAccessFile raf = new RandomAccessFile(newFilepath, "rw")) {
ByteBuffer bb = ByteBuffer.allocate(1024 * 1024).order(ByteOrder.BIG_ENDIAN); // Buffer for writing
bb.put("CDF".getBytes(StandardCharsets.US_ASCII));
bb.put((byte) props.get("version"));
bb.putInt((int) props.get("numrecs"));
// Dimensions
List<Map<String, Object>> dims = (List<Map<String, Object>>) props.get("dimensions");
if (!dims.isEmpty()) {
bb.putInt(10);
bb.putInt(dims.size());
for (Map<String, Object> dim : dims) {
byte[] nameB = ((String) dim.get("name")).getBytes(StandardCharsets.UTF_8);
bb.putInt(nameB.length);
bb.put(nameB);
bb.put(new byte[pad(nameB.length)]);
bb.putInt((int) dim.get("length"));
}
} else {
bb.putInt(0);
bb.putInt(0);
}
// Global attributes
List<Map<String, Object>> gAttrs = (List<Map<String, Object>>) props.get("global_attributes");
if (!gAttrs.isEmpty()) {
bb.putInt(12);
bb.putInt(gAttrs.size());
for (Map<String, Object> attr : gAttrs) {
writeAttr(bb, attr);
}
} else {
bb.putInt(0);
bb.putInt(0);
}
// Variables
List<Map<String, Object>> vars = (List<Map<String, Object>>) props.get("variables");
if (!vars.isEmpty()) {
bb.putInt(11);
bb.putInt(vars.size());
for (Map<String, Object> var : vars) {
byte[] nameB = ((String) var.get("name")).getBytes(StandardCharsets.UTF_8);
bb.putInt(nameB.length);
bb.put(nameB);
bb.put(new byte[pad(nameB.length)]);
bb.putInt((int) var.get("rank"));
List<Integer> dimids = (List<Integer>) var.get("dimids");
for (int dimid : dimids) {
bb.putInt(dimid);
}
List<Map<String, Object>> vAttrs = (List<Map<String, Object>>) var.get("attributes");
if (!vAttrs.isEmpty()) {
bb.putInt(12);
bb.putInt(vAttrs.size());
for (Map<String, Object> attr : vAttrs) {
writeAttr(bb, attr);
}
} else {
bb.putInt(0);
bb.putInt(0);
}
int type = strToType((String) var.get("type"));
bb.putInt(type);
bb.putInt((int) var.get("vsize"));
bb.putInt((int) var.get("begin"));
}
} else {
bb.putInt(0);
bb.putInt(0);
}
bb.flip();
raf.getChannel().write(bb);
}
// Note: Data not written; header only.
}
private int strToType(String typeStr) {
switch (typeStr) {
case "NC_BYTE": return 1;
case "NC_CHAR": return 2;
case "NC_SHORT": return 3;
case "NC_INT": return 4;
case "NC_FLOAT": return 5;
case "NC_DOUBLE": return 6;
default: return 4;
}
}
private void writeAttr(ByteBuffer bb, Map<String, Object> attr) {
byte[] nameB = ((String) attr.get("name")).getBytes(StandardCharsets.UTF_8);
bb.putInt(nameB.length);
bb.put(nameB);
bb.put(new byte[pad(nameB.length)]);
int type = strToType((String) attr.get("type"));
List<Object> values = (List<Object>) attr.get("values");
bb.putInt(type);
bb.putInt(values.size());
for (Object val : values) {
writeValue(bb, val, type);
}
bb.put(new byte[pad(values.size() * getTypeSize(type))]);
}
private void writeValue(ByteBuffer bb, Object val, int type) {
switch (type) {
case 1: bb.put((byte) val); break;
case 2: bb.put((byte) ((Character) val).charValue()); break;
case 3: bb.putShort((short) val); break;
case 4: bb.putInt((int) val); break;
case 5: bb.putFloat((float) val); break;
case 6: bb.putDouble((double) val); break;
}
}
public static void main(String[] args) throws IOException {
NetCDFHandler handler = new NetCDFHandler("example.nc");
handler.printProperties();
handler.write("new.nc", null);
}
}
- JavaScript Class for .NC Files (Node.js for Console)
Here's a JavaScript class for Node.js (using fs for file I/O). It opens, reads, writes, and prints properties to console.
const fs = require('fs');
class NetCDFHandler {
constructor(filepath) {
this.filepath = filepath;
this.properties = null;
}
read() {
const data = fs.readFileSync(this.filepath);
const dv = new DataView(data.buffer);
this.properties = this.parse(dv);
return this.properties;
}
parse(dv) {
// Similar to the browser parser in part 3, but complete for Node
let offset = 0;
const magic = String.fromCharCode(dv.getUint8(offset++), dv.getUint8(offset++), dv.getUint8(offset++));
if (magic !== 'CDF') throw new Error('Not a NetCDF file');
const version = dv.getUint8(offset++);
if (version !== 1) throw new Error('Only Classic format supported');
const numrecs = dv.getUint32(offset); offset += 4;
// Dimensions
let dim_tag = dv.getUint32(offset); offset += 4;
const dims = [];
if (dim_tag === 10) {
const nelems = dv.getUint32(offset); offset += 4;
for (let i = 0; i < nelems; i++) {
const nameLen = dv.getUint32(offset); offset += 4;
const name = this.getString(dv, offset, nameLen); offset += nameLen;
offset += this.pad(nameLen);
const length = dv.getUint32(offset); offset += 4;
dims.push({ name, length });
}
} else if (dim_tag !== 0) throw new Error('Invalid dim tag');
// Global attributes
const gatt_tag = dv.getUint32(offset); offset += 4;
const globalAttrs = [];
if (gatt_tag === 12) {
const nelems = dv.getUint32(offset); offset += 4;
for (let i = 0; i < nelems; i++) {
const { attr, newOffset } = this.parseAttr(dv, offset);
globalAttrs.push(attr);
offset = newOffset;
}
} else if (gatt_tag !== 0) throw new Error('Invalid gatt tag');
// Variables
const var_tag = dv.getUint32(offset); offset += 4;
const vars = [];
if (var_tag === 11) {
const nelems = dv.getUint32(offset); offset += 4;
for (let i = 0; i < nelems; i++) {
const nameLen = dv.getUint32(offset); offset += 4;
const name = this.getString(dv, offset, nameLen); offset += nameLen;
offset += this.pad(nameLen);
const rank = dv.getUint32(offset); offset += 4;
const dimids = [];
for (let j = 0; j < rank; j++) {
dimids.push(dv.getUint32(offset)); offset += 4;
}
const vatt_tag = dv.getUint32(offset); offset += 4;
const varAttrs = [];
if (vatt_tag === 12) {
const nelemsAttr = dv.getUint32(offset); offset += 4;
for (let j = 0; j < nelemsAttr; j++) {
const { attr, newOffset } = this.parseAttr(dv, offset);
varAttrs.push(attr);
offset = newOffset;
}
} else if (vatt_tag !== 0) throw new Error('Invalid vatt tag');
const type = dv.getUint32(offset); offset += 4;
const vsize = dv.getUint32(offset); offset += 4;
const begin = dv.getUint32(offset); offset += 4;
vars.push({ name, rank, dimids, attributes: varAttrs, type: this.typeToString(type), vsize, begin });
}
} else if (var_tag !== 0) throw new Error('Invalid var tag');
return {
magic: 'CDF',
version,
numrecs,
dimensions: dims,
globalAttributes: globalAttrs,
variables: vars
};
}
getString(dv, offset, len) {
let str = '';
for (let i = 0; i < len; i++) {
str += String.fromCharCode(dv.getUint8(offset + i));
}
return str;
}
pad(len) {
return (4 - (len % 4)) % 4;
}
typeToString(type) {
const types = {1: 'NC_BYTE', 2: 'NC_CHAR', 3: 'NC_SHORT', 4: 'NC_INT', 5: 'NC_FLOAT', 6: 'NC_DOUBLE'};
return types[type] || 'Unknown';
}
getTypeSize(type) {
const sizes = {1: 1, 2: 1, 3: 2, 4: 4, 5: 4, 6: 8};
return sizes[type] || 0;
}
parseAttr(dv, startOffset) {
let offset = startOffset;
const nameLen = dv.getUint32(offset); offset += 4;
const name = this.getString(dv, offset, nameLen); offset += nameLen;
offset += this.pad(nameLen);
const type = dv.getUint32(offset); offset += 4;
const nelems = dv.getUint32(offset); offset += 4;
const values = [];
const size = this.getTypeSize(type);
for (let i = 0; i < nelems; i++) {
values.push(this.readValue(dv, offset, type));
offset += size;
}
offset += this.pad(nelems * size);
return { attr: { name, type: this.typeToString(type), values }, newOffset: offset };
}
readValue(dv, offset, type) {
if (type === 1 || type === 2) return dv.getUint8(offset);
if (type === 3) return dv.getInt16(offset);
if (type === 4) return dv.getInt32(offset);
if (type === 5) return dv.getFloat32(offset);
if (type === 6) return dv.getFloat64(offset);
return 0;
}
printProperties() {
if (!this.properties) this.read();
console.log(JSON.stringify(this.properties, null, 2));
}
write(newFilepath, properties = null) {
if (!properties) properties = this.properties || this.read();
let buffer = Buffer.alloc(1024 * 1024); // Large enough buffer
let offset = 0;
buffer.write('CDF', offset); offset += 3;
buffer.writeUInt8(properties.version, offset); offset += 1;
buffer.writeUInt32BE(properties.numrecs, offset); offset += 4;
// Dimensions
if (properties.dimensions.length > 0) {
buffer.writeUInt32BE(10, offset); offset += 4;
buffer.writeUInt32BE(properties.dimensions.length, offset); offset += 4;
for (let dim of properties.dimensions) {
const nameB = Buffer.from(dim.name, 'utf-8');
buffer.writeUInt32BE(nameB.length, offset); offset += 4;
nameB.copy(buffer, offset); offset += nameB.length;
for (let p = 0; p < this.pad(nameB.length); p++) {
buffer.writeUInt8(0, offset); offset += 1;
}
buffer.writeUInt32BE(dim.length, offset); offset += 4;
}
} else {
buffer.writeUInt32BE(0, offset); offset += 4;
buffer.writeUInt32BE(0, offset); offset += 4;
}
// Global attributes
if (properties.globalAttributes.length > 0) {
buffer.writeUInt32BE(12, offset); offset += 4;
buffer.writeUInt32BE(properties.globalAttributes.length, offset); offset += 4;
for (let attr of properties.globalAttributes) {
offset = this.writeAttr(buffer, offset, attr);
}
} else {
buffer.writeUInt32BE(0, offset); offset += 4;
buffer.writeUInt32BE(0, offset); offset += 4;
}
// Variables
if (properties.variables.length > 0) {
buffer.writeUInt32BE(11, offset); offset += 4;
buffer.writeUInt32BE(properties.variables.length, offset); offset += 4;
for (let var_ of properties.variables) {
const nameB = Buffer.from(var_.name, 'utf-8');
buffer.writeUInt32BE(nameB.length, offset); offset += 4;
nameB.copy(buffer, offset); offset += nameB.length;
for (let p = 0; p < this.pad(nameB.length); p++) {
buffer.writeUInt8(0, offset); offset += 1;
}
buffer.writeUInt32BE(var_.rank, offset); offset += 4;
for (let dimid of var_.dimids) {
buffer.writeUInt32BE(dimid, offset); offset += 4;
}
if (var_.attributes.length > 0) {
buffer.writeUInt32BE(12, offset); offset += 4;
buffer.writeUInt32BE(var_.attributes.length, offset); offset += 4;
for (let attr of var_.attributes) {
offset = this.writeAttr(buffer, offset, attr);
}
} else {
buffer.writeUInt32BE(0, offset); offset += 4;
buffer.writeUInt32BE(0, offset); offset += 4;
}
const type = this.strToType(var_.type);
buffer.writeUInt32BE(type, offset); offset += 4;
buffer.writeUInt32BE(var_.vsize, offset); offset += 4;
buffer.writeUInt32BE(var_.begin, offset); offset += 4;
}
} else {
buffer.writeUInt32BE(0, offset); offset += 4;
buffer.writeUInt32BE(0, offset); offset += 4;
}
fs.writeFileSync(newFilepath, buffer.slice(0, offset));
// Note: Header only; data not written.
}
strToType(typeStr) {
const types = {'NC_BYTE': 1, 'NC_CHAR': 2, 'NC_SHORT': 3, 'NC_INT': 4, 'NC_FLOAT': 5, 'NC_DOUBLE': 6};
return types[typeStr] || 4;
}
writeAttr(buffer, startOffset, attr) {
let offset = startOffset;
const nameB = Buffer.from(attr.name, 'utf-8');
buffer.writeUInt32BE(nameB.length, offset); offset += 4;
nameB.copy(buffer, offset); offset += nameB.length;
for (let p = 0; p < this.pad(nameB.length); p++) {
buffer.writeUInt8(0, offset); offset += 1;
}
const type = this.strToType(attr.type);
buffer.writeUInt32BE(type, offset); offset += 4;
buffer.writeUInt32BE(attr.values.length, offset); offset += 4;
const size = this.getTypeSize(type);
for (let val of attr.values) {
this.writeValue(buffer, offset, val, type);
offset += size;
}
for (let p = 0; p < this.pad(attr.values.length * size); p++) {
buffer.writeUInt8(0, offset); offset += 1;
}
return offset;
}
writeValue(buffer, offset, val, type) {
if (type === 1 || type === 2) buffer.writeUInt8(val, offset);
else if (type === 3) buffer.writeInt16BE(val, offset);
else if (type === 4) buffer.writeInt32BE(val, offset);
else if (type === 5) buffer.writeFloatBE(val, offset);
else if (type === 6) buffer.writeDoubleBE(val, offset);
}
}
// Example usage:
// const handler = new NetCDFHandler('example.nc');
// handler.printProperties();
// handler.write('new.nc');
- C Class for .NC Files (C++)
Here's a C++ class using fstream for binary I/O. It opens, reads, writes, and prints properties to console (as JSON-like text).
#include <iostream>
#include <fstream>
#include <vector>
#include <map>
#include <string>
#include <iomanip>
#include <endian.h> // For bigendian, assume available or use htobe32 etc.
class NetCDFHandler {
private:
std::string filepath;
std::map<std::string, std::any> properties; // Use std::any for mixed types
public:
NetCDFHandler(const std::string& fp) : filepath(fp) {}
void read() {
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) throw std::runtime_error("Cannot open file");
auto size = file.tellg();
file.seekg(0);
std::vector<char> data(size);
file.read(data.data(), size);
parse(data.data());
}
void parse(const char* data) {
const char* ptr = data;
std::string magic(ptr, 3);
ptr += 3;
if (magic != "CDF") throw std::runtime_error("Not a NetCDF file");
properties["magic"] = std::string("CDF");
uint8_t version = *reinterpret_cast<const uint8_t*>(ptr++);
if (version != 1) throw std::runtime_error("Only Classic format supported");
properties["version"] = version;
uint32_t numrecs = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
ptr += 4;
properties["numrecs"] = numrecs;
// Dimensions
uint32_t dim_tag = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
ptr += 4;
std::vector<std::map<std::string, std::any>> dims;
if (dim_tag == 10) {
uint32_t nelems = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
ptr += 4;
for (uint32_t i = 0; i < nelems; ++i) {
uint32_t name_len = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
ptr += 4;
std::string name(ptr, name_len);
ptr += name_len;
ptr += pad(name_len);
uint32_t length = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
ptr += 4;
std::map<std::string, std::any> dim;
dim["name"] = name;
dim["length"] = length;
dims.push_back(dim);
}
} else if (dim_tag != 0) throw std::runtime_error("Invalid dim tag");
properties["dimensions"] = dims;
// Global attributes
uint32_t gatt_tag = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
ptr += 4;
std::vector<std::map<std::string, std::any>> global_attrs;
if (gatt_tag == 12) {
uint32_t nelems = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
ptr += 4;
for (uint32_t i = 0; i < nelems; ++i) {
auto [attr, new_ptr] = parse_attr(ptr);
global_attrs.push_back(attr);
ptr = new_ptr;
}
} else if (gatt_tag != 0) throw std::runtime_error("Invalid gatt tag");
properties["global_attributes"] = global_attrs;
// Variables
uint32_t var_tag = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
ptr += 4;
std::vector<std::map<std::string, std::any>> vars;
if (var_tag == 11) {
uint32_t nelems = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
ptr += 4;
for (uint32_t i = 0; i < nelems; ++i) {
uint32_t name_len = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
ptr += 4;
std::string name(ptr, name_len);
ptr += name_len;
ptr += pad(name_len);
uint32_t rank = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
ptr += 4;
std::vector<uint32_t> dimids;
for (uint32_t j = 0; j < rank; ++j) {
dimids.push_back(be32toh(*reinterpret_cast<const uint32_t*>(ptr)));
ptr += 4;
}
uint32_t vatt_tag = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
ptr += 4;
std::vector<std::map<std::string, std::any>> var_attrs;
if (vatt_tag == 12) {
uint32_t nelems_attr = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
ptr += 4;
for (uint32_t j = 0; j < nelems_attr; ++j) {
auto [attr, new_ptr] = parse_attr(ptr);
var_attrs.push_back(attr);
ptr = new_ptr;
}
} else if (vatt_tag != 0) throw std::runtime_error("Invalid vatt tag");
uint32_t type = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
ptr += 4;
uint32_t vsize = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
ptr += 4;
uint32_t begin = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
ptr += 4;
std::map<std::string, std::any> var;
var["name"] = name;
var["rank"] = rank;
var["dimids"] = dimids;
var["attributes"] = var_attrs;
var["type"] = type_to_string(type);
var["vsize"] = vsize;
var["begin"] = begin;
vars.push_back(var);
}
} else if (var_tag != 0) throw std::runtime_error("Invalid var tag");
properties["variables"] = vars;
}
int pad(uint32_t len) {
return (4 - (len % 4)) % 4;
}
std::string type_to_string(uint32_t type) {
switch (type) {
case 1: return "NC_BYTE";
case 2: return "NC_CHAR";
case 3: return "NC_SHORT";
case 4: return "NC_INT";
case 5: return "NC_FLOAT";
case 6: return "NC_DOUBLE";
default: return "Unknown";
}
}
int get_type_size(uint32_t type) {
switch (type) {
case 1: case 2: return 1;
case 3: return 2;
case 4: case 5: return 4;
case 6: return 8;
default: return 0;
}
}
std::pair<std::map<std::string, std::any>, const char*> parse_attr(const char* ptr) {
std::map<std::string, std::any> attr;
uint32_t name_len = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
ptr += 4;
std::string name(ptr, name_len);
ptr += name_len;
ptr += pad(name_len);
uint32_t type = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
ptr += 4;
uint32_t nelems = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
ptr += 4;
std::vector<std::any> values;
int size = get_type_size(type);
for (uint32_t i = 0; i < nelems; ++i) {
std::any val = read_value(ptr, type);
values.push_back(val);
ptr += size;
}
ptr += pad(nelems * size);
attr["name"] = name;
attr["type"] = type_to_string(type);
attr["values"] = values;
return {attr, ptr};
}
std::any read_value(const char* ptr, uint32_t type) {
switch (type) {
case 1: return static_cast<uint8_t>(*ptr);
case 2: return static_cast<char>(*ptr);
case 3: return static_cast<int16_t>(be16toh(*reinterpret_cast<const uint16_t*>(ptr)));
case 4: return static_cast<int32_t>(be32toh(*reinterpret_cast<const uint32_t*>(ptr)));
case 5: { // Float
uint32_t i = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
float f;
std::memcpy(&f, &i, sizeof(float));
return f;
}
case 6: { // Double
uint64_t i = be64toh(*reinterpret_cast<const uint64_t*>(ptr));
double d;
std::memcpy(&d, &i, sizeof(double));
return d;
}
default: return 0;
}
}
void print_properties() {
// Implement printing recursively, e.g., as JSON-like
std::cout << "{\n";
std::cout << " \"magic\": \"" << std::any_cast<std::string>(properties["magic"]) << "\",\n";
std::cout << " \"version\": " << static_cast<int>(std::any_cast<uint8_t>(properties["version"])) << ",\n";
std::cout << " \"numrecs\": " << std::any_cast<uint32_t>(properties["numrecs"]) << ",\n";
// Add printing for dims, attrs, vars similarly (omitted for brevity; use recursion or loops)
std::cout << "}\n";
}
void write(const std::string& new_filepath) {
std::ofstream file(new_filepath, std::ios::binary);
if (!file) throw std::runtime_error("Cannot write file");
file.write("CDF", 3);
uint8_t version = std::any_cast<uint8_t>(properties["version"]);
file.write(reinterpret_cast<const char*>(&version), 1);
uint32_t numrecs_be = htobe32(std::any_cast<uint32_t>(properties["numrecs"]));
file.write(reinterpret_cast<const char*>(&numrecs_be), 4);
// Dimensions, attrs, vars writing similar to above, with htobe32 etc. (omitted for brevity)
// Header only.
}
};
// Example
// int main() {
// NetCDFHandler handler("example.nc");
// handler.read();
// handler.print_properties();
// handler.write("new.nc");
// return 0;
// }
Note: C++ code uses <endian.h> for byte order conversion (available on Linux; for portability, implement manually). Printing is partial; extend for full maps/vectors. Write is header-only.