BMP Compression File Structure
This section documents the compressed image format PositronPixelFoundry.exe produces, and EpdBmpFromCode() reads — useful if you're building or debugging tooling around this format, or just want to understand what's actually stored in a Flash8 image table before it reaches the decoder.
Overall layout
Every compressed image table has the same three-part shape:
[4-byte header] [Plane 1: mode byte + payload] [Plane 2: mode byte + payload]
Whether the library actually reads that second plane back out is a separate question, controlled by EpdBmpFromCode()'s own $if EpdType <> BW check — which depends on how the panel is configured in your project, not on anything about the specific image being decoded.
A BW-panel setup skips reading the second plane entirely (there'd be nothing useful to do with it even if it read it), while a BWR/BWY-panel setup always reads it, whatever mode it happens to be.
Coordinate convention
The encoder stores image rows bottom-up — the first row of pixel data in the file corresponds to the bottom of the image, not the top.
This deliberately matches the standard Windows BMP format's own row order, and extends the same bottom-up convention the whole TFT & EPD library is already built around ((0,0) = bottom-left, Y increasing upward) — not a coincidence, but a consistent design choice for three concrete reasons:
Compression modes
The simplest mode: one bit per pixel, packed 8 pixels to a byte, MSB-first — bit 7 of a byte is the leftmost of that byte's 8 pixels, bit 0 is the rightmost.
Each row occupies ceil(width / 8) bytes, with any unused bits in a row's final byte (when width isn't a multiple of 8) left as padding.
Rows are stored consecutively, bottom row first, with no gaps or alignment padding between them.
Total payload size is fixed and predictable: ceil(width/8) × height bytes — this is why EpdBmpBitmap's own decode can compute exactly where this plane's data ends without needing to scan through it.
Bit value meaning is plane-dependent, matching the library's own color-plane convention: on the BW plane, 1 = white, 0 = black; on the second (RY) plane, 1 = the plane's accent color (red or yellow, whichever the panel supports), 0 = "not that color, defer to BW plane."
When you'd see this mode: for content where run-length encoding wouldn't help — genuinely noisy or fine-grained image data, where pixel-to-pixel color changes are frequent enough that RLE runs would mostly be very short, adding overhead rather than saving space.
The compression mode used for most real content — anything with runs of same-colored pixels, which most icons, badges, and simple graphics have in abundance.
Structure:
A concrete worked example: the byte sequence 0xFF, 0x00, 0xFF, 0x00, 0xE8 decodes as two consecutive 255-pixel runs of the same color (each 0xFF followed by 0x00 — a continuation, not a toggle), followed by a final 232-pixel run (0xE8 = 232) of that same color — for a total of 255 + 255 + 232 = 742 pixels of one uninterrupted color before the first real toggle occurs.
Why the format needs a lookahead, not just the current byte: correctly parsing this requires peeking at least one byte ahead of the current run-length byte, since whether this run is a continuation or a genuine end depends on whether the next byte happens to be 0x00.
This lookahead requirement is exactly why RLE can't be randomly accessed — decoding any specific pixel requires walking every run before it, sequentially, from the start of the plane's data.
The simplest possible payload: no payload at all. The mode byte alone tells the decoder the entire plane is uniformly 0 — solid black on the BW plane, or "no accent color anywhere" on the RY plane.
EpdBmpFromCode()'s own handling reflects this directly: for the BW plane, this fills the whole region with black; for the RY plane with "No accent color" or 0 — since a red/yellow plane that's entirely 0 means "let the BW plane show through everywhere," which is already the default state.
When you'd see this mode: an image with a solid black background and no accent color anywhere, or (much more commonly) the RY plane of a purely black-and-white image inside a BWR/BWY table — since there's no reason to spend even a single RLE run-byte encoding "no red anywhere" when a single mode byte says the same thing for free.
The mirror image of Mode 2: the entire plane is uniformly 1 — solid white on the BW plane, or the accent color (red/yellow) everywhere on the RY plane.
Also carries no payload beyond the mode byte itself.
When you'd see this mode: a solid white background with no black content, or an RY plane where the accent color genuinely does cover the entire image (e.g., a plain colored rectangle or fill).
How the encoder picks a mode for a given plane
Mode selection happens independently for each plane — the BW plane and RY plane can (and often do) end up using completely different modes for the same image.
Step 1 — All-Zero/All-One check, unconditional, always first. A single pass scans the entire plane, tracking two flags simultaneously: is every pixel 0, and is every pixel 1.
If either holds, that mode is used immediately — no comparison against anything else, since zero payload bytes can't be beaten.
This check runs regardless of any other setting.
Step 2 — if neither special case applies, the choice depends on a UI setting. The encoder tool exposes three options:
In Auto mode, the encoder doesn't estimate or guess — it fully computes both the Bitmap and RLE payloads for that plane, counts the resulting bytes for each, and keeps whichever is smaller.
Step 3 — the tie-break rule, and why it's deliberate. If both payloads come out to exactly the same size, Bitmap wins, not RLE — favoring decode simplicity on the MCU side when there's no size benefit either way.
So the practical picture: for most real content (icons, badges, simple line art with genuine runs of same-colored pixels), RLE will generally win on size and get selected — but there's no guarantee, and a plane with unusually fine-grained or noisy content could legitimately end up as Bitmap instead, purely because RLE's overhead exceeded raw bitmap's fixed cost for that specific plane's content.
One step upstream of compression: how source pixels get classified
Worth documenting as context, since it explains why a given source image ends up looking the way it does after conversion, even though it's technically a separate step from mode selection above.
Before any plane gets compressed, each source pixel (in RGB565 format) is reduced to a BW bit and an RY bit.
This step is explicitly built for near-flat-color source art — icons and line art meant to render as pure black, white, red, or yellow — not as a general-purpose photo-to-EPD converter.
A pixel is checked against two accent-color tests first:
with thresholds deliberately loosened from a hard "must be fully saturated" cutoff, so pixels blended partway toward white or black by anti-aliasing or dithering at an edge still classify correctly as the accent color rather than falling through.
If neither accent test matches, the pixel falls back to a luminance-based black/white decision — no gray or intermediate value survives; every pixel becomes one of exactly 3 values.
This has a direct, practical consequence worth knowing when converting your own source images: photographic content will not classify well.
Skin tones, wood grain, warm-toned fur, or any other naturally reddish photographic color will legitimately fall through to plain black/white, by design — the tool assumes icon-style art as input, not photographs, and doesn't try to guess that a reddish tone in continuous-tone source material is probably meant to be the accent color.
Created with the Personal Edition of HelpNDoc: Make Documentation a Breeze with a Help Authoring Tool