# KBQR 1.0

A standalone offline text-to-QR transfer tool, with a synchronous ES module
encoder and an incremental decoder. No install, build, account, CDN or backend.

## Start

1. Extract the ZIP, preserving the KBQR folder and its vendor subfolder.
2. Open index.html in a modern browser. Classic scripts allow direct file://
   opening for generation, printing, frame-text ingestion and QR-photo reading.
3. For module imports and the most reliable camera support, serve this folder
   locally. If Python is already installed, run from the KBQR folder:

   Windows:
   ```powershell
   py -m http.server 8765 --bind 127.0.0.1
   ```

   macOS / Linux:
   ```sh
   python3 -m http.server 8765 --bind 127.0.0.1
   ```

   Open http://localhost:8765. No internet is required. Ctrl+C stops the server.
   Python is optional hosting software, not an application dependency.

A phone reading another screen must open its own copy of KBQR, using a local
secure origin or HTTPS. An HTTP LAN IP is generally not a secure camera origin;
localhost refers to the phone itself, not your computer. For phone deployment,
copy the static folder to an HTTPS host. There are no application network calls,
but a hosted page must first load its files. There is no service worker or
persistent cache promise. Keep the folder locally for durable offline use.
Browser policies for file:// cameras vary; the demo reports errors and has a
local photo input and decoded-frame text fallback.

## Send

Paste text, optionally label it, and select Generate QR set. Load the included
4 KiB sample if you want a disposable test. Play starts a loop through index
frame 0 and every data frame; pause/previous/next let you display a missing code.
The receiver can join anywhere. Rate and display/print sizes apply immediately;
regenerate after changing text, label, chunk size or correction level.

Print A4 grid opens the browser print dialog. Select A4, 100% scale and disable
browser headers/footers. The default is six codes per sheet (two columns, three
rows), at 65 mm including white margins. Save printable HTML creates a completely
self-contained SVG print document, requiring no scripts or adjacent files.
Open it and use the browser's Print / Save as PDF if desired.

The 4,107-byte sample uses 27 frames and five A4 sheets at default settings.
At 250 ms/frame the ideal first loop is 6.75 seconds, excluding rendering and
misses. Missed frames require another loop or manual navigation to that frame.
Save frame text is a newline-separated wire export, not a secure/encrypted file.

## Restore

Open another copy of index.html. Optionally enter the expected 16-character
payload id before scanning. Start camera and place one full QR within the white
square, including its quiet zone. The decoder locks to the first valid set.
Foreign sets and conflicting duplicates are rejected without replacing it.
Reset reader unlocks it and clears the collected frames and restored output.

You can also select cropped QR photos or paste decoded wire strings, one per
line. The photo reader expects one full code per photo; it does not segment
whole sheets. Scan printed codes one at a time. Receipt tiles and a missing-index
list track progress. Index 0 can arrive last; until then the label and original
byte length are unknown. Text is exposed only after all frames are present,
lengths match and the whole SHA-256 verifies. Download restored text preserves
the recovered UTF-8 bytes; copying from a textarea may normalize line endings.

Camera tracks stop on Stop, Reset, successful completion, page hide or tab hide.
The app asks for video only. Nothing persists across reloads.

## Module API

Keep kbqr.js and kbqr-core.js beside each other. Import from a static origin
(localhost or HTTPS); browser ES modules normally cannot import from file://.

```js
import { encode, Decoder } from './kbqr.js'

const backupLine = 'KPOPBACKUP1 ' + yourBase64
const { frames, index } = encode(backupLine, {
  chunkSize: 160,
  label: 'KPOP backup'
})

const decoder = new Decoder({ expectedId: index.id }) // optional id lock
for (const scannedText of frames.slice().reverse()) {
  const progress = decoder.add(scannedText)
  if (progress.complete) console.log(progress.text === backupLine)
}
```

`encode(text, opts)` is synchronous and returns `{ frames: string[], index }`.
`opts`: chunkSize (integer 32–1024, default 160), label (up to 80 UTF-8 bytes,
default empty). The module never interprets or decodes the KPOP base64 field.
It preserves the complete prefix, base64, spaces and line endings supplied to it.
The browser textarea follows browser newline normalization; use the module with
a file's decoded contents for exact original line endings.

`index`: format, id, checksum, byteLength, frameCount (includes index 0),
dataFrameCount, chunkSize, label. `frames[0]` is the manifest QR wire string.

`new Decoder({ expectedId? })`, `add(wireString)`, `progress()`, `reset()`.
All calls are synchronous. Progress is `{ id, received, total, missing, index,
complete, text }`. `text` is null before verified completion, including when
an empty payload is still incomplete; a verified empty payload returns ''.
`missing` includes 0. `index` is null until frame 0 is received. Invalid input
throws Error. Catch scanner errors, display progress, and keep scanning.
A rejected candidate is not committed. Earlier corrupted frames may require
resetting the set; the decoder never silently replaces a conflicting frame.
Reset retains an expectedId provided to the constructor; construct a new Decoder
to change that lock. Progress snapshots can be edited without mutating receipt
state. Index metadata is frozen.

Additional exports: `parseFrame`, `sha256(Uint8Array)`, `limits`.
Core limit: 1 MiB UTF-8, 4,096 total frames; larger chunk sizes may be necessary.
Demo limit: 64 KiB to bound print and rendering work. Invalid UTF-16 strings with
unpaired surrogates are rejected rather than silently altered.

### QR rendering / embedding

The core module has no dependencies. For the supplied browser renderer, load
vendor/qrcode.js and qr-adapter.js as classic scripts; for image decoding also
load vendor/jsQR.js. They expose `KBQR_QR`:

```js
const canvas = KBQR_QR.render(frames[0], { ecc: 'Q', size: 400 })
document.querySelector('#qr').replaceChildren(canvas)
const raw = KBQR_QR.decodeImage(imageData)
if (raw) decoder.add(raw)
```

`matrix(frame, ecc)` exposes size and dark(row, col). `svg(frame, ecc)` returns
an SVG string for printing. `decodeImage(ImageData)` returns a wire string or
null. The ES module uses the shared classic core and leaves a global KBQR
namespace; the demo also uses KBQR_QR plus vendor globals qrcode and jsQR.

QR ECC and code size are optical settings, not protocol fields. A 1024-byte
chunk fits L/M/Q but can exceed H capacity with the KBQR header. The demo
preflights the longest frame and asks you to reduce chunk size if needed. Use frame wire
strings with any conforming QR encoder/reader. The first implementation uses QR
byte mode because base64url is case-sensitive. No base64 alphabet assumptions
are made about the user's input.

## Included documentation

- FORMAT.md: wire specification and decoding requirements.
- PRIOR-ART.md: UR/txqr/qr-backup decision and print/timing rationale.
- TESTING.md and tests/results.json: actual automated evidence and limits.
- AGENT-HANDOVER.md: integration map, extension points, remaining hardware QA.
- vendor/README.md: pinned dependency provenance, licenses and checksums.

## Local tests

With Node 20+ already installed, run `node tests/run.mjs` from this folder.
No npm install is required. Tests use Node's crypto implementation to verify
SHA-256 and the vendored encoder/jsQR for real rasterized optical round-trips.
Node is a development/test tool only; not needed to use the page.

## Privacy and scope

The page makes no fetch, XHR, WebSocket, analytics, font or upload requests.
A restrictive CSP blocks connection destinations. Input and camera frames remain
in page memory. The app uses no localStorage, cookies or database. Exported files,
print queues and the host browser/OS are outside that memory-only scope.
KBQR is transport, not encryption or sender authentication. The label and
checksum are public. Validate a restored KPOP line in KPOP before applying it.
KBQR1 is not UR, txqr or qr-backup compatible; general camera apps only extract
individual frame strings. A KBQR decoder performs the reassembly.
