IMAG / GAMI — bitmap images
IMAG / GAMI — bitmap images
Section titled “IMAG / GAMI — bitmap images”Backgrounds, sprites, portraits, maps and UI art in all five games are
IMAG resources (Amiga, big-endian) or GAMI (DOS and Apple IIGS,
little-endian — the same FourCC byte-reversed). They live inside a
resource fork.
The pixel encoding never changes; the header does. Every variant is Amiga interleaved bitplanes with a word-aligned row stride. What differs between games is a 28-, 27- or 6-byte header in front of it — and a minority of resources have no compression wrapper at all. Detect the header, don’t assume it.
Container
Section titled “Container”+0x00 u32 decompressedSize native endianness+0x04 ... compressedStream to the end of the resourceTwo compression schemes are in use, and the decoder auto-detects: it tries PackBits first and falls back to LZSS when the output doesn’t match the declared size.
PackBits — Apple’s run-length scheme, as in MacPaint and TIFF:
Control byte n |
Meaning |
|---|---|
0x00–0x7F |
Copy the next n + 1 bytes literally |
0x80 |
No-op |
0x81–0xFF |
Repeat the next byte 257 − n times (2–128 reps) |
LZSS — byte-aligned, with 8-bit flag bytes (LSB first). A 1 flag bit
means “copy the next literal byte”; a 0 means a 2-byte little-endian match
token whose top 10 bits are the back-reference offset and whose bottom 6 bits
plus 3 are the match length. Offset 0 is a special case that outputs nothing.
In the Excal-family games the split runs roughly 322 LZSS to 9 PackBits.
Pixel data — identical in every variant
Section titled “Pixel data — identical in every variant”bytesPerRow = ceil(width / 16) × 2 (word-aligned)total = bytesPerRow × height × depthEach scanline stores all depth bitplanes consecutively:
Row 0: [plane0 row0] [plane1 row0] … [planeN row0]Row 1: [plane0 row1] [plane1 row1] … [planeN row1]Recovering a pixel’s colour index:
rowOffset = y × (bytesPerRow × depth)for plane p in 0 … depth-1: bit = pixelData[rowOffset + p × bytesPerRow + (x >> 3)] >> (7 - (x & 7)) & 1 colorIndex |= bit << pThe palette is not in the resource. Each game supplies its own — see that game’s section.
Header variants
Section titled “Header variants”WIME Amiga — 28 bytes
Section titled “WIME Amiga — 28 bytes”+0x00 1 format 0x02 standard, 0x05 variant+0x01 1 reserved always 0x00+0x02 1 priority z-order, written at runtime by the loader+0x03 2 reserved+0x05 1 depth bitplane count (4 = 16 colours, 5 = 32)+0x06 14 reserved zeroed runtime fields for the display engine+0x14 2 width u16 BE+0x16 2 height u16 BE+0x18 4 reservedSize check: decompressedSize == 28 + ceil(width/16) × 2 × height × depth.
Holds for all 113 standard-format resources.
WIME Apple IIGS — 27 bytes
Section titled “WIME Apple IIGS — 27 bytes”One fewer reserved byte at +0x06 (13 rather than 14) shifts width and
height back to the odd-aligned offsets +0x13 and +0x15, stored
little-endian, with the outer container’s size prefix also LE. The IIGS port
is always depth 4 — never 5.
Spirit / Vengeance / Conan / Legend — 6 bytes
Section titled “Spirit / Vengeance / Conan / Legend — 6 bytes”+0x00 1 marker always 0x00+0x01 1 height u8, or 0 when it must be derived+0x02 2 width u16 BE+0x04 1 padding always 0x00+0x05 1 depth 4 or 5Pixel data starts at offset 6. When height is 0 the decoder derives it:
height = (payloadSize − 6) / (ceil(width/16) × 2 × depth).
Detection: check byte0 == 0x00 and byte4 == 0x00, then validate
width <= 1024 and depth in 1–8. On a match, use the 6-byte layout;
otherwise fall through to the 27/28-byte ones.
The uncompressed variant
Section titled “The uncompressed variant”A minority of 6-byte-header resources have no compression wrapper at all
— the header sits at byte 0 of the resource. The check is an exact match of
6 + ceil(width/16) × 2 × height × depth against the resource’s total
length, confirmed byte-exact against 12 real Warriors of Legend GAMI
resources.
These previously failed for a reason worth knowing: the resource-fork
parser’s LE fix-up unconditionally byte-swapped the first 4 bytes of every
GAMI/LMRF resource, assuming they were always a size prefix. In the
uncompressed variant those 4 bytes are the header, and swapping them
corrupted it before the image decoder ever saw it. Fixed by checking the
exact-length invariant against the pre-swap bytes.
Decode status
Section titled “Decode status”| Game / platform | Total | Decoded | Notes |
|---|---|---|---|
| WIME Amiga | 115 | 115 | 2 needed the uncompressed-payload path and still show unexplained pixel noise |
| WIME Apple IIGS | 84 | 82 | The 2 failures are uninitialised placeholder data in the source files, not decoder bugs |
| Spirit of Excalibur | 333 | 330 | 3 genuinely corrupt — implausible decompressed sizes, ~458 K–589 K |
| Vengeance of Excalibur | 496 | 496 | Was 476/496; 18 fixed by the uncompressed-header path, the last 2 by the too-small-guard fix below |
| Conan the Cimmerian | 336 | — | Not yet run through the 6-byte path |
| Warriors of Legend | 1,070 | 1,070 | 12 recovered by the uncompressed-header path, 1 by the too-small-guard fix |
The too-small guard was measuring the wrong thing
Section titled “The too-small guard was measuring the wrong thing”decodeIMAG() used to reject any resource shorter than 27 bytes — the IIGS
header size, a threshold with nothing to do with a compressed resource’s
on-disk length. A well-compressed tiny image can be under 27 bytes raw and
still decompress to a valid payload. Confirmed on Vengeance’s TITLE32A.RES
#110 and #112 (26 bytes raw) and Legend’s res32/object.res #7049 (22
bytes): all three decompress to their declared size exactly. The guard now
requires only the 4 bytes needed to read the size prefix, and the real
too-small check happens downstream on the decompressed payload.
The Excal family’s runtime header
Section titled “The Excal family’s runtime header”The Excal engine doesn’t read the 6-byte header directly. _OpenImag loads
the resource and calls _AddHeaderBytes(8) to prepend 8 bytes; all rendering
then works against a 14-byte in-memory bitmap header whose bytesPerRow is
computed as ((width − 1) >> 4 + 1) << 1 and whose height, width and depth
are copied straight through from the on-disk header. The engine never
recomputes them.
Resources holding multiple sub-images carry a frame table immediately after
that 14-byte header: a 4-byte { u16 frameCount, u16 flags } head, then one
6-byte entry per sub-image giving a packed pixel offset, a flags/plane-count
byte, a sub-image index, and the sub-image’s own width and height. _DrawPrim
reads per-frame dimensions from these entries, not from the 6-byte IMAG
header.
Where the full evidence lives
Section titled “Where the full evidence lives”docs/formats/imag.md in the main repository (not part of this site), plus
the per-game palette and catalogue notes it links: WIME’s
amiga/imag-catalog.md and amiga/palettes.md, the DOS/IIGS specifics in
wime/dosvga/gami-format.md, and each Excal game’s own engine.md.
Undecoded header variants are tracked separately in
docs/reference/eliminated/imag-frml-open-variants.md.