quadqr-js 1.5.6 → 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 +1284 -1284
- package/dist/camera-scanner-worker.js +453 -448
- package/dist/esm/camera-scanner-worker-core.js +1081 -794
- package/dist/esm/camera-scanner-worker.js +453 -448
- package/dist/esm/quadqr.js +7460 -7154
- package/dist/quadqr.js +7394 -7088
- package/dist/quadqr.min.js +476 -171
- package/docs/API.md +593 -592
- package/package.json +103 -103
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.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 **
|
|
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.
|