# 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)
│ ├── 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