Task 840: .XP File Format

Task 840: .XP File Format

1. Properties of the .XP File Format Intrinsic to Its File System

The .XP file format, used by REXPaint for storing ASCII art images, is a binary format compressed using zlib (specifically in gzip format). Once decompressed, it consists of the following intrinsic properties structured in binary data:

  • Format version: A 32-bit integer (typically negative in modern versions to distinguish from legacy files).
  • Number of layers: A 32-bit integer (ranging from 1 to 9 in supported versions).
  • For each layer:
  • Image width: A 32-bit integer.
  • Image height: A 32-bit integer.
  • Cell data: A sequence of width × height cells, where each cell includes:
  • ASCII/Unicode character code: A 32-bit integer (stored in little-endian byte order).
  • Foreground color: Three 8-bit unsigned integers representing red, green, and blue components.
  • Background color: Three 8-bit unsigned integers representing red, green, and blue components.

Additional notes on the format:

  • Transparent cells are indicated by a background color of RGB(255, 0, 255).
  • Cell data is stored in column-major order (1D array where x = index / height, y = index % height).
  • When rendering, transparent cells on the base layer are treated as black backgrounds.

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

The following is a complete, standalone HTML file with embedded JavaScript. It allows users to drag and drop a .XP file, decompresses it using the Pako library (included via CDN), parses the binary structure, and dumps all properties to the screen in a structured text format within a <div> element.

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

4. Python Class for .XP File Handling

The following Python class can open a .XP file, decode its contents, print all properties to the console, and write a new or modified .XP file.

import gzip
import struct

class XPFile:
    def __init__(self, filepath=None):
        self.version = 0
        self.num_layers = 0
        self.layers = []  # List of dicts: {'width': int, 'height': int, 'cells': list of tuples (char_code, fg_rgb, bg_rgb)}
        if filepath:
            self.read(filepath)

    def read(self, filepath):
        with gzip.open(filepath, 'rb') as f:
            data = f.read()
        offset = 0
        self.version, = struct.unpack_from('<i', data, offset); offset += 4
        self.num_layers, = struct.unpack_from('<i', data, offset); offset += 4
        self.layers = []
        for _ in range(self.num_layers):
            width, = struct.unpack_from('<i', data, offset); offset += 4
            height, = struct.unpack_from('<i', data, offset); offset += 4
            cells = []
            for _ in range(width * height):
                char_code, = struct.unpack_from('<i', data, offset); offset += 4
                fg_r, fg_g, fg_b = struct.unpack_from('BBB', data, offset); offset += 3
                bg_r, bg_g, bg_b = struct.unpack_from('BBB', data, offset); offset += 3
                cells.append((char_code, (fg_r, fg_g, fg_b), (bg_r, bg_g, bg_b)))
            self.layers.append({'width': width, 'height': height, 'cells': cells})

    def print_properties(self):
        print(f"Format Version: {self.version}")
        print(f"Number of Layers: {self.num_layers}")
        for i, layer in enumerate(self.layers):
            print(f"\nLayer {i + 1}:")
            print(f"  Width: {layer['width']}")
            print(f"  Height: {layer['height']}")
            for j, cell in enumerate(layer['cells']):
                x = j // layer['height']
                y = j % layer['height']
                char_code, fg, bg = cell
                print(f"    Cell ({x}, {y}): Char={chr(char_code)} (code={char_code}), FG={fg}, BG={bg}")

    def write(self, filepath):
        data = bytearray()
        data.extend(struct.pack('<i', self.version))
        data.extend(struct.pack('<i', self.num_layers))
        for layer in self.layers:
            data.extend(struct.pack('<i', layer['width']))
            data.extend(struct.pack('<i', layer['height']))
            for cell in layer['cells']:
                char_code, fg, bg = cell
                data.extend(struct.pack('<i', char_code))
                data.extend(struct.pack('BBB', *fg))
                data.extend(struct.pack('BBB', *bg))
        with gzip.open(filepath, 'wb') as f:
            f.write(data)

5. Java Class for .XP File Handling

The following Java class can open a .XP file, decode its contents, print all properties to the console, and write a new or modified .XP file.

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

public class XPFile {
    private int version;
    private int numLayers;
    private List<Layer> layers = new ArrayList<>();

    static class Layer {
        int width;
        int height;
        List<Cell> cells = new ArrayList<>();

        static class Cell {
            int charCode;
            int[] fgRGB = new int[3];
            int[] bgRGB = new int[3];
        }
    }

    public XPFile(String filepath) throws IOException {
        if (filepath != null) {
            read(filepath);
        }
    }

    public void read(String filepath) throws IOException {
        try (FileInputStream fis = new FileInputStream(filepath);
             GZIPInputStream gis = new GZIPInputStream(fis)) {
            byte[] data = gis.readAllBytes();
            ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
            version = bb.getInt();
            numLayers = bb.getInt();
            layers.clear();
            for (int i = 0; i < numLayers; i++) {
                Layer layer = new Layer();
                layer.width = bb.getInt();
                layer.height = bb.getInt();
                for (int j = 0; j < layer.width * layer.height; j++) {
                    Layer.Cell cell = new Layer.Cell();
                    cell.charCode = bb.getInt();
                    cell.fgRGB[0] = bb.get() & 0xFF;
                    cell.fgRGB[1] = bb.get() & 0xFF;
                    cell.fgRGB[2] = bb.get() & 0xFF;
                    cell.bgRGB[0] = bb.get() & 0xFF;
                    cell.bgRGB[1] = bb.get() & 0xFF;
                    cell.bgRGB[2] = bb.get() & 0xFF;
                    layer.cells.add(cell);
                }
                layers.add(layer);
            }
        }
    }

    public void printProperties() {
        System.out.println("Format Version: " + version);
        System.out.println("Number of Layers: " + numLayers);
        for (int i = 0; i < layers.size(); i++) {
            Layer layer = layers.get(i);
            System.out.println("\nLayer " + (i + 1) + ":");
            System.out.println("  Width: " + layer.width);
            System.out.println("  Height: " + layer.height);
            for (int j = 0; j < layer.cells.size(); j++) {
                Layer.Cell cell = layer.cells.get(j);
                int x = j / layer.height;
                int y = j % layer.height;
                System.out.printf("    Cell (%d, %d): Char=%c (code=%d), FG=(%d,%d,%d), BG=(%d,%d,%d)\n",
                        x, y, (char) cell.charCode, cell.charCode,
                        cell.fgRGB[0], cell.fgRGB[1], cell.fgRGB[2],
                        cell.bgRGB[0], cell.bgRGB[1], cell.bgRGB[2]);
            }
        }
    }

    public void write(String filepath) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ByteBuffer bb = ByteBuffer.allocate(calculateSize()).order(ByteOrder.LITTLE_ENDIAN);
        bb.putInt(version);
        bb.putInt(numLayers);
        for (Layer layer : layers) {
            bb.putInt(layer.width);
            bb.putInt(layer.height);
            for (Layer.Cell cell : layer.cells) {
                bb.putInt(cell.charCode);
                bb.put((byte) cell.fgRGB[0]);
                bb.put((byte) cell.fgRGB[1]);
                bb.put((byte) cell.fgRGB[2]);
                bb.put((byte) cell.bgRGB[0]);
                bb.put((byte) cell.bgRGB[1]);
                bb.put((byte) cell.bgRGB[2]);
            }
        }
        try (FileOutputStream fos = new FileOutputStream(filepath);
             GZIPOutputStream gos = new GZIPOutputStream(fos)) {
            gos.write(bb.array());
        }
    }

    private int calculateSize() {
        int size = 8; // version + numLayers
        for (Layer layer : layers) {
            size += 8; // width + height
            size += layer.cells.size() * (4 + 3 + 3); // char + fg + bg per cell
        }
        return size;
    }
}

6. JavaScript Class for .XP File Handling

The following JavaScript class can open a .XP file (using Node.js with 'fs' and 'zlib' modules), decode its contents, print all properties to the console, and write a new or modified .XP file.

const fs = require('fs');
const zlib = require('zlib');

class XPFile {
    constructor(filepath = null) {
        this.version = 0;
        this.numLayers = 0;
        this.layers = []; // Array of {width, height, cells: array of {charCode, fg: [r,g,b], bg: [r,g,b]}}
        if (filepath) {
            this.read(filepath);
        }
    }

    read(filepath) {
        const compressedData = fs.readFileSync(filepath);
        const data = zlib.gunzipSync(compressedData);
        const dv = new DataView(data.buffer);
        let offset = 0;
        this.version = dv.getInt32(offset, true); offset += 4;
        this.numLayers = dv.getInt32(offset, true); offset += 4;
        this.layers = [];
        for (let i = 0; i < this.numLayers; i++) {
            const width = dv.getInt32(offset, true); offset += 4;
            const height = dv.getInt32(offset, true); offset += 4;
            const cells = [];
            for (let j = 0; j < width * height; j++) {
                const charCode = dv.getInt32(offset, true); offset += 4;
                const fg = [dv.getUint8(offset++), dv.getUint8(offset++), dv.getUint8(offset++)];
                const bg = [dv.getUint8(offset++), dv.getUint8(offset++), dv.getUint8(offset++)];
                cells.push({charCode, fg, bg});
            }
            this.layers.push({width, height, cells});
        }
    }

    printProperties() {
        console.log(`Format Version: ${this.version}`);
        console.log(`Number of Layers: ${this.numLayers}`);
        this.layers.forEach((layer, i) => {
            console.log(`\nLayer ${i + 1}:`);
            console.log(`  Width: ${layer.width}`);
            console.log(`  Height: ${layer.height}`);
            layer.cells.forEach((cell, j) => {
                const x = Math.floor(j / layer.height);
                const y = j % layer.height;
                console.log(`    Cell (${x}, ${y}): Char=${String.fromCharCode(cell.charCode)} (code=${cell.charCode}), FG=(${cell.fg}), BG=(${cell.bg})`);
            });
        });
    }

    write(filepath) {
        let size = 8; // version + numLayers
        this.layers.forEach(layer => {
            size += 8; // width + height
            size += layer.cells.length * (4 + 3 + 3);
        });
        const buffer = new ArrayBuffer(size);
        const dv = new DataView(buffer);
        let offset = 0;
        dv.setInt32(offset, this.version, true); offset += 4;
        dv.setInt32(offset, this.numLayers, true); offset += 4;
        this.layers.forEach(layer => {
            dv.setInt32(offset, layer.width, true); offset += 4;
            dv.setInt32(offset, layer.height, true); offset += 4;
            layer.cells.forEach(cell => {
                dv.setInt32(offset, cell.charCode, true); offset += 4;
                dv.setUint8(offset++, cell.fg[0]);
                dv.setUint8(offset++, cell.fg[1]);
                dv.setUint8(offset++, cell.fg[2]);
                dv.setUint8(offset++, cell.bg[0]);
                dv.setUint8(offset++, cell.bg[1]);
                dv.setUint8(offset++, cell.bg[2]);
            });
        });
        const compressed = zlib.gzipSync(Buffer.from(buffer));
        fs.writeFileSync(filepath, compressed);
    }
}

7. C++ Class for .XP File Handling

The following C++ class (using C++ for class support, as pure C lacks classes) can open a .XP file, decode its contents, print all properties to the console, and write a new or modified .XP file. It requires the zlib library for compression handling.

#include <iostream>
#include <vector>
#include <fstream>
#include <zlib.h>
#include <cstring> // For memcpy

struct Cell {
    int32_t charCode;
    uint8_t fg[3];
    uint8_t bg[3];
};

struct Layer {
    int32_t width;
    int32_t height;
    std::vector<Cell> cells;
};

class XPFile {
public:
    int32_t version;
    int32_t numLayers;
    std::vector<Layer> layers;

    XPFile(const std::string& filepath = "") {
        if (!filepath.empty()) {
            read(filepath);
        }
    }

    void read(const std::string& filepath) {
        std::ifstream file(filepath, std::ios::binary | std::ios::ate);
        if (!file) {
            throw std::runtime_error("Failed to open file");
        }
        size_t size = file.tellg();
        file.seekg(0);
        std::vector<uint8_t> compressed(size);
        file.read(reinterpret_cast<char*>(compressed.data()), size);

        uLongf decompressedSize = size * 10; // Estimate
        std::vector<uint8_t> data(decompressedSize);
        while (uncompress(data.data(), &decompressedSize, compressed.data(), size) == Z_BUF_ERROR) {
            decompressedSize *= 2;
            data.resize(decompressedSize);
        }

        const uint8_t* ptr = data.data();
        memcpy(&version, ptr, 4); ptr += 4;
        memcpy(&numLayers, ptr, 4); ptr += 4;
        layers.clear();
        for (int i = 0; i < numLayers; ++i) {
            Layer layer;
            memcpy(&layer.width, ptr, 4); ptr += 4;
            memcpy(&layer.height, ptr, 4); ptr += 4;
            layer.cells.resize(layer.width * layer.height);
            for (auto& cell : layer.cells) {
                memcpy(&cell.charCode, ptr, 4); ptr += 4;
                memcpy(cell.fg, ptr, 3); ptr += 3;
                memcpy(cell.bg, ptr, 3); ptr += 3;
            }
            layers.push_back(layer);
        }
    }

    void printProperties() const {
        std::cout << "Format Version: " << version << std::endl;
        std::cout << "Number of Layers: " << numLayers << std::endl;
        for (size_t i = 0; i < layers.size(); ++i) {
            const auto& layer = layers[i];
            std::cout << "\nLayer " << (i + 1) << ":" << std::endl;
            std::cout << "  Width: " << layer.width << std::endl;
            std::cout << "  Height: " << layer.height << std::endl;
            for (size_t j = 0; j < layer.cells.size(); ++j) {
                const auto& cell = layer.cells[j];
                int x = j / layer.height;
                int y = j % layer.height;
                std::cout << "    Cell (" << x << ", " << y << "): Char=" << static_cast<char>(cell.charCode)
                          << " (code=" << cell.charCode << "), FG=(" << static_cast<int>(cell.fg[0]) << ","
                          << static_cast<int>(cell.fg[1]) << "," << static_cast<int>(cell.fg[2]) << "), BG=("
                          << static_cast<int>(cell.bg[0]) << "," << static_cast<int>(cell.bg[1]) << ","
                          << static_cast<int>(cell.bg[2]) << ")" << std::endl;
            }
        }
    }

    void write(const std::string& filepath) const {
        size_t size = 8; // version + numLayers
        for (const auto& layer : layers) {
            size += 8; // width + height
            size += layer.cells.size() * (4 + 3 + 3);
        }
        std::vector<uint8_t> data(size);
        uint8_t* ptr = data.data();
        memcpy(ptr, &version, 4); ptr += 4;
        memcpy(ptr, &numLayers, 4); ptr += 4;
        for (const auto& layer : layers) {
            memcpy(ptr, &layer.width, 4); ptr += 4;
            memcpy(ptr, &layer.height, 4); ptr += 4;
            for (const auto& cell : layer.cells) {
                memcpy(ptr, &cell.charCode, 4); ptr += 4;
                memcpy(ptr, cell.fg, 3); ptr += 3;
                memcpy(ptr, cell.bg, 3); ptr += 3;
            }
        }

        uLongf compressedSize = compressBound(size);
        std::vector<uint8_t> compressed(compressedSize);
        compress(compressed.data(), &compressedSize, data.data(), size);

        std::ofstream file(filepath, std::ios::binary);
        file.write(reinterpret_cast<const char*>(compressed.data()), compressedSize);
    }
};