# QR Stream - Architecture This document describes the internal design, wire format, and algorithms used by QR Stream. For user-facing installation and usage instructions, see [README.md](README.md). --- ## Table of Contents 1. [High-Level Architecture](#high-level-architecture) 2. [Project Structure](#project-structure) 3. [Dependencies](#dependencies) 4. [The Protocol](#the-protocol) 5. [Algorithms](#algorithms) 6. [Data Flow](#data-flow) 7. [Design Decisions](#design-decisions) 8. [Common Pitfalls](#common-pitfalls) --- ## High-Level Architecture ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Sender UI │──────▶│ Encode Worker │──────▶│ GIF Worker │ │ (Preact hooks) │ │ (Web Worker) │ │ (Web Worker) │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ ▼ ┌────────────┐ │ .gif file │ │ (animated) │ └────────────┘ │ ┌─────────────────┐ ┌─────────────────┐ │ │ Receiver UI │◀──────│ Decode Worker │◀──────├──────── camera / file │ (Preact hooks) │ │ (Web Worker) │ │ └─────────────────┘ └─────────────────┘ └──────────────────────────┘ ``` Everything heavy (compression, RLNC encoding, GIF encoding, QR decoding) runs in dedicated Web Workers so the UI stays responsive. --- ## Project Structure ``` src/ ├── app/ │ ├── app.tsx # App shell with hash-based tab routing │ └── routes/ │ ├── sender.tsx # Text/file input, GIF preview, download │ └── receiver.tsx # Camera scanner, GIF upload, results ├── core/ │ ├── fec/ # Forward Error Correction (RLNC) │ │ ├── gf256.ts # GF(256) arithmetic (log/antilog tables) │ │ ├── rlnc_encoder.ts # Systematic RLNC encoder │ │ ├── rlnc_decoder.ts # Incremental Gaussian-elimination decoder │ │ └── xoshiro.ts # xoshiro128** PRNG for deterministic coeffs │ ├── gif/ │ │ ├── gif_parser.ts # GIF decoding (LZW, frame extraction) │ │ └── gif_render.ts # GIF encoding (2-colour palette, gifenc) │ ├── preprocess/ │ │ └── compress.ts # (legacy) deflate helpers │ ├── protocol/ │ │ ├── constants.ts # Protocol constants: K/R, sizes, flags │ │ ├── crc32c.ts # CRC32-C (Castagnoli) with lookup table │ │ └── packet.ts # 8-byte fixed header + payload + CRC │ ├── qr/ │ │ ├── qr_encode.ts # QR matrix generation (qrcode-generator) │ │ ├── qr_decode.ts # QR decoding wrapper (jsQR) │ │ └── frame_raster.ts # Matrix → RGBA raster (scale, quiet zone) │ ├── reconstruct/ │ │ └── assemble.ts # Concatenate generations, trim padding │ └── sender/ │ ├── packetizer.ts # Data → packets: compress, split, wrap metadata │ └── scheduler.ts # Interleave systematic/coded symbols into frames ├── workers/ │ ├── encode.worker.ts # Orchestrates packetizer + scheduler │ ├── decode.worker.ts # Feeds frames to RLNC decoder, reassembles │ └── gif.worker.ts # Rasters packets → RGBA → GIF via gifenc ├── tests/ │ ├── complete.test.ts # Unit tests for all core modules │ ├── frame_decode.test.ts # QR encode→decode roundtrip per frame │ ├── gif_roundtrip.test.ts# Full GIF encode→parse→decode roundtrip │ ├── prod_roundtrip.test.ts# Deterministic frame-loss recovery test │ ├── test_qr_modules.test.ts# QR capacity, raster, GIF render tests │ └── setup.ts # happy-dom test environment setup ├── types/ │ └── gifenc.d.ts # Type declarations for gifenc ├── cli/ │ ├── qr-stream.ts # CLI entry point (terminal QR display) │ │ # stdin → isText=true (text mode) │ │ # → isText=false, embeds filename+MIME │ ├── terminal_raster.ts # Half-block Unicode renderer │ └── static_server.ts # Built-in preview server (--serve) ├── index.html # Single-page app entry └── main.tsx # Renders into #root ``` --- ## Dependencies ### Runtime - **preact** - UI framework (React-compatible, ~10 KB) - **qrcode-generator** - QR matrix generation (versions 1–40, all ECC levels) - **jsqr** - QR decoding from `ImageData` (grayscale + adaptive thresholding internally) - **gifenc** - Animated GIF encoder (2-colour palette, LZW compression) - **fflate** - Fast deflate/inflate (compression for large payloads) ### Dev / Build - **vite** - Build tool, dev server, worker bundling - **@preact/preset-vite** - Preact JSX transform for Vite - **vitest** - Test runner - **happy-dom** - DOM environment for headless tests - **typescript** - Type checking - **esbuild** - CLI bundle (via `build:cli` script) --- ## The Protocol There is **one hardcoded profile** - no negotiation, no manifest, no session IDs. ### Profile Constants - **QR Version:** V10 (57×57 modules) - **ECC Level:** M (~15% correction) - **Source symbols per generation (K):** 16 - **Repair symbols per generation (R):** 8 - **Symbol payload:** 201 bytes - **Max packet size:** 213 bytes (fits exactly in V10-M) - **Frame delay:** 200 ms (5 fps) - **Max file size:** ~8 MB ### Packet Format (fixed 8-byte header) All multi-byte fields are **little-endian**. ``` Offset Size Field ───────────────────────────────────────────────────────────────────────────────── 0 1 Magic: 0x51 ('Q') 1 4 Packed word (32 bits): ├─ bits 0–11 : generation index (0–4095) ├─ bits 12–23 : total generations (0–4095) ├─ bits 24–28 : symbol index (0–31) ├─ bit 29 : isText flag (1 = text, 0 = file) ├─ bit 30 : isLastGeneration flag └─ bit 31 : compressed flag 5 3 Data length (preprocessed size, 24-bit, 0–16,777,215) 8 201 Payload (zero-padded to 201 B) 209 4 CRC32-C over bytes 0–208 ``` Total: **213 bytes** → fits in a V10-M QR code. **Symbol index convention:** - `0–15` = systematic symbol (`sourceIndex = symbolIndex`) - `16–23` = coded repair symbol (`codedSymbolIndex = symbolIndex − 16`) - `24–31` = reserved ### File Metadata Wrapping For file transfers (not text), the raw file bytes are prefixed with a tiny metadata envelope before compression: ``` [1 byte: filename length (0–255)] [N bytes: filename UTF-8] [1 byte: MIME type length (0–255)] [M bytes: MIME type UTF-8] [rest: actual file data] ``` This lets the receiver restore the original filename and MIME type on download. --- ## Algorithms ### 1. RLNC over GF(256) We use **Random Linear Network Coding** with a systematic encoding. **Encoder (`rlnc_encoder.ts`):** - Given `K` source symbols, output `K` systematic + `R` coded symbols. - Systematic symbols are the original data (identity coefficient vector). - Each coded symbol is a random linear combination: `C_j = Σ coeff[i] · S_i` (multiplication and addition in GF(256)). - Coefficients are deterministically derived from `(generationIndex, codedSymbolIndex)` via a xoshiro128** PRNG. **Decoder (`rlnc_decoder.ts`):** - Maintains an augmented coefficient matrix in **reduced row-echelon form (RREF)**. - Each incoming symbol is forward-eliminated against existing pivots, then if it has a new pivot: 1. Scale the row so pivot = 1 2. Eliminate the new pivot from all existing rows 3. Insert maintaining pivot-column order - When `rank == K`, the matrix is identity and the RHS data is the reconstructed source symbols. ### 2. GF(256) Arithmetic (`gf256.ts`) - Irreducible polynomial: `x^8 + x^4 + x^3 + x^2 + 1` (0x11d, same as AES). - Pre-computed **log/antilog tables** at module load time for O(1) multiply/divide/inverse. - Addition/subtraction = XOR (same operation in characteristic-2 fields). ### 3. QR Code Generation (`qr_encode.ts`, `frame_raster.ts`) - Uses `qrcode-generator` library to produce boolean module matrices. - Capacity is computed from an embedded RS block table (versions 1–40, all ECC levels). - `rasterizeQR()` scales each module to `scale × scale` pixels and adds a 4-module white quiet zone. - Output is pure black/white RGBA `ImageData`. ### 4. QR Decoding (`qr_decode.ts`) - Thin wrapper around `jsQR`. - `jsQR` internally converts RGBA → grayscale and applies adaptive thresholding (8×8 regions with 5×5 averaging). No external preprocessing is needed. - For camera scanning we pass `inversionAttempts: 'attemptBoth'` (handles glare/reflections). For GIF file mode we use `'dontInvert'` (our QRs are black-on-white, giving ~50% speedup). ### 5. GIF Encoding (`gif_render.ts`) - Uses `gifenc` with a **2-colour global palette** (white, black). - Each frame is converted from RGBA to indexed (threshold at 50% brightness). - The NETSCAPE 2.0 extension sets loop count to infinity. - Default delay: 200 ms per frame (5 fps). ### 6. Frame Scheduling (`scheduler.ts`) - Systematic symbols are interleaved across generations first, then coded symbols. - Generation order is deterministically shuffled using `totalGenerations` as a seed. - This spreads redundancy evenly: if you watch any prefix of the GIF, you see some symbols from every generation. --- ## Data Flow ### Sender ``` Text or File │ ▼ [Wrap metadata if file] │ ▼ [Optional deflate compression (fflate)] │ ▼ Split into 201-byte symbols │ ▼ Group into generations of K=16 │ ▼ RLNC encode each generation → 16 systematic + 8 coded symbols │ ▼ Build packets (8-byte header + payload + CRC32C) │ ▼ Schedule frames (interleave systematic, then coded, shuffle generations) │ ▼ Rasterize each packet to QR code (V10-M, scale=3, 4-module quiet zone) │ ▼ Encode frames into animated GIF (2-colour palette, 200 ms delay) │ ▼ Blob URL → preview + download ``` ### Receiver ``` Camera frames or GIF file │ ▼ [If camera: software crop center 50% (2× zoom), optional camera zoom API] │ ▼ Decode QR with jsQR → raw bytes │ ▼ Parse packet (verify magic, verify CRC32C) │ ▼ Deduplicate by (generationIndex, symbolIndex) │ ▼ Feed to RLNC decoder (systematic or coded based on symbolIndex) │ ▼ When rank == K for a generation → mark solved │ ▼ When all generations solved: │ ├── Text mode → decompress → TextDecoder → show in