#import "@preview/codly:1.3.0": * #import "@preview/codly-languages:0.1.1": * #show: codly-init.with() #codly( languages: codly-languages, number-format: none, zebra-fill: none, display-name: false, display-icon: false ) #set page( paper: "a4", flipped: true, margin: 1cm, ) #set text(size: 8pt) #align(center)[ #text(20pt)[*BitFLip (BFL) Image Format*] \ #text(12pt)[ Specification *v1.0*, #datetime.today().display("[year]-[month]-[day]"), Slendi <#link("mailto:slendi@socopon.com")> ] #line(length: 100%, stroke: 0.5pt + gray) #v(8pt) ] #set heading(numbering: "1.1.") #columns(2)[ = Overview BFL is a compact, monochrome bitmap image format with optional 1-bit transparency. Each image has two bitplanes that are encoded: 1. *Image bitplane* (required): 1 = white pixel, 0 = black pixel 2. *Alpha bitplane* (optional): 1 = transparent pixel, 0 = opaque pixel Each bitplane is independently encoded using a run-length encoding or stored raw bit-packed. Each resulting byte stream can then be optionally LZSS-compressed. All multi-byte integers are little-endian. = File Structure ``` struct Header { char magic[3]; // Magic bytes "BFL" u16 width; // Width of the image in pixels u16 height; // Height of the image in pixels u8 flags; // Various flags u32 img_len; // Image stream length u32 al_len; // Alpha stream length, 0 if none u8 img[]; // Image stream bytes u8 al[]; // Alpha stream bytes } ``` == Dimensions - Width and height are 16-bit unsigned integers. - Pixel order is row-major, top-to-bottom, left-to-right. - Total pixel count `N = Width * Height`. All bitplane encoders/decoders operate on exactly `N` bits. == Flags - `0x01` *FLAG_HAS_ALPHA* - Alpha bitplane is present. - `0x02` *FLAG_IMG_RAW* - Image stream is raw bit-packed. - `0x04` *FLAG_TRA_RAW* - Alpha stream is raw bit-packed. - `0x08` *FLAG_IMG_NOLZ* - Image stream is *not* LZSS-compressed. - `0x10` *FLAG_TRA_NOLZ* - Alpha stream is *not* LZSS-compressed. It is up to the encoder to determine which combination of those flags results in a smaller file. All remaining bits are reserved and should be set to 0. = Bit-packing RAW streams When a stream is marked `RAW` in flags, bits are packed LSB-first within each byte. - Bit for pixel index `i` is stored at byte `i / 8`, bit position `i % 8`. - Unused high bits of the final byte (if `N` is not a multiple of 8) *must* be zero when encoding and *must* be ignored when decoding. #colbreak() = Coinflip RLE Unless a stream is marked `RAW`, each bitplane is encoded using "coinflip" run-length coding: ``` Byte 0: Initial state (0 = start with 0-runs, 1 = start with 1-runs) Byte 1..k: Repeated groups of: Count (u8, number of pixels to emit of the current state) [Optional 0] (u8, do not toggle state if present) ``` = LZSS After coinflip RLE or RAW bit-packing, each resulting byte stream may be compressed independently using LZSS with the following parameters: - *Window size*: 4096 - *Lookahead*: 18 - *Minimum match*: 3 == Block format Streams are encoded as a sequence of 8‑item groups preceded by a flag byte: - For each bit b (0..7) in the flag (LSB first): - If `(flag >> b) & 1 == 1`: Literal - copy next byte to output. - Else: Match - read two bytes b0, b1 and emit: - `length = (b0 >> 4) + 3` (range 3..18) - `offset = ((b0 & 0x0F) << 8) | b1` (range 1..4095) - Copy length bytes from `out_size - offset`. ]