Task 810: .WKS File Format

Task 810: .WKS File Format

1. List of Properties of the .WKS File Format Intrinsic to Its File Structure

The .WKS file format, used by Lotus 1-2-3 Release 1A, is a binary, record-based structure with little-endian byte order. The intrinsic properties include the following key elements, derived from its specifications:

  • File Signature: The first 6 bytes are 0x00 0x00 0x02 0x00 0x04 0x06, representing the BOF record with version indicator.
  • Version Indicator: Encoded in the BOF record data (0x0406 for Lotus 1-2-3 Release 1A).
  • Byte Order: Little-endian for all multi-byte values.
  • Record Structure: Each record consists of a 2-byte opcode, a 2-byte length (excluding header), followed by the specified length of data bytes.
  • Record Types (Opcodes): The format supports a set of opcodes, including but not limited to:
  • 0x0000: BOF (Beginning of File)
  • 0x0001: EOF (End of File)
  • 0x0002: CALCMODE (Calculation mode)
  • 0x0003: CALCORDER (Calculation order)
  • 0x0004: SPLIT (Window split type)
  • 0x0005: SYNC (Window sync)
  • 0x0006: RANGE (Worksheet dimensions: min/max columns and rows)
  • 0x0007: WINDOW1 (Window settings 1)
  • 0x0008: COLW1 (Column widths 1)
  • 0x0009: WINTWO (Window settings 2)
  • 0x000A: COLW2 (Column widths 2)
  • 0x000B: NAME (Named range)
  • 0x000C: BLANK (Blank cell)
  • 0x000D: INTEGER (Integer cell value)
  • 0x000E: NUMBER (Floating-point cell value)
  • 0x000F: LABEL (Text label cell)
  • 0x0010: FORMULA (Formula cell)
  • Additional opcodes for headers, footers, margins, protection, and embedded graphics.
  • Embedded Graphics Compression: LZW algorithm with adaptive code size (9-12 bits), maximum table size of 4096 entries, Clear code at 4094, optional horizontal predictor for differencing.
  • Cell Data Encoding: Cells include column (1 byte), row (2 bytes), format (1 byte), and value (variable based on type).
  • Endianness Independence for Codes: Raw values in LZW streams, supporting both II and MM byte orders.
  • Planar Configuration Support: Single stream for multi-sample pixels (e.g., RGB), regardless of configuration.

These properties define the format's structure and behavior, enabling parsing and generation of .WKS files.

3. HTML with Embedded JavaScript for Drag-and-Drop .WKS File Dump

The following is an HTML page with embedded JavaScript that can be embedded in a Ghost blog. It allows users to drag and drop a .WKS file, parses the file, and dumps the properties (including version, dimensions, and record details) to the screen.

.WKS File Dumper
Drag and drop a .WKS file here

4. Python Class for .WKS File Handling

The following Python class can open, decode, read, write, and print the properties of a .WKS file to the console.

import struct
import sys

class WKSFile:
    def __init__(self, filename=None):
        self.filename = filename
        self.records = []
        self.version = None
        self.dimensions = None
        if filename:
            self.read(filename)

    def read(self, filename):
        self.filename = filename
        with open(filename, 'rb') as f:
            data = f.read()
        offset = 0
        while offset < len(data):
            if offset + 4 > len(data): break
            opcode, length = struct.unpack('<HH', data[offset:offset+4])
            offset += 4
            record_data = data[offset:offset+length]
            self.records.append((opcode, length, record_data))
            offset += length
            if opcode == 0x0000:  # BOF
                (ver,) = struct.unpack('<H', record_data)
                self.version = hex(ver)
            elif opcode == 0x0006:  # RANGE
                min_col, min_row, max_col, max_row = struct.unpack('<HHHH', record_data)
                self.dimensions = (min_col, min_row, max_col, max_row)
            if opcode == 0x0001: break  # EOF

    def print_properties(self):
        print("Properties of the .WKS file:")
        print(f"File Signature: 00 00 02 00 04 06")
        print(f"Version: {self.version} (Lotus 1-2-3 Release 1A)")
        print("Byte Order: Little-endian")
        print("Record Structure: 2-byte opcode, 2-byte length, data")
        if self.dimensions:
            print(f"Dimensions: Columns {self.dimensions[0]}-{self.dimensions[2]}, Rows {self.dimensions[1]}-{self.dimensions[3]}")
        print("Records:")
        for opcode, length, data in self.records:
            print(f"Opcode: 0x{opcode:04X}, Length: {length}")
            if opcode == 0x000F:  # LABEL
                col = data[0]
                row, = struct.unpack('<H', data[1:3])
                fmt = data[3]
                label = data[4:].decode('ascii', errors='ignore').rstrip('\x00')
                print(f"  - Label at Col {col}, Row {row}: {label}")
            elif opcode == 0x000E:  # NUMBER
                col = data[0]
                row, = struct.unpack('<H', data[1:3])
                fmt = data[3]
                value, = struct.unpack('<d', data[4:12])
                print(f"  - Number at Col {col}, Row {row}: {value}")
            # Add more decoders as needed
        print("Embedded Graphics Compression: LZW (adaptive 9-12 bits, max table 4096, Clear code 4094)")

    def write(self, filename):
        with open(filename, 'wb') as f:
            # Write BOF
            f.write(struct.pack('<HH', 0x0000, 0x0002))
            f.write(struct.pack('<H', 0x0406))
            # Write RANGE (example: 0-255 cols, 0-8191 rows)
            f.write(struct.pack('<HH', 0x0006, 0x0008))
            f.write(struct.pack('<HHHH', 0, 0, 255, 8191))
            # Write a sample label
            f.write(struct.pack('<HH', 0x000F, 10))  # Length example
            f.write(b'\x00\x00\x00\x27Test\0')  # Col 0, Row 0, format ', label 'Test
            # Write EOF
            f.write(struct.pack('<HH', 0x0001, 0x0000))

if __name__ == '__main__':
    if len(sys.argv) > 1:
        wks = WKSFile(sys.argv[1])
        wks.print_properties()

5. Java Class for .WKS File Handling

The following Java class can open, decode, read, write, and print the properties of a .WKS file to the console.

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class WKSFile {
    private String filename;
    private String version;
    private int[] dimensions;
    // List of records as byte arrays for simplicity
    private byte[][] records;

    public WKSFile(String filename) throws IOException {
        this.filename = filename;
        read(filename);
    }

    public WKSFile() {}

    private void read(String filename) throws IOException {
        try (DataInputStream dis = new DataInputStream(new FileInputStream(filename))) {
            byte[] data = dis.readAllBytes();
            ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
            int offset = 0;
            while (offset < data.length) {
                if (offset + 4 > data.length) break;
                short opcode = bb.getShort(offset);
                short length = bb.getShort(offset + 2);
                offset += 4;
                byte[] recordData = new byte[length];
                bb.position(offset);
                bb.get(recordData);
                offset += length;
                // Store record
                // For demonstration, process specific
                if (opcode == 0x0000) { // BOF
                    ByteBuffer rbb = ByteBuffer.wrap(recordData).order(ByteOrder.LITTLE_ENDIAN);
                    version = Integer.toHexString(rbb.getShort(0) & 0xFFFF).toUpperCase();
                } else if (opcode == 0x0006) { // RANGE
                    ByteBuffer rbb = ByteBuffer.wrap(recordData).order(ByteOrder.LITTLE_ENDIAN);
                    dimensions = new int[] {rbb.getShort(0) & 0xFFFF, rbb.getShort(2) & 0xFFFF, rbb.getShort(4) & 0xFFFF, rbb.getShort(6) & 0xFFFF};
                }
                if (opcode == 0x0001) break; // EOF
            }
        }
    }

    public void printProperties() {
        System.out.println("Properties of the .WKS file:");
        System.out.println("File Signature: 00 00 02 00 04 06");
        System.out.println("Version: " + version + " (Lotus 1-2-3 Release 1A)");
        System.out.println("Byte Order: Little-endian");
        System.out.println("Record Structure: 2-byte opcode, 2-byte length, data");
        if (dimensions != null) {
            System.out.println("Dimensions: Columns " + dimensions[0] + "-" + dimensions[2] + ", Rows " + dimensions[1] + "-" + dimensions[3]);
        }
        System.out.println("Embedded Graphics Compression: LZW (adaptive 9-12 bits, max table 4096, Clear code 4094)");
        // Add more as needed
    }

    public void write(String filename) throws IOException {
        try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(filename))) {
            // BOF
            dos.writeShort(0x0000);
            dos.writeShort(0x0002);
            dos.writeShort(0x0406);
            // RANGE example
            dos.writeShort(0x0006);
            dos.writeShort(0x0008);
            dos.writeShort(0);
            dos.writeShort(0);
            dos.writeShort(255);
            dos.writeShort(8191);
            // Sample label
            dos.writeShort(0x000F);
            dos.writeShort(10);
            dos.writeByte(0); // col
            dos.writeShort(0); // row
            dos.writeByte(39); // format '
            dos.writeBytes("Test\0");
            // EOF
            dos.writeShort(0x0001);
            dos.writeShort(0x0000);
        }
    }

    public static void main(String[] args) throws IOException {
        if (args.length > 0) {
            WKSFile wks = new WKSFile(args[0]);
            wks.printProperties();
        }
    }
}

6. JavaScript Class for .WKS File Handling

The following JavaScript class can open (via ArrayBuffer), decode, read, write (generate Blob), and print the properties of a .WKS file to the console.

class WKSFile {
    constructor(buffer = null) {
        this.version = null;
        this.dimensions = null;
        this.records = [];
        if (buffer) this.read(buffer);
    }

    read(buffer) {
        const view = new DataView(buffer);
        let offset = 0;
        while (offset < buffer.byteLength) {
            if (offset + 4 > buffer.byteLength) break;
            const opcode = view.getUint16(offset, true);
            const length = view.getUint16(offset + 2, true);
            offset += 4;
            const recordData = new Uint8Array(buffer, offset, length);
            this.records.push({opcode, length, data: recordData});
            offset += length;
            if (opcode === 0x0000) { // BOF
                this.version = view.getUint16(offset - length, true).toString(16).toUpperCase();
            } else if (opcode === 0x0006) { // RANGE
                this.dimensions = [
                    view.getUint16(offset - length, true),
                    view.getUint16(offset - length + 2, true),
                    view.getUint16(offset - length + 4, true),
                    view.getUint16(offset - length + 6, true)
                ];
            }
            if (opcode === 0x0001) break; // EOF
        }
    }

    printProperties() {
        console.log('Properties of the .WKS file:');
        console.log('File Signature: 00 00 02 00 04 06');
        console.log(`Version: ${this.version} (Lotus 1-2-3 Release 1A)`);
        console.log('Byte Order: Little-endian');
        console.log('Record Structure: 2-byte opcode, 2-byte length, data');
        if (this.dimensions) {
            console.log(`Dimensions: Columns ${this.dimensions[0]}-${this.dimensions[2]}, Rows ${this.dimensions[1]}-${this.dimensions[3]}`);
        }
        console.log('Records:');
        this.records.forEach(r => {
            console.log(`Opcode: 0x${r.opcode.toString(16).padStart(4, '0').toUpperCase()}, Length: ${r.length}`);
            // Decode more as needed
        });
        console.log('Embedded Graphics Compression: LZW (adaptive 9-12 bits, max table 4096, Clear code 4094)');
    }

    write() {
        const buffer = new ArrayBuffer(100); // Approximate size
        const view = new DataView(buffer);
        let offset = 0;
        // BOF
        view.setUint16(offset, 0x0000, true); offset += 2;
        view.setUint16(offset, 0x0002, true); offset += 2;
        view.setUint16(offset, 0x0406, true); offset += 2;
        // RANGE
        view.setUint16(offset, 0x0006, true); offset += 2;
        view.setUint16(offset, 0x0008, true); offset += 2;
        view.setUint16(offset, 0, true); offset += 2;
        view.setUint16(offset, 0, true); offset += 2;
        view.setUint16(offset, 255, true); offset += 2;
        view.setUint16(offset, 8191, true); offset += 2;
        // Sample label
        view.setUint16(offset, 0x000F, true); offset += 2;
        view.setUint16(offset, 10, true); offset += 2;
        view.setUint8(offset, 0); offset += 1;
        view.setUint16(offset, 0, true); offset += 2;
        view.setUint8(offset, 39); offset += 1;
        const label = new TextEncoder().encode('Test\0');
        for (let i = 0; i < label.length; i++) {
            view.setUint8(offset + i, label[i]);
        }
        offset += label.length;
        // EOF
        view.setUint16(offset, 0x0001, true); offset += 2;
        view.setUint16(offset, 0x0000, true); offset += 2;
        return new Blob([buffer.slice(0, offset)]);
    }
}

// Example usage
// const wks = new WKSFile(arrayBuffer);
// wks.printProperties();
// const blob = wks.write();
// // Save blob as file

7. C++ Class for .WKS File Handling

The following C++ class can open, decode, read, write, and print the properties of a .WKS file to the console.

#include <fstream>
#include <iostream>
#include <vector>
#include <cstring>
#include <cstdint>
#include <iomanip>

struct Record {
    uint16_t opcode;
    uint16_t length;
    std::vector<uint8_t> data;
};

class WKSFile {
private:
    std::string filename;
    std::string version;
    int dimensions[4];
    std::vector<Record> records;

public:
    WKSFile(const std::string& fn = "") : filename(fn) {
        memset(dimensions, 0, sizeof(dimensions));
        if (!fn.empty()) read(fn);
    }

    void read(const std::string& fn) {
        filename = fn;
        std::ifstream file(fn, std::ios::binary);
        if (!file) return;
        file.seekg(0, std::ios::end);
        size_t size = file.tellg();
        file.seekg(0);
        std::vector<uint8_t> data(size);
        file.read(reinterpret_cast<char*>(data.data()), size);
        size_t offset = 0;
        while (offset < size) {
            if (offset + 4 > size) break;
            uint16_t opcode, length;
            memcpy(&opcode, data.data() + offset, 2);
            memcpy(&length, data.data() + offset + 2, 2);
            offset += 4;
            std::vector<uint8_t> rec_data(data.begin() + offset, data.begin() + offset + length);
            records.push_back({opcode, length, rec_data});
            offset += length;
            if (opcode == 0x0000) { // BOF
                uint16_t ver;
                memcpy(&ver, rec_data.data(), 2);
                version = std::to_string(ver);
            } else if (opcode == 0x0006) { // RANGE
                memcpy(dimensions, rec_data.data(), sizeof(dimensions));
            }
            if (opcode == 0x0001) break; // EOF
        }
    }

    void printProperties() const {
        std::cout << "Properties of the .WKS file:" << std::endl;
        std::cout << "File Signature: 00 00 02 00 04 06" << std::endl;
        std::cout << "Version: " << version << " (Lotus 1-2-3 Release 1A)" << std::endl;
        std::cout << "Byte Order: Little-endian" << std::endl;
        std::cout << "Record Structure: 2-byte opcode, 2-byte length, data" << std::endl;
        if (dimensions[0] || dimensions[1] || dimensions[2] || dimensions[3]) {
            std::cout << "Dimensions: Columns " << dimensions[0] << "-" << dimensions[2] << ", Rows " << dimensions[1] << "-" << dimensions[3] << std::endl;
        }
        std::cout << "Records:" << std::endl;
        for (const auto& r : records) {
            std::cout << "Opcode: 0x" << std::setfill('0') << std::setw(4) << std::hex << std::uppercase << r.opcode << ", Length: " << std::dec << r.length << std::endl;
            // Decode more as needed
        }
        std::cout << "Embedded Graphics Compression: LZW (adaptive 9-12 bits, max table 4096, Clear code 4094)" << std::endl;
    }

    void write(const std::string& fn) {
        std::ofstream file(fn, std::ios::binary);
        if (!file) return;
        // BOF
        uint16_t bof[] = {0x0000, 0x0002, 0x0406};
        file.write(reinterpret_cast<char*>(bof), sizeof(bof));
        // RANGE
        uint16_t range_hdr[] = {0x0006, 0x0008};
        file.write(reinterpret_cast<char*>(range_hdr), sizeof(range_hdr));
        uint16_t range_data[] = {0, 0, 255, 8191};
        file.write(reinterpret_cast<char*>(range_data), sizeof(range_data));
        // Sample label
        uint16_t label_hdr[] = {0x000F, 10};
        file.write(reinterpret_cast<char*>(label_hdr), sizeof(label_hdr));
        uint8_t label_data[] = {0, 0, 0, 39, 'T', 'e', 's', 't', 0}; // Col, row low, row high, format, string
        file.write(reinterpret_cast<char*>(label_data), sizeof(label_data));
        // EOF
        uint16_t eof[] = {0x0001, 0x0000};
        file.write(reinterpret_cast<char*>(eof), sizeof(eof));
    }
};

int main(int argc, char** argv) {
    if (argc > 1) {
        WKSFile wks(argv[1]);
        wks.printProperties();
    }
    return 0;
}