semmixcolor and perception

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

BytesFieldExample
0separator 2C (",")2C
1-2left position on the logical screen00 00 = 0
3-4top position00 00 = 0
5-6image width04 00 = 4
7-8image height02 00 = 2
9flags00

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

bit 7 local color table present bit 6 interlaced image bit 5 local table sorted bits 4-3 reserved (0) bits 2-0 N: the local table has 2^(N+1) colors example: 00 = 00000000 no local table (uses the global one), not interlaced

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

02 LZW minimum code size 05 04 12 30 13 05 sub-block: 5 data bytes 00 terminator

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:

04 12 30 13 05 → 0 0 1 1 0 3 3 1 → row 0: 0 0 1 1 row 1: 0 3 3 1
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 };
}