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.

  1. 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.
  1. Two direct download links for files of format .PI2:
  1. Ghost blog embedded HTML JavaScript for drag-and-drop .PI2 file parsing:
Drag and drop a .PI2 file here
  1. 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))
  1. 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();
    }
}
  1. 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();
  1. 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);
        }
    }
};