At 0x06A0 an eight-byte animation header precedes a table of four-byte frame offsets. The image payload begins after that table. A table entry locates encoded frame bytes; it is not a fixed-size bitmap offset. The original serialiser aligns the encoded frame payload to four bytes and places sound-effect flags after it. [1][2]
Frame boundaries
Read the frame count from the 16-byte file header as last index plus one. Validate that the offset table fits inside the animation section, then validate every referenced frame before decoding. Do not assume offset values increase strictly: published files may reuse an offset. A parser should not infer a frame's length by blindly subtracting the next entry without considering duplicates and the section end. [1][2]
Layer row coding
Each frame has a compact header with paper and ink choices, frame type and translation flag. Both 256 × 192 one-bit layers are encoded row by row. For each layer, 48 bytes supply 192 two-bit row types. Type 0 carries no row data; types 1 and 2 use a 32-chunk mask for eight-pixel chunks (type 2 begins from an ink-filled row); type 3 stores all 32 chunks. Each chunk byte yields eight monochrome pixels. [2]
Difference frames
A difference frame combines decoded bits with the previous frame using XOR, optionally after an X/Y translation. A standalone decode of such a frame can look like noise. Keep prior decoded layers available when seeking, or decode forward from a suitable full frame. Clip translated reads to the canvas bounds. [2]
Presentation
After reconstructing each layer, apply the frame's paper and ink colours and the animation header's hide-layer flags. Hidden ink remains in the file. If a thumbnail path ignores the flags, it may reveal a layer the creator hid. The research compared the row decoder on specific test inputs; its behaviour for every possible corrupt frame has not been established. [1][2]
Frame header and row encodings
| Frame-header bits | Field |
|---|---|
| 7 | 0: difference frame; 1: complete keyframe |
| 6–5 | Translation marker; nonzero difference frames may carry signed X and Y bytes |
| 4–3 | Second-layer ink colour index |
| 2–1 | First-layer ink colour index |
| 0 | Paper colour: black at 0, white at 1 |
The normal ink indices select the inverse of the paper colour, red or blue. This renderer's documented palette uses paper black #0e0e0e, white #ffffff, red #ff2a2a and blue #0a39ff. Ink index zero is unusual in ordinary files, but its presence alone should not be treated as a validation error. [3]
Each layer begins with 48 row-type bytes. Four 2-bit selectors are packed into each byte from the least significant pair upward, yielding 192 row types. Type 0 is empty and consumes no row payload. Type 1 has a 32-bit chunk-occupancy mask followed by bytes only for occupied eight-pixel chunks; type 2 uses the same structure after initialising the row to ink. Type 3 consumes all 32 chunk bytes without a mask. Within a chunk byte, the low bit maps to the first pixel. Keep row position and input byte position separate: an absent chunk advances eight output pixels without advancing the payload cursor. [3]
Reconstruct a difference frame by XORing its decoded layer with the previous full layer, applying any signed displacement and clipping at the edges. Frame offsets can be reused and need not follow playback order. To seek to a frame, start from a checked full frame and reconstruct the frames after it. Simply subtracting consecutive offsets can produce a negative or zero length. [3]
Locate frame bytes before decoding them
Let A be the animation length at file offset 0x04, and T the offset-table length at 0x06A0. The eight-byte animation header is followed by the table; encoded frame payload starts at 0x06A8 + T and occupies A - T - 8 bytes. Frame table offsets are relative to this payload, so a table value r addresses file offset 0x06A8 + T + r. The source's later wording calls the length a metadata value, but its actual field map places it in the main file header. [2]
The two halfwords at animation-header offsets +0x02 and +0x04 have been seen as zero and may represent sizes; their exact purpose is not resolved. The flags halfword is at +0x06. The table does not impose increasing playback-order addresses, and separate frames can point to the same bytes. [2]
Record order and compressed masks
- Read the one-byte frame header and any signed translation coordinates.
- Read all 48 row-type bytes for layer one.
- Read all 48 row-type bytes for layer two.
- Decode layer one's row payload.
- Decode layer two's row payload.
- If required, reconstruct both layers against the previous decoded frame.
Both row-type tables precede the bitmap payloads. Within a row-type byte, extract bit pairs at shifts 0, 2, 4 and 6, in that order. For example, 0xE4 yields row types 0, 1, 2, 3. The two row-type tables together always consume 96 bytes. [2]
For compressed row types 1 and 2, interpret the four-byte occupancy mask in big-endian order. Bit 31 - c controls eight-pixel chunk c, starting at the left edge. A set mask bit consumes a byte and replaces that chunk's eight pixels, low bit first; a clear bit consumes no chunk byte. Type 1 starts from zero pixels and type 2 from one pixels, so absent chunks retain different defaults. Type 3 consumes 32 chunk bytes directly; type 0 consumes no row payload. A type-zero row in a difference frame can still acquire pixels during the subsequent XOR reconstruction. [2]
Keyframes and difference frames
Use bit 7 equal to one for a complete keyframe and zero for a difference frame. The Collective's frame-header discussion and the working flipnote.js decoder agree on that polarity: previous-frame XOR runs when the bit is zero. A later diffing paragraph in the wiki instead says type one. This contradiction with the frame-header description and the decoder does not establish a second encoding rule. [2] [4]
Translation uses signed eight-bit X and Y coordinates. During XOR, destination (x, y) reads the previous layer at (x - tx, y - ty) only when that coordinate falls inside 256 × 192. Outside that overlap, retain the decoded current-frame bits instead of wrapping rows or reading beyond the buffer. [2]
Ink indices 0 and 1 both denote the inverse of paper in the wiki's table, although index zero is not normally used. Indices 2 and 3 select red and blue. These per-frame indices are separate from the sixteen-colour embedded-thumbnail palette. [2]
Decode the five-byte row example
DSiBrew's worked compressed row is 80 00 00 00 20. The first four bytes form big-endian mask 0x80000000, so only chunk zero has a stored byte. Its literal byte 0x20 has bit 5 set; because chunk pixels are read low bit first, this is pixel x = 5, the sixth pixel from the left. The row consumes five bytes in total. The original prose's “sixth byte” refers to a pixel position, not an extra input byte. [5]
| Row type | Pixels x=0 through x=7 | Pixels x=8 through x=255 |
|---|---|---|
| 1: default-zero coded row | 0 0 0 0 0 1 0 0 | All 0 |
| 2: default-one coded row | 0 0 0 0 0 1 0 0 | All 1 |
The type-two result above follows the Collective format description and the checked flipnote.js decoder: initialise the row to one, then replace each explicitly stored chunk with its literal eight bits. Type two changes the default for omitted chunks and keeps the supplied 0x20 unchanged. DSiBrew's older explanation and example routine suggest different inversion behaviour, so their decoding results will differ. [5] [2] [4]
In a difference frame, these are the decoded row bits before the previous-frame XOR. A zero layer bit leaves the other layer visible beneath it instead of covering it with the paper colour. Final composition uses both ink masks, their visibility and colour settings, then the paper background. [2]
Fixed offsets assume no translation bytes
The historical frame table places layer-one row types at frame-relative 0x01, layer-two row types at 0x31 and the first bitmap payload at 0x61. Those positions apply to a one-byte frame header. When the translation marker carries two signed coordinate bytes, the corresponding positions advance to 0x03, 0x33 and 0x63. Inspect the marker before applying the compact table. [5] [2] [4]
