Task 271: .GRP File Format

Task 271: .GRP File Format

1. List of all the properties of this file format intrinsic to its file system

The .GRP file format in this context refers to the Blizzard GRP format used for multi-frame graphics in StarCraft. It functions as a simple container "file system" for multiple image frames, with a header providing global dimensions, a table of frame entries (like directory entries), and per-frame data blocks containing line offsets (like file offsets within each frame "directory"). The format uses little-endian byte order and no signature. All values are unsigned. The intrinsic properties are:

  • Frame Count: Number of frames in the GRP (UINT16, 2 bytes). This determines the number of frame entries in the table.
  • X-Dimension (Width): Global width of the frames in pixels (UINT16, 2 bytes). Used to interpret the overall canvas size.
  • Y-Dimension (Height): Global height of the frames in pixels (UINT16, 2 bytes). Determines the number of line offsets per frame.
  • Frame Table: Array of frame entries (size: Frame Count × 8 bytes), where each entry includes:
  • X-Offset: Horizontal position offset for the frame (UINT8, 1 byte).
  • Y-Offset: Vertical position offset for the frame (UINT8, 1 byte).
  • Unknown Field: Unused or reserved 2-byte field (UINT16, 2 bytes).
  • Frame Data Offset: Absolute file offset to the start of the frame's data block (UINT32, 4 bytes).
  • Per-Frame Line Offsets: For each frame, an array of offsets to line data (size: Y-Dimension × 2 bytes). Each is a UINT16 (2 bytes) representing the relative offset from the frame data start to the compressed data for that line (row) of pixels.
  • Per-Frame Line Data: RLE-compressed pixel data for each line (variable length bytes, 8-bit indices referencing an external palette). This is the payload, stored contiguously after line offsets in each frame block.
  • Data Layout: Contiguous storage after the header and frame table; no compression for the structure itself, but RLE for pixel data; no subdirectories, encryption, or metadata beyond offsets.

These properties form a basic hierarchical structure: global header → frame directory → per-frame line offsets → line payloads.

3. Ghost blog embedded HTML JavaScript

Paste the following as an HTML card in Ghost blog (it includes a drop zone, parses the dropped .GRP file using FileReader and DataView, and dumps properties to a

element below):

Drag and drop a .GRP file here to parse its properties.

4. Python class

import struct

class GRPFile:
    def __init__(self, filename=None):
        self.filename = filename
        self.frame_count = 0
        self.width = 0
        self.height = 0
        self.frames = []  # list of dicts: {'x_offset': int, 'y_offset': int, 'unknown': int, 'data_offset': int, 'line_offsets': list[int]}

    def read(self):
        with open(self.filename, 'rb') as f:
            data = f.read()
        offset = 0
        self.frame_count, = struct.unpack_from('<H', data, offset); offset += 2
        self.width, = struct.unpack_from('<H', data, offset); offset += 2
        self.height, = struct.unpack_from('<H', data, offset); offset += 2
        self.frames = []
        for i in range(self.frame_count):
            x_off, = struct.unpack_from('B', data, offset); offset += 1
            y_off, = struct.unpack_from('B', data, offset); offset += 1
            unk, = struct.unpack_from('<H', data, offset); offset += 2
            d_off, = struct.unpack_from('<I', data, offset); offset += 4
            # Read line offsets
            line_offs_start = d_off
            line_offsets = []
            for j in range(self.height):
                lo, = struct.unpack_from('<H', data, line_offs_start)
                line_offsets.append(lo)
                line_offs_start += 2
            self.frames.append({'x_offset': x_off, 'y_offset': y_off, 'unknown': unk, 'data_offset': d_off, 'line_offsets': line_offsets})
        self.print_properties()

    def print_properties(self):
        print(f"Frame Count: {self.frame_count}")
        print(f"Width: {self.width}")
        print(f"Height: {self.height}")
        print("\nFrame Table:")
        for i, frame in enumerate(self.frames):
            print(f"Frame {i}: X-Offset={frame['x_offset']}, Y-Offset={frame['y_offset']}, Unknown={frame['unknown']}, Data Offset={frame['data_offset']}")
            print(f"  Line Offsets:")
            for j, lo in enumerate(frame['line_offsets']):
                print(f"    Line {j}: {lo}")

    def write(self, output_filename):
        with open(output_filename, 'wb') as f:
            offset = 0
            f.write(struct.pack('<H', self.frame_count)); offset += 2
            f.write(struct.pack('<H', self.width)); offset += 2
            f.write(struct.pack('<H', self.height)); offset += 2
            frame_table_end = offset + (8 * self.frame_count)
            current_data_pos = frame_table_end
            for frame in self.frames:
                f.write(struct.pack('B', frame['x_offset'])); offset += 1
                f.write(struct.pack('B', frame['y_offset'])); offset += 1
                f.write(struct.pack('<H', frame['unknown'])); offset += 2
                f.write(struct.pack('<I', current_data_pos)); offset += 4
                # Write line offsets at current_data_pos
                line_pos = current_data_pos
                for lo in frame['line_offsets']:
                    f.seek(line_pos)
                    f.write(struct.pack('<H', lo))
                    line_pos += 2
                # Line data would be written after, but skipped for example; adjust current_data_pos accordingly
                current_data_pos += (2 * self.height)  # + line data length
        print(f"Written to {output_filename}")

# Usage
grp = GRPFile('example.grp')
grp.read()
# To write: grp.write('output.grp')  # Requires setting properties first

5. Java class

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class GRPFile {
    private String filename;
    private int frameCount;
    private int width;
    private int height;
    private List<Frame> frames = new ArrayList<>();

    public static class Frame {
        public int xOffset;
        public int yOffset;
        public int unknown;
        public int dataOffset;
        public List<Integer> lineOffsets = new ArrayList<>();
    }

    public GRPFile(String filename) {
        this.filename = filename;
    }

    public void read() throws IOException {
        byte[] data = Files.readAllBytes(Paths.get(filename));
        ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
        int offset = 0;
        frameCount = buffer.getShort(offset); offset += 2;
        width = buffer.getShort(offset); offset += 2;
        height = buffer.getShort(offset); offset += 2;
        frames.clear();
        for (int i = 0; i < frameCount; i++) {
            Frame frame = new Frame();
            frame.xOffset = buffer.get(offset) & 0xFF; offset += 1;
            frame.yOffset = buffer.get(offset) & 0xFF; offset += 1;
            frame.unknown = buffer.getShort(offset); offset += 2;
            frame.dataOffset = buffer.getInt(offset); offset += 4;
            // Read line offsets
            int lineOffset = frame.dataOffset;
            for (int j = 0; j < height; j++) {
                frame.lineOffsets.add(buffer.getShort(lineOffset));
                lineOffset += 2;
            }
            frames.add(frame);
        }
        printProperties();
    }

    public void printProperties() {
        System.out.println("Frame Count: " + frameCount);
        System.out.println("Width: " + width);
        System.out.println("Height: " + height);
        System.out.println("\nFrame Table:");
        for (int i = 0; i < frames.size(); i++) {
            Frame frame = frames.get(i);
            System.out.println("Frame " + i + ": X-Offset=" + frame.xOffset + ", Y-Offset=" + frame.yOffset +
                               ", Unknown=" + frame.unknown + ", Data Offset=" + frame.dataOffset);
            System.out.print("  Line Offsets:");
            for (int j = 0; j < frame.lineOffsets.size(); j++) {
                System.out.print(" " + frame.lineOffsets.get(j));
            }
            System.out.println();
        }
    }

    public void write(String outputFilename) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(outputFilename);
             BufferedOutputStream bos = new BufferedOutputStream(fos)) {
            int offset = 0;
            bos.write(ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort((short) frameCount).array()); offset += 2;
            bos.write(ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort((short) width).array()); offset += 2;
            bos.write(ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort((short) height).array()); offset += 2;
            int frameTableEnd = offset + (8 * frameCount);
            int currentDataPos = frameTableEnd;
            for (Frame frame : frames) {
                bos.write(ByteBuffer.allocate(1).put((byte) frame.xOffset).array()); offset += 1;
                bos.write(ByteBuffer.allocate(1).put((byte) frame.yOffset).array()); offset += 1;
                bos.write(ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort((short) frame.unknown).array()); offset += 2;
                bos.write(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(currentDataPos).array()); offset += 4;
                // Write line offsets
                int linePos = currentDataPos;
                for (int lo : frame.lineOffsets) {
                    bos.write(ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort((short) lo).array());
                    linePos += 2;
                }
                currentDataPos += (2 * height);  // + line data
            }
            // Line data would be written here if available
        }
        System.out.println("Written to " + outputFilename);
    }

    // Usage
    public static void main(String[] args) throws IOException {
        GRPFile grp = new GRPFile("example.grp");
        grp.read();
        // grp.write("output.grp");
    }
}

6. JavaScript class

(Assumes Node.js; uses fs module for file I/O.)

const fs = require('fs');

class GRPFile {
  constructor(filename = null) {
    this.filename = filename;
    this.frameCount = 0;
    this.width = 0;
    this.height = 0;
    this.frames = [];  // array of {xOffset, yOffset, unknown, dataOffset, lineOffsets: []}
  }

  read() {
    const buffer = fs.readFileSync(this.filename);
    let offset = 0;
    this.frameCount = buffer.readUInt16LE(offset); offset += 2;
    this.width = buffer.readUInt16LE(offset); offset += 2;
    this.height = buffer.readUInt16LE(offset); offset += 2;
    this.frames = [];
    for (let i = 0; i < this.frameCount; i++) {
      const xOff = buffer.readUInt8(offset); offset += 1;
      const yOff = buffer.readUInt8(offset); offset += 1;
      const unk = buffer.readUInt16LE(offset); offset += 2;
      const dOff = buffer.readUInt32LE(offset); offset += 4;
      const lineOffsets = [];
      let lineOffset = dOff;
      for (let j = 0; j < this.height; j++) {
        lineOffsets.push(buffer.readUInt16LE(lineOffset));
        lineOffset += 2;
      }
      this.frames.push({xOffset: xOff, yOffset: yOff, unknown: unk, dataOffset: dOff, lineOffsets});
    }
    this.printProperties();
  }

  printProperties() {
    console.log(`Frame Count: ${this.frameCount}`);
    console.log(`Width: ${this.width}`);
    console.log(`Height: ${this.height}`);
    console.log('\nFrame Table:');
    this.frames.forEach((frame, i) => {
      console.log(`Frame ${i}: X-Offset=${frame.xOffset}, Y-Offset=${frame.yOffset}, Unknown=${frame.unknown}, Data Offset=${frame.dataOffset}`);
      console.log('  Line Offsets:', frame.lineOffsets.join(' '));
    });
  }

  write(outputFilename) {
    const bufferSize = 6 + (8 * this.frameCount) + (this.height * 2 * this.frameCount);  // Approx, without line data
    const buffer = Buffer.alloc(bufferSize);
    let offset = 0;
    buffer.writeUInt16LE(this.frameCount, offset); offset += 2;
    buffer.writeUInt16LE(this.width, offset); offset += 2;
    buffer.writeUInt16LE(this.height, offset); offset += 2;
    let currentDataPos = 6 + (8 * this.frameCount);
    this.frames.forEach(frame => {
      buffer.writeUInt8(frame.xOffset, offset); offset += 1;
      buffer.writeUInt8(frame.yOffset, offset); offset += 1;
      buffer.writeUInt16LE(frame.unknown, offset); offset += 2;
      buffer.writeUInt32LE(currentDataPos, offset); offset += 4;
      // Write line offsets
      frame.lineOffsets.forEach(lo => {
        buffer.writeUInt16LE(lo, currentDataPos);
        currentDataPos += 2;
      });
      currentDataPos += 0;  // Line data length
    });
    fs.writeFileSync(outputFilename, buffer);
    console.log(`Written to ${outputFilename}`);
  }
}

// Usage
const grp = new GRPFile('example.grp');
grp.read();
// grp.write('output.grp');

7. C class

(Uses structs and functions; compile with gcc, e.g., gcc grp.c -o grp.)

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

typedef struct {
    uint8_t x_offset;
    uint8_t y_offset;
    uint16_t unknown;
    uint32_t data_offset;
    uint16_t* line_offsets;
} Frame;

typedef struct {
    char* filename;
    uint16_t frame_count;
    uint16_t width;
    uint16_t height;
    Frame* frames;
} GRPFile;

GRPFile* grp_create(const char* filename) {
    GRPFile* grp = malloc(sizeof(GRPFile));
    grp->filename = strdup(filename);
    grp->frames = NULL;
    return grp;
}

void grp_destroy(GRPFile* grp) {
    if (grp->frames) {
        for (int i = 0; i < grp->frame_count; i++) {
            free(grp->frames[i].line_offsets);
        }
        free(grp->frames);
    }
    free(grp->filename);
    free(grp);
}

void grp_read(GRPFile* grp) {
    FILE* f = fopen(grp->filename, "rb");
    if (!f) { perror("Open failed"); return; }
    fseek(f, 0, SEEK_END);
    long size = ftell(f);
    fseek(f, 0, SEEK_SET);
    uint8_t* data = malloc(size);
    fread(data, 1, size, f);
    fclose(f);

    int offset = 0;
    grp->frame_count = *(uint16_t*)(data + offset); offset += 2;
    grp->width = *(uint16_t*)(data + offset); offset += 2;
    grp->height = *(uint16_t*)(data + offset); offset += 2;
    grp->frames = malloc(grp->frame_count * sizeof(Frame));
    for (int i = 0; i < grp->frame_count; i++) {
        Frame* frame = &grp->frames[i];
        frame->x_offset = data[offset++]; 
        frame->y_offset = data[offset++];
        frame->unknown = *(uint16_t*)(data + offset); offset += 2;
        frame->data_offset = *(uint32_t*)(data + offset); offset += 4;
        frame->line_offsets = malloc(grp->height * sizeof(uint16_t));
        int line_off = frame->data_offset;
        for (int j = 0; j < grp->height; j++) {
            frame->line_offsets[j] = *(uint16_t*)(data + line_off);
            line_off += 2;
        }
    }
    free(data);
    grp_print_properties(grp);
}

void grp_print_properties(GRPFile* grp) {
    printf("Frame Count: %u\n", grp->frame_count);
    printf("Width: %u\n", grp->width);
    printf("Height: %u\n", grp->height);
    printf("\nFrame Table:\n");
    for (int i = 0; i < grp->frame_count; i++) {
        Frame* frame = &grp->frames[i];
        printf("Frame %d: X-Offset=%u, Y-Offset=%u, Unknown=%u, Data Offset=%u\n",
               i, frame->x_offset, frame->y_offset, frame->unknown, frame->data_offset);
        printf("  Line Offsets: ");
        for (int j = 0; j < grp->height; j++) {
            printf("%u ", frame->line_offsets[j]);
        }
        printf("\n");
    }
}

void grp_write(GRPFile* grp, const char* output_filename) {
    FILE* f = fopen(output_filename, "wb");
    if (!f) { perror("Write failed"); return; }
    int offset = 0;
    fwrite(&grp->frame_count, 2, 1, f); offset += 2;
    fwrite(&grp->width, 2, 1, f); offset += 2;
    fwrite(&grp->height, 2, 1, f); offset += 2;
    int frame_table_end = offset + (8 * grp->frame_count);
    int current_data_pos = frame_table_end;
    for (int i = 0; i < grp->frame_count; i++) {
        Frame* frame = &grp->frames[i];
        fwrite(&frame->x_offset, 1, 1, f); offset += 1;
        fwrite(&frame->y_offset, 1, 1, f); offset += 1;
        fwrite(&frame->unknown, 2, 1, f); offset += 2;
        uint32_t pos = current_data_pos;
        fwrite(&pos, 4, 1, f); offset += 4;
        // Write line offsets
        int line_pos = current_data_pos;
        for (int j = 0; j < grp->height; j++) {
            fwrite(&frame->line_offsets[j], 2, 1, f);
            line_pos += 2;
        }
        current_data_pos += (2 * grp->height);  // + line data
    }
    fclose(f);
    printf("Written to %s\n", output_filename);
}

// Usage
int main() {
    GRPFile* grp = grp_create("example.grp");
    grp_read(grp);
    // grp_write(grp, "output.grp");
    grp_destroy(grp);
    return 0;
}