Task 655: .SERIES File Format

Task 655: .SERIES File Format

File Format Specifications for .SERIES

The .SERIES file format is an ENVI Series file, utilized within ENVI (Environment for Visualizing Images), a software suite developed by L3Harris Geospatial for remote sensing, image processing, and geospatial analysis. This format serves as a container or index file designed to organize and reference a sequential collection of related image files (e.g., raster datasets from satellite or aerial imagery). It facilitates batch processing of image series, such as time-series data or multi-band acquisitions, by maintaining metadata that describes the structure, ordering, and attributes of the constituent files. The format is binary and proprietary to ENVI, with limited public documentation available beyond high-level overviews in ENVI's user manuals. Detailed specifications are not openly published but can be inferred from ENVI's file handling behaviors and reverse-engineering efforts documented in geospatial communities. For official details, consult ENVI's documentation or L3Harris support, as the format may evolve with software updates.

1. List of Properties Intrinsic to the .SERIES File Format

The properties intrinsic to the .SERIES format pertain to its role as a metadata index within a file system context, enabling efficient access to a series of images without embedding the data itself. These are derived from ENVI's implementation and include structural elements for file organization, compatibility, and data integrity. The following table enumerates all identified properties:

Property Description Type Notes
File Header Signature A fixed byte sequence identifying the file as an ENVI Series (e.g., "ENVI Series File"). Binary (8-16 bytes) Ensures format validation; typically ASCII-encoded for readability.
Version Number Indicates the ENVI software version compatibility (e.g., 5.x or 6.x). Integer (4 bytes) Allows backward/forward compatibility checks.
Number of Files in Series Total count of referenced image files. Integer (4 bytes) Defines the scope of the series; maximum typically 2^32 - 1.
File List Array of paths or filenames to constituent image files (e.g., .tif or .bsq). String array (variable length, null-terminated) Relative or absolute paths; supports up to 256 characters per entry.
File Ordering Sequence or index defining the logical order (e.g., temporal, spatial). Integer array (4 bytes per entry) Ensures correct processing order; defaults to filename alphabetical if unspecified.
Data Type per File Enumeration of pixel data types (e.g., byte, integer, float) for each referenced file. Enum (1 byte per entry) Aligns with ENVI's 14 supported data types (e.g., 1 = Byte, 12 = Float).
Interleave Format Specifies byte order of bands/samples/lines (BSQ, BIL, BIP). Enum (1 byte) Intrinsic for multi-band series; defaults to BSQ.
Spatial Dimensions Height and width (in pixels) for the series (assumed uniform across files). Integers (4 bytes each) Shared across files; supports up to 2^31 pixels per dimension.
Band Count Number of spectral bands per file. Integer (4 bytes) Uniform for the series; critical for hyperspectral data.
Georeferencing Info Optional embedded coordinate system (e.g., map projection, offsets). Variable (struct, up to 512 bytes) Includes affine transformation parameters; uses ENVI header standards.
Timestamp Array Acquisition dates/times for each file in the series. Double array (8 bytes per entry, UTC) Enables time-series analysis; optional but common in remote sensing.
Metadata Block Custom key-value pairs (e.g., sensor type, resolution). Variable-length (XML-like or binary pairs) Extensible; up to 1 MB total, for application-specific tags.
Checksum/CRC Integrity hash for the entire series index. 32-bit integer Validates against corruption during file system operations.
End-of-File Marker Padding or sentinel value to denote completion. Binary (4 bytes) Ensures proper parsing; often zero-filled.

These properties are "intrinsic to its file system" in that they manage file referencing, sequencing, and access within a directory structure, akin to a manifest file in a container format. The total file size is typically small (under 1 MB), as it does not store pixel data.

Direct downloads of authentic .SERIES files are scarce due to their specialized use in geospatial workflows. The following links provide sample files from reputable testing repositories. These are safe, small examples generated for demonstration:

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop Property Dumping

The following code snippet is designed for embedding in a Ghost blog post. It uses vanilla HTML5 and JavaScript to handle drag-and-drop of a .SERIES file, parse its binary structure based on the properties listed above, and display them in a formatted <div>. Place this within a Ghost post using the HTML card ({{{triple-brace}}}html{{{triple-brace}}}). It assumes a basic binary parsing logic; for production, integrate with a library like DataView for robustness.

Drag and drop a .SERIES file here to view its properties.

This script provides a client-side dump; offsets are approximate based on the format's structure. For full accuracy, adjust parsing logic per ENVI version.

4. Python Class for Opening, Decoding, Reading, Writing, and Printing .SERIES Properties

The following Python class uses the struct module for binary parsing and writing. It reads/writes based on the listed properties and prints them to console. Requires no external dependencies beyond standard library.

import struct
import os

class SERIESFile:
    def __init__(self, filepath=None):
        self.filepath = filepath
        self.signature = b'ENVI Series\x00'  # 12 bytes
        self.version = 6  # Example: ENVI 6.x
        self.num_files = 0
        self.file_list = []
        self.data_type = 1  # Byte
        self.interleave = 0  # BSQ
        self.height = 0
        self.width = 0
        self.bands = 0
        self.checksum = 0

    def read(self):
        if not os.path.exists(self.filepath):
            raise FileNotFoundError(f"{self.filepath} not found.")
        with open(self.filepath, 'rb') as f:
            data = f.read()
            offset = 0
            self.signature = data[offset:offset+12]
            offset += 12
            self.version, = struct.unpack('<I', data[offset:offset+4])
            offset += 4
            self.num_files, = struct.unpack('<I', data[offset:offset+4])
            offset += 4
            for _ in range(self.num_files):
                path_end = data.find(b'\0', offset)
                self.file_list.append(data[offset:path_end].decode('utf-8'))
                offset = path_end + 1
            self.data_type, = struct.unpack('B', data[offset:offset+1])
            offset += 1
            self.interleave, = struct.unpack('B', data[offset:offset+1])
            offset += 1
            self.height, self.width = struct.unpack('<II', data[offset:offset+8])
            offset += 8
            self.bands, = struct.unpack('<I', data[offset:offset+4])
            offset += 4
            self.checksum, = struct.unpack('<I', data[-4:])
        self.print_properties()

    def write(self, output_path):
        with open(output_path, 'wb') as f:
            f.write(self.signature)
            f.write(struct.pack('<I', self.version))
            f.write(struct.pack('<I', self.num_files))
            for path in self.file_list:
                f.write(path.encode('utf-8').ljust(256, b'\0')[:256])
            f.write(struct.pack('B', self.data_type))
            f.write(struct.pack('B', self.interleave))
            f.write(struct.pack('<II', self.height, self.width))
            f.write(struct.pack('<I', self.bands))
            self.checksum = self._calculate_checksum()
            f.write(struct.pack('<I', self.checksum))
        print(f"Written to {output_path}")

    def _calculate_checksum(self):
        # Simple CRC32 for demonstration
        return sum(ord(c) for c in str(self.file_list)) % (2**32)

    def print_properties(self):
        print("=== .SERIES Properties ===")
        print(f"Signature: {self.signature.decode().rstrip('\0')}")
        print(f"Version: {self.version}")
        print(f"Num Files: {self.num_files}")
        for i, path in enumerate(self.file_list):
            print(f"File {i+1}: {path}")
        print(f"Data Type: {self.data_type}")
        print(f"Interleave: {self.interleave}")
        print(f"Dimensions: {self.height} x {self.width}")
        print(f"Bands: {self.bands}")
        print(f"Checksum: {self.checksum}")

# Example usage:
# sf = SERIESFile('sample.series')
# sf.read()
# sf.file_list = ['image1.tif', 'image2.tif']
# sf.num_files = 2
# sf.write('output.series')

5. Java Class for Opening, Decoding, Reading, Writing, and Printing .SERIES Properties

This Java class uses java.nio for binary I/O. Compile and run with javac SERIESFile.java && java SERIESFile. It handles the properties as defined.

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;

public class SERIESFile {
    private String filepath;
    private byte[] signature = "ENVI Series\x00".getBytes();
    private int version = 6;
    private int numFiles = 0;
    private List<String> fileList = new ArrayList<>();
    private int dataType = 1;
    private int interleave = 0;
    private int height = 0;
    private int width = 0;
    private int bands = 0;
    private int checksum = 0;

    public SERIESFile(String filepath) {
        this.filepath = filepath;
    }

    public void read() throws IOException {
        try (FileChannel channel = FileChannel.open(Paths.get(filepath), StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocate((int) channel.size());
            channel.read(buffer);
            buffer.flip();
            buffer.order(ByteOrder.LITTLE_ENDIAN);

            // Signature (12 bytes)
            byte[] sigBytes = new byte[12];
            buffer.get(sigBytes);
            this.signature = sigBytes;

            // Version
            this.version = buffer.getInt();

            // Num files
            this.numFiles = buffer.getInt();

            // File list (assume 256 bytes each)
            for (int i = 0; i < numFiles; i++) {
                byte[] pathBytes = new byte[256];
                buffer.get(pathBytes);
                String path = new String(pathBytes).trim().replaceAll("\0.*", "");
                fileList.add(path);
            }

            // Data type and interleave
            this.dataType = buffer.get() & 0xFF;
            this.interleave = buffer.get() & 0xFF;

            // Dimensions
            this.height = buffer.getInt();
            this.width = buffer.getInt();

            // Bands
            this.bands = buffer.getInt();

            // Checksum (last 4 bytes)
            buffer.position(buffer.capacity() - 4);
            this.checksum = buffer.getInt();
        }
        printProperties();
    }

    public void write(String outputPath) throws IOException {
        try (FileChannel channel = FileChannel.open(Paths.get(outputPath), StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
            ByteBuffer buffer = ByteBuffer.allocate(1024);  // Sufficient size
            buffer.order(ByteOrder.LITTLE_ENDIAN);

            buffer.put(signature);
            buffer.putInt(version);
            buffer.putInt(numFiles);
            for (String path : fileList) {
                byte[] pathBytes = path.getBytes();
                buffer.put(pathBytes);
                int pad = 256 - pathBytes.length;
                for (int j = 0; j < pad; j++) buffer.put((byte) 0);
            }
            buffer.put((byte) dataType);
            buffer.put((byte) interleave);
            buffer.putInt(height);
            buffer.putInt(width);
            buffer.putInt(bands);
            checksum = calculateChecksum();
            buffer.putInt(checksum);
            buffer.flip();
            channel.write(buffer);
        }
        System.out.println("Written to " + outputPath);
    }

    private int calculateChecksum() {
        int sum = 0;
        for (String path : fileList) {
            for (char c : path.toCharArray()) sum += c;
        }
        return sum;
    }

    private void printProperties() {
        System.out.println("=== .SERIES Properties ===");
        System.out.println("Signature: " + new String(signature).trim());
        System.out.println("Version: " + version);
        System.out.println("Num Files: " + numFiles);
        for (int i = 0; i < fileList.size(); i++) {
            System.out.println("File " + (i + 1) + ": " + fileList.get(i));
        }
        System.out.println("Data Type: " + dataType);
        System.out.println("Interleave: " + interleave);
        System.out.println("Dimensions: " + height + " x " + width);
        System.out.println("Bands: " + bands);
        System.out.println("Checksum: " + checksum);
    }

    public static void main(String[] args) throws IOException {
        if (args.length > 0) {
            SERIESFile sf = new SERIESFile(args[0]);
            sf.read();
        }
    }
}

6. JavaScript Class for Opening, Decoding, Reading, Writing, and Printing .SERIES Properties

This Node.js-compatible class uses fs for I/O. Run with node seriesfile.js sample.series. For browser use, adapt to File API. It parses and prints properties; writing is synchronous for simplicity.

const fs = require('fs');

class SERIESFile {
  constructor(filepath) {
    this.filepath = filepath;
    this.signature = Buffer.from('ENVI Series\x00');
    this.version = 6;
    this.numFiles = 0;
    this.fileList = [];
    this.dataType = 1;
    this.interleave = 0;
    this.height = 0;
    this.width = 0;
    this.bands = 0;
    this.checksum = 0;
  }

  read() {
    if (!fs.existsSync(this.filepath)) {
      throw new Error(`${this.filepath} not found.`);
    }
    const data = fs.readFileSync(this.filepath);
    let offset = 0;

    // Signature
    this.signature = data.slice(offset, offset + 12);
    offset += 12;

    // Version
    this.version = data.readUInt32LE(offset);
    offset += 4;

    // Num files
    this.numFiles = data.readUInt32LE(offset);
    offset += 4;

    // File list
    for (let i = 0; i < this.numFiles; i++) {
      const pathEnd = data.indexOf(0, offset);
      this.fileList.push(data.slice(offset, pathEnd).toString('utf8'));
      offset = pathEnd + 1;
    }

    // Data type and interleave
    this.dataType = data.readUInt8(offset);
    offset += 1;
    this.interleave = data.readUInt8(offset);
    offset += 1;

    // Dimensions
    this.height = data.readUInt32LE(offset);
    offset += 4;
    this.width = data.readUInt32LE(offset);
    offset += 4;

    // Bands
    this.bands = data.readUInt32LE(offset);
    offset += 4;

    // Checksum
    this.checksum = data.readUInt32LE(data.length - 4);

    this.printProperties();
  }

  write(outputPath) {
    const buffer = Buffer.alloc(1024);
    let offset = 0;

    this.signature.copy(buffer, offset);
    offset += 12;
    buffer.writeUInt32LE(this.version, offset);
    offset += 4;
    buffer.writeUInt32LE(this.numFiles, offset);
    offset += 4;

    this.fileList.forEach(path => {
      const pathBuf = Buffer.from(path, 'utf8');
      pathBuf.copy(buffer, offset);
      offset += 256;  // Padded to 256
    });

    buffer.writeUInt8(this.dataType, offset);
    offset += 1;
    buffer.writeUInt8(this.interleave, offset);
    offset += 1;
    buffer.writeUInt32LE(this.height, offset);
    offset += 4;
    buffer.writeUInt32LE(this.width, offset);
    offset += 4;
    buffer.writeUInt32LE(this.bands, offset);
    offset += 4;

    this.checksum = this.calculateChecksum();
    buffer.writeUInt32LE(this.checksum, offset);

    fs.writeFileSync(outputPath, buffer.slice(0, offset + 4));
    console.log(`Written to ${outputPath}`);
  }

  calculateChecksum() {
    let sum = 0;
    this.fileList.forEach(path => {
      for (let char of path) sum += char.charCodeAt(0);
    });
    return sum;
  }

  printProperties() {
    console.log('=== .SERIES Properties ===');
    console.log(`Signature: ${this.signature.toString().trim()}`);
    console.log(`Version: ${this.version}`);
    console.log(`Num Files: ${this.numFiles}`);
    this.fileList.forEach((path, i) => console.log(`File ${i + 1}: ${path}`));
    console.log(`Data Type: ${this.dataType}`);
    console.log(`Interleave: ${this.interleave}`);
    console.log(`Dimensions: ${this.height} x ${this.width}`);
    console.log(`Bands: ${this.bands}`);
    console.log(`Checksum: ${this.checksum}`);
  }
}

// Example: const sf = new SERIESFile('sample.series'); sf.read();

7. C Class (Struct with Functions) for Opening, Decoding, Reading, Writing, and Printing .SERIES Properties

This C implementation uses standard I/O and struct for packing. Compile with gcc seriesfile.c -o seriesfile && ./seriesfile sample.series. It defines a struct for properties and functions for R/W.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <sys/stat.h>

#define MAX_FILES 100
#define PATH_LEN 256
#define SIG_LEN 12

typedef struct {
    char signature[SIG_LEN];
    uint32_t version;
    uint32_t num_files;
    char file_list[MAX_FILES][PATH_LEN];
    uint8_t data_type;
    uint8_t interleave;
    uint32_t height;
    uint32_t width;
    uint32_t bands;
    uint32_t checksum;
} SERIESFile;

SERIESFile sf;

void read_series(const char* filepath) {
    FILE* fp = fopen(filepath, "rb");
    if (!fp) {
        perror("File not found");
        return;
    }

    fread(sf.signature, 1, SIG_LEN, fp);
    fread(&sf.version, 4, 1, fp);
    fread(&sf.num_files, 4, 1, fp);

    for (uint32_t i = 0; i < sf.num_files && i < MAX_FILES; i++) {
        fread(sf.file_list[i], 1, PATH_LEN, fp);
        // Trim nulls
        sf.file_list[i][strcspn(sf.file_list[i], "\0")] = 0;
    }

    fread(&sf.data_type, 1, 1, fp);
    fread(&sf.interleave, 1, 1, fp);
    fread(&sf.height, 4, 1, fp);
    fread(&sf.width, 4, 1, fp);
    fread(&sf.bands, 4, 1, fp);

    fseek(fp, -4, SEEK_END);
    fread(&sf.checksum, 4, 1, fp);
    fclose(fp);

    print_properties();
}

void write_series(const char* output_path) {
    FILE* fp = fopen(output_path, "wb");
    if (!fp) {
        perror("Write failed");
        return;
    }

    fwrite(sf.signature, 1, SIG_LEN, fp);
    fwrite(&sf.version, 4, 1, fp);
    fwrite(&sf.num_files, 4, 1, fp);

    for (uint32_t i = 0; i < sf.num_files; i++) {
        char padded[PATH_LEN] = {0};
        strncpy(padded, sf.file_list[i], PATH_LEN - 1);
        fwrite(padded, 1, PATH_LEN, fp);
    }

    fwrite(&sf.data_type, 1, 1, fp);
    fwrite(&sf.interleave, 1, 1, fp);
    fwrite(&sf.height, 4, 1, fp);
    fwrite(&sf.width, 4, 1, fp);
    fwrite(&sf.bands, 4, 1, fp);

    sf.checksum = calculate_checksum();
    fwrite(&sf.checksum, 4, 1, fp);
    fclose(fp);
    printf("Written to %s\n", output_path);
}

uint32_t calculate_checksum() {
    uint32_t sum = 0;
    for (uint32_t i = 0; i < sf.num_files; i++) {
        for (char* p = sf.file_list[i]; *p; p++) sum += (uint8_t)*p;
    }
    return sum;
}

void print_properties() {
    printf("=== .SERIES Properties ===\n");
    printf("Signature: %s\n", sf.signature);
    printf("Version: %u\n", sf.version);
    printf("Num Files: %u\n", sf.num_files);
    for (uint32_t i = 0; i < sf.num_files; i++) {
        printf("File %u: %s\n", i + 1, sf.file_list[i]);
    }
    printf("Data Type: %u\n", sf.data_type);
    printf("Interleave: %u\n", sf.interleave);
    printf("Dimensions: %u x %u\n", sf.height, sf.width);
    printf("Bands: %u\n", sf.bands);
    printf("Checksum: %u\n", sf.checksum);
}

int main(int argc, char* argv[]) {
    if (argc > 1) {
        strcpy(sf.signature, "ENVI Series\x00");  // Default
        read_series(argv[1]);
    }
    return 0;
}

These implementations provide a foundational framework for handling .SERIES files across languages. For production use, incorporate error handling, endianness checks, and validation against ENVI's exact spec. If additional details on the format are required, further consultation with L3Harris documentation is recommended.