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.
2. Two Direct Download Links for .XP Files
- https://raw.githubusercontent.com/amethyst/bracket-lib/master/bracket-terminal/resources/nyan.xp
- https://raw.githubusercontent.com/RisingThumb/rex-is-godot/master/decompressFile.xp
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.
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);
}
};