Task 523: .PCX File Format

Task 523: .PCX File Format

1. Properties of the .PCX File Format

The .PCX file format, originally developed by ZSoft Corporation for PC Paintbrush, is a raster image format that supports various color depths and uses run-length encoding (RLE) for compression. The format consists of a fixed 128-byte header, followed by compressed or uncompressed image data, and optionally an extended 256-color palette appended at the end. The intrinsic properties refer to the structural elements defined in the header, which describe the file's metadata, dimensions, color information, and encoding. These are derived from the official specifications and include the following fields, with their offsets, data types, and descriptions:

  • Manufacturer (Offset 0x00, UINT8): Identifies the file as a PCX format. Always set to 0x0A (10 in decimal).
  • Version (Offset 0x01, UINT8): Indicates the PC Paintbrush version and file format variant. Possible values: 0 (v2.5), 2 (v2.8 with palette), 3 (v2.8 without palette), 4 (Paintbrush for Windows), 5 (v3.0 or higher).
  • Encoding (Offset 0x02, UINT8): Specifies the compression method. 0 for uncompressed (unofficial but supported by some software), 1 for PCX RLE compression.
  • BitsPerPlane (Offset 0x03, UINT8): Number of bits per pixel per color plane. Common values: 1, 2, 4, 8, or 24.
  • WindowXmin (Offset 0x04, UINT16LE): Minimum X-coordinate of the image window (typically 0).
  • WindowYmin (Offset 0x06, UINT16LE): Minimum Y-coordinate of the image window (typically 0).
  • WindowXmax (Offset 0x08, UINT16LE): Maximum X-coordinate of the image window. Image width is calculated as (Xmax - Xmin + 1).
  • WindowYmax (Offset 0x0A, UINT16LE): Maximum Y-coordinate of the image window. Image height is calculated as (Ymax - Ymin + 1).
  • HorzDPI (Offset 0x0C, UINT16LE): Horizontal resolution in dots per inch (DPI). Often unreliable and may contain image dimensions or zero.
  • VertDPI (Offset 0x0E, UINT16LE): Vertical resolution in dots per inch (DPI). Similarly unreliable.
  • Palette (Offset 0x10, UINT8[48]): 16-color palette stored as 16 RGB triplets (3 bytes each), padded to 48 bytes. Used for images with 16 colors or fewer; ignored for 256-color images.
  • Reserved (Offset 0x40, UINT8): Reserved byte, typically set to 0 but may contain arbitrary data.
  • ColorPlanes (Offset 0x41, UINT8): Number of color planes (e.g., 1 for grayscale/indexed, 3 for RGB, 4 for RGBA in some variants).
  • BytesPerPlaneLine (Offset 0x42, UINT16LE): Number of bytes per scanline per color plane. Must be even; used for decoding and should not be derived from dimensions.
  • PaletteInfo (Offset 0x44, UINT16LE): Palette interpretation mode. 1 for color/black-and-white, 2 for grayscale (ignored in later versions).
  • HorScrSize (Offset 0x46, UINT16LE): Horizontal screen size for scrolling (supported in PC Paintbrush IV+; often ignored).
  • VerScrSize (Offset 0x48, UINT16LE): Vertical screen size for scrolling (supported in PC Paintbrush IV+; often ignored).
  • Padding (Offset 0x4A, UINT8[54]): Filler bytes to reach 128 bytes total. May contain arbitrary data.

Derived properties (not stored directly but calculated from the above):

  • Image Width: (WindowXmax - WindowXmin + 1)
  • Image Height: (WindowYmax - WindowYmin + 1)
  • Color Depth: (ColorPlanes * BitsPerPlane)

Additional format notes:

  • Image data starts at offset 0x80 and is RLE-compressed if Encoding is 1. Each scanline is compressed independently.
  • For 256-color images (Version >= 3, ColorPlanes = 1, BitsPerPlane = 8), a 769-byte extended palette may be appended: starts with 0x0C, followed by 768 bytes of RGB data (256 triplets).

The following are direct download links to sample .PCX files from publicly available sources:

3. HTML/JavaScript for Drag-and-Drop .PCX Property Viewer

The following is a self-contained HTML page with embedded JavaScript that can be embedded in a Ghost blog or similar platform. It allows users to drag and drop a .PCX file, parses the header, and displays all properties listed above on the screen. It uses the FileReader API for browser-based file handling.

PCX Property Viewer
Drag and drop a .PCX file here

4. Python Class for .PCX Handling

The following Python class can open a .PCX file, decode and read the header properties, print them to the console, and write the properties back to a new file (copying the original image data for completeness).

import struct

class PCXParser:
    def __init__(self, filename):
        self.filename = filename
        self.header = None
        self.image_data = None
        self.read()

    def read(self):
        with open(self.filename, 'rb') as f:
            header_data = f.read(128)
            if len(header_data) < 128:
                raise ValueError("Invalid PCX file: header too short")
            self.header = struct.unpack('<B B B B H H H H H H 48B B B H H H H 54B', header_data)
            self.manufacturer, self.version, self.encoding, self.bits_per_plane, \
            self.x_min, self.y_min, self.x_max, self.y_max, self.horz_dpi, self.vert_dpi, \
            *self.palette, self.reserved, self.color_planes, self.bytes_per_plane_line, \
            self.palette_info, self.hor_scr_size, self.ver_scr_size, *self.padding = self.header
            
            # Read remaining image data
            self.image_data = f.read()

    def print_properties(self):
        if not self.header:
            raise ValueError("No header data available")
        print(f"Manufacturer: {self.manufacturer}")
        print(f"Version: {self.version}")
        print(f"Encoding: {self.encoding}")
        print(f"BitsPerPlane: {self.bits_per_plane}")
        print(f"WindowXmin: {self.x_min}")
        print(f"WindowYmin: {self.y_min}")
        print(f"WindowXmax: {self.x_max}")
        print(f"WindowYmax: {self.y_max}")
        print(f"HorzDPI: {self.horz_dpi}")
        print(f"VertDPI: {self.vert_dpi}")
        print(f"Palette: {self.palette}")
        print(f"Reserved: {self.reserved}")
        print(f"ColorPlanes: {self.color_planes}")
        print(f"BytesPerPlaneLine: {self.bytes_per_plane_line}")
        print(f"PaletteInfo: {self.palette_info}")
        print(f"HorScrSize: {self.hor_scr_size}")
        print(f"VerScrSize: {self.ver_scr_size}")
        # Derived
        width = self.x_max - self.x_min + 1
        height = self.y_max - self.y_min + 1
        color_depth = self.color_planes * self.bits_per_plane
        print(f"Width (derived): {width}")
        print(f"Height (derived): {height}")
        print(f"ColorDepth (derived): {color_depth}")

    def write(self, output_filename):
        if not self.header:
            raise ValueError("No header data available")
        header_data = struct.pack('<B B B B H H H H H H 48B B B H H H H 54B', *self.header)
        with open(output_filename, 'wb') as f:
            f.write(header_data)
            if self.image_data:
                f.write(self.image_data)

# Example usage:
# parser = PCXParser('input.pcx')
# parser.print_properties()
# parser.write('output.pcx')

5. Java Class for .PCX Handling

The following Java class can open a .PCX file, decode and read the header properties, print them to the console, and write the properties back to a new file (copying the original image data).

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.nio.file.*;

public class PCXParser {
    private String filename;
    private byte[] header = new byte[128];
    private byte[] imageData;
    private int manufacturer, version, encoding, bitsPerPlane;
    private int xMin, yMin, xMax, yMax, horzDpi, vertDpi;
    private byte[] palette = new byte[48];
    private int reserved, colorPlanes, bytesPerPlaneLine;
    private int paletteInfo, horScrSize, verScrSize;
    private byte[] padding = new byte[54];

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

    private void read() throws IOException {
        try (FileInputStream fis = new FileInputStream(filename);
             FileChannel channel = fis.getChannel()) {
            ByteBuffer buffer = ByteBuffer.allocate(128).order(ByteOrder.LITTLE_ENDIAN);
            channel.read(buffer);
            buffer.flip();
            manufacturer = buffer.get() & 0xFF;
            version = buffer.get() & 0xFF;
            encoding = buffer.get() & 0xFF;
            bitsPerPlane = buffer.get() & 0xFF;
            xMin = buffer.getShort() & 0xFFFF;
            yMin = buffer.getShort() & 0xFFFF;
            xMax = buffer.getShort() & 0xFFFF;
            yMax = buffer.getShort() & 0xFFFF;
            horzDpi = buffer.getShort() & 0xFFFF;
            vertDpi = buffer.getShort() & 0xFFFF;
            buffer.get(palette);
            reserved = buffer.get() & 0xFF;
            colorPlanes = buffer.get() & 0xFF;
            bytesPerPlaneLine = buffer.getShort() & 0xFFFF;
            paletteInfo = buffer.getShort() & 0xFFFF;
            horScrSize = buffer.getShort() & 0xFFFF;
            verScrSize = buffer.getShort() & 0xFFFF;
            buffer.get(padding);

            // Read image data
            long remaining = channel.size() - 128;
            if (remaining > 0) {
                ByteBuffer dataBuffer = ByteBuffer.allocate((int) remaining);
                channel.read(dataBuffer);
                imageData = dataBuffer.array();
            }
        }
    }

    public void printProperties() {
        System.out.println("Manufacturer: " + manufacturer);
        System.out.println("Version: " + version);
        System.out.println("Encoding: " + encoding);
        System.out.println("BitsPerPlane: " + bitsPerPlane);
        System.out.println("WindowXmin: " + xMin);
        System.out.println("WindowYmin: " + yMin);
        System.out.println("WindowXmax: " + xMax);
        System.out.println("WindowYmax: " + yMax);
        System.out.println("HorzDPI: " + horzDpi);
        System.out.println("VertDPI: " + vertDpi);
        System.out.print("Palette: ");
        for (byte b : palette) System.out.print((b & 0xFF) + " ");
        System.out.println();
        System.out.println("Reserved: " + reserved);
        System.out.println("ColorPlanes: " + colorPlanes);
        System.out.println("BytesPerPlaneLine: " + bytesPerPlaneLine);
        System.out.println("PaletteInfo: " + paletteInfo);
        System.out.println("HorScrSize: " + horScrSize);
        System.out.println("VerScrSize: " + verScrSize);
        // Derived
        int width = xMax - xMin + 1;
        int height = yMax - yMin + 1;
        int colorDepth = colorPlanes * bitsPerPlane;
        System.out.println("Width (derived): " + width);
        System.out.println("Height (derived): " + height);
        System.out.println("ColorDepth (derived): " + colorDepth);
    }

    public void write(String outputFilename) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(128).order(ByteOrder.LITTLE_ENDIAN);
        buffer.put((byte) manufacturer);
        buffer.put((byte) version);
        buffer.put((byte) encoding);
        buffer.put((byte) bitsPerPlane);
        buffer.putShort((short) xMin);
        buffer.putShort((short) yMin);
        buffer.putShort((short) xMax);
        buffer.putShort((short) yMax);
        buffer.putShort((short) horzDpi);
        buffer.putShort((short) vertDpi);
        buffer.put(palette);
        buffer.put((byte) reserved);
        buffer.put((byte) colorPlanes);
        buffer.putShort((short) bytesPerPlaneLine);
        buffer.putShort((short) paletteInfo);
        buffer.putShort((short) horScrSize);
        buffer.putShort((short) verScrSize);
        buffer.put(padding);
        buffer.flip();

        try (FileOutputStream fos = new FileOutputStream(outputFilename)) {
            fos.write(buffer.array());
            if (imageData != null) {
                fos.write(imageData);
            }
        }
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     PCXParser parser = new PCXParser("input.pcx");
    //     parser.printProperties();
    //     parser.write("output.pcx");
    // }
}

6. JavaScript Class for .PCX Handling

The following JavaScript class (for Node.js) can open a .PCX file, decode and read the header properties, print them to the console, and write the properties back to a new file (using Node's fs module).

const fs = require('fs');

class PCXParser {
    constructor(filename) {
        this.filename = filename;
        this.header = null;
        this.imageData = null;
        this.read();
    }

    read() {
        const data = fs.readFileSync(this.filename);
        if (data.length < 128) {
            throw new Error('Invalid PCX file: header too short');
        }
        const view = new DataView(data.buffer);
        this.manufacturer = view.getUint8(0);
        this.version = view.getUint8(1);
        this.encoding = view.getUint8(2);
        this.bitsPerPlane = view.getUint8(3);
        this.xMin = view.getUint16(4, true);
        this.yMin = view.getUint16(6, true);
        this.xMax = view.getUint16(8, true);
        this.yMax = view.getUint16(10, true);
        this.horzDpi = view.getUint16(12, true);
        this.vertDpi = view.getUint16(14, true);
        this.palette = [];
        for (let i = 16; i < 64; i++) {
            this.palette.push(view.getUint8(i));
        }
        this.reserved = view.getUint8(64);
        this.colorPlanes = view.getUint8(65);
        this.bytesPerPlaneLine = view.getUint16(66, true);
        this.paletteInfo = view.getUint16(68, true);
        this.horScrSize = view.getUint16(70, true);
        this.verScrSize = view.getUint16(72, true);
        this.padding = [];
        for (let i = 74; i < 128; i++) {
            this.padding.push(view.getUint8(i));
        }
        this.imageData = data.slice(128);
    }

    printProperties() {
        if (!this.manufacturer) {
            throw new Error('No header data available');
        }
        console.log(`Manufacturer: ${this.manufacturer}`);
        console.log(`Version: ${this.version}`);
        console.log(`Encoding: ${this.encoding}`);
        console.log(`BitsPerPlane: ${this.bitsPerPlane}`);
        console.log(`WindowXmin: ${this.xMin}`);
        console.log(`WindowYmin: ${this.yMin}`);
        console.log(`WindowXmax: ${this.xMax}`);
        console.log(`WindowYmax: ${this.yMax}`);
        console.log(`HorzDPI: ${this.horzDpi}`);
        console.log(`VertDPI: ${this.vertDpi}`);
        console.log(`Palette: ${this.palette.join(' ')}`);
        console.log(`Reserved: ${this.reserved}`);
        console.log(`ColorPlanes: ${this.colorPlanes}`);
        console.log(`BytesPerPlaneLine: ${this.bytesPerPlaneLine}`);
        console.log(`PaletteInfo: ${this.paletteInfo}`);
        console.log(`HorScrSize: ${this.horScrSize}`);
        console.log(`VerScrSize: ${this.verScrSize}`);
        // Derived
        const width = this.xMax - this.xMin + 1;
        const height = this.yMax - this.yMin + 1;
        const colorDepth = this.colorPlanes * this.bitsPerPlane;
        console.log(`Width (derived): ${width}`);
        console.log(`Height (derived): ${height}`);
        console.log(`ColorDepth (derived): ${colorDepth}`);
    }

    write(outputFilename) {
        if (!this.manufacturer) {
            throw new Error('No header data available');
        }
        const buffer = Buffer.alloc(128);
        const view = new DataView(buffer.buffer);
        view.setUint8(0, this.manufacturer);
        view.setUint8(1, this.version);
        view.setUint8(2, this.encoding);
        view.setUint8(3, this.bitsPerPlane);
        view.setUint16(4, this.xMin, true);
        view.setUint16(6, this.yMin, true);
        view.setUint16(8, this.xMax, true);
        view.setUint16(10, this.yMax, true);
        view.setUint16(12, this.horzDpi, true);
        view.setUint16(14, this.vertDpi, true);
        for (let i = 0; i < 48; i++) {
            view.setUint8(16 + i, this.palette[i]);
        }
        view.setUint8(64, this.reserved);
        view.setUint8(65, this.colorPlanes);
        view.setUint16(66, this.bytesPerPlaneLine, true);
        view.setUint16(68, this.paletteInfo, true);
        view.setUint16(70, this.horScrSize, true);
        view.setUint16(72, this.verScrSize, true);
        for (let i = 0; i < 54; i++) {
            view.setUint8(74 + i, this.padding[i]);
        }
        const fullData = Buffer.concat([buffer, this.imageData]);
        fs.writeFileSync(outputFilename, fullData);
    }
}

// Example usage:
// const parser = new PCXParser('input.pcx');
// parser.printProperties();
// parser.write('output.pcx');

7. C Implementation for .PCX Handling

Since C does not support classes natively, the following provides a struct for the header and functions to open a .PCX file, decode and read the properties, print them to the console, and write to a new file (copying the original image data).

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

typedef struct {
    uint8_t manufacturer;
    uint8_t version;
    uint8_t encoding;
    uint8_t bits_per_plane;
    uint16_t x_min;
    uint16_t y_min;
    uint16_t x_max;
    uint16_t y_max;
    uint16_t horz_dpi;
    uint16_t vert_dpi;
    uint8_t palette[48];
    uint8_t reserved;
    uint8_t color_planes;
    uint16_t bytes_per_plane_line;
    uint16_t palette_info;
    uint16_t hor_scr_size;
    uint16_t ver_scr_size;
    uint8_t padding[54];
} PCXHeader;

void read_pcx(const char* filename, PCXHeader* header, uint8_t** image_data, size_t* image_size) {
    FILE* file = fopen(filename, "rb");
    if (!file) {
        perror("Failed to open file");
        exit(1);
    }
    if (fread(header, sizeof(PCXHeader), 1, file) != 1) {
        fprintf(stderr, "Invalid PCX file: header too short\n");
        fclose(file);
        exit(1);
    }
    // Adjust for little-endian if needed (assuming host is little-endian)
    
    fseek(file, 0, SEEK_END);
    long total_size = ftell(file);
    *image_size = total_size - 128;
    rewind(file);
    fseek(file, 128, SEEK_SET);
    *image_data = malloc(*image_size);
    if (*image_data && fread(*image_data, 1, *image_size, file) != *image_size) {
        free(*image_data);
        *image_data = NULL;
    }
    fclose(file);
}

void print_properties(const PCXHeader* header) {
    printf("Manufacturer: %u\n", header->manufacturer);
    printf("Version: %u\n", header->version);
    printf("Encoding: %u\n", header->encoding);
    printf("BitsPerPlane: %u\n", header->bits_per_plane);
    printf("WindowXmin: %u\n", header->x_min);
    printf("WindowYmin: %u\n", header->y_min);
    printf("WindowXmax: %u\n", header->x_max);
    printf("WindowYmax: %u\n", header->y_max);
    printf("HorzDPI: %u\n", header->horz_dpi);
    printf("VertDPI: %u\n", header->vert_dpi);
    printf("Palette: ");
    for (int i = 0; i < 48; i++) printf("%u ", header->palette[i]);
    printf("\n");
    printf("Reserved: %u\n", header->reserved);
    printf("ColorPlanes: %u\n", header->color_planes);
    printf("BytesPerPlaneLine: %u\n", header->bytes_per_plane_line);
    printf("PaletteInfo: %u\n", header->palette_info);
    printf("HorScrSize: %u\n", header->hor_scr_size);
    printf("VerScrSize: %u\n", header->ver_scr_size);
    // Derived
    uint16_t width = header->x_max - header->x_min + 1;
    uint16_t height = header->y_max - header->y_min + 1;
    uint16_t color_depth = header->color_planes * header->bits_per_plane;
    printf("Width (derived): %u\n", width);
    printf("Height (derived): %u\n", height);
    printf("ColorDepth (derived): %u\n", color_depth);
}

void write_pcx(const char* output_filename, const PCXHeader* header, const uint8_t* image_data, size_t image_size) {
    FILE* file = fopen(output_filename, "wb");
    if (!file) {
        perror("Failed to open output file");
        exit(1);
    }
    fwrite(header, sizeof(PCXHeader), 1, file);
    if (image_data) {
        fwrite(image_data, 1, image_size, file);
    }
    fclose(file);
}

// Example usage:
// int main() {
//     PCXHeader header;
//     uint8_t* image_data = NULL;
//     size_t image_size = 0;
//     read_pcx("input.pcx", &header, &image_data, &image_size);
//     print_properties(&header);
//     write_pcx("output.pcx", &header, image_data, image_size);
//     free(image_data);
//     return 0;
// }