quadqr-js 1.5.4 → 1.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,1284 +1,1284 @@
1
- # QuadQR (QQR)
2
-
3
- <p align="center">
4
- <img src="assets/banner.png?raw=true" alt="QuadQR banner">
5
- </p>
6
-
7
- <p align="center">
8
- <strong>Normal RGBW mode by default, with an optional experimental High Density Mode using Triangle16 split cells.</strong>
9
- </p>
10
-
11
- <p align="center">
12
- <a href="https://akanshsirohi.github.io/QuadQR/demo/"><strong>Try the Live Demo</strong></a>
13
- ·
14
- <a href="https://akanshsirohi.github.io/QuadQR/documentation/"><strong>Documentation Site</strong></a>
15
- ·
16
- <a href="https://www.npmjs.com/package/quadqr-js">npm</a>
17
- ·
18
- <a href="#current-benchmark">Benchmark</a>
19
- ·
20
- <a href="#getting-started">Use the Library</a>
21
- ·
22
- <a href="https://akanshsirohi.github.io/QuadQR/documentation/format.html">Format Reference</a>
23
- ·
24
- <a href="https://akanshsirohi.github.io/QuadQR/documentation/specification.html">Technical Specification</a>
25
- </p>
26
-
27
- **QuadQR** is an experimental open-source 2D matrix code that uses RGBW color states instead of the two states used by a traditional black-and-white QR module. Normal mode stores **2 bits per RGBW data cell**. This experimental branch also includes an optional **High Density Mode**, implemented with Triangle16 split cells, that stores **4 raw bits per body cell**. High Density Mode is disabled by default.
28
-
29
- Default RGBW mapping:
30
-
31
- | Color | Bits |
32
- |---|---|
33
- | Red | `00` |
34
- | Green | `01` |
35
- | Blue | `10` |
36
- | White | `11` |
37
-
38
- That gives QuadQR a four-symbol alphabet and a raw density of **2 bits per data cell**.
39
-
40
- ### Experimental High Density Mode
41
-
42
- When `highDensity: true` is enabled, Triangle16 splits each payload cell along one fixed `/` diagonal. The upper-left and lower-right triangles independently use Red, Green, Blue, or White:
43
-
44
- ```text
45
- 4 colors × 4 colors = 16 states
46
- log2(16) = 4 bits per data cell
47
- ```
48
-
49
- The protected bootstrap/header deliberately remains solid-color even in High Density Mode, while the ECC-protected body uses the full 16-state alphabet. This sacrifices a small amount of theoretical capacity to make mode detection and damaged-camera recovery more reliable. Finder, timing, alignment, calibration, ECC, CRC, and matrix dimensions remain unchanged.
50
-
51
- ```js
52
- const code = encodeText("High-density QuadQR", {
53
- ecc: "M",
54
- highDensity: true
55
- });
56
- ```
57
-
58
- Image and camera scanning automatically detect High Density Mode, so a separate scanner mode is not required. High Density Mode is experimental and should be stress-tested at the intended physical size and camera distance.
59
-
60
- See the [High Density Mode documentation](https://akanshsirohi.github.io/QuadQR/documentation/high-density.html) for the physical cell mapping, protected-header strategy, scanner sampling rules, and reliability caveats.
61
-
62
- QuadQR keeps the parts that make QR-like codes practical, such as a square matrix, finder patterns, timing structures, error correction, masking, perspective recovery, and camera scanning, while experimenting with a higher-density color-based data layer.
63
-
64
- > **Important:** QuadQR is an experimental custom format. It is **not ISO QR Code**, and normal QR scanner apps cannot decode it.
65
-
66
- ## Examples
67
-
68
- Below are QuadQR symbols generated with different error-correction profiles. They use the same four-state RGBW data alphabet while varying the amount of space dedicated to Reed-Solomon protection.
69
-
70
- <table>
71
- <tr>
72
- <td align="center">
73
- <img src="assets/quadqr-normal.png?raw=true" alt="QuadQR example using ECC profile L" width="320"><br>
74
- <sub><strong>QuadQR</strong> · Normal (v3)</sub>
75
- </td>
76
- <td align="center">
77
- <img src="assets/quadqr-high-density.png?raw=true" alt="QuadQR example using ECC profile M" width="320"><br>
78
- <sub><strong>QuadQR</strong> · Higher Data Density (v2)</sub>
79
- </td>
80
- </tr>
81
- </table>
82
-
83
- <p align="center">
84
- <a href="https://akanshsirohi.github.io/QuadQR/demo/"><strong>Generate, scan, and benchmark QuadQR in your browser →</strong></a>
85
- </p>
86
-
87
- ---
88
-
89
- ## Why I built QuadQR
90
-
91
- I have always found QR codes fascinating. They are compact, practical, extremely well optimized, and they have continued to work reliably for decades.
92
-
93
- But one thing caught my attention: the basic visual data representation is still binary. A module is essentially one of two states, black or white, representing `0` or `1`.
94
-
95
- That made me wonder:
96
-
97
- > If modern cameras, displays, image processing, and computing are much more capable today, can a QR-like matrix code reliably use more than two visible states and store more information in the same area?
98
-
99
- I started researching the idea and working through the math. Four reliably distinguishable states are especially interesting because:
100
-
101
- ```text
102
- log2(4) = 2 bits
103
- ```
104
-
105
- So a four-state module can represent exactly two bits without requiring fractional-bit packing.
106
-
107
- That eventually became QuadQR.
108
-
109
- The current format uses **Red, Green, Blue, and White**, giving every data cell four possible states while keeping a square module grid that is easy to generate, sample, correct for perspective, and scan.
110
-
111
- This project is an experiment in exploring how far a QR-inspired design can be pushed with modern hardware and software.
112
-
113
- ### Development note
114
-
115
- I designed the format, researched the approach, iterated on the encoding structure, and built the project as an independent experiment. **OpenAI Codex** helped significantly with some of the more complex implementation work, including parts of the codec, error correction, image-processing logic, testing, and optimization.
116
-
117
- ---
118
-
119
- ## What makes QuadQR different?
120
-
121
- A traditional QR data module has two possible states:
122
-
123
- ```text
124
- Black
125
- White
126
- ```
127
-
128
- That gives:
129
-
130
- ```text
131
- log2(2) = 1 bit per module
132
- ```
133
-
134
- QuadQR uses four data states:
135
-
136
- ```text
137
- Red
138
- Green
139
- Blue
140
- White
141
- ```
142
-
143
- That gives:
144
-
145
- ```text
146
- log2(4) = 2 bits per data cell
147
- ```
148
-
149
- So at the raw data-cell level:
150
-
151
- | Format | States per data cell | Raw information |
152
- |---|---:|---:|
153
- | Binary QR | 2 | 1 bit |
154
- | QuadQR RGBW | 4 | 2 bits |
155
- | QuadQR High Density Mode (Triangle16) | 16 | 4 bits |
156
-
157
- This is a **2× raw symbol-density advantage**.
158
-
159
- ### Spectrum ECC 2.0: confidence-aware + soft decoding
160
-
161
- QuadQR uses the fact that a color scanner knows more than only the winning state. For each sampled data cell it now retains the selected state, a confidence score, and a bounded second hypothesis. Triangle16 does the same after classifying both color regions.
162
-
163
- The first recovery layer is still confidence-aware GF(256) Reed-Solomon: if normal hard-decision decoding fails, the least-confident byte symbols can be promoted to **known erasures**, allowing the existing parity budget to be spent more efficiently.
164
-
165
- **Spectrum ECC 2.0** adds a bounded soft-decision fallback. If hard decoding and erasure decoding still fail, the decoder tries the second hypothesis for a small number of the least-confident data cells, first singly and then in tightly bounded pairs. Every candidate must still pass the normal Reed-Solomon checks and final CRC-32, so soft decoding does not relax integrity validation or add parity overhead.
166
-
167
- QuadQR also applies a deterministic **spectral-spatial interleaver** after ECC. Neighboring logical codeword cells are scattered across distant physical data positions, so a scratch, glare patch, shadow, or localized print defect tends to affect many different RS symbols instead of destroying a contiguous run. The permutation is reversible and consumes **zero extra data cells**.
168
-
169
- These features do **not** change the RGBW alphabet, module count, ECC parity count, or payload-capacity calculation. They improve how the existing redundancy is used.
170
-
171
- However, raw cell density is not the same thing as final user-payload capacity. Finder patterns, timing structures, calibration cells, headers, CRC, masking, error correction, and other reserved cells all consume space.
172
-
173
- That is why QuadQR includes its own benchmark instead of assuming that every completed symbol will contain exactly twice the payload of a standard QR code.
174
-
175
- ---
176
-
177
- ## Secure Payload v1
178
-
179
- QuadQR now includes an **optional authenticated-encryption layer**. It sits above the matrix codec, so normal QuadQR symbols keep the same RGBW mapping, Spectrum ECC, scanner, and capacity behavior.
180
-
181
- Two security modes are supported:
182
-
183
- | Mode | Key source | Best suited for |
184
- |---|---|---|
185
- | Password | PBKDF2-HMAC-SHA-256 derives a 256-bit key from a password | Human-to-human protected payloads |
186
- | Raw 256-bit key | Exact 32-byte random key supplied by the application | Apps, tickets, provisioning, enterprise scanners |
187
-
188
- Both modes use **AES-256-GCM**, which provides encryption and authentication. A wrong password/key or modified encrypted payload fails decryption instead of returning silent garbage.
189
-
190
- Password mode currently defaults to **600,000 PBKDF2-HMAC-SHA-256 iterations** with a random 16-byte salt. Raw-key mode avoids password-KDF work and automatically stores an 8-byte SHA-256 key fingerprint as a non-secret key ID unless disabled or overridden. The actual raw key is never embedded in the symbol.
191
-
192
- The security envelope is versioned independently from the QuadQR matrix format so a future release can add another KDF or encryption mode without redesigning the RGBW/ECC layer.
193
-
194
- Security is intentionally opt-in. Unencrypted codes remain the default.
195
-
196
- The browser demo supports secure scanning in **both scanner paths**:
197
-
198
- - **Image scanner:** upload/exported images and photographs are decoded normally; secure symbols are detected automatically and show the matching password or raw-key decrypt control inline.
199
- - **Camera scanner:** live camera frames use the same secure-aware decode path. When an encrypted symbol is verified, scanning stops, the result is marked **Secure QR**, and plaintext is revealed only after successful authenticated decryption.
200
-
201
- The scanner never expects the secret to be embedded in the QuadQR itself. Raw-key symbols expose only their non-secret key ID/fingerprint.
202
-
203
- ---
204
-
205
- ## Compression, signatures, and robustness tooling
206
-
207
- QuadQR keeps the application payload simple: **text stays text and bytes stay bytes**. There is no public payload-type registry to maintain. Compression and signing metadata are added only as internal implementation details when those features are enabled.
208
-
209
- ```js
210
- import {
211
- encodeText,
212
- encodeSignedText,
213
- generateSigningKeyPair,
214
- decodeMatrix,
215
- verifyDecodedSignature
216
- } from "quadqr-js";
217
-
218
- // Auto compression is zero-overhead when it does not help.
219
- const compressed = encodeText("repeated repeated repeated", {
220
- compression: "auto",
221
- ecc: "M"
222
- });
223
-
224
- // Smart is CPU-heavy and only escalates when stronger compression can
225
- // realistically reduce the physical QuadQR version.
226
- const smart = encodeText(largeStructuredText, { compression: "smart" });
227
-
228
- // Explicit codecs can choose their encoder level.
229
- const lz = encodeText(largeStructuredText, {
230
- compression: "lz",
231
- compressionLevel: 9
232
- });
233
- const brotli = encodeText("hello ".repeat(1000), {
234
- compression: "brotli",
235
- compressionLevel: 11
236
- });
237
- const deflate = encodeText(largeStructuredText, {
238
- compression: "deflate",
239
- compressionLevel: 9
240
- });
241
-
242
- const keys = await generateSigningKeyPair();
243
- const signed = await encodeSignedText("verified offline", {
244
- compression: "auto",
245
- privateKey: keys.privateKey,
246
- keyId: keys.keyId
247
- });
248
-
249
- const decoded = decodeMatrix(signed.matrix);
250
- const verified = await verifyDecodedSignature(decoded, {
251
- publicKey: keys.publicKey
252
- });
253
- console.log(verified.signatureVerified); // true
254
- console.log(verified.signatureTrusted); // true
255
- ```
256
-
257
- Compression modes are `none`, `auto`, `smart`, `brotli`, `deflate`, and `lz`. `auto` is the fast default: it compares LZ level 6, DEFLATE level 6, and Brotli quality 6 once, including envelope overhead, and keeps the smallest final representation. `smart` is an opt-in CPU-heavy mode. It starts with the same balanced pass, checks the resulting QuadQR version, and only escalates to DEFLATE 8 / Brotli 9 and then DEFLATE 9 / Brotli 11 when a smaller physical version is realistically reachable; LZ stays at its default level in Auto/Smart. Explicit `lz` accepts `compressionLevel: 1..9` with default 6, explicit `deflate` accepts `1..9` with default 6, and explicit `brotli` accepts `0..11` with default 11. Compression level is an encoder-only setting and is not stored in the symbol because the decoder does not need it. All codecs are synchronous and bundled with QuadQR, so the same compression path works in browsers and server-side Node.js without `node:zlib`, `CompressionStream`, or a runtime dependency. The demo keeps these CPU-heavy operations in module Web Workers so the browser UI remains responsive. See the [compression documentation](https://akanshsirohi.github.io/QuadQR/documentation/compression.html) for the exact Smart escalation policy and level API. Ed25519 signing stores the signature plus an optional compact `keyId`; the public verification key stays outside the QuadQR by default. Applications do not need to choose or maintain content types.
258
-
259
- Signing can also be composed with Secure Payload. QuadQR compresses if requested, signs the normal payload with the private key, then encrypts the protected bytes with AES-256-GCM. A verifier supplies the trusted public key separately, or resolves it from `keyId`.
260
-
261
- The renderer supports an explicit `mode: "print"`. Print mode uses darker print-safe RGB defaults and prefers Classic solid modules, while `quietZone` behaves exactly as it does in screen mode. Four modules remains the recommended default. `getPrintGuidance()` converts a chosen physical size into module millimeters/pixels so print layouts can be checked before production testing.
262
-
263
- Centered logos support `size: "auto"`, which estimates a conservative ECC-aware ratio from code utilization and rendering choices. `findMaxSafeLogoSize()` can additionally probe ImageData output and empirically search for the largest size that still decodes.
264
-
265
- Scanner results include normalized diagnostics such as `confidence`, `geometryConfidence`, `calibrationConfidence`, `structureConfidence`, `eccUtilization`, and `correctedErrors`. Set `debug: true` or call `debugScanImageData()` to inspect finder/geometry candidates, the sampled matrix, color-confidence data, ECC stages, and the stage that failed.
266
-
267
- For regression and demo testing, `runImageStressTest()` / `assessScanability()` apply deterministic blur, brightness, exposure, shadow, contrast, perspective, JPEG-like artifacts, and downscaling. The browser demo exposes the same tools as an interactive stress-test lab and shows an overall scanability rating.
268
-
269
- The interoperability details are covered in the [technical specification](https://akanshsirohi.github.io/QuadQR/documentation/specification.html).
270
-
271
- ---
272
-
273
- ## Current benchmark
274
-
275
- Run:
276
-
277
- ```bash
278
- npm run benchmark
279
- ```
280
-
281
- The benchmark compares QuadQR with standard QR **at the same matrix dimensions** using byte-mode QR reference capacities.
282
-
283
- Current representative results at the project's `M` ECC profile:
284
-
285
- | Version | Matrix | QuadQR payload | Standard QR payload | Gain | Ratio |
286
- |---:|---:|---:|---:|---:|---:|
287
- | 1 | 21×21 | **24 B** | 14 B | +10 B | **1.71×** |
288
- | 2 | 25×25 | **48 B** | 26 B | +22 B | **1.85×** |
289
- | 5 | 37×37 | **227 B** | 84 B | +143 B | **2.70×** |
290
- | 10 | 57×57 | **630 B** | 213 B | +417 B | **2.96×** |
291
- | 20 | 97×97 | **1992 B** | 666 B | +1326 B | **2.99×** |
292
- | 30 | 137×137 | **4054 B** | 1370 B | +2684 B | **2.96×** |
293
- | 40 | 177×177 | **6858 B** | 2331 B | +4527 B | **2.94×** |
294
-
295
- ### How to read this table
296
-
297
- For example:
298
-
299
- ```text
300
- Version 10
301
- Matrix: 57×57
302
-
303
- QuadQR: 630 bytes
304
- Standard QR: 213 bytes
305
-
306
- Gain: 417 bytes
307
- Ratio: 2.96×
308
- ```
309
-
310
- This means that under the benchmark's current assumptions, a 57×57 QuadQR symbol can carry 630 user-payload bytes, while the standard QR byte-mode reference at the same dimensions and `M` label carries 213 bytes.
311
-
312
- ### Important benchmark warning
313
-
314
- The letters `L`, `M`, `Q`, and `H` in QuadQR are **project-defined ECC profiles**.
315
-
316
- They do **not** currently claim the same standardized recovery percentages as ISO QR Code ECC levels with the same letters.
317
-
318
- Therefore:
319
-
320
- > The capacity benchmark is a same-dimension and same-label comparison, not yet an equal-damage-tolerance comparison.
321
-
322
- The published baseline benchmark uses **normal RGBW mode at exactly 2 bits per data cell**. Ratios approaching ~3× in that RGBW usable-payload benchmark are caused by differences in total structural and ECC overhead between the two formats, not because an RGBW QuadQR cell contains 3 bits. The experimental **Triangle16** profile is a separate 4-bit/body-cell mode and should be benchmarked independently because its real-world advantage depends on camera resolution, perspective, blur, resizing, and print quality.
323
-
324
- A future goal is to add **equal-reliability benchmarking**, where QuadQR and standard QR are compared after calibrating both to similar real-world damage recovery.
325
-
326
- ---
327
-
328
- ## Performance benchmark
329
-
330
- The benchmark also measures direct codec performance.
331
-
332
- It reports:
333
-
334
- - payload size;
335
- - automatically selected version;
336
- - matrix dimensions;
337
- - mean encode time;
338
- - 95th percentile encode time;
339
- - mean decode time;
340
- - 95th percentile decode time.
341
-
342
- Example output:
343
-
344
- ```text
345
- payload_B matrix version encode_mean encode_p95 decode_mean decode_p95
346
- 24 21x21 v1 ...
347
- 128 33x33 v4 ...
348
- 512 53x53 v9 ...
349
- 1024 73x73 v14 ...
350
- 2048 101x101 v21 ...
351
- ```
352
-
353
- These timings measure the **matrix codec**, not the complete camera-scanning pipeline.
354
-
355
- Camera scanning additionally includes finder detection, perspective correction, calibration, module sampling, and color classification.
356
-
357
- For more stable local timing results, use more iterations:
358
-
359
- ```bash
360
- npm run benchmark -- --iterations=500
361
- ```
362
-
363
- For machine-readable results:
364
-
365
- ```bash
366
- npm run --silent benchmark -- --json
367
- ```
368
-
369
- ---
370
-
371
- ## Version 1 optimization
372
-
373
- Small symbols have a difficult tradeoff because fixed metadata and ECC consume a much larger fraction of the matrix.
374
-
375
- The original 21×21 QuadQR design could not fit a valid `M` payload because the protected framing itself was larger than the available data area.
376
-
377
- Version 1 now uses a dedicated compact small-symbol profile.
378
-
379
- At `M`:
380
-
381
- ```text
382
- 21×21 QuadQR
383
- 24 bytes user payload
384
- ```
385
-
386
- compared with:
387
-
388
- ```text
389
- 21×21 standard QR reference
390
- 14 bytes byte-mode payload
391
- ```
392
-
393
- Version 2 and above continue to use the normal framing and ECC structure.
394
-
395
- ---
396
-
397
- ## Encoding pipeline
398
-
399
- The current QuadQR pipeline is:
400
-
401
- ```text
402
- payload bytes
403
- ↓
404
- optional Secure Payload v1 (AES-256-GCM)
405
- ↓
406
- header + CRC-32
407
- ↓
408
- GF(256) Reed-Solomon error correction
409
- ↓
410
- interleaved byte codewords
411
- ↓
412
- split every encoded byte into four 2-bit values
413
- ↓
414
- zero-overhead spectral-spatial permutation
415
- ↓
416
- 00 / 01 / 10 / 11
417
- ↓
418
- Red / Green / Blue / White
419
- ↓
420
- quaternary masking at physical positions
421
- ↓
422
- square QuadQR matrix
423
- ```
424
-
425
- Because four states map naturally to two bits, QuadQR does not require base-3 conversion or fractional-bit packing.
426
-
427
- One encoded byte maps naturally to four data cells:
428
-
429
- ```text
430
- 8 bits ÷ 2 bits/cell = 4 cells
431
- ```
432
-
433
- ---
434
-
435
- ## Symbol structure
436
-
437
- QuadQR currently uses:
438
-
439
- - a square overall symbol;
440
- - square modules;
441
- - three 7×7 black-and-white finder patterns;
442
- - black-and-white timing structures;
443
- - distributed 5×5 nested alignment eyes on versions that use alignment patterns;
444
- - RGB calibration swatches;
445
- - structural black/white references;
446
- - a two-column zig-zag physical data-position path;
447
- - deterministic spectral-spatial interleaving of logical codeword cells;
448
- - four-state masking;
449
- - GF(256) Reed-Solomon ECC with error + erasure decoding;
450
- - per-module RGBW confidence for scanner-assisted erasures;
451
- - CRC-32 integrity verification;
452
- - optional versioned AES-256-GCM Secure Payload envelope.
453
-
454
- White is a valid data state.
455
-
456
- QuadQR still uses exactly three large finder patterns, just like standard QR. Starting at version 2, Format v6 alignment markers follow the standard QR version-dependent center schedule and every scheduled alignment marker is a full 5×5 nested black/white/black eye. These distributed references give the detector stronger anchors for high-version and projectively distorted symbols. Version 1 keeps one QuadQR-specific 5×5 bottom-right bootstrap marker because it otherwise would have no fourth projective reference. The decoder retains the compact 3×3 secondary-marker profile used by Format v5 so existing v5 symbols remain readable.
457
-
458
- The decoder does not treat a white-looking area as automatically empty. It reconstructs the matrix geometry first and then determines whether a sampled position is structural or data.
459
-
460
- ---
461
-
462
- ## Color mapping
463
-
464
- The current mapping is intentionally simple:
465
-
466
- ```text
467
- 00 → Red
468
- 01 → Green
469
- 10 → Blue
470
- 11 → White
471
- ```
472
-
473
- Ideal display-space reference colors are conceptually:
474
-
475
- ```text
476
- Red → (255, 0, 0)
477
- Green → ( 0, 255, 0)
478
- Blue → ( 0, 0, 255)
479
- White → (255, 255, 255)
480
- ```
481
-
482
- Real camera input is not expected to match those exact values.
483
-
484
- QuadQR includes calibration and nearest-color classification so the scanner can work with observed colors after lighting, camera processing, perspective changes, and other image transformations. The clean-frame path starts with a streaming 1:1:3:1:1 finder detector on the RGB value channel, direct cross-checks, local-threshold fallback, and directional module-size/version estimation. Once the three finder eyes form a valid geometry, QuadQR immediately tries the highest-ranked perspective candidate instead of waiting for every lower-ranked geometry. Near-front-facing symbols can use a three-finder affine fast path, while projectively distorted symbols continue through the full homography/alignment solver. Format v6 distributed 5×5 alignment eyes provide stronger local perspective anchors, and a cheap nested-eye locator is tried before the broader alignment search. If a steep angle leaves exactly two strong finder patterns, the bounded looser third-finder pass still runs before heavier color recovery. Only after geometry/color decoding still fails does QuadQR progressively try the existing stronger recovery, including white balancing, a 3×4 affine color-calibration model learned from the known black/white/R/G/B references, spatial normalization, Auto Tone / Auto Contrast / QuadQR Auto Color-style enhancement, soft-decision ECC, and bounded sub-module geometry refinement. For live video, normal detection works from the CSS-visible `object-fit: cover` camera region at a 640 px working dimension. A bounded 960 px retry is reserved for difficult dense frames that already show useful finder evidence, rather than being part of the ordinary camera loop. If finder geometry is already strong but color decoding fails, a QR-only rectified pixel enhancement retry is performed immediately; whole-frame enhancement remains reserved for harder locator failures.
485
-
486
- ---
487
-
488
- ## Reed-Solomon error correction
489
-
490
- QuadQR uses Reed-Solomon over:
491
-
492
- ```text
493
- GF(2^8) = GF(256)
494
- ```
495
-
496
- Primitive polynomial:
497
-
498
- ```text
499
- x^8 + x^4 + x^3 + x^2 + 1
500
- 0x11d
501
- ```
502
-
503
- One Reed-Solomon symbol is one byte.
504
-
505
- Since one byte becomes four QuadQR data cells, ECC symbols stay naturally aligned with the RGBW representation.
506
-
507
- ### Versions 2 through 40
508
-
509
- | Profile | Parity bytes per body block | Correctable byte symbols per block |
510
- |---|---:|---:|
511
- | L | 12 | 6 |
512
- | M | 24 | 12 |
513
- | Q | 36 | 18 |
514
- | H | 48 | 24 |
515
-
516
- ### Version 1 compact profile
517
-
518
- | Profile | Body parity bytes | Correctable body byte symbols |
519
- |---|---:|---:|
520
- | L | 4 | 2 |
521
- | M | 8 | 4 |
522
- | Q | 12 | 6 |
523
- | H | 16 | 8 |
524
-
525
- Version 1 also uses a compact 4-byte logical header protected by 4 Reed-Solomon parity bytes, correcting up to 2 damaged header byte symbols.
526
-
527
- CRC-32 remains the final integrity check.
528
-
529
- ---
530
-
531
- ## Scanner pipeline
532
-
533
- The image/camera scanner currently follows this general pipeline:
534
-
535
- ```text
536
- camera or image RGB frame
537
- ↓
538
- grayscale structural analysis
539
- ↓
540
- finder candidate detection
541
- ↓
542
- version hypothesis
543
- ↓
544
- primary alignment search
545
- ↓
546
- initial homography / perspective correction
547
- ↓
548
- distributed alignment-grid validation
549
- ↓ (when geometry is plausible but imperfect)
550
- secondary alignment multi-point homography refinement
551
- ↓
552
- module-grid reconstruction
553
- ↓
554
- fast observed-RGB decode attempt
555
- ↓ (only if needed)
556
- white balance + affine cross-channel calibration + spatial normalization
557
- ↓ (only if still needed)
558
- Auto Tone / Auto Contrast / QuadQR Auto Color-style recovery
559
- ↓ (only if still needed)
560
- sub-module geometry refinement
561
- ↓
562
- RGB + structural black/white calibration
563
- ↓
564
- RGBW/Triangle16 classification + confidence + second hypothesis
565
- ↓
566
- unmasking
567
- ↓
568
- reverse spectral-spatial permutation
569
- ↓
570
- protected header Reed-Solomon hard decode
571
- ↓
572
- confidence-guided erasure retry when needed
573
- ↓
574
- bounded Spectrum ECC 2.0 soft-hypothesis retry when needed
575
- ↓
576
- body deinterleaving + error/erasure Reed-Solomon decode
577
- ↓
578
- CRC-32 verification
579
- ↓
580
- payload
581
- ```
582
-
583
- An axis-aligned fallback is also available for clean generated images and simple inputs.
584
-
585
- ---
586
-
587
- ## Rendering styles
588
-
589
- QuadQR keeps visual styling separate from the wire format. The encoded matrix is unchanged, so applications can choose a renderer without creating a new barcode format.
590
-
591
- The current render profiles are:
592
-
593
- | Style | Behavior |
594
- |---|---|
595
- | `classic` | Original fully solid square modules. |
596
- | `depth` | Deterministically mixes normal and lightly faded data tiles with subtle edge shading for a layered/3D appearance. |
597
- | `soft` | Uses rounded data tiles for a softer visual treatment. |
598
- | `inset` | Uses narrow recessed edge lighting while preserving the exact encoded center color. |
599
-
600
- Finder patterns, timing structures, alignment markers, and calibration cells stay **square and fully solid** in styled output so the visual treatment does not weaken the scanner's geometric/color references.
601
-
602
- The `inset` style preserves the exact encoded R/G/B/W color around the center of every data module. Its recessed shading is confined to a narrow outer band, and white data modules remain pure white. This matches the scanner's center-sampling strategy and avoids visual effects bleeding into neighboring cells.
603
-
604
- ```js
605
- renderToCanvas(code, canvas, {
606
- imageSize: 720,
607
- quietZone: 4,
608
- style: "inset" // classic | depth | soft | inset
609
- });
610
- ```
611
-
612
- The styling is deterministic. Generating the same matrix with the same style produces the same visual tile treatment rather than changing randomly on every render.
613
-
614
- `imageSize` sets the exact square output size in pixels. When neither `imageSize` nor `moduleSize` is supplied, QuadQR renders at **720 × 720 px** by default. `moduleSize` remains available as the lower-level legacy sizing control; when `imageSize` is supplied, the exact image size takes precedence.
615
-
616
- ### Logo overlays, quiet zones, and SVG export
617
-
618
- The renderer can place a centered logo over the symbol. Transparent pixels in the logo stay transparent, so the QuadQR modules remain visible through those areas. Enable `clearBackground` when you want a clean padded white area behind the logo instead.
619
-
620
- ```js
621
- const logoImage = new Image();
622
- logoImage.src = "/brand-mark.png";
623
- await logoImage.decode();
624
-
625
- renderToCanvas(code, canvas, {
626
- imageSize: 720,
627
- quietZone: 6,
628
- style: "classic",
629
- logo: {
630
- source: logoImage,
631
- size: 0.12,
632
- clearBackground: true,
633
- padding: 0.65,
634
- radius: 0.8
635
- }
636
- });
637
- ```
638
-
639
- `quietZone` is measured in modules and can be set to `0` or increased for print/camera use. Four modules remains the recommended default.
640
-
641
- SVG uses the same matrix, palette, styles, quiet-zone size, and logo geometry:
642
-
643
- ```js
644
- import { renderToSVG } from "quadqr-js";
645
-
646
- const svg = renderToSVG(code, {
647
- imageSize: 720,
648
- quietZone: 4,
649
- logo: {
650
- source: "data:image/png;base64,...",
651
- size: 0.12,
652
- clearBackground: true
653
- }
654
- });
655
- ```
656
-
657
- Logo overlays intentionally consume some ECC margin because they cover encoded cells. Keep logos conservative, especially with `L`/`M` ECC. The browser demo verifies the final rendered image before enabling downloads.
658
-
659
- ---
660
-
661
- ## Demo
662
-
663
- **Live demo:** https://akanshsirohi.github.io/QuadQR/demo/
664
-
665
- The browser demo runs directly on GitHub Pages and is split into separate views so the interface does not become overloaded. The generator keeps only payload, version, and ECC visible by default; optional capabilities are grouped into independent advanced accordions. CPU-heavy encoding/compression, rendered-image verification, uploaded-image scanning, scanability tests, Reliability Lab runs, perspective sweeps, and codec benchmarks are dispatched to module Web Workers. Generation shows explicit progress instead of freezing the browser, repeated UI changes are coalesced, and render-only changes reuse the already encoded matrix instead of recompressing the payload.
666
-
667
- ### Generator & Image Scanner
668
-
669
- Use this tab to:
670
-
671
- - enter text/data;
672
- - generate a QuadQR code;
673
- - select ECC;
674
- - open Output & Rendering controls for image size, quiet zone, style, and print mode;
675
- - open the Center Logo accordion for branding;
676
- - enable internal automatic compression without choosing a payload type;
677
- - optionally add an Ed25519 signature;
678
- - optionally encrypt using a password or raw 256-bit key;
679
- - inspect version and capacity;
680
- - download PNG or SVG output;
681
- - scan an uploaded image.
682
-
683
- ### Camera Scanner
684
-
685
- The live camera scanner has its own dedicated tab.
686
-
687
- This keeps the generator interface lightweight and prevents an always-visible video element from making the main page bulky.
688
-
689
- The camera stream automatically stops when you leave the camera tab.
690
-
691
- ### Benchmark
692
-
693
- The benchmark tab provides an easier visual view of:
694
-
695
- - capacity by matrix size;
696
- - comparison with standard QR;
697
- - byte gain;
698
- - capacity ratio;
699
- - codec timing;
700
- - capacity planning;
701
- - interactive stress testing.
702
-
703
- Benchmark tools are also separated into accordions so the page stays compact until a tool is needed.
704
-
705
- ---
706
-
707
- ## New API highlights
708
-
709
- ```js
710
- import {
711
- encodeText,
712
- encodeUint8Array,
713
- decodeUint8Array,
714
- renderToImageData,
715
- assessScanability,
716
- getPrintGuidance
717
- } from "quadqr-js";
718
-
719
- const binary = encodeUint8Array(new Uint8Array([1, 2, 3, 4]), { ecc: "M" });
720
- const bytes = decodeUint8Array(binary.matrix);
721
-
722
- const code = encodeText("Hello hello hello hello", {
723
- compression: "auto",
724
- ecc: "Q"
725
- });
726
-
727
- const printImage = renderToImageData(code, {
728
- imageSize: 1200,
729
- mode: "print",
730
- logo: { source: logoImageData, size: "auto", clearBackground: true }
731
- });
732
-
733
- console.log(getPrintGuidance(code, { physicalSizeMm: 45, dpi: 300 }));
734
- console.log(assessScanability(code, { imageSize: 480 }));
735
- ```
736
-
737
- ---
738
-
739
- ## Getting started
740
-
741
- QuadQR can be used as an npm package, directly from a CDN, or from this repository.
742
-
743
- ### Requirements
744
-
745
- - **Node.js 20.19+** for Node.js and CommonJS usage
746
- - npm or another Node package manager for installation
747
- - A modern browser for the demo and documentation site
748
- - HTTPS or localhost for browser camera access
749
-
750
- ### Install from npm
751
-
752
- ```bash
753
- npm install quadqr-js
754
- ```
755
-
756
- Core usage:
757
-
758
- ```js
759
- import { encodeText, decodeMatrix } from "quadqr-js";
760
-
761
- const code = encodeText("Hello from QuadQR", { ecc: "M" });
762
- const result = decodeMatrix(code.matrix);
763
- console.log(result.text);
764
- ```
765
-
766
- Node PNG/SVG usage:
767
-
768
- ```js
769
- import { encodeText } from "quadqr-js";
770
- import { savePNG, saveSVG, scanFile } from "quadqr-js/node";
771
-
772
- const code = encodeText("Generated on Node.js");
773
- await savePNG(code, "quadqr.png", { imageSize: 720, quietZone: 4 });
774
- await saveSVG(code, "quadqr.svg", { imageSize: 720, quietZone: 4 });
775
-
776
- const result = await scanFile("quadqr.png");
777
- console.log(result.text);
778
- ```
779
-
780
- ### CDN / script tag
781
-
782
- The `quadqr-js` package can be loaded directly from npm-backed CDNs:
783
-
784
- ```html
785
- <script src="https://cdn.jsdelivr.net/npm/quadqr-js@1.5.1/dist/quadqr.min.js"></script>
786
- <script>
787
- const code = QuadQR.encodeText("Hello from a script tag");
788
- </script>
789
- ```
790
-
791
- The same file is available through unpkg. Pin an exact version in production.
792
-
793
- ### CLI
794
-
795
- Encode and decode directly from the terminal:
796
-
797
- ```bash
798
- npx quadqr-js encode "Hello QuadQR" -o hello.png
799
- npx quadqr-js decode hello.png
800
- ```
801
-
802
- Compressed, print-safe, and signed output are also available:
803
-
804
- ```bash
805
- npx quadqr-js encode "repeat repeat repeat" --compression auto -o compressed.svg
806
- npx quadqr-js encode "Print me" --print -o print.svg
807
- npx quadqr-js signkeygen -o signing-key.json
808
- npx quadqr-js encode "Verified ticket" --sign-key signing-key.json -o signed.png
809
- npx quadqr-js decode signed.png --verify-key signing-key.json
810
- ```
811
-
812
- Password-protected payloads use the same commands:
813
-
814
- ```bash
815
- npx quadqr-js encode "Private data" --password "my-password" -o secure.png
816
- npx quadqr-js decode secure.png --password "my-password"
817
- ```
818
-
819
- See the [CLI reference](https://akanshsirohi.github.io/QuadQR/documentation/cli.html) for all CLI options, including compression, raw 256-bit key mode, signing, print mode, and scanner diagnostics.
820
-
821
- ### Run from source
822
-
823
- ```bash
824
- git clone https://github.com/akanshsirohi/QuadQR.git
825
- cd QuadQR
826
- npm install
827
- ```
828
-
829
- Run the complete test suite:
830
-
831
- ```bash
832
- npm test
833
- ```
834
-
835
- Build the distributable browser, Node.js, CDN, and WASM files:
836
-
837
- ```bash
838
- npm run build
839
- ```
840
-
841
- `npm run build` verifies the checked-in WASM binary against `wasm-src/quadqr_core.c`, the WASM compiler flags, and the recorded binary hash. If everything matches, the verified prebuilt binary is reused. If the C source or build flags changed, the build automatically recompiles WASM with LLVM/Clang. If WASM is stale and `clang` is unavailable, the build fails instead of silently shipping an old binary.
842
-
843
- To force only the WASM rebuild:
844
-
845
- ```bash
846
- npm run build:wasm
847
- ```
848
-
849
- A forced WASM rebuild requires `clang` in `PATH`. No Rust, Cargo, or Emscripten toolchain is required. The build records its verification data in `wasm/quadqr-core.build.json`.
850
-
851
- Start the interactive demo:
852
-
853
- ```bash
854
- npm start
855
- ```
856
-
857
- Start the documentation site:
858
-
859
- ```bash
860
- npm run docs
861
- ```
862
-
863
- Run benchmarks:
864
-
865
- ```bash
866
- npm run benchmark
867
- ```
868
-
869
- ### Optional WASM acceleration
870
-
871
- The package ships a prebuilt WASM helper but never requires it. It accelerates CRC-32 plus the scanner's RGBA-to-grayscale, Otsu-threshold, and binary finder preprocessing hot path.
872
-
873
- ```js
874
- import { initWasm } from "quadqr-js";
875
-
876
- await initWasm();
877
- ```
878
-
879
- If WASM cannot load, the normal JavaScript codec/scanner remains available. For camera or repeated image scanning, initialize WASM once during application startup.
880
-
881
- ---
882
-
883
- ## Package entry points
884
-
885
- | Import | Purpose |
886
- |---|---|
887
- | `quadqr-js` | Runtime-neutral core, secure payloads, rendering, scanning, optional WASM |
888
- | `quadqr-js/browser` | Browser ESM entry |
889
- | `quadqr-js/node` | Node core plus PNG/file/buffer helpers |
890
- | `quadqr-js/benchmark` | Capacity and codec benchmark helpers |
891
- | `quadqr-js/quadqr.min.js` | Classic browser global/CDN bundle |
892
-
893
- The Node PNG path is dependency-free. For JPEG, WebP, or AVIF input, the Node adapter can use `sharp` when the consuming application already has it installed.
894
-
895
- ---
896
-
897
- ## Repository layout
898
-
899
- ```text
900
- library/
901
- quadqr.js Core encoder, decoder, renderer, scanner, and public API
902
- security.js Secure Payload v1 encryption and key handling
903
- reed-solomon.js GF(256) Reed-Solomon implementation
904
- geometry.js Version, size, and alignment geometry
905
- vision.js Finder detection, perspective correction, and color sampling
906
- node.js Node PNG/file/buffer adapters
907
- wasm.js Optional prebuilt WASM loader
908
- benchmark.js Reusable benchmark utilities
909
-
910
- wasm-src/
911
- quadqr_core.c Small portable WASM accelerator source
912
-
913
- wasm/
914
- quadqr-core.wasm Source-tree WASM build output
915
- quadqr-core.build.json Source/build/binary verification metadata
916
-
917
- dist/
918
- index.js ESM package entry
919
- index.cjs Modern CommonJS wrapper
920
- browser.js Browser ESM entry
921
- node.js Node entry
922
- quadqr.js Classic global browser bundle
923
- quadqr.min.js Compact CDN browser bundle
924
- wasm/ Prebuilt WASM package asset
925
-
926
- demo/
927
- index.html Interactive generator, image scanner, camera scanner, benchmark
928
- app.js Responsive UI + worker orchestration
929
- compute-worker.js Background encode/scan/reliability/benchmark tasks
930
- styles.css
931
-
932
- documentation/
933
- index.html Documentation home
934
- getting-started.html
935
- browser.html
936
- node.html
937
- scanning.html
938
- compression.html
939
- security.html
940
- high-density.html
941
- reliability.html
942
- api.html
943
- cli.html
944
- wasm.html
945
- format.html
946
- specification.html
947
- app.js Navigation, search, copy, and theme behavior
948
- search-index.js Client-side documentation search index
949
- styles.css Responsive documentation UI
950
-
951
- bin/ `quadqr` CLI (`npx quadqr-js`)
952
- scripts/ Build, benchmark, and local server scripts
953
- tests/ Codec and package distribution tests
954
- FORMAT.md Wire-format specification
955
- AGENT.md Development guidance
956
- README.md Project overview
957
- ```
958
-
959
- ---
960
-
961
- ## Main API
962
-
963
- ### `encodeText(text, options?)`
964
-
965
- ```js
966
- const code = encodeText("Hello from QuadQR", {
967
- version: "auto",
968
- minVersion: 1,
969
- maxVersion: 40,
970
- ecc: "M"
971
- });
972
- ```
973
-
974
- ### `encodeBytes(bytes, options?)`
975
-
976
- Encodes arbitrary binary data from a `Uint8Array`.
977
-
978
- ```js
979
- const code = encodeBytes(myBytes, {
980
- version: "auto",
981
- ecc: "M"
982
- });
983
- ```
984
-
985
- ### `encodeSecureText(text, options?)`
986
-
987
- Secure encoding is asynchronous because it uses Web Crypto.
988
-
989
- Password mode:
990
-
991
- ```js
992
- const code = await encodeSecureText("Private message", {
993
- ecc: "M",
994
- security: {
995
- mode: "password",
996
- password: "correct horse battery staple"
997
- }
998
- });
999
- ```
1000
-
1001
- Raw 256-bit key mode:
1002
-
1003
- ```js
1004
- const key = generateRaw256Key();
1005
-
1006
- const code = await encodeSecureText("Device configuration", {
1007
- ecc: "M",
1008
- security: {
1009
- mode: "raw-key",
1010
- key
1011
- }
1012
- });
1013
- ```
1014
-
1015
- Raw keys may be supplied as an exact 32-byte `Uint8Array` or as a 64-character hexadecimal string. By default, raw-key envelopes include a short SHA-256 key fingerprint (`keyIdHex`) that helps an application select the correct secret key without storing the key inside the QuadQR.
1016
-
1017
- ### `encodeSecureBytes(bytes, options?)`
1018
-
1019
- Binary equivalent of `encodeSecureText()`.
1020
-
1021
- ### `decryptDecoded(result, credentials)`
1022
-
1023
- A secure matrix/image scan first returns encrypted metadata without exposing plaintext:
1024
-
1025
- ```js
1026
- const locked = decodeMatrix(matrix);
1027
-
1028
- console.log(locked.secure); // true
1029
- console.log(locked.requiresDecryption); // true
1030
- console.log(locked.security.mode); // password | raw-key
1031
- ```
1032
-
1033
- Then decrypt it:
1034
-
1035
- ```js
1036
- const result = await decryptDecoded(locked, {
1037
- password: "correct horse battery staple"
1038
- });
1039
-
1040
- console.log(result.text);
1041
- ```
1042
-
1043
- For raw-key mode:
1044
-
1045
- ```js
1046
- const result = await decryptDecoded(locked, { key });
1047
- ```
1048
-
1049
- The decrypted result preserves the encrypted envelope as `encryptedPayload` for applications that need both forms.
1050
-
1051
- ### `decodeMatrix(matrix, options?)`
1052
-
1053
- Decodes an already reconstructed QuadQR matrix.
1054
-
1055
- ```js
1056
- const result = decodeMatrix(matrix);
1057
- ```
1058
-
1059
- When a scanner has per-cell confidence values, they can also be supplied directly:
1060
-
1061
- ```js
1062
- const result = decodeMatrix(matrix, {
1063
- cellConfidence: confidenceMatrix
1064
- });
1065
- ```
1066
-
1067
- The result reports fields such as `spectralInterleaving`, `confidenceAssisted`, `erasureSymbols`, and `correctedSymbols`. Secure symbols additionally report `secure`, `requiresDecryption`, and parsed `security` metadata while keeping `text` unset until successful decryption.
1068
-
1069
- ### `renderToCanvas(codeOrMatrix, canvas, options?)`
1070
-
1071
- Renders a QuadQR symbol into a browser canvas. `imageSize` sets the exact square pixel output and defaults to 720 when neither sizing option is supplied. `moduleSize` remains available for legacy pixels-per-module sizing. `options.style` supports `classic`, `depth`, `soft`, and `inset`. `quietZone` controls the border in modules. `logo` accepts a loaded image/canvas source or `{ source, size, clearBackground, padding, radius, backgroundColor }`.
1072
-
1073
- ### `renderToImageData(codeOrMatrix, options?)`
1074
-
1075
- Returns an ImageData-like object and supports the same rendering styles as `renderToCanvas()`:
1076
-
1077
- ```js
1078
- {
1079
- width,
1080
- height,
1081
- data
1082
- }
1083
- ```
1084
-
1085
- This is also useful for tests and non-DOM workflows.
1086
-
1087
- When a logo is used with `renderToImageData()`, its source must be an ImageData-like `{ width, height, data }` object so the renderer can composite it without DOM APIs.
1088
-
1089
- ### `renderToSVG(codeOrMatrix, options?)`
1090
-
1091
- Returns a standalone SVG string using the same exact `imageSize`, render styles, and quiet-zone controls. SVG logo sources can be a URL/data URL string or an object with a `src` string. The SVG remains vector-sharp regardless of how large the preview is displayed.
1092
-
1093
- ### `scanImageData(imageData, options?)`
1094
-
1095
- Runs the complete perspective-aware and color-aware image scanner. The scanner first tries the normal detected geometry with the observed RGB palette, preserving the fast path for clean images. Dense versions use distributed alignment markers to refine a plausible but imperfect projective solution, and a two-finder recovery pass can rescue a third locator that has been stretched by perspective. Only after that fails does it progressively fall back to per-channel white balancing, spatial black/white normalization, tighter centre sampling, a cheap module-grid Auto Tone / Auto Contrast / QuadQR Auto Color-style recovery, a rectified QR-region pixel enhancement pass, and finally bounded sub-module geometry micro-refinement. If locator detection itself is weakened by a flat/yellow frame, a full-image enhancement retry is also available. RGBW confidence values are carried into Reed-Solomon so ambiguous cells can be treated as erasures when ordinary hard-decision ECC is insufficient.
1096
-
1097
- ### `scanFile(file, options?)`
1098
-
1099
- Scans an uploaded browser image file.
1100
-
1101
- ### `scanVideoFrame(video, options?)`
1102
-
1103
- Scans one frame from an HTML video element. By default, if the video is displayed with `object-fit: cover`, QuadQR scans the source crop that is actually visible in the element rather than hidden sensor pixels outside the preview. Set `videoCropMode: "full"` to opt out.
1104
-
1105
- ### `startCameraScanner(video, options?)`
1106
-
1107
- Starts a reusable live-camera scanning loop. On supported browsers it requests continuous focus/exposure/white-balance camera modes and scans the CSS-visible preview crop. Modern browsers use a **dual-worker camera engine**: a lightweight fresh-frame worker continuously runs normal finder/geometry/decode attempts, while an independent recovery worker retains the complete high-resolution, Auto Color, precise-alignment, perspective, multi-frame, ECC, and damaged-code recovery stack. A slow recovery attempt therefore cannot prevent the fast worker from inspecting a newer camera frame. Finder detection remains JavaScript; optional WASM accelerates grayscale/binary preprocessing and CRC beneath the same detector. The scheduler uses `requestVideoFrameCallback()` when available and does not queue stale fast-path frames. Normal camera acquisition requests an environment camera around 1280×720 and crops/resizes the visible preview to a 640 px working bitmap **before** transferring it to the worker. Once a candidate validates structure, Spectrum ECC, and CRC, scanning returns immediately. If the fast worker misses, full recovery runs concurrently on a fresh frame at up to 960 px. Strong finder evidence dispatches recovery quickly. Finder-less frames remain on the lightweight fresh-frame detector only, so pointing the camera at an empty scene never starts Auto Color or deeper recovery work. QuadQR Auto Color crop profiles, center-weighted histograms, threshold bracketing, precise alignment, projective recovery, QR-region enhancement, multi-frame confidence fusion, and soft-decision Spectrum ECC are unchanged. `cameraHighResolutionMaxDimension` defaults to 960. The optional `onDiagnostic(event)` callback exposes whether an event came from the fast or recovery worker, finder candidates, active locator method, crop/geometry/version hypothesis, recovery method, timing, and scan dimensions. `onResult(result, frame)` receives the exact frame that decoded, including enhanced recovery pixels when applicable, so UIs can keep the frozen frame and finder overlay aligned.
1108
-
1109
- ### `getVersionInfo(version, options?)`
1110
-
1111
- Returns information such as:
1112
-
1113
- - matrix size;
1114
- - number of data cells;
1115
- - theoretical raw bits;
1116
- - usable payload capacity;
1117
- - structural metadata.
1118
-
1119
- ---
1120
-
1121
- ## Tests
1122
-
1123
- Run:
1124
-
1125
- ```bash
1126
- npm test
1127
- ```
1128
-
1129
- The current test suite covers areas including:
1130
-
1131
- - RGBW 2-bit mapping;
1132
- - binary round trips;
1133
- - Unicode text round trips;
1134
- - automatic version selection;
1135
- - all ECC profiles;
1136
- - version 1 compact framing;
1137
- - capacity boundaries;
1138
- - deliberate Reed-Solomon corruption recovery;
1139
- - Reed-Solomon error + erasure recovery;
1140
- - zero-overhead spectral-spatial permutation validation;
1141
- - confidence-assisted recovery beyond the ordinary hard-error limit;
1142
- - rotation handling;
1143
- - generated-image scanning;
1144
- - perspective distortion;
1145
- - color-cast scanning;
1146
- - dirty-camera stress scanning with strong yellow cast, haze, blue-channel suppression, and blur;
1147
- - low-contrast warm-camera regression where normal scanning fails but progressive Auto Tone / Contrast / Color recovery succeeds;
1148
- - multi-frame confidence fusion and tracked-symbol consistency;
1149
- - Spectrum ECC 2.0 bounded soft-decision recovery;
1150
- - affine cross-channel color calibration;
1151
- - multi-point Triangle16 region sampling with instability-aware confidence;
1152
- - benchmark reference data;
1153
- - timed codec round trips;
1154
- - password-mode secure round trips and wrong-password rejection;
1155
- - raw 256-bit key round trips, key fingerprinting, and wrong-key rejection;
1156
- - secure rendered-image scan and decryption.
1157
-
1158
- ---
1159
-
1160
- ## Current limitations
1161
-
1162
- QuadQR is still experimental.
1163
-
1164
- Important areas that need more research and real-device testing include:
1165
-
1166
- - printed codes across different printers and inks;
1167
- - paper color and reflectivity;
1168
- - display brightness and color profiles;
1169
- - screen glare;
1170
- - moiré patterns;
1171
- - motion blur;
1172
- - very small modules;
1173
- - extreme camera angles;
1174
- - low-light scanning;
1175
- - RGBW confusion under difficult illumination;
1176
- - damaged or partially hidden symbols;
1177
- - standardized recovery percentages;
1178
- - equal-reliability comparison with ISO QR Code;
1179
- - local/non-projective distortion correction using the distributed alignment grid;
1180
- - formal print-quality grading;
1181
- - performance across different phones and camera systems;
1182
- - memory-hard password KDF option such as Argon2id for environments where a small WASM/runtime dependency is acceptable;
1183
- - public/private-key secure payload mode.
1184
-
1185
- The project should currently be treated as a research and experimental implementation rather than a replacement for standardized QR Code in production-critical environments.
1186
-
1187
- ---
1188
-
1189
- ## Roadmap
1190
-
1191
- - [ ] Equal-reliability benchmark against standard QR
1192
- - [ ] Automated camera torture-test suite
1193
- - [ ] Blur, JPEG, noise, perspective, and lighting benchmarks
1194
- - [ ] Print-and-rescan dataset
1195
- - [x] Confidence-based RGBW classification
1196
- - [x] Improved adaptive color calibration
1197
- - [x] Distributed alignment patterns for large versions
1198
- - [ ] Interleaving tuned for localized physical damage
1199
- - [ ] Real-device benchmark dataset
1200
- - [ ] Formal versioned QuadQR specification
1201
- - [ ] Implementations in additional languages
1202
- - [ ] Independent decoder implementation
1203
-
1204
- ---
1205
-
1206
- ## Why keep the square design?
1207
-
1208
- QuadQR intentionally keeps square modules and a square overall matrix.
1209
-
1210
- Square cells:
1211
-
1212
- - tessellate without gaps;
1213
- - provide predictable row/column addressing;
1214
- - maximize colored area inside each module;
1215
- - are easy to sample at their center;
1216
- - work naturally with perspective correction;
1217
- - keep the geometry relatively simple for camera scanning.
1218
-
1219
- Alternative module shapes may be interesting visually, but the current focus is data density, reliability, and scan robustness.
1220
-
1221
- ---
1222
-
1223
- ## Is QuadQR a QR Code replacement?
1224
-
1225
- Not currently.
1226
-
1227
- Standard QR Code has enormous advantages:
1228
-
1229
- - decades of deployment;
1230
- - international standardization;
1231
- - extremely mature decoders;
1232
- - broad device support;
1233
- - extensive real-world testing;
1234
- - proven print reliability.
1235
-
1236
- QuadQR is exploring a different question:
1237
-
1238
- > **What can a QR-inspired matrix code look like if we design its data layer around modern color-capable cameras and displays?**
1239
-
1240
- The goal is experimentation, measurement, and learning.
1241
-
1242
- ---
1243
-
1244
- ## Contributing
1245
-
1246
- Contributions, experiments, test images, scanner improvements, benchmarking ideas, and independent implementations are welcome.
1247
-
1248
- If you are contributing changes to the wire format, please also update:
1249
-
1250
- ```text
1251
- FORMAT.md
1252
- ```
1253
-
1254
- Changes that affect capacity, ECC, scanning behavior, or version selection should include tests where practical.
1255
-
1256
- ---
1257
-
1258
- ## Security
1259
-
1260
- Decoded payloads are untrusted input.
1261
-
1262
- Do not automatically execute decoded:
1263
-
1264
- - HTML;
1265
- - JavaScript;
1266
- - shell commands;
1267
- - application commands;
1268
- - URLs.
1269
-
1270
- Applications using QuadQR should validate and safely handle decoded content just as they would any other external input.
1271
-
1272
- ---
1273
-
1274
- ## License
1275
-
1276
- AGPL v3.0. See `LICENSE`.
1277
-
1278
- ---
1279
-
1280
- ## Project status
1281
-
1282
- **Experimental / research project**
1283
-
1284
- QuadQR is actively evolving. Format details may change between versions until the wire format is considered stable.
1
+ # QuadQR (QQR)
2
+
3
+ <p align="center">
4
+ <img src="assets/banner.png?raw=true" alt="QuadQR banner">
5
+ </p>
6
+
7
+ <p align="center">
8
+ <strong>Normal RGBW mode by default, with an optional experimental High Density Mode using Triangle16 split cells.</strong>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://akanshsirohi.github.io/QuadQR/demo/"><strong>Try the Live Demo</strong></a>
13
+ ·
14
+ <a href="https://akanshsirohi.github.io/QuadQR/documentation/"><strong>Documentation Site</strong></a>
15
+ ·
16
+ <a href="https://www.npmjs.com/package/quadqr-js">npm</a>
17
+ ·
18
+ <a href="#current-benchmark">Benchmark</a>
19
+ ·
20
+ <a href="#getting-started">Use the Library</a>
21
+ ·
22
+ <a href="https://akanshsirohi.github.io/QuadQR/documentation/format.html">Format Reference</a>
23
+ ·
24
+ <a href="https://akanshsirohi.github.io/QuadQR/documentation/specification.html">Technical Specification</a>
25
+ </p>
26
+
27
+ **QuadQR** is an experimental open-source 2D matrix code that uses RGBW color states instead of the two states used by a traditional black-and-white QR module. Normal mode stores **2 bits per RGBW data cell**. This experimental branch also includes an optional **High Density Mode**, implemented with Triangle16 split cells, that stores **4 raw bits per body cell**. High Density Mode is disabled by default.
28
+
29
+ Default RGBW mapping:
30
+
31
+ | Color | Bits |
32
+ |---|---|
33
+ | Red | `00` |
34
+ | Green | `01` |
35
+ | Blue | `10` |
36
+ | White | `11` |
37
+
38
+ That gives QuadQR a four-symbol alphabet and a raw density of **2 bits per data cell**.
39
+
40
+ ### Experimental High Density Mode
41
+
42
+ When `highDensity: true` is enabled, Triangle16 splits each payload cell along one fixed `/` diagonal. The upper-left and lower-right triangles independently use Red, Green, Blue, or White:
43
+
44
+ ```text
45
+ 4 colors × 4 colors = 16 states
46
+ log2(16) = 4 bits per data cell
47
+ ```
48
+
49
+ The protected bootstrap/header deliberately remains solid-color even in High Density Mode, while the ECC-protected body uses the full 16-state alphabet. This sacrifices a small amount of theoretical capacity to make mode detection and damaged-camera recovery more reliable. Finder, timing, alignment, calibration, ECC, CRC, and matrix dimensions remain unchanged.
50
+
51
+ ```js
52
+ const code = encodeText("High-density QuadQR", {
53
+ ecc: "M",
54
+ highDensity: true
55
+ });
56
+ ```
57
+
58
+ Image and camera scanning automatically detect High Density Mode, so a separate scanner mode is not required. High Density Mode is experimental and should be stress-tested at the intended physical size and camera distance.
59
+
60
+ See the [High Density Mode documentation](https://akanshsirohi.github.io/QuadQR/documentation/high-density.html) for the physical cell mapping, protected-header strategy, scanner sampling rules, and reliability caveats.
61
+
62
+ QuadQR keeps the parts that make QR-like codes practical, such as a square matrix, finder patterns, timing structures, error correction, masking, perspective recovery, and camera scanning, while experimenting with a higher-density color-based data layer.
63
+
64
+ > **Important:** QuadQR is an experimental custom format. It is **not ISO QR Code**, and normal QR scanner apps cannot decode it.
65
+
66
+ ## Examples
67
+
68
+ Below are QuadQR symbols generated with different error-correction profiles. They use the same four-state RGBW data alphabet while varying the amount of space dedicated to Reed-Solomon protection.
69
+
70
+ <table>
71
+ <tr>
72
+ <td align="center">
73
+ <img src="assets/quadqr-normal.png?raw=true" alt="QuadQR example using ECC profile L" width="320"><br>
74
+ <sub><strong>QuadQR</strong> · Normal (v3)</sub>
75
+ </td>
76
+ <td align="center">
77
+ <img src="assets/quadqr-high-density.png?raw=true" alt="QuadQR example using ECC profile M" width="320"><br>
78
+ <sub><strong>QuadQR</strong> · Higher Data Density (v2)</sub>
79
+ </td>
80
+ </tr>
81
+ </table>
82
+
83
+ <p align="center">
84
+ <a href="https://akanshsirohi.github.io/QuadQR/demo/"><strong>Generate, scan, and benchmark QuadQR in your browser →</strong></a>
85
+ </p>
86
+
87
+ ---
88
+
89
+ ## Why I built QuadQR
90
+
91
+ I have always found QR codes fascinating. They are compact, practical, extremely well optimized, and they have continued to work reliably for decades.
92
+
93
+ But one thing caught my attention: the basic visual data representation is still binary. A module is essentially one of two states, black or white, representing `0` or `1`.
94
+
95
+ That made me wonder:
96
+
97
+ > If modern cameras, displays, image processing, and computing are much more capable today, can a QR-like matrix code reliably use more than two visible states and store more information in the same area?
98
+
99
+ I started researching the idea and working through the math. Four reliably distinguishable states are especially interesting because:
100
+
101
+ ```text
102
+ log2(4) = 2 bits
103
+ ```
104
+
105
+ So a four-state module can represent exactly two bits without requiring fractional-bit packing.
106
+
107
+ That eventually became QuadQR.
108
+
109
+ The current format uses **Red, Green, Blue, and White**, giving every data cell four possible states while keeping a square module grid that is easy to generate, sample, correct for perspective, and scan.
110
+
111
+ This project is an experiment in exploring how far a QR-inspired design can be pushed with modern hardware and software.
112
+
113
+ ### Development note
114
+
115
+ I designed the format, researched the approach, iterated on the encoding structure, and built the project as an independent experiment. **OpenAI Codex** helped significantly with some of the more complex implementation work, including parts of the codec, error correction, image-processing logic, testing, and optimization.
116
+
117
+ ---
118
+
119
+ ## What makes QuadQR different?
120
+
121
+ A traditional QR data module has two possible states:
122
+
123
+ ```text
124
+ Black
125
+ White
126
+ ```
127
+
128
+ That gives:
129
+
130
+ ```text
131
+ log2(2) = 1 bit per module
132
+ ```
133
+
134
+ QuadQR uses four data states:
135
+
136
+ ```text
137
+ Red
138
+ Green
139
+ Blue
140
+ White
141
+ ```
142
+
143
+ That gives:
144
+
145
+ ```text
146
+ log2(4) = 2 bits per data cell
147
+ ```
148
+
149
+ So at the raw data-cell level:
150
+
151
+ | Format | States per data cell | Raw information |
152
+ |---|---:|---:|
153
+ | Binary QR | 2 | 1 bit |
154
+ | QuadQR RGBW | 4 | 2 bits |
155
+ | QuadQR High Density Mode (Triangle16) | 16 | 4 bits |
156
+
157
+ This is a **2× raw symbol-density advantage**.
158
+
159
+ ### Spectrum ECC 2.0: confidence-aware + soft decoding
160
+
161
+ QuadQR uses the fact that a color scanner knows more than only the winning state. For each sampled data cell it now retains the selected state, a confidence score, and a bounded second hypothesis. Triangle16 does the same after classifying both color regions.
162
+
163
+ The first recovery layer is still confidence-aware GF(256) Reed-Solomon: if normal hard-decision decoding fails, the least-confident byte symbols can be promoted to **known erasures**, allowing the existing parity budget to be spent more efficiently.
164
+
165
+ **Spectrum ECC 2.0** adds a bounded soft-decision fallback. If hard decoding and erasure decoding still fail, the decoder tries the second hypothesis for a small number of the least-confident data cells, first singly and then in tightly bounded pairs. Every candidate must still pass the normal Reed-Solomon checks and final CRC-32, so soft decoding does not relax integrity validation or add parity overhead.
166
+
167
+ QuadQR also applies a deterministic **spectral-spatial interleaver** after ECC. Neighboring logical codeword cells are scattered across distant physical data positions, so a scratch, glare patch, shadow, or localized print defect tends to affect many different RS symbols instead of destroying a contiguous run. The permutation is reversible and consumes **zero extra data cells**.
168
+
169
+ These features do **not** change the RGBW alphabet, module count, ECC parity count, or payload-capacity calculation. They improve how the existing redundancy is used.
170
+
171
+ However, raw cell density is not the same thing as final user-payload capacity. Finder patterns, timing structures, calibration cells, headers, CRC, masking, error correction, and other reserved cells all consume space.
172
+
173
+ That is why QuadQR includes its own benchmark instead of assuming that every completed symbol will contain exactly twice the payload of a standard QR code.
174
+
175
+ ---
176
+
177
+ ## Secure Payload v1
178
+
179
+ QuadQR now includes an **optional authenticated-encryption layer**. It sits above the matrix codec, so normal QuadQR symbols keep the same RGBW mapping, Spectrum ECC, scanner, and capacity behavior.
180
+
181
+ Two security modes are supported:
182
+
183
+ | Mode | Key source | Best suited for |
184
+ |---|---|---|
185
+ | Password | PBKDF2-HMAC-SHA-256 derives a 256-bit key from a password | Human-to-human protected payloads |
186
+ | Raw 256-bit key | Exact 32-byte random key supplied by the application | Apps, tickets, provisioning, enterprise scanners |
187
+
188
+ Both modes use **AES-256-GCM**, which provides encryption and authentication. A wrong password/key or modified encrypted payload fails decryption instead of returning silent garbage.
189
+
190
+ Password mode currently defaults to **600,000 PBKDF2-HMAC-SHA-256 iterations** with a random 16-byte salt. Raw-key mode avoids password-KDF work and automatically stores an 8-byte SHA-256 key fingerprint as a non-secret key ID unless disabled or overridden. The actual raw key is never embedded in the symbol.
191
+
192
+ The security envelope is versioned independently from the QuadQR matrix format so a future release can add another KDF or encryption mode without redesigning the RGBW/ECC layer.
193
+
194
+ Security is intentionally opt-in. Unencrypted codes remain the default.
195
+
196
+ The browser demo supports secure scanning in **both scanner paths**:
197
+
198
+ - **Image scanner:** upload/exported images and photographs are decoded normally; secure symbols are detected automatically and show the matching password or raw-key decrypt control inline.
199
+ - **Camera scanner:** live camera frames use the same secure-aware decode path. When an encrypted symbol is verified, scanning stops, the result is marked **Secure QR**, and plaintext is revealed only after successful authenticated decryption.
200
+
201
+ The scanner never expects the secret to be embedded in the QuadQR itself. Raw-key symbols expose only their non-secret key ID/fingerprint.
202
+
203
+ ---
204
+
205
+ ## Compression, signatures, and robustness tooling
206
+
207
+ QuadQR keeps the application payload simple: **text stays text and bytes stay bytes**. There is no public payload-type registry to maintain. Compression and signing metadata are added only as internal implementation details when those features are enabled.
208
+
209
+ ```js
210
+ import {
211
+ encodeText,
212
+ encodeSignedText,
213
+ generateSigningKeyPair,
214
+ decodeMatrix,
215
+ verifyDecodedSignature
216
+ } from "quadqr-js";
217
+
218
+ // Auto compression is zero-overhead when it does not help.
219
+ const compressed = encodeText("repeated repeated repeated", {
220
+ compression: "auto",
221
+ ecc: "M"
222
+ });
223
+
224
+ // Smart is CPU-heavy and only escalates when stronger compression can
225
+ // realistically reduce the physical QuadQR version.
226
+ const smart = encodeText(largeStructuredText, { compression: "smart" });
227
+
228
+ // Explicit codecs can choose their encoder level.
229
+ const lz = encodeText(largeStructuredText, {
230
+ compression: "lz",
231
+ compressionLevel: 9
232
+ });
233
+ const brotli = encodeText("hello ".repeat(1000), {
234
+ compression: "brotli",
235
+ compressionLevel: 11
236
+ });
237
+ const deflate = encodeText(largeStructuredText, {
238
+ compression: "deflate",
239
+ compressionLevel: 9
240
+ });
241
+
242
+ const keys = await generateSigningKeyPair();
243
+ const signed = await encodeSignedText("verified offline", {
244
+ compression: "auto",
245
+ privateKey: keys.privateKey,
246
+ keyId: keys.keyId
247
+ });
248
+
249
+ const decoded = decodeMatrix(signed.matrix);
250
+ const verified = await verifyDecodedSignature(decoded, {
251
+ publicKey: keys.publicKey
252
+ });
253
+ console.log(verified.signatureVerified); // true
254
+ console.log(verified.signatureTrusted); // true
255
+ ```
256
+
257
+ Compression modes are `none`, `auto`, `smart`, `brotli`, `deflate`, and `lz`. `auto` is the fast default: it compares LZ level 6, DEFLATE level 6, and Brotli quality 6 once, including envelope overhead, and keeps the smallest final representation. `smart` is an opt-in CPU-heavy mode. It starts with the same balanced pass, checks the resulting QuadQR version, and only escalates to DEFLATE 8 / Brotli 9 and then DEFLATE 9 / Brotli 11 when a smaller physical version is realistically reachable; LZ stays at its default level in Auto/Smart. Explicit `lz` accepts `compressionLevel: 1..9` with default 6, explicit `deflate` accepts `1..9` with default 6, and explicit `brotli` accepts `0..11` with default 11. Compression level is an encoder-only setting and is not stored in the symbol because the decoder does not need it. All codecs are synchronous and bundled with QuadQR, so the same compression path works in browsers and server-side Node.js without `node:zlib`, `CompressionStream`, or a runtime dependency. The demo keeps these CPU-heavy operations in module Web Workers so the browser UI remains responsive. See the [compression documentation](https://akanshsirohi.github.io/QuadQR/documentation/compression.html) for the exact Smart escalation policy and level API. Ed25519 signing stores the signature plus an optional compact `keyId`; the public verification key stays outside the QuadQR by default. Applications do not need to choose or maintain content types.
258
+
259
+ Signing can also be composed with Secure Payload. QuadQR compresses if requested, signs the normal payload with the private key, then encrypts the protected bytes with AES-256-GCM. A verifier supplies the trusted public key separately, or resolves it from `keyId`.
260
+
261
+ The renderer supports an explicit `mode: "print"`. Print mode uses darker print-safe RGB defaults and prefers Classic solid modules, while `quietZone` behaves exactly as it does in screen mode. Four modules remains the recommended default. `getPrintGuidance()` converts a chosen physical size into module millimeters/pixels so print layouts can be checked before production testing.
262
+
263
+ Centered logos support `size: "auto"`, which estimates a conservative ECC-aware ratio from code utilization and rendering choices. `findMaxSafeLogoSize()` can additionally probe ImageData output and empirically search for the largest size that still decodes.
264
+
265
+ Scanner results include normalized diagnostics such as `confidence`, `geometryConfidence`, `calibrationConfidence`, `structureConfidence`, `eccUtilization`, and `correctedErrors`. Set `debug: true` or call `debugScanImageData()` to inspect finder/geometry candidates, the sampled matrix, color-confidence data, ECC stages, and the stage that failed.
266
+
267
+ For regression and demo testing, `runImageStressTest()` / `assessScanability()` apply deterministic blur, brightness, exposure, shadow, contrast, perspective, JPEG-like artifacts, and downscaling. The browser demo exposes the same tools as an interactive stress-test lab and shows an overall scanability rating.
268
+
269
+ The interoperability details are covered in the [technical specification](https://akanshsirohi.github.io/QuadQR/documentation/specification.html).
270
+
271
+ ---
272
+
273
+ ## Current benchmark
274
+
275
+ Run:
276
+
277
+ ```bash
278
+ npm run benchmark
279
+ ```
280
+
281
+ The benchmark compares QuadQR with standard QR **at the same matrix dimensions** using byte-mode QR reference capacities.
282
+
283
+ Current representative results at the project's `M` ECC profile:
284
+
285
+ | Version | Matrix | QuadQR payload | Standard QR payload | Gain | Ratio |
286
+ |---:|---:|---:|---:|---:|---:|
287
+ | 1 | 21×21 | **24 B** | 14 B | +10 B | **1.71×** |
288
+ | 2 | 25×25 | **48 B** | 26 B | +22 B | **1.85×** |
289
+ | 5 | 37×37 | **227 B** | 84 B | +143 B | **2.70×** |
290
+ | 10 | 57×57 | **630 B** | 213 B | +417 B | **2.96×** |
291
+ | 20 | 97×97 | **1992 B** | 666 B | +1326 B | **2.99×** |
292
+ | 30 | 137×137 | **4054 B** | 1370 B | +2684 B | **2.96×** |
293
+ | 40 | 177×177 | **6858 B** | 2331 B | +4527 B | **2.94×** |
294
+
295
+ ### How to read this table
296
+
297
+ For example:
298
+
299
+ ```text
300
+ Version 10
301
+ Matrix: 57×57
302
+
303
+ QuadQR: 630 bytes
304
+ Standard QR: 213 bytes
305
+
306
+ Gain: 417 bytes
307
+ Ratio: 2.96×
308
+ ```
309
+
310
+ This means that under the benchmark's current assumptions, a 57×57 QuadQR symbol can carry 630 user-payload bytes, while the standard QR byte-mode reference at the same dimensions and `M` label carries 213 bytes.
311
+
312
+ ### Important benchmark warning
313
+
314
+ The letters `L`, `M`, `Q`, and `H` in QuadQR are **project-defined ECC profiles**.
315
+
316
+ They do **not** currently claim the same standardized recovery percentages as ISO QR Code ECC levels with the same letters.
317
+
318
+ Therefore:
319
+
320
+ > The capacity benchmark is a same-dimension and same-label comparison, not yet an equal-damage-tolerance comparison.
321
+
322
+ The published baseline benchmark uses **normal RGBW mode at exactly 2 bits per data cell**. Ratios approaching ~3× in that RGBW usable-payload benchmark are caused by differences in total structural and ECC overhead between the two formats, not because an RGBW QuadQR cell contains 3 bits. The experimental **Triangle16** profile is a separate 4-bit/body-cell mode and should be benchmarked independently because its real-world advantage depends on camera resolution, perspective, blur, resizing, and print quality.
323
+
324
+ A future goal is to add **equal-reliability benchmarking**, where QuadQR and standard QR are compared after calibrating both to similar real-world damage recovery.
325
+
326
+ ---
327
+
328
+ ## Performance benchmark
329
+
330
+ The benchmark also measures direct codec performance.
331
+
332
+ It reports:
333
+
334
+ - payload size;
335
+ - automatically selected version;
336
+ - matrix dimensions;
337
+ - mean encode time;
338
+ - 95th percentile encode time;
339
+ - mean decode time;
340
+ - 95th percentile decode time.
341
+
342
+ Example output:
343
+
344
+ ```text
345
+ payload_B matrix version encode_mean encode_p95 decode_mean decode_p95
346
+ 24 21x21 v1 ...
347
+ 128 33x33 v4 ...
348
+ 512 53x53 v9 ...
349
+ 1024 73x73 v14 ...
350
+ 2048 101x101 v21 ...
351
+ ```
352
+
353
+ These timings measure the **matrix codec**, not the complete camera-scanning pipeline.
354
+
355
+ Camera scanning additionally includes finder detection, perspective correction, calibration, module sampling, and color classification.
356
+
357
+ For more stable local timing results, use more iterations:
358
+
359
+ ```bash
360
+ npm run benchmark -- --iterations=500
361
+ ```
362
+
363
+ For machine-readable results:
364
+
365
+ ```bash
366
+ npm run --silent benchmark -- --json
367
+ ```
368
+
369
+ ---
370
+
371
+ ## Version 1 optimization
372
+
373
+ Small symbols have a difficult tradeoff because fixed metadata and ECC consume a much larger fraction of the matrix.
374
+
375
+ The original 21×21 QuadQR design could not fit a valid `M` payload because the protected framing itself was larger than the available data area.
376
+
377
+ Version 1 now uses a dedicated compact small-symbol profile.
378
+
379
+ At `M`:
380
+
381
+ ```text
382
+ 21×21 QuadQR
383
+ 24 bytes user payload
384
+ ```
385
+
386
+ compared with:
387
+
388
+ ```text
389
+ 21×21 standard QR reference
390
+ 14 bytes byte-mode payload
391
+ ```
392
+
393
+ Version 2 and above continue to use the normal framing and ECC structure.
394
+
395
+ ---
396
+
397
+ ## Encoding pipeline
398
+
399
+ The current QuadQR pipeline is:
400
+
401
+ ```text
402
+ payload bytes
403
+ ↓
404
+ optional Secure Payload v1 (AES-256-GCM)
405
+ ↓
406
+ header + CRC-32
407
+ ↓
408
+ GF(256) Reed-Solomon error correction
409
+ ↓
410
+ interleaved byte codewords
411
+ ↓
412
+ split every encoded byte into four 2-bit values
413
+ ↓
414
+ zero-overhead spectral-spatial permutation
415
+ ↓
416
+ 00 / 01 / 10 / 11
417
+ ↓
418
+ Red / Green / Blue / White
419
+ ↓
420
+ quaternary masking at physical positions
421
+ ↓
422
+ square QuadQR matrix
423
+ ```
424
+
425
+ Because four states map naturally to two bits, QuadQR does not require base-3 conversion or fractional-bit packing.
426
+
427
+ One encoded byte maps naturally to four data cells:
428
+
429
+ ```text
430
+ 8 bits ÷ 2 bits/cell = 4 cells
431
+ ```
432
+
433
+ ---
434
+
435
+ ## Symbol structure
436
+
437
+ QuadQR currently uses:
438
+
439
+ - a square overall symbol;
440
+ - square modules;
441
+ - three 7×7 black-and-white finder patterns;
442
+ - black-and-white timing structures;
443
+ - distributed 5×5 nested alignment eyes on versions that use alignment patterns;
444
+ - RGB calibration swatches;
445
+ - structural black/white references;
446
+ - a two-column zig-zag physical data-position path;
447
+ - deterministic spectral-spatial interleaving of logical codeword cells;
448
+ - four-state masking;
449
+ - GF(256) Reed-Solomon ECC with error + erasure decoding;
450
+ - per-module RGBW confidence for scanner-assisted erasures;
451
+ - CRC-32 integrity verification;
452
+ - optional versioned AES-256-GCM Secure Payload envelope.
453
+
454
+ White is a valid data state.
455
+
456
+ QuadQR still uses exactly three large finder patterns, just like standard QR. Starting at version 2, Format v6 alignment markers follow the standard QR version-dependent center schedule and every scheduled alignment marker is a full 5×5 nested black/white/black eye. These distributed references give the detector stronger anchors for high-version and projectively distorted symbols. Version 1 keeps one QuadQR-specific 5×5 bottom-right bootstrap marker because it otherwise would have no fourth projective reference. The decoder retains the compact 3×3 secondary-marker profile used by Format v5 so existing v5 symbols remain readable.
457
+
458
+ The decoder does not treat a white-looking area as automatically empty. It reconstructs the matrix geometry first and then determines whether a sampled position is structural or data.
459
+
460
+ ---
461
+
462
+ ## Color mapping
463
+
464
+ The current mapping is intentionally simple:
465
+
466
+ ```text
467
+ 00 → Red
468
+ 01 → Green
469
+ 10 → Blue
470
+ 11 → White
471
+ ```
472
+
473
+ Ideal display-space reference colors are conceptually:
474
+
475
+ ```text
476
+ Red → (255, 0, 0)
477
+ Green → ( 0, 255, 0)
478
+ Blue → ( 0, 0, 255)
479
+ White → (255, 255, 255)
480
+ ```
481
+
482
+ Real camera input is not expected to match those exact values.
483
+
484
+ QuadQR includes calibration and nearest-color classification so the scanner can work with observed colors after lighting, camera processing, perspective changes, and other image transformations. The clean-frame path starts with a streaming 1:1:3:1:1 finder detector on the RGB value channel, direct cross-checks, local-threshold fallback, and directional module-size/version estimation. Once the three finder eyes form a valid geometry, QuadQR immediately tries the highest-ranked perspective candidate instead of waiting for every lower-ranked geometry. Near-front-facing symbols can use a three-finder affine fast path, while projectively distorted symbols continue through the full homography/alignment solver. Format v6 distributed 5×5 alignment eyes provide stronger local perspective anchors, and a cheap nested-eye locator is tried before the broader alignment search. If a steep angle leaves exactly two strong finder patterns, the bounded looser third-finder pass still runs before heavier color recovery. Only after geometry/color decoding still fails does QuadQR progressively try the existing stronger recovery, including white balancing, a 3×4 affine color-calibration model learned from the known black/white/R/G/B references, spatial normalization, Auto Tone / Auto Contrast / QuadQR Auto Color-style enhancement, soft-decision ECC, and bounded sub-module geometry refinement. For live video, normal detection works from the CSS-visible `object-fit: cover` camera region at a 640 px working dimension. A bounded 960 px retry is reserved for difficult dense frames that already show useful finder evidence, rather than being part of the ordinary camera loop. If finder geometry is already strong but color decoding fails, a QR-only rectified pixel enhancement retry is performed immediately; whole-frame enhancement remains reserved for harder locator failures.
485
+
486
+ ---
487
+
488
+ ## Reed-Solomon error correction
489
+
490
+ QuadQR uses Reed-Solomon over:
491
+
492
+ ```text
493
+ GF(2^8) = GF(256)
494
+ ```
495
+
496
+ Primitive polynomial:
497
+
498
+ ```text
499
+ x^8 + x^4 + x^3 + x^2 + 1
500
+ 0x11d
501
+ ```
502
+
503
+ One Reed-Solomon symbol is one byte.
504
+
505
+ Since one byte becomes four QuadQR data cells, ECC symbols stay naturally aligned with the RGBW representation.
506
+
507
+ ### Versions 2 through 40
508
+
509
+ | Profile | Parity bytes per body block | Correctable byte symbols per block |
510
+ |---|---:|---:|
511
+ | L | 12 | 6 |
512
+ | M | 24 | 12 |
513
+ | Q | 36 | 18 |
514
+ | H | 48 | 24 |
515
+
516
+ ### Version 1 compact profile
517
+
518
+ | Profile | Body parity bytes | Correctable body byte symbols |
519
+ |---|---:|---:|
520
+ | L | 4 | 2 |
521
+ | M | 8 | 4 |
522
+ | Q | 12 | 6 |
523
+ | H | 16 | 8 |
524
+
525
+ Version 1 also uses a compact 4-byte logical header protected by 4 Reed-Solomon parity bytes, correcting up to 2 damaged header byte symbols.
526
+
527
+ CRC-32 remains the final integrity check.
528
+
529
+ ---
530
+
531
+ ## Scanner pipeline
532
+
533
+ The image/camera scanner currently follows this general pipeline:
534
+
535
+ ```text
536
+ camera or image RGB frame
537
+ ↓
538
+ grayscale structural analysis
539
+ ↓
540
+ finder candidate detection
541
+ ↓
542
+ version hypothesis
543
+ ↓
544
+ primary alignment search
545
+ ↓
546
+ initial homography / perspective correction
547
+ ↓
548
+ distributed alignment-grid validation
549
+ ↓ (when geometry is plausible but imperfect)
550
+ secondary alignment multi-point homography refinement
551
+ ↓
552
+ module-grid reconstruction
553
+ ↓
554
+ fast observed-RGB decode attempt
555
+ ↓ (only if needed)
556
+ white balance + affine cross-channel calibration + spatial normalization
557
+ ↓ (only if still needed)
558
+ Auto Tone / Auto Contrast / QuadQR Auto Color-style recovery
559
+ ↓ (only if still needed)
560
+ sub-module geometry refinement
561
+ ↓
562
+ RGB + structural black/white calibration
563
+ ↓
564
+ RGBW/Triangle16 classification + confidence + second hypothesis
565
+ ↓
566
+ unmasking
567
+ ↓
568
+ reverse spectral-spatial permutation
569
+ ↓
570
+ protected header Reed-Solomon hard decode
571
+ ↓
572
+ confidence-guided erasure retry when needed
573
+ ↓
574
+ bounded Spectrum ECC 2.0 soft-hypothesis retry when needed
575
+ ↓
576
+ body deinterleaving + error/erasure Reed-Solomon decode
577
+ ↓
578
+ CRC-32 verification
579
+ ↓
580
+ payload
581
+ ```
582
+
583
+ An axis-aligned fallback is also available for clean generated images and simple inputs.
584
+
585
+ ---
586
+
587
+ ## Rendering styles
588
+
589
+ QuadQR keeps visual styling separate from the wire format. The encoded matrix is unchanged, so applications can choose a renderer without creating a new barcode format.
590
+
591
+ The current render profiles are:
592
+
593
+ | Style | Behavior |
594
+ |---|---|
595
+ | `classic` | Original fully solid square modules. |
596
+ | `depth` | Deterministically mixes normal and lightly faded data tiles with subtle edge shading for a layered/3D appearance. |
597
+ | `soft` | Uses rounded data tiles for a softer visual treatment. |
598
+ | `inset` | Uses narrow recessed edge lighting while preserving the exact encoded center color. |
599
+
600
+ Finder patterns, timing structures, alignment markers, and calibration cells stay **square and fully solid** in styled output so the visual treatment does not weaken the scanner's geometric/color references.
601
+
602
+ The `inset` style preserves the exact encoded R/G/B/W color around the center of every data module. Its recessed shading is confined to a narrow outer band, and white data modules remain pure white. This matches the scanner's center-sampling strategy and avoids visual effects bleeding into neighboring cells.
603
+
604
+ ```js
605
+ renderToCanvas(code, canvas, {
606
+ imageSize: 720,
607
+ quietZone: 4,
608
+ style: "inset" // classic | depth | soft | inset
609
+ });
610
+ ```
611
+
612
+ The styling is deterministic. Generating the same matrix with the same style produces the same visual tile treatment rather than changing randomly on every render.
613
+
614
+ `imageSize` sets the exact square output size in pixels. When neither `imageSize` nor `moduleSize` is supplied, QuadQR renders at **720 × 720 px** by default. `moduleSize` remains available as the lower-level legacy sizing control; when `imageSize` is supplied, the exact image size takes precedence.
615
+
616
+ ### Logo overlays, quiet zones, and SVG export
617
+
618
+ The renderer can place a centered logo over the symbol. Transparent pixels in the logo stay transparent, so the QuadQR modules remain visible through those areas. Enable `clearBackground` when you want a clean padded white area behind the logo instead.
619
+
620
+ ```js
621
+ const logoImage = new Image();
622
+ logoImage.src = "/brand-mark.png";
623
+ await logoImage.decode();
624
+
625
+ renderToCanvas(code, canvas, {
626
+ imageSize: 720,
627
+ quietZone: 6,
628
+ style: "classic",
629
+ logo: {
630
+ source: logoImage,
631
+ size: 0.12,
632
+ clearBackground: true,
633
+ padding: 0.65,
634
+ radius: 0.8
635
+ }
636
+ });
637
+ ```
638
+
639
+ `quietZone` is measured in modules and can be set to `0` or increased for print/camera use. Four modules remains the recommended default.
640
+
641
+ SVG uses the same matrix, palette, styles, quiet-zone size, and logo geometry:
642
+
643
+ ```js
644
+ import { renderToSVG } from "quadqr-js";
645
+
646
+ const svg = renderToSVG(code, {
647
+ imageSize: 720,
648
+ quietZone: 4,
649
+ logo: {
650
+ source: "data:image/png;base64,...",
651
+ size: 0.12,
652
+ clearBackground: true
653
+ }
654
+ });
655
+ ```
656
+
657
+ Logo overlays intentionally consume some ECC margin because they cover encoded cells. Keep logos conservative, especially with `L`/`M` ECC. The browser demo verifies the final rendered image before enabling downloads.
658
+
659
+ ---
660
+
661
+ ## Demo
662
+
663
+ **Live demo:** https://akanshsirohi.github.io/QuadQR/demo/
664
+
665
+ The browser demo runs directly on GitHub Pages and is split into separate views so the interface does not become overloaded. The generator keeps only payload, version, and ECC visible by default; optional capabilities are grouped into independent advanced accordions. CPU-heavy encoding/compression, rendered-image verification, uploaded-image scanning, scanability tests, Reliability Lab runs, perspective sweeps, and codec benchmarks are dispatched to module Web Workers. Generation shows explicit progress instead of freezing the browser, repeated UI changes are coalesced, and render-only changes reuse the already encoded matrix instead of recompressing the payload.
666
+
667
+ ### Generator & Image Scanner
668
+
669
+ Use this tab to:
670
+
671
+ - enter text/data;
672
+ - generate a QuadQR code;
673
+ - select ECC;
674
+ - open Output & Rendering controls for image size, quiet zone, style, and print mode;
675
+ - open the Center Logo accordion for branding;
676
+ - enable internal automatic compression without choosing a payload type;
677
+ - optionally add an Ed25519 signature;
678
+ - optionally encrypt using a password or raw 256-bit key;
679
+ - inspect version and capacity;
680
+ - download PNG or SVG output;
681
+ - scan an uploaded image.
682
+
683
+ ### Camera Scanner
684
+
685
+ The live camera scanner has its own dedicated tab.
686
+
687
+ This keeps the generator interface lightweight and prevents an always-visible video element from making the main page bulky.
688
+
689
+ The camera stream automatically stops when you leave the camera tab.
690
+
691
+ ### Benchmark
692
+
693
+ The benchmark tab provides an easier visual view of:
694
+
695
+ - capacity by matrix size;
696
+ - comparison with standard QR;
697
+ - byte gain;
698
+ - capacity ratio;
699
+ - codec timing;
700
+ - capacity planning;
701
+ - interactive stress testing.
702
+
703
+ Benchmark tools are also separated into accordions so the page stays compact until a tool is needed.
704
+
705
+ ---
706
+
707
+ ## New API highlights
708
+
709
+ ```js
710
+ import {
711
+ encodeText,
712
+ encodeUint8Array,
713
+ decodeUint8Array,
714
+ renderToImageData,
715
+ assessScanability,
716
+ getPrintGuidance
717
+ } from "quadqr-js";
718
+
719
+ const binary = encodeUint8Array(new Uint8Array([1, 2, 3, 4]), { ecc: "M" });
720
+ const bytes = decodeUint8Array(binary.matrix);
721
+
722
+ const code = encodeText("Hello hello hello hello", {
723
+ compression: "auto",
724
+ ecc: "Q"
725
+ });
726
+
727
+ const printImage = renderToImageData(code, {
728
+ imageSize: 1200,
729
+ mode: "print",
730
+ logo: { source: logoImageData, size: "auto", clearBackground: true }
731
+ });
732
+
733
+ console.log(getPrintGuidance(code, { physicalSizeMm: 45, dpi: 300 }));
734
+ console.log(assessScanability(code, { imageSize: 480 }));
735
+ ```
736
+
737
+ ---
738
+
739
+ ## Getting started
740
+
741
+ QuadQR can be used as an npm package, directly from a CDN, or from this repository.
742
+
743
+ ### Requirements
744
+
745
+ - **Node.js 20.19+** for Node.js and CommonJS usage
746
+ - npm or another Node package manager for installation
747
+ - A modern browser for the demo and documentation site
748
+ - HTTPS or localhost for browser camera access
749
+
750
+ ### Install from npm
751
+
752
+ ```bash
753
+ npm install quadqr-js
754
+ ```
755
+
756
+ Core usage:
757
+
758
+ ```js
759
+ import { encodeText, decodeMatrix } from "quadqr-js";
760
+
761
+ const code = encodeText("Hello from QuadQR", { ecc: "M" });
762
+ const result = decodeMatrix(code.matrix);
763
+ console.log(result.text);
764
+ ```
765
+
766
+ Node PNG/SVG usage:
767
+
768
+ ```js
769
+ import { encodeText } from "quadqr-js";
770
+ import { savePNG, saveSVG, scanFile } from "quadqr-js/node";
771
+
772
+ const code = encodeText("Generated on Node.js");
773
+ await savePNG(code, "quadqr.png", { imageSize: 720, quietZone: 4 });
774
+ await saveSVG(code, "quadqr.svg", { imageSize: 720, quietZone: 4 });
775
+
776
+ const result = await scanFile("quadqr.png");
777
+ console.log(result.text);
778
+ ```
779
+
780
+ ### CDN / script tag
781
+
782
+ The `quadqr-js` package can be loaded directly from npm-backed CDNs:
783
+
784
+ ```html
785
+ <script src="https://cdn.jsdelivr.net/npm/quadqr-js@1.5.5/dist/quadqr.min.js"></script>
786
+ <script>
787
+ const code = QuadQR.encodeText("Hello from a script tag");
788
+ </script>
789
+ ```
790
+
791
+ The same file is available through unpkg. Pin an exact version in production.
792
+
793
+ ### CLI
794
+
795
+ Encode and decode directly from the terminal:
796
+
797
+ ```bash
798
+ npx quadqr-js encode "Hello QuadQR" -o hello.png
799
+ npx quadqr-js decode hello.png
800
+ ```
801
+
802
+ Compressed, print-safe, and signed output are also available:
803
+
804
+ ```bash
805
+ npx quadqr-js encode "repeat repeat repeat" --compression auto -o compressed.svg
806
+ npx quadqr-js encode "Print me" --print -o print.svg
807
+ npx quadqr-js signkeygen -o signing-key.json
808
+ npx quadqr-js encode "Verified ticket" --sign-key signing-key.json -o signed.png
809
+ npx quadqr-js decode signed.png --verify-key signing-key.json
810
+ ```
811
+
812
+ Password-protected payloads use the same commands:
813
+
814
+ ```bash
815
+ npx quadqr-js encode "Private data" --password "my-password" -o secure.png
816
+ npx quadqr-js decode secure.png --password "my-password"
817
+ ```
818
+
819
+ See the [CLI reference](https://akanshsirohi.github.io/QuadQR/documentation/cli.html) for all CLI options, including compression, raw 256-bit key mode, signing, print mode, and scanner diagnostics.
820
+
821
+ ### Run from source
822
+
823
+ ```bash
824
+ git clone https://github.com/akanshsirohi/QuadQR.git
825
+ cd QuadQR
826
+ npm install
827
+ ```
828
+
829
+ Run the complete test suite:
830
+
831
+ ```bash
832
+ npm test
833
+ ```
834
+
835
+ Build the distributable browser, Node.js, CDN, and WASM files:
836
+
837
+ ```bash
838
+ npm run build
839
+ ```
840
+
841
+ `npm run build` verifies the checked-in WASM binary against `wasm-src/quadqr_core.c`, the WASM compiler flags, and the recorded binary hash. If everything matches, the verified prebuilt binary is reused. If the C source or build flags changed, the build automatically recompiles WASM with LLVM/Clang. If WASM is stale and `clang` is unavailable, the build fails instead of silently shipping an old binary.
842
+
843
+ To force only the WASM rebuild:
844
+
845
+ ```bash
846
+ npm run build:wasm
847
+ ```
848
+
849
+ A forced WASM rebuild requires `clang` in `PATH`. No Rust, Cargo, or Emscripten toolchain is required. The build records its verification data in `wasm/quadqr-core.build.json`.
850
+
851
+ Start the interactive demo:
852
+
853
+ ```bash
854
+ npm start
855
+ ```
856
+
857
+ Start the documentation site:
858
+
859
+ ```bash
860
+ npm run docs
861
+ ```
862
+
863
+ Run benchmarks:
864
+
865
+ ```bash
866
+ npm run benchmark
867
+ ```
868
+
869
+ ### Optional WASM acceleration
870
+
871
+ The package ships a prebuilt WASM helper but never requires it. It accelerates CRC-32 plus the scanner's RGBA-to-grayscale, Otsu-threshold, and binary finder preprocessing hot path.
872
+
873
+ ```js
874
+ import { initWasm } from "quadqr-js";
875
+
876
+ await initWasm();
877
+ ```
878
+
879
+ If WASM cannot load, the normal JavaScript codec/scanner remains available. For camera or repeated image scanning, initialize WASM once during application startup.
880
+
881
+ ---
882
+
883
+ ## Package entry points
884
+
885
+ | Import | Purpose |
886
+ |---|---|
887
+ | `quadqr-js` | Runtime-neutral core, secure payloads, rendering, scanning, optional WASM |
888
+ | `quadqr-js/browser` | Browser ESM entry |
889
+ | `quadqr-js/node` | Node core plus PNG/file/buffer helpers |
890
+ | `quadqr-js/benchmark` | Capacity and codec benchmark helpers |
891
+ | `quadqr-js/quadqr.min.js` | Classic browser global/CDN bundle |
892
+
893
+ The Node PNG path is dependency-free. For JPEG, WebP, or AVIF input, the Node adapter can use `sharp` when the consuming application already has it installed.
894
+
895
+ ---
896
+
897
+ ## Repository layout
898
+
899
+ ```text
900
+ library/
901
+ quadqr.js Core encoder, decoder, renderer, scanner, and public API
902
+ security.js Secure Payload v1 encryption and key handling
903
+ reed-solomon.js GF(256) Reed-Solomon implementation
904
+ geometry.js Version, size, and alignment geometry
905
+ vision.js Finder detection, perspective correction, and color sampling
906
+ node.js Node PNG/file/buffer adapters
907
+ wasm.js Optional prebuilt WASM loader
908
+ benchmark.js Reusable benchmark utilities
909
+
910
+ wasm-src/
911
+ quadqr_core.c Small portable WASM accelerator source
912
+
913
+ wasm/
914
+ quadqr-core.wasm Source-tree WASM build output
915
+ quadqr-core.build.json Source/build/binary verification metadata
916
+
917
+ dist/
918
+ index.js ESM package entry
919
+ index.cjs Modern CommonJS wrapper
920
+ browser.js Browser ESM entry
921
+ node.js Node entry
922
+ quadqr.js Classic global browser bundle
923
+ quadqr.min.js Compact CDN browser bundle
924
+ wasm/ Prebuilt WASM package asset
925
+
926
+ demo/
927
+ index.html Interactive generator, image scanner, camera scanner, benchmark
928
+ app.js Responsive UI + worker orchestration
929
+ compute-worker.js Background encode/scan/reliability/benchmark tasks
930
+ styles.css
931
+
932
+ documentation/
933
+ index.html Documentation home
934
+ getting-started.html
935
+ browser.html
936
+ node.html
937
+ scanning.html
938
+ compression.html
939
+ security.html
940
+ high-density.html
941
+ reliability.html
942
+ api.html
943
+ cli.html
944
+ wasm.html
945
+ format.html
946
+ specification.html
947
+ app.js Navigation, search, copy, and theme behavior
948
+ search-index.js Client-side documentation search index
949
+ styles.css Responsive documentation UI
950
+
951
+ bin/ `quadqr` CLI (`npx quadqr-js`)
952
+ scripts/ Build, benchmark, and local server scripts
953
+ tests/ Codec and package distribution tests
954
+ FORMAT.md Wire-format specification
955
+ AGENT.md Development guidance
956
+ README.md Project overview
957
+ ```
958
+
959
+ ---
960
+
961
+ ## Main API
962
+
963
+ ### `encodeText(text, options?)`
964
+
965
+ ```js
966
+ const code = encodeText("Hello from QuadQR", {
967
+ version: "auto",
968
+ minVersion: 1,
969
+ maxVersion: 40,
970
+ ecc: "M"
971
+ });
972
+ ```
973
+
974
+ ### `encodeBytes(bytes, options?)`
975
+
976
+ Encodes arbitrary binary data from a `Uint8Array`.
977
+
978
+ ```js
979
+ const code = encodeBytes(myBytes, {
980
+ version: "auto",
981
+ ecc: "M"
982
+ });
983
+ ```
984
+
985
+ ### `encodeSecureText(text, options?)`
986
+
987
+ Secure encoding is asynchronous because it uses Web Crypto.
988
+
989
+ Password mode:
990
+
991
+ ```js
992
+ const code = await encodeSecureText("Private message", {
993
+ ecc: "M",
994
+ security: {
995
+ mode: "password",
996
+ password: "correct horse battery staple"
997
+ }
998
+ });
999
+ ```
1000
+
1001
+ Raw 256-bit key mode:
1002
+
1003
+ ```js
1004
+ const key = generateRaw256Key();
1005
+
1006
+ const code = await encodeSecureText("Device configuration", {
1007
+ ecc: "M",
1008
+ security: {
1009
+ mode: "raw-key",
1010
+ key
1011
+ }
1012
+ });
1013
+ ```
1014
+
1015
+ Raw keys may be supplied as an exact 32-byte `Uint8Array` or as a 64-character hexadecimal string. By default, raw-key envelopes include a short SHA-256 key fingerprint (`keyIdHex`) that helps an application select the correct secret key without storing the key inside the QuadQR.
1016
+
1017
+ ### `encodeSecureBytes(bytes, options?)`
1018
+
1019
+ Binary equivalent of `encodeSecureText()`.
1020
+
1021
+ ### `decryptDecoded(result, credentials)`
1022
+
1023
+ A secure matrix/image scan first returns encrypted metadata without exposing plaintext:
1024
+
1025
+ ```js
1026
+ const locked = decodeMatrix(matrix);
1027
+
1028
+ console.log(locked.secure); // true
1029
+ console.log(locked.requiresDecryption); // true
1030
+ console.log(locked.security.mode); // password | raw-key
1031
+ ```
1032
+
1033
+ Then decrypt it:
1034
+
1035
+ ```js
1036
+ const result = await decryptDecoded(locked, {
1037
+ password: "correct horse battery staple"
1038
+ });
1039
+
1040
+ console.log(result.text);
1041
+ ```
1042
+
1043
+ For raw-key mode:
1044
+
1045
+ ```js
1046
+ const result = await decryptDecoded(locked, { key });
1047
+ ```
1048
+
1049
+ The decrypted result preserves the encrypted envelope as `encryptedPayload` for applications that need both forms.
1050
+
1051
+ ### `decodeMatrix(matrix, options?)`
1052
+
1053
+ Decodes an already reconstructed QuadQR matrix.
1054
+
1055
+ ```js
1056
+ const result = decodeMatrix(matrix);
1057
+ ```
1058
+
1059
+ When a scanner has per-cell confidence values, they can also be supplied directly:
1060
+
1061
+ ```js
1062
+ const result = decodeMatrix(matrix, {
1063
+ cellConfidence: confidenceMatrix
1064
+ });
1065
+ ```
1066
+
1067
+ The result reports fields such as `spectralInterleaving`, `confidenceAssisted`, `erasureSymbols`, and `correctedSymbols`. Secure symbols additionally report `secure`, `requiresDecryption`, and parsed `security` metadata while keeping `text` unset until successful decryption.
1068
+
1069
+ ### `renderToCanvas(codeOrMatrix, canvas, options?)`
1070
+
1071
+ Renders a QuadQR symbol into a browser canvas. `imageSize` sets the exact square pixel output and defaults to 720 when neither sizing option is supplied. `moduleSize` remains available for legacy pixels-per-module sizing. `options.style` supports `classic`, `depth`, `soft`, and `inset`. `quietZone` controls the border in modules. `logo` accepts a loaded image/canvas source or `{ source, size, clearBackground, padding, radius, backgroundColor }`.
1072
+
1073
+ ### `renderToImageData(codeOrMatrix, options?)`
1074
+
1075
+ Returns an ImageData-like object and supports the same rendering styles as `renderToCanvas()`:
1076
+
1077
+ ```js
1078
+ {
1079
+ width,
1080
+ height,
1081
+ data
1082
+ }
1083
+ ```
1084
+
1085
+ This is also useful for tests and non-DOM workflows.
1086
+
1087
+ When a logo is used with `renderToImageData()`, its source must be an ImageData-like `{ width, height, data }` object so the renderer can composite it without DOM APIs.
1088
+
1089
+ ### `renderToSVG(codeOrMatrix, options?)`
1090
+
1091
+ Returns a standalone SVG string using the same exact `imageSize`, render styles, and quiet-zone controls. SVG logo sources can be a URL/data URL string or an object with a `src` string. The SVG remains vector-sharp regardless of how large the preview is displayed.
1092
+
1093
+ ### `scanImageData(imageData, options?)`
1094
+
1095
+ Runs the complete perspective-aware and color-aware image scanner. The scanner first tries the normal detected geometry with the observed RGB palette, preserving the fast path for clean images. Dense versions use distributed alignment markers to refine a plausible but imperfect projective solution, and a two-finder recovery pass can rescue a third locator that has been stretched by perspective. Only after that fails does it progressively fall back to per-channel white balancing, spatial black/white normalization, tighter centre sampling, a cheap module-grid Auto Tone / Auto Contrast / QuadQR Auto Color-style recovery, a rectified QR-region pixel enhancement pass, and finally bounded sub-module geometry micro-refinement. If locator detection itself is weakened by a flat/yellow frame, a full-image enhancement retry is also available. RGBW confidence values are carried into Reed-Solomon so ambiguous cells can be treated as erasures when ordinary hard-decision ECC is insufficient.
1096
+
1097
+ ### `scanFile(file, options?)`
1098
+
1099
+ Scans an uploaded browser image file.
1100
+
1101
+ ### `scanVideoFrame(video, options?)`
1102
+
1103
+ Scans one frame from an HTML video element. By default, if the video is displayed with `object-fit: cover`, QuadQR scans the source crop that is actually visible in the element rather than hidden sensor pixels outside the preview. Set `videoCropMode: "full"` to opt out.
1104
+
1105
+ ### `startCameraScanner(video, options?)`
1106
+
1107
+ Starts a reusable live-camera scanning loop. On supported browsers it requests continuous focus/exposure/white-balance camera modes and scans the CSS-visible preview crop. Modern browsers use a **locator + recovery worker engine**: a small locator frame (384 px by default) performs only structural finder/geometry detection, while an independent full worker retains the complete normal decode, high-resolution, Auto Color, precise-alignment, perspective, multi-frame, ECC, and damaged-code recovery stack. A slow decode therefore cannot prevent the locator from inspecting newer camera frames. The scheduler uses `requestVideoFrameCallback()` when available and follows a latest-frame-wins policy instead of queueing stale captures. Camera transport is runtime-probed: QuadQR prefers resized `ImageBitmap` transfer, automatically falls back to Canvas/transferable `ImageData` for Safari/WebKit-style failures, can restart a failed worker, and keeps a main-thread compatibility path as a final fallback. Strong finder evidence dispatches full decoding immediately; finder-less frames remain on the cheap structural locator only, so pointing the camera at an empty scene never starts color/ECC recovery work. QuadQR Auto Color crop profiles, center-weighted histograms, threshold bracketing, precise alignment, projective recovery, QR-region enhancement, multi-frame confidence fusion, and soft-decision Spectrum ECC are unchanged. `cameraHighResolutionMaxDimension` defaults to 960. The optional `onDiagnostic(event)` callback exposes whether an event came from the fast or recovery worker, finder candidates, active locator method, crop/geometry/version hypothesis, recovery method, timing, and scan dimensions. `onResult(result, frame)` receives the exact frame that decoded, including enhanced recovery pixels when applicable, so UIs can keep the frozen frame and finder overlay aligned. For repeated scanning, `continuous: true` is the preferred shorthand for keeping the stream alive (`stopOnResult: false` remains supported). Continuous mode suppresses the same CRC result for `duplicateCooldown` milliseconds by default (1200 ms), invalidates late results from the other worker lane after a successful decode, and resets tracking before the next symbol. `pauseWhenHidden` defaults to `true`; hidden tabs stop scheduling scan work and resume with fresh state. The returned scanner also exposes `pause()` and `resume()`, accepts an `AbortSignal` through `signal`, and cleans up if the camera track ends unexpectedly. The main-thread fallback now uses the same candidate-gated recovery rule, so miss count alone never wakes Auto Color or deeper recovery.
1108
+
1109
+ ### `getVersionInfo(version, options?)`
1110
+
1111
+ Returns information such as:
1112
+
1113
+ - matrix size;
1114
+ - number of data cells;
1115
+ - theoretical raw bits;
1116
+ - usable payload capacity;
1117
+ - structural metadata.
1118
+
1119
+ ---
1120
+
1121
+ ## Tests
1122
+
1123
+ Run:
1124
+
1125
+ ```bash
1126
+ npm test
1127
+ ```
1128
+
1129
+ The current test suite covers areas including:
1130
+
1131
+ - RGBW 2-bit mapping;
1132
+ - binary round trips;
1133
+ - Unicode text round trips;
1134
+ - automatic version selection;
1135
+ - all ECC profiles;
1136
+ - version 1 compact framing;
1137
+ - capacity boundaries;
1138
+ - deliberate Reed-Solomon corruption recovery;
1139
+ - Reed-Solomon error + erasure recovery;
1140
+ - zero-overhead spectral-spatial permutation validation;
1141
+ - confidence-assisted recovery beyond the ordinary hard-error limit;
1142
+ - rotation handling;
1143
+ - generated-image scanning;
1144
+ - perspective distortion;
1145
+ - color-cast scanning;
1146
+ - dirty-camera stress scanning with strong yellow cast, haze, blue-channel suppression, and blur;
1147
+ - low-contrast warm-camera regression where normal scanning fails but progressive Auto Tone / Contrast / Color recovery succeeds;
1148
+ - multi-frame confidence fusion and tracked-symbol consistency;
1149
+ - Spectrum ECC 2.0 bounded soft-decision recovery;
1150
+ - affine cross-channel color calibration;
1151
+ - multi-point Triangle16 region sampling with instability-aware confidence;
1152
+ - benchmark reference data;
1153
+ - timed codec round trips;
1154
+ - password-mode secure round trips and wrong-password rejection;
1155
+ - raw 256-bit key round trips, key fingerprinting, and wrong-key rejection;
1156
+ - secure rendered-image scan and decryption.
1157
+
1158
+ ---
1159
+
1160
+ ## Current limitations
1161
+
1162
+ QuadQR is still experimental.
1163
+
1164
+ Important areas that need more research and real-device testing include:
1165
+
1166
+ - printed codes across different printers and inks;
1167
+ - paper color and reflectivity;
1168
+ - display brightness and color profiles;
1169
+ - screen glare;
1170
+ - moiré patterns;
1171
+ - motion blur;
1172
+ - very small modules;
1173
+ - extreme camera angles;
1174
+ - low-light scanning;
1175
+ - RGBW confusion under difficult illumination;
1176
+ - damaged or partially hidden symbols;
1177
+ - standardized recovery percentages;
1178
+ - equal-reliability comparison with ISO QR Code;
1179
+ - local/non-projective distortion correction using the distributed alignment grid;
1180
+ - formal print-quality grading;
1181
+ - performance across different phones and camera systems;
1182
+ - memory-hard password KDF option such as Argon2id for environments where a small WASM/runtime dependency is acceptable;
1183
+ - public/private-key secure payload mode.
1184
+
1185
+ The project should currently be treated as a research and experimental implementation rather than a replacement for standardized QR Code in production-critical environments.
1186
+
1187
+ ---
1188
+
1189
+ ## Roadmap
1190
+
1191
+ - [ ] Equal-reliability benchmark against standard QR
1192
+ - [ ] Automated camera torture-test suite
1193
+ - [ ] Blur, JPEG, noise, perspective, and lighting benchmarks
1194
+ - [ ] Print-and-rescan dataset
1195
+ - [x] Confidence-based RGBW classification
1196
+ - [x] Improved adaptive color calibration
1197
+ - [x] Distributed alignment patterns for large versions
1198
+ - [ ] Interleaving tuned for localized physical damage
1199
+ - [ ] Real-device benchmark dataset
1200
+ - [ ] Formal versioned QuadQR specification
1201
+ - [ ] Implementations in additional languages
1202
+ - [ ] Independent decoder implementation
1203
+
1204
+ ---
1205
+
1206
+ ## Why keep the square design?
1207
+
1208
+ QuadQR intentionally keeps square modules and a square overall matrix.
1209
+
1210
+ Square cells:
1211
+
1212
+ - tessellate without gaps;
1213
+ - provide predictable row/column addressing;
1214
+ - maximize colored area inside each module;
1215
+ - are easy to sample at their center;
1216
+ - work naturally with perspective correction;
1217
+ - keep the geometry relatively simple for camera scanning.
1218
+
1219
+ Alternative module shapes may be interesting visually, but the current focus is data density, reliability, and scan robustness.
1220
+
1221
+ ---
1222
+
1223
+ ## Is QuadQR a QR Code replacement?
1224
+
1225
+ Not currently.
1226
+
1227
+ Standard QR Code has enormous advantages:
1228
+
1229
+ - decades of deployment;
1230
+ - international standardization;
1231
+ - extremely mature decoders;
1232
+ - broad device support;
1233
+ - extensive real-world testing;
1234
+ - proven print reliability.
1235
+
1236
+ QuadQR is exploring a different question:
1237
+
1238
+ > **What can a QR-inspired matrix code look like if we design its data layer around modern color-capable cameras and displays?**
1239
+
1240
+ The goal is experimentation, measurement, and learning.
1241
+
1242
+ ---
1243
+
1244
+ ## Contributing
1245
+
1246
+ Contributions, experiments, test images, scanner improvements, benchmarking ideas, and independent implementations are welcome.
1247
+
1248
+ If you are contributing changes to the wire format, please also update:
1249
+
1250
+ ```text
1251
+ FORMAT.md
1252
+ ```
1253
+
1254
+ Changes that affect capacity, ECC, scanning behavior, or version selection should include tests where practical.
1255
+
1256
+ ---
1257
+
1258
+ ## Security
1259
+
1260
+ Decoded payloads are untrusted input.
1261
+
1262
+ Do not automatically execute decoded:
1263
+
1264
+ - HTML;
1265
+ - JavaScript;
1266
+ - shell commands;
1267
+ - application commands;
1268
+ - URLs.
1269
+
1270
+ Applications using QuadQR should validate and safely handle decoded content just as they would any other external input.
1271
+
1272
+ ---
1273
+
1274
+ ## License
1275
+
1276
+ AGPL v3.0. See `LICENSE`.
1277
+
1278
+ ---
1279
+
1280
+ ## Project status
1281
+
1282
+ **Experimental / research project**
1283
+
1284
+ QuadQR is actively evolving. Format details may change between versions until the wire format is considered stable.