The BODY chunk holds the pixels. It must come after the
bitmap header, whose width, height,
nPlanes, masking and compression say how to read it. The rows are stored
as they are with compression 0, packed with ByteRun1 with
compression 1, or as vertical runs with compression 2.
Rows of bitplanes
The rows run from the top of the picture to the bottom. Each row is made of
lines, one for each bitplane in turn: plane 0, then plane 1, up to plane
nPlanes − 1, and then a mask line when masking is 1.
Every line is a whole number of 16-bit words long:
RowBytes = 2 × floor((w + 15) ÷ 16)
w pixels wide- In each byte the highest bit is the leftmost pixel. The bits after the last pixel of a line are padding.
- Plane 0 holds the lowest bit of each pixel’s colour number. A pixel’s number is the sum of 2p for every plane p whose bit is set.
- The unpacked size of the
BODYish × (nPlanes + 1 if masking is 1) × RowBytes.
0Row 0 plane 0, plane 1 … planenPlanes − 1, then the mask line if there is one(nPlanes + mask) × RowBytes…Row 1 the same(nPlanes + mask) × RowBytes…Row h − 1 the bottom row(nPlanes + mask) × RowBytes
BODYAn example
A 320-pixel-wide picture has lines of 2 × floor(335 ÷ 16) = 40 bytes. With 8 planes and no mask, each row unpacks to 8 × 40 = 320 bytes, and a 200-row picture to 64,000 bytes.
In a 4-plane picture, if the first byte of a row’s four lines is
80, 00, 80 and 80, the
leftmost pixel has bits 1, 0, 1, 1 in planes 0 to 3: colour 1 + 4 + 8 = 13. The
other seven pixels of that byte are colour 0.
The mask plane
With masking 1, each row ends with one more line, the mask. A 1 bit keeps the pixel
and a 0 bit makes it transparent. The mask is not part of the colour number and is not
counted in nPlanes. Some 24-bit brushes have a mask line after their 24
planes.
ByteRun1
With compression 1, each row is packed with ByteRun1, a run-length encoding that is the same as Apple’s PackBits. The ILBM document says “Do not compress across rows”: each row starts with a fresh control byte. Within a row the lines are packed one after another.
To unpack, read a control byte n as a signed number:
| n | Byte | Meaning |
|---|---|---|
| 0 to 127 | 00 to 7F | Copy the next n + 1 bytes as they are (1 to 128). |
| −1 to −127 | FF to 81 | Repeat the next byte −n + 1 times (2 to 128). Read unsigned, that is 257 − n times. |
| −128 | 80 | Do nothing. |
- Work out how many bytes the row unpacks to:
(nPlanes + mask) × RowBytes. - Read a control byte and do what the table says, until the row has that many bytes.
- The next byte starts the next row.
The document’s advice for packers: a run of 3 or more equal bytes is always a repeat; a run of 2 is a repeat unless it sits between two literal runs.
Readers differ in one detail. The explorer unpacks a row’s lines as one unit, so a run that crosses from one line into the next still works; FFmpeg unpacks each line on its own. When the runs break at the end of every line, the explorer shows the row’s lines; otherwise it shows the runs.
000003E0 42 4F 44 59 00 00 18 1B 07 01 FE 03 FF F0 00 00
000003F0 3F FD FF 04 FC 00 0F FF E0 FE 00 FF FF 0A FC 00
BODY of 6,171 bytes, with the control bytes marked07 copies the next 8 bytes, 01 FE 03 FF F0 00 00 3F.
FD is −3: four bytes of FF. 04 copies 5 bytes,
FC 00 0F FF E0. FE is −2: three bytes of 00.
FF is −1: two bytes of FF. 0A starts a copy of
11 bytes. The first 22 bytes of plane 0’s 40-byte line are done.
Compression 2: VDAT
Deluxe Paint for the Atari ST packs pictures down the columns instead of along the
rows. Wikipedia names this compression (“2 ‘Vertical RLE’ from Deluxe Paint for
Atari ST”); the byte layout below is FFmpeg’s decoder, the only full description
found. The BODY then holds one chunk with the ID VDAT for each
bitplane, plane 0 first. Each VDAT’s data is:
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0 | 2 | UWORD | count | The number of command bytes, plus 2. |
| 2 | count − 2 | bytes | commands | Signed command bytes. |
| count | the rest | UWORDs | data | 16-bit big-endian words, and the counts some commands read. |
The plane is filled with 16-bit words a column at a time: the 2-byte-wide column at byte 0 of every line, from the top row to the bottom, then the column at byte 2, and so on to the right. For each command byte c, in order:
| c | Read from the data | Output |
|---|---|---|
| 0 | a word n, then n words | those n words as they are |
| −1 to −128 | −c words | those words as they are |
| 1 | a word n, then a word v | v, n times |
| 2 to 127 | a word v | v, c times |
When every plane is unpacked, the picture is read as usual: row y is line y of each plane in turn.
FFmpeg ignores each VDAT’s size and moves on to the next one where the
data ends; the explorer uses the size. No file with compression 2 was among the files
examined for this site, so the explorer’s decoder was checked only against files built
for the test. Whether a VDAT can be followed by a pad byte, and whether a
mask plane can be packed this way, is not known.
Short, long and damaged data
- Some writers count a pad byte in the
BODY’s size, so one byte, usually 0, follows the last row. The explorer shows it as a pad byte and does not count it as a problem. - More bytes after the last row are reported, and readers ignore them.
- When the data ends early, the explorer reports the row it ends in, such as “The data ends partway through row 179”, and shows the rows it has.
- A run that carries on past the end of a row is reported, and its extra bytes are dropped.