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.
2. Two direct download links for files of format .GRP
- https://staredit.net/sc1db/download/3981/ (sc2viper.grp, a sample Viper unit graphics GRP file)
- https://staredit.net/sc1db/download/3175/ (Infested Battlecruiser GRP file, a sample unit graphics file)
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;
}