Image block ($2C)
The actual image has three parts: a 10-byte descriptor, an optional local color table and the compressed pixels. A file can hold many image blocks: that is how animations and images assembled from several pieces are made.
Image descriptor
| Bytes | Field | Example |
|---|---|---|
| 0 | separator 2C (",") | 2C |
| 1-2 | left position on the logical screen | 00 00 = 0 |
| 3-4 | top position | 00 00 = 0 |
| 5-6 | image width | 04 00 = 4 |
| 7-8 | image height | 02 00 = 2 |
| 9 | flags | 00 |
An image does not have to fill the whole logical screen. Animations often store in later frames only the rectangle that changed, at the right position. That shrinks the file considerably.
The flag byte
With bit 6 set, rows come in the order of the four passes described in the article on interlacing.
Local color table
If bit 7 is set, a local color table follows the descriptor: 3 · 2^(N+1) bytes, built like the global one. It applies only to this image. That way every frame of an animation can have its own 256 colors, and the whole file more than 256.
Image data
The minimum code size is the number of bits needed for a color index (2 for 2- and 4-color palettes, 3 for 8 colors, up to 8 for 256). LZW codes start one bit wider (see LZW in GIF).
Then come data sub-blocks of at most 255 bytes, and the 00 terminator (see general structure). Before decompression the sub-blocks are joined into one byte stream. Sub-block boundaries have nothing to do with code boundaries: a code can start in one sub-block and end in the next.
Once decoded, the data are simply color indices, row by row, left to right:
function readImage(b, pos) { // pos points at byte 2C
const u16 = (i) => b[i] | (b[i + 1] << 8);
const desc = { left: u16(pos + 1), top: u16(pos + 3), w: u16(pos + 5), h: u16(pos + 7) };
const flags = b[pos + 9];
pos += 10;
if (flags & 0x80) pos += 3 * (2 << (flags & 7)); // skip the local table
const minCodeSize = b[pos++];
const data = [];
for (let n = b[pos++]; n !== 0; n = b[pos++]) { // join the sub-blocks
data.push(...b.subarray(pos, pos + n));
pos += n;
}
return { ...desc, interlaced: (flags & 0x40) !== 0, minCodeSize, data, next: pos };
}