Task 548: .PI2 File Format
Task 548: .PI2 File Format
The .PI2 file format is the uncompressed medium-resolution variant of the DEGAS Elite image format used on Atari ST computers, supporting 640x200 pixels with 4 colors. The full file is always 32066 bytes, with big-endian byte order throughout.
- List of all the properties of this file format intrinsic to its file system:
- Resolution: A 2-byte unsigned integer (word) at offset 0, with value 1 indicating medium resolution (.PI2).
- Palette: 32 bytes (16 words) at offset 2, representing 16 colors in Atari ST RGB format (3 bits each for R, G, B, packed as 0rrr0ggg0bbb0000 in binary; only the first 4 colors are typically used for .PI2).
- Bitmap data: 32000 bytes at offset 34, containing the raw screen memory (interleaved bitplanes: 2 bitplanes for 4 colors, laid out as 80 words per scanline for 200 scanlines).
- Left color animation limits: 8 bytes (4 words) at offset 32034, specifying the starting color index (0-15) for each of 4 animation channels.
- Right color animation limits: 8 bytes (4 words) at offset 32042, specifying the ending color index (0-15) for each of 4 animation channels.
- Animation channel directions: 8 bytes (4 words) at offset 32050, with values 0 (left), 1 (off), or 2 (right) for each of 4 channels.
- Animation channel delays: 8 bytes (4 words) at offset 32058, with values 0-128 indicating delay in 1/60th seconds (actual delay = 128 - value) for each of 4 channels.
- Two direct download links for files of format .PI2:
- https://snisurset.net/code/abydos/samples/abydos.pi2
- https://sembiance.com/fileFormatSamples/image/degasMed/MEDIUM.PI2
- Ghost blog embedded HTML JavaScript for drag-and-drop .PI2 file parsing:
Drag and drop a .PI2 file here
- Python class:
import struct
import os
class PI2File:
def __init__(self, filename):
with open(filename, 'rb') as f:
data = f.read()
if len(data) != 32066:
raise ValueError("Invalid .PI2 file size")
self.resolution = struct.unpack('>H', data[0:2])[0]
self.palette = [struct.unpack('>H', data[2 + i*2:4 + i*2])[0] for i in range(16)]
self.bitmap_data = data[34:32034]
offset = 32034
self.left_limits = [struct.unpack('>H', data[offset + i*2:offset + (i+1)*2])[0] for i in range(4)]
offset += 8
self.right_limits = [struct.unpack('>H', data[offset + i*2:offset + (i+1)*2])[0] for i in range(4)]
offset += 8
self.directions = [struct.unpack('>H', data[offset + i*2:offset + (i+1)*2])[0] for i in range(4)]
offset += 8
self.delays = [struct.unpack('>H', data[offset + i*2:offset + (i+1)*2])[0] for i in range(4)]
def print_properties(self):
print(f"Resolution: {self.resolution}")
for i, color in enumerate(self.palette):
print(f"Color {i}: 0x{color:04X}")
print(f"Bitmap data size: {len(self.bitmap_data)} bytes")
print("Left color animation limits:", ' '.join(str(x) for x in self.left_limits))
print("Right color animation limits:", ' '.join(str(x) for x in self.right_limits))
print("Animation channel directions:", ' '.join(str(x) for x in self.directions))
print("Animation channel delays:", ' '.join(str(x) for x in self.delays))
def write(self, filename):
with open(filename, 'wb') as f:
f.write(struct.pack('>H', self.resolution))
for color in self.palette:
f.write(struct.pack('>H', color))
f.write(self.bitmap_data)
for limit in self.left_limits:
f.write(struct.pack('>H', limit))
for limit in self.right_limits:
f.write(struct.pack('>H', limit))
for dir in self.directions:
f.write(struct.pack('>H', dir))
for delay in self.delays:
f.write(struct.pack('>H', delay))
- Java class:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class PI2File {
private int resolution;
private int[] palette = new int[16];
private byte[] bitmapData = new byte[32000];
private int[] leftLimits = new int[4];
private int[] rightLimits = new int[4];
private int[] directions = new int[4];
private int[] delays = new int[4];
public PI2File(String filename) throws IOException {
FileInputStream fis = new FileInputStream(filename);
byte[] data = new byte[32066];
if (fis.read(data) != 32066) {
throw new IOException("Invalid .PI2 file size");
}
fis.close();
resolution = ((data[0] & 0xFF) << 8) | (data[1] & 0xFF);
for (int i = 0; i < 16; i++) {
palette[i] = ((data[2 + i * 2] & 0xFF) << 8) | (data[3 + i * 2] & 0xFF);
}
System.arraycopy(data, 34, bitmapData, 0, 32000);
int offset = 32034;
for (int i = 0; i < 4; i++) {
leftLimits[i] = ((data[offset + i * 2] & 0xFF) << 8) | (data[offset + i * 2 + 1] & 0xFF);
}
offset += 8;
for (int i = 0; i < 4; i++) {
rightLimits[i] = ((data[offset + i * 2] & 0xFF) << 8) | (data[offset + i * 2 + 1] & 0xFF);
}
offset += 8;
for (int i = 0; i < 4; i++) {
directions[i] = ((data[offset + i * 2] & 0xFF) << 8) | (data[offset + i * 2 + 1] & 0xFF);
}
offset += 8;
for (int i = 0; i < 4; i++) {
delays[i] = ((data[offset + i * 2] & 0xFF) << 8) | (data[offset + i * 2 + 1] & 0xFF);
}
}
public void printProperties() {
System.out.println("Resolution: " + resolution);
for (int i = 0; i < 16; i++) {
System.out.println("Color " + i + ": 0x" + Integer.toHexString(palette[i]).toUpperCase());
}
System.out.println("Bitmap data size: " + bitmapData.length + " bytes");
System.out.print("Left color animation limits: ");
for (int limit : leftLimits) {
System.out.print(limit + " ");
}
System.out.println();
System.out.print("Right color animation limits: ");
for (int limit : rightLimits) {
System.out.print(limit + " ");
}
System.out.println();
System.out.print("Animation channel directions: ");
for (int dir : directions) {
System.out.print(dir + " ");
}
System.out.println();
System.out.print("Animation channel delays: ");
for (int delay : delays) {
System.out.print(delay + " ");
}
System.out.println();
}
public void write(String filename) throws IOException {
FileOutputStream fos = new FileOutputStream(filename);
fos.write((resolution >> 8) & 0xFF);
fos.write(resolution & 0xFF);
for (int color : palette) {
fos.write((color >> 8) & 0xFF);
fos.write(color & 0xFF);
}
fos.write(bitmapData);
for (int limit : leftLimits) {
fos.write((limit >> 8) & 0xFF);
fos.write(limit & 0xFF);
}
for (int limit : rightLimits) {
fos.write((limit >> 8) & 0xFF);
fos.write(limit & 0xFF);
}
for (int dir : directions) {
fos.write((dir >> 8) & 0xFF);
fos.write(dir & 0xFF);
}
for (int delay : delays) {
fos.write((delay >> 8) & 0xFF);
fos.write(delay & 0xFF);
}
fos.close();
}
}
- JavaScript class:
class PI2File {
constructor(arrayBuffer) {
if (arrayBuffer.byteLength !== 32066) {
throw new Error('Invalid .PI2 file size');
}
const dv = new DataView(arrayBuffer);
this.resolution = dv.getUint16(0, false); // big-endian
this.palette = [];
for (let i = 0; i < 16; i++) {
this.palette.push(dv.getUint16(2 + i * 2, false));
}
this.bitmapData = new Uint8Array(arrayBuffer.slice(34, 32034));
let offset = 32034;
this.leftLimits = [];
for (let i = 0; i < 4; i++) {
this.leftLimits.push(dv.getUint16(offset + i * 2, false));
}
offset += 8;
this.rightLimits = [];
for (let i = 0; i < 4; i++) {
this.rightLimits.push(dv.getUint16(offset + i * 2, false));
}
offset += 8;
this.directions = [];
for (let i = 0; i < 4; i++) {
this.directions.push(dv.getUint16(offset + i * 2, false));
}
offset += 8;
this.delays = [];
for (let i = 0; i < 4; i++) {
this.delays.push(dv.getUint16(offset + i * 2, false));
}
}
printProperties() {
console.log(`Resolution: ${this.resolution}`);
this.palette.forEach((color, i) => {
console.log(`Color ${i}: 0x${color.toString(16).toUpperCase()}`);
});
console.log(`Bitmap data size: ${this.bitmapData.length} bytes`);
console.log('Left color animation limits:', this.leftLimits.join(' '));
console.log('Right color animation limits:', this.rightLimits.join(' '));
console.log('Animation channel directions:', this.directions.join(' '));
console.log('Animation channel delays:', this.delays.join(' '));
}
toArrayBuffer() {
const buffer = new ArrayBuffer(32066);
const dv = new DataView(buffer);
dv.setUint16(0, this.resolution, false);
for (let i = 0; i < 16; i++) {
dv.setUint16(2 + i * 2, this.palette[i], false);
}
new Uint8Array(buffer).set(this.bitmapData, 34);
let offset = 32034;
this.leftLimits.forEach((limit) => {
dv.setUint16(offset, limit, false);
offset += 2;
});
this.rightLimits.forEach((limit) => {
dv.setUint16(offset, limit, false);
offset += 2;
});
this.directions.forEach((dir) => {
dv.setUint16(offset, dir, false);
offset += 2;
});
this.delays.forEach((delay) => {
dv.setUint16(offset, delay, false);
offset += 2;
});
return buffer;
}
}
// Example usage: const pi2 = new PI2File(someArrayBuffer); pi2.printProperties();
- C class (implemented as C++ for class support, with endian handling assuming host is little-endian):
#include <iostream>
#include <fstream>
#include <vector>
#include <cstdint>
#include <cstring>
#include <byteswap.h> // For bswap_16 on little-endian systems
class PI2File {
public:
uint16_t resolution;
uint16_t palette[16];
uint8_t* bitmap_data;
uint16_t left_limits[4];
uint16_t right_limits[4];
uint16_t directions[4];
uint16_t delays[4];
PI2File(const std::string& filename) {
std::ifstream file(filename, std::ios::binary | std::ios::ate);
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
if (size != 32066) {
throw std::runtime_error("Invalid .PI2 file size");
}
std::vector<uint8_t> data(size);
file.read(reinterpret_cast<char*>(data.data()), size);
memcpy(&resolution, data.data(), 2);
resolution = __bswap_16(resolution); // Convert big-endian to host
for (int i = 0; i < 16; ++i) {
memcpy(&palette[i], data.data() + 2 + i * 2, 2);
palette[i] = __bswap_16(palette[i]);
}
bitmap_data = new uint8_t[32000];
memcpy(bitmap_data, data.data() + 34, 32000);
size_t offset = 32034;
for (int i = 0; i < 4; ++i) {
memcpy(&left_limits[i], data.data() + offset + i * 2, 2);
left_limits[i] = __bswap_16(left_limits[i]);
}
offset += 8;
for (int i = 0; i < 4; ++i) {
memcpy(&right_limits[i], data.data() + offset + i * 2, 2);
right_limits[i] = __bswap_16(right_limits[i]);
}
offset += 8;
for (int i = 0; i < 4; ++i) {
memcpy(&directions[i], data.data() + offset + i * 2, 2);
directions[i] = __bswap_16(directions[i]);
}
offset += 8;
for (int i = 0; i < 4; ++i) {
memcpy(&delays[i], data.data() + offset + i * 2, 2);
delays[i] = __bswap_16(delays[i]);
}
}
~PI2File() {
delete[] bitmap_data;
}
void print_properties() {
std::cout << "Resolution: " << resolution << std::endl;
for (int i = 0; i < 16; ++i) {
std::cout << "Color " << i << ": 0x" << std::hex << palette[i] << std::dec << std::endl;
}
std::cout << "Bitmap data size: 32000 bytes" << std::endl;
std::cout << "Left color animation limits: ";
for (int i = 0; i < 4; ++i) {
std::cout << left_limits[i] << " ";
}
std::cout << std::endl;
std::cout << "Right color animation limits: ";
for (int i = 0; i < 4; ++i) {
std::cout << right_limits[i] << " ";
}
std::cout << std::endl;
std::cout << "Animation channel directions: ";
for (int i = 0; i < 4; ++i) {
std::cout << directions[i] << " ";
}
std::cout << std::endl;
std::cout << "Animation channel delays: ";
for (int i = 0; i < 4; ++i) {
std::cout << delays[i] << " ";
}
std::cout << std::endl;
}
void write(const std::string& filename) {
std::ofstream file(filename, std::ios::binary);
uint16_t be_res = __bswap_16(resolution);
file.write(reinterpret_cast<const char*>(&be_res), 2);
for (int i = 0; i < 16; ++i) {
uint16_t be_color = __bswap_16(palette[i]);
file.write(reinterpret_cast<const char*>(&be_color), 2);
}
file.write(reinterpret_cast<const char*>(bitmap_data), 32000);
for (int i = 0; i < 4; ++i) {
uint16_t be_limit = __bswap_16(left_limits[i]);
file.write(reinterpret_cast<const char*>(&be_limit), 2);
}
for (int i = 0; i < 4; ++i) {
uint16_t be_limit = __bswap_16(right_limits[i]);
file.write(reinterpret_cast<const char*>(&be_limit), 2);
}
for (int i = 0; i < 4; ++i) {
uint16_t be_dir = __bswap_16(directions[i]);
file.write(reinterpret_cast<const char*>(&be_dir), 2);
}
for (int i = 0; i < 4; ++i) {
uint16_t be_delay = __bswap_16(delays[i]);
file.write(reinterpret_cast<const char*>(&be_delay), 2);
}
}
};