quadqr-js 1.3.0 → 1.4.2
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/FORMAT.md +8 -6
- package/README.md +68 -43
- package/SPECIFICATION.md +23 -8
- package/bin/quadqr.js +4 -2
- package/dist/esm/benchmark.js +37 -6
- package/dist/esm/brotli.js +2210 -0
- package/dist/esm/deflate.js +381 -0
- package/dist/esm/quadqr.js +1053 -121
- package/dist/esm/security.js +19 -0
- package/dist/esm/vision.js +244 -66
- package/dist/quadqr.js +4085 -370
- package/dist/quadqr.min.js +3799 -366
- package/docs/API.md +42 -11
- package/docs/BROWSER_CDN.md +9 -5
- package/docs/CLI.md +13 -2
- package/docs/COMPRESSION.md +170 -0
- package/docs/GETTING_STARTED.md +12 -2
- package/docs/HIGH_DENSITY_MODE.md +2 -2
- package/docs/NODE.md +22 -0
- package/docs/README.md +7 -5
- package/docs/RELIABILITY_LAB.md +64 -0
- package/docs/TRIANGLE16.md +7 -17
- package/docs/WASM.md +1 -1
- package/package.json +9 -10
- package/types/benchmark.d.ts +0 -8
- package/types/index.d.ts +0 -403
- package/types/node.d.ts +0 -11
package/FORMAT.md
CHANGED
|
@@ -361,13 +361,13 @@ The decoder also tries legacy physical order as a fallback for older QuadQR matr
|
|
|
361
361
|
|
|
362
362
|
Unused data positions are filled with deterministic pseudo-random values in the range 0..3. Padding is not semantically decoded.
|
|
363
363
|
|
|
364
|
-
##
|
|
364
|
+
## Spectrum ECC 2.0 confidence-aware and soft decoding
|
|
365
365
|
|
|
366
|
-
For image/camera scans, classification retains more than the winning RGBW state. Each sampled
|
|
366
|
+
For image/camera scans, classification retains more than the winning RGBW/Triangle16 state. Each sampled data cell receives a confidence score and a bounded second hypothesis derived from the calibrated palette classification. Triangle16 also incorporates the measured disagreement between its three interior samples per triangle so edge bleed or sub-module geometry instability lowers confidence instead of becoming false certainty.
|
|
367
367
|
|
|
368
|
-
|
|
368
|
+
In normal RGBW mode, one GF(256) byte symbol corresponds to four 2-bit cells. In High Density Mode, a body byte corresponds to two 4-bit Triangle16 cells, while the protected header remains four solid RGBW-equivalent cells per byte. Symbol confidence is the minimum confidence of its constituent cells because an error in any constituent cell changes the reconstructed byte.
|
|
369
369
|
|
|
370
|
-
Decoding first attempts normal hard-decision Reed-Solomon correction. If that fails, low-confidence byte positions are progressively promoted to known erasures and the decoder retries error/erasure RS correction.
|
|
370
|
+
Decoding first attempts normal hard-decision Reed-Solomon correction. If that fails, low-confidence byte positions are progressively promoted to known erasures and the decoder retries error/erasure RS correction. If that still fails and scanner second hypotheses are available, Spectrum ECC 2.0 performs a bounded Chase-style search over the least-confident cells, trying single alternate substitutions and then a small pair set. Every candidate is passed through the unchanged RS and CRC validation path.
|
|
371
371
|
|
|
372
372
|
The RS budget follows the usual error/erasure relationship:
|
|
373
373
|
|
|
@@ -391,11 +391,13 @@ RGB frame
|
|
|
391
391
|
-> distributed alignment-grid validation
|
|
392
392
|
-> projective module sampling
|
|
393
393
|
-> observed black/white/R/G/B calibration
|
|
394
|
-
->
|
|
394
|
+
-> observed-palette / white-balance / affine calibration attempts
|
|
395
|
+
-> RGBW or Triangle16 classification + confidence + second hypothesis
|
|
395
396
|
-> four-state XOR unmasking
|
|
396
397
|
-> reverse spectral-spatial permutation
|
|
397
398
|
-> protected header GF(256) hard RS decode
|
|
398
399
|
-> confidence-guided error/erasure retry when needed
|
|
400
|
+
-> bounded second-hypothesis soft retry when needed
|
|
399
401
|
-> body block deinterleaving
|
|
400
402
|
-> body GF(256) error/erasure correction
|
|
401
403
|
-> CRC-32 verification
|
|
@@ -424,4 +426,4 @@ bit 4 = internal payload-extension metadata present
|
|
|
424
426
|
bit 5 = signed-payload hint
|
|
425
427
|
```
|
|
426
428
|
|
|
427
|
-
These bits are not a public payload-type mode. Applications continue to encode normal UTF-8 text or arbitrary bytes. Compression and Ed25519 signing use a compact internal extension envelope only when metadata is required. The envelope, print profile, diagnostics, and scanability test model are specified in [`SPECIFICATION.md`](./SPECIFICATION.md). None of these features changes RGBW cell mapping or Spectrum ECC.
|
|
429
|
+
These bits are not a public payload-type mode. Applications continue to encode normal UTF-8 text or arbitrary bytes. Compression 3.0 (fast Auto, CPU-heavy Smart, explicit LZ/DEFLATE/Brotli levels with legacy LZ wire compatibility) and Ed25519 signing use a compact internal extension envelope only when metadata is required. The envelope, print profile, diagnostics, and scanability test model are specified in [`SPECIFICATION.md`](./SPECIFICATION.md). None of these features changes RGBW cell mapping or Spectrum ECC.
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://akanshsirohi.github.io/QuadQR/demo/"><strong>Try the Live Demo</strong></a>
|
|
13
13
|
·
|
|
14
|
-
<a href="https://akanshsirohi.github.io/QuadQR/
|
|
14
|
+
<a href="https://akanshsirohi.github.io/QuadQR/documentation/"><strong>Documentation Site</strong></a>
|
|
15
15
|
·
|
|
16
16
|
<a href="https://www.npmjs.com/package/quadqr-js">npm</a>
|
|
17
17
|
·
|
|
@@ -19,11 +19,9 @@
|
|
|
19
19
|
·
|
|
20
20
|
<a href="#getting-started">Use the Library</a>
|
|
21
21
|
·
|
|
22
|
-
<a href="
|
|
22
|
+
<a href="https://akanshsirohi.github.io/QuadQR/documentation/format.html">Format Reference</a>
|
|
23
23
|
·
|
|
24
|
-
<a href="
|
|
25
|
-
·
|
|
26
|
-
<a href="SPECIFICATION.md">Technical Specification</a>
|
|
24
|
+
<a href="https://akanshsirohi.github.io/QuadQR/documentation/specification.html">Technical Specification</a>
|
|
27
25
|
</p>
|
|
28
26
|
|
|
29
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.
|
|
@@ -59,7 +57,7 @@ const code = encodeText("High-density QuadQR", {
|
|
|
59
57
|
|
|
60
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.
|
|
61
59
|
|
|
62
|
-
See [
|
|
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.
|
|
63
61
|
|
|
64
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.
|
|
65
63
|
|
|
@@ -72,12 +70,12 @@ Below are QuadQR symbols generated with different error-correction profiles. The
|
|
|
72
70
|
<table>
|
|
73
71
|
<tr>
|
|
74
72
|
<td align="center">
|
|
75
|
-
<img src="assets/quadqr-
|
|
76
|
-
<sub><strong>
|
|
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>
|
|
77
75
|
</td>
|
|
78
76
|
<td align="center">
|
|
79
|
-
<img src="assets/quadqr-
|
|
80
|
-
<sub><strong>
|
|
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>
|
|
81
79
|
</td>
|
|
82
80
|
</tr>
|
|
83
81
|
</table>
|
|
@@ -158,13 +156,13 @@ So at the raw data-cell level:
|
|
|
158
156
|
|
|
159
157
|
This is a **2× raw symbol-density advantage**.
|
|
160
158
|
|
|
161
|
-
### Spectrum ECC:
|
|
159
|
+
### Spectrum ECC 2.0: confidence-aware + soft decoding
|
|
162
160
|
|
|
163
|
-
QuadQR
|
|
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.
|
|
164
162
|
|
|
165
|
-
|
|
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.
|
|
166
164
|
|
|
167
|
-
|
|
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.
|
|
168
166
|
|
|
169
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**.
|
|
170
168
|
|
|
@@ -223,6 +221,24 @@ const compressed = encodeText("repeated repeated repeated", {
|
|
|
223
221
|
ecc: "M"
|
|
224
222
|
});
|
|
225
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
|
+
|
|
226
242
|
const keys = await generateSigningKeyPair();
|
|
227
243
|
const signed = await encodeSignedText("verified offline", {
|
|
228
244
|
compression: "auto",
|
|
@@ -238,7 +254,7 @@ console.log(verified.signatureVerified); // true
|
|
|
238
254
|
console.log(verified.signatureTrusted); // true
|
|
239
255
|
```
|
|
240
256
|
|
|
241
|
-
Compression modes are `none`, `auto`, and `lz`. `auto` keeps the
|
|
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.
|
|
242
258
|
|
|
243
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`.
|
|
244
260
|
|
|
@@ -250,7 +266,7 @@ Scanner results include normalized diagnostics such as `confidence`, `geometryCo
|
|
|
250
266
|
|
|
251
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.
|
|
252
268
|
|
|
253
|
-
The interoperability details are
|
|
269
|
+
The interoperability details are covered in the [technical specification](https://akanshsirohi.github.io/QuadQR/documentation/specification.html).
|
|
254
270
|
|
|
255
271
|
---
|
|
256
272
|
|
|
@@ -465,7 +481,7 @@ White → (255, 255, 255)
|
|
|
465
481
|
|
|
466
482
|
Real camera input is not expected to match those exact values.
|
|
467
483
|
|
|
468
|
-
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 stays fast: QuadQR tries the normal detected geometry and observed palette first. Dense versions can refine an imperfect four-point homography with reliable secondary alignment markers already present in the matrix, without reserving any new cells. If a steep angle leaves exactly two strong finder patterns, a bounded looser third-finder pass runs before heavier color recovery. Only after geometry/color decoding still fails does QuadQR progressively try stronger recovery, including white balancing, spatial normalization, Auto Tone / Auto Contrast / Auto Color-style enhancement, and bounded sub-module geometry refinement. For live video, QuadQR scans the CSS-visible `object-fit: cover` camera region instead of the hidden full sensor frame, so the code keeps the same apparent size/resolution the user sees in the guide. When a dense frame already exposes at least two finders, the camera scanner can also retry the visible ROI at up to 1600 px before expensive color recovery. 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.
|
|
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 stays fast: QuadQR tries the normal detected geometry and observed palette first. Dense versions can refine an imperfect four-point homography with reliable secondary alignment markers already present in the matrix, without reserving any new cells. If a steep angle leaves exactly two strong finder patterns, a bounded looser third-finder pass runs before heavier color recovery. Only after geometry/color decoding still fails does QuadQR progressively try 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, and bounded sub-module geometry refinement. For live video, QuadQR scans the CSS-visible `object-fit: cover` camera region instead of the hidden full sensor frame, so the code keeps the same apparent size/resolution the user sees in the guide. When a dense frame already exposes at least two finders, the camera scanner can also retry the visible ROI at up to 1600 px before expensive color recovery. 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.
|
|
469
485
|
|
|
470
486
|
---
|
|
471
487
|
|
|
@@ -537,17 +553,17 @@ module-grid reconstruction
|
|
|
537
553
|
↓
|
|
538
554
|
fast observed-RGB decode attempt
|
|
539
555
|
↓ (only if needed)
|
|
540
|
-
white balance + spatial normalization
|
|
556
|
+
white balance + affine cross-channel calibration + spatial normalization
|
|
541
557
|
↓ (only if still needed)
|
|
542
|
-
Auto Tone / Auto Contrast / Auto Color-style recovery
|
|
558
|
+
Auto Tone / Auto Contrast / QuadQR Auto Color-style recovery
|
|
543
559
|
↓ (only if still needed)
|
|
544
560
|
sub-module geometry refinement
|
|
545
561
|
↓
|
|
546
562
|
RGB + structural black/white calibration
|
|
547
563
|
↓
|
|
548
|
-
|
|
564
|
+
RGBW/Triangle16 classification + confidence + second hypothesis
|
|
549
565
|
↓
|
|
550
|
-
|
|
566
|
+
unmasking
|
|
551
567
|
↓
|
|
552
568
|
reverse spectral-spatial permutation
|
|
553
569
|
↓
|
|
@@ -555,6 +571,8 @@ protected header Reed-Solomon hard decode
|
|
|
555
571
|
↓
|
|
556
572
|
confidence-guided erasure retry when needed
|
|
557
573
|
↓
|
|
574
|
+
bounded Spectrum ECC 2.0 soft-hypothesis retry when needed
|
|
575
|
+
↓
|
|
558
576
|
body deinterleaving + error/erasure Reed-Solomon decode
|
|
559
577
|
↓
|
|
560
578
|
CRC-32 verification
|
|
@@ -644,7 +662,7 @@ Logo overlays intentionally consume some ECC margin because they cover encoded c
|
|
|
644
662
|
|
|
645
663
|
**Live demo:** https://akanshsirohi.github.io/QuadQR/demo/
|
|
646
664
|
|
|
647
|
-
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.
|
|
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.
|
|
648
666
|
|
|
649
667
|
### Generator & Image Scanner
|
|
650
668
|
|
|
@@ -764,7 +782,7 @@ console.log(result.text);
|
|
|
764
782
|
The `quadqr-js` package can be loaded directly from npm-backed CDNs:
|
|
765
783
|
|
|
766
784
|
```html
|
|
767
|
-
<script src="https://cdn.jsdelivr.net/npm/quadqr-js@1.1
|
|
785
|
+
<script src="https://cdn.jsdelivr.net/npm/quadqr-js@1.4.1/dist/quadqr.min.js"></script>
|
|
768
786
|
<script>
|
|
769
787
|
const code = QuadQR.encodeText("Hello from a script tag");
|
|
770
788
|
</script>
|
|
@@ -798,7 +816,7 @@ npx quadqr-js encode "Private data" --password "my-password" -o secure.png
|
|
|
798
816
|
npx quadqr-js decode secure.png --password "my-password"
|
|
799
817
|
```
|
|
800
818
|
|
|
801
|
-
See [
|
|
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.
|
|
802
820
|
|
|
803
821
|
### Run from source
|
|
804
822
|
|
|
@@ -896,25 +914,29 @@ dist/
|
|
|
896
914
|
|
|
897
915
|
demo/
|
|
898
916
|
index.html Interactive generator, image scanner, camera scanner, benchmark
|
|
899
|
-
app.js
|
|
917
|
+
app.js Responsive UI + worker orchestration
|
|
918
|
+
compute-worker.js Background encode/scan/reliability/benchmark tasks
|
|
900
919
|
styles.css
|
|
901
920
|
|
|
902
|
-
|
|
903
|
-
index.html
|
|
904
|
-
|
|
905
|
-
|
|
921
|
+
documentation/
|
|
922
|
+
index.html Documentation home
|
|
923
|
+
getting-started.html
|
|
924
|
+
browser.html
|
|
925
|
+
node.html
|
|
926
|
+
scanning.html
|
|
927
|
+
compression.html
|
|
928
|
+
security.html
|
|
929
|
+
high-density.html
|
|
930
|
+
reliability.html
|
|
931
|
+
api.html
|
|
932
|
+
cli.html
|
|
933
|
+
wasm.html
|
|
934
|
+
format.html
|
|
935
|
+
specification.html
|
|
936
|
+
app.js Navigation, search, copy, and theme behavior
|
|
937
|
+
search-index.js Client-side documentation search index
|
|
938
|
+
styles.css Responsive documentation UI
|
|
906
939
|
|
|
907
|
-
docs/
|
|
908
|
-
README.md Markdown documentation index
|
|
909
|
-
GETTING_STARTED.md
|
|
910
|
-
API.md
|
|
911
|
-
BROWSER_CDN.md
|
|
912
|
-
NODE.md
|
|
913
|
-
SECURITY.md
|
|
914
|
-
CLI.md
|
|
915
|
-
WASM.md
|
|
916
|
-
|
|
917
|
-
types/ TypeScript declarations for JavaScript consumers
|
|
918
940
|
bin/ `quadqr` CLI (`npx quadqr-js`)
|
|
919
941
|
scripts/ Build, benchmark, and local server scripts
|
|
920
942
|
tests/ Codec and package distribution tests
|
|
@@ -1059,7 +1081,7 @@ Returns a standalone SVG string using the same exact `imageSize`, render styles,
|
|
|
1059
1081
|
|
|
1060
1082
|
### `scanImageData(imageData, options?)`
|
|
1061
1083
|
|
|
1062
|
-
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 / 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.
|
|
1084
|
+
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.
|
|
1063
1085
|
|
|
1064
1086
|
### `scanFile(file, options?)`
|
|
1065
1087
|
|
|
@@ -1071,7 +1093,7 @@ Scans one frame from an HTML video element. By default, if the video is displaye
|
|
|
1071
1093
|
|
|
1072
1094
|
### `startCameraScanner(video, options?)`
|
|
1073
1095
|
|
|
1074
|
-
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. A normal frame always gets the fast RGB-value finder pass first. If a miss still exposes at least two strong finder patterns, QuadQR retries the visible camera ROI at up to 1600 px by default so dense symbols retain more pixels per module. This high-resolution retry is bounded and does not run on empty frames. If it still fails, the **same captured frame** enters a QR-guide recovery path: QuadQR progressively crops away 8%, 16%, and 22% of the surrounding camera frame (then tries the full frame as a final fallback), applies the
|
|
1096
|
+
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. A normal frame always gets the fast RGB-value finder pass first. If a miss still exposes at least two strong finder patterns, QuadQR retries the visible camera ROI at up to 1600 px by default so dense symbols retain more pixels per module. This high-resolution retry is bounded and does not run on empty frames. If it still fails, the **same captured frame** enters a QR-guide recovery path: QuadQR progressively crops away 8%, 16%, and 22% of the surrounding camera frame (then tries the full frame as a final fallback), applies the QuadQR-specific QuadQR Auto Color correction inside that code-centric region, and runs finder detection again. This matters because a live preview can contain dark room pixels, browser chrome, a monitor bezel, or other content that completely changes global QuadQR Auto Color/Otsu statistics even though a manually cropped screenshot scans instantly. Normal scanning stays unchanged and fast because these recovery paths run only after a miss. Finder-only recovery also tries multiple center-weighted QuadQR Auto Color histograms before threshold bracketing. `cameraHighResolutionMaxDimension` defaults to 1600, `cameraHighResolutionEvery` defaults to 2, and `cameraAutoColorEvery` defaults to 1. Multi-frame confidence fusion remains enabled by default with a four-frame history. Frames are kept only when their high-confidence data cells are consistent with the current tracked symbol. Per-cell evidence is weighted by confidence, frame quality, and recency, while second hypotheses are retained for Spectrum ECC 2.0. The optional `onDiagnostic(event)` callback exposes finder candidates, active locator method, crop/geometry/version hypothesis, recovery method, timing, and scan dimensions. `onResult(result, frame)` receives the exact raw decoded camera frame and, when QuadQR Auto Color was used, the enhanced recovery pixels and their crop rectangle, so UIs can keep the frozen frame and finder overlay aligned.
|
|
1075
1097
|
|
|
1076
1098
|
### `getVersionInfo(version, options?)`
|
|
1077
1099
|
|
|
@@ -1112,7 +1134,10 @@ The current test suite covers areas including:
|
|
|
1112
1134
|
- color-cast scanning;
|
|
1113
1135
|
- dirty-camera stress scanning with strong yellow cast, haze, blue-channel suppression, and blur;
|
|
1114
1136
|
- low-contrast warm-camera regression where normal scanning fails but progressive Auto Tone / Contrast / Color recovery succeeds;
|
|
1115
|
-
- multi-frame
|
|
1137
|
+
- multi-frame confidence fusion and tracked-symbol consistency;
|
|
1138
|
+
- Spectrum ECC 2.0 bounded soft-decision recovery;
|
|
1139
|
+
- affine cross-channel color calibration;
|
|
1140
|
+
- multi-point Triangle16 region sampling with instability-aware confidence;
|
|
1116
1141
|
- benchmark reference data;
|
|
1117
1142
|
- timed codec round trips;
|
|
1118
1143
|
- password-mode secure round trips and wrong-password rejection;
|
package/SPECIFICATION.md
CHANGED
|
@@ -42,7 +42,7 @@ Structural black is separate from the data alphabet. One encoded byte maps to ex
|
|
|
42
42
|
|
|
43
43
|
### High Density Mode
|
|
44
44
|
|
|
45
|
-
When `highDensity: true` is selected, each payload module has a fixed `/` diagonal and two independently classified RGBW regions. This creates 16 states and 4 raw bits per body cell. The protected header stays solid-color at 2 bits per cell for robust bootstrap recovery, then header flag bit 6 tells the decoder that the ECC/body stream uses Triangle16 packing. Scanner sampling uses
|
|
45
|
+
When `highDensity: true` is selected, each payload module has a fixed `/` diagonal and two independently classified RGBW regions. This creates 16 states and 4 raw bits per body cell. The protected header stays solid-color at 2 bits per cell for robust bootstrap recovery, then header flag bit 6 tells the decoder that the ECC/body stream uses Triangle16 packing. Scanner sampling uses three interior anchors per triangle, robust aggregation, and a spatial-instability penalty while excluding the diagonal boundary.
|
|
46
46
|
|
|
47
47
|
## 3. Matrix sizing
|
|
48
48
|
|
|
@@ -87,7 +87,7 @@ Version 2+ parity profiles:
|
|
|
87
87
|
| Q | 36 | 18 |
|
|
88
88
|
| H | 48 | 24 |
|
|
89
89
|
|
|
90
|
-
The decoder retains per-cell color confidence. Low-confidence bytes can be promoted to known erasures, allowing Reed-Solomon recovery to use the existing parity budget more efficiently.
|
|
90
|
+
The decoder retains per-cell color confidence and one bounded alternate hypothesis from image/camera classification. Low-confidence bytes can first be promoted to known erasures, allowing Reed-Solomon recovery to use the existing parity budget more efficiently. If hard and error/erasure decoding still fail, Spectrum ECC 2.0 may try the alternate state for a bounded set of the least-confident cells. Candidate search is limited to single substitutions and a small pair set by default, and every successful path must still satisfy Reed-Solomon verification and CRC-32.
|
|
91
91
|
|
|
92
92
|
## 6. Internal payload extension envelope
|
|
93
93
|
|
|
@@ -104,12 +104,12 @@ Fixed header size: **16 bytes**.
|
|
|
104
104
|
| Offset | Size | Field |
|
|
105
105
|
|---:|---:|---|
|
|
106
106
|
| 0 | 4 | ASCII magic `QPX1` |
|
|
107
|
-
| 4 | 1 | Extension version (`
|
|
107
|
+
| 4 | 1 | Extension version (`3`; decoder also accepts legacy `1` and `2`) |
|
|
108
108
|
| 5 | 1 | Flags |
|
|
109
109
|
| 6 | 1 | Compression ID |
|
|
110
110
|
| 7 | 1 | Signature algorithm ID |
|
|
111
111
|
| 8 | 4 | Original application payload length, big-endian |
|
|
112
|
-
| 12 | 1 | Signing key-ID length (v2); legacy signer-label length in v1 |
|
|
112
|
+
| 12 | 1 | Signing key-ID length (v2+); legacy signer-label length in v1 |
|
|
113
113
|
| 13 | 1 | Optional embedded public-key length |
|
|
114
114
|
| 14 | 1 | Signature length |
|
|
115
115
|
| 15 | 1 | Reserved (`0`) |
|
|
@@ -136,25 +136,40 @@ Compression IDs:
|
|
|
136
136
|
|
|
137
137
|
```text
|
|
138
138
|
0 = none
|
|
139
|
-
1 = QuadQR portable LZ
|
|
139
|
+
1 = QuadQR portable LZ (legacy)
|
|
140
|
+
2 = QuadQR portable raw DEFLATE
|
|
141
|
+
3 = bundled Brotli
|
|
140
142
|
```
|
|
141
143
|
|
|
142
|
-
|
|
144
|
+
Compression ID `1` is the original LZSS-style stream with groups of up to eight tokens. Each group begins with one flag byte. A flag bit of `0` means a one-byte literal. A flag bit of `1` means a two-byte back-reference:
|
|
143
145
|
|
|
144
146
|
```text
|
|
145
147
|
12-bit offset: 1..4095 bytes
|
|
146
148
|
4-bit length: stored value + 3, therefore 3..18 bytes
|
|
147
149
|
```
|
|
148
150
|
|
|
151
|
+
Compression ID `1` keeps the original QuadQR LZSS-style stream. Encoder levels `1..9` control candidate-history depth and bounded lazy-match effort; level 6 preserves the historical 32-candidate search depth. The 4095-byte window, 3..18 byte back-reference format, and decoder remain unchanged, and the level is not serialized.
|
|
152
|
+
|
|
153
|
+
Compression ID `2` is a raw RFC 1951 DEFLATE stream. QuadQR's portable encoder uses a deterministic fixed-Huffman block, a 32 KiB LZ77 window, distances up to 32768 bytes, and matches up to 258 bytes. Encoder levels `1..9` control candidate-chain depth and lazy-match effort; the level is not serialized and does not change decoder behavior. The QuadQR decoder accepts the stored/fixed block forms emitted by the library.
|
|
154
|
+
|
|
155
|
+
Compression ID `3` is a standard Brotli stream produced and decoded by QuadQR's bundled synchronous JavaScript codec. It uses no Node `zlib`, browser `CompressionStream`, DOM API, native addon, or network-loaded codec at runtime. Brotli is especially effective for repetitive text and structured payloads, while DEFLATE and legacy LZ remain available for deterministic selection and backward compatibility.
|
|
156
|
+
|
|
149
157
|
Public compression modes are:
|
|
150
158
|
|
|
151
159
|
```text
|
|
152
160
|
none
|
|
153
161
|
auto
|
|
162
|
+
smart
|
|
163
|
+
brotli
|
|
164
|
+
deflate
|
|
154
165
|
lz
|
|
155
166
|
```
|
|
156
167
|
|
|
157
|
-
`compression: "auto"`
|
|
168
|
+
`compression: "auto"` is the fast balanced policy. It evaluates LZ level 6, DEFLATE level 6, and Brotli quality 6 exactly once and chooses the smallest complete stored representation. For unsigned payloads that comparison includes the 16-byte extension-envelope cost, so Auto remains zero-overhead when compression is not useful.
|
|
169
|
+
|
|
170
|
+
`compression: "smart"` is the opt-in CPU-heavy policy. It begins with the Auto candidates, computes the resulting Format v5 QuadQR version using the selected ECC/profile/version bounds, and only escalates when the next smaller matrix is plausibly reachable. The strong stage tests DEFLATE 8 and Brotli 9. If the symbol remains close to a smaller-version boundary, the maximum stage tests DEFLATE 9 and Brotli 11. With an explicitly requested fixed version, Smart does not chase a smaller matrix, but it may escalate when the balanced result does not fit and stronger compression can plausibly make that requested version fit. Signed and secure pipelines include their fixed envelope overhead when evaluating those version boundaries.
|
|
171
|
+
|
|
172
|
+
Explicit `compression: "lz"` and `compression: "deflate"` accept `compressionLevel` `1..9` and default to 6. Explicit `compression: "brotli"` accepts `compressionLevel` `0..11` and defaults to 11. Compression levels are encoder-only parameters and are intentionally not stored in the extension envelope because LZ, RFC 1951, and Brotli decoders do not require them. Auto and Smart use LZ level 6; Smart's staged escalation remains focused on DEFLATE/Brotli.
|
|
158
173
|
|
|
159
174
|
Compression occurs before signing, encryption, and Format v5 ECC.
|
|
160
175
|
|
|
@@ -162,7 +177,7 @@ Compression occurs before signing, encryption, and Format v5 ECC.
|
|
|
162
177
|
|
|
163
178
|
Signature algorithm ID `1` is **Ed25519**.
|
|
164
179
|
|
|
165
|
-
Signed payloads in extension v2 store:
|
|
180
|
+
Signed payloads in extension v2/v3 store:
|
|
166
181
|
|
|
167
182
|
```text
|
|
168
183
|
optional compact key ID
|
package/bin/quadqr.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
import { savePNG, saveSVG, scanFile } from "../dist/node.js";
|
|
16
16
|
|
|
17
17
|
function help() {
|
|
18
|
-
console.log(`QuadQR CLI\n\nUsage:\n quadqr encode <text> [-o file.png|file.svg] [--ecc M] [--version auto|1..40] [--high-density]\n quadqr encode <text> [--compression auto]\n quadqr encode <text> --sign-key signing-key.json [--key-id issuer-main]\n quadqr encode <text> --password <password> [-o file.png|file.svg]\n quadqr decode <file.png> [--password <password> | --key <64-hex-key>] [--verify-key signing-key.json] [--debug]\n quadqr keygen\n quadqr signkeygen [-o signing-key.json]\n\nOptions:\n -o, --output <file> Output PNG/SVG path, or signing-key JSON for signkeygen\n --ecc <L|M|Q|H> ECC profile (default: M)\n --version <auto|1..40> Symbol version (default: auto)\n --compression <mode> none|auto|lz (default: auto)\n --high-density Enable experimental Triangle16 High Density Mode\n --sign-key <file> Sign using a signkeygen JSON bundle\n --key-id <id> Override the signing key ID stored in the QuadQR\n --embed-public-key Also embed the public key for untrusted/self-contained checks\n --verify-key <file> Verify a signed QuadQR with a trusted key bundle\n --password <text> Encrypt/decrypt with password mode\n --key <hex> Encrypt/decrypt with raw 256-bit key mode\n --print Use the print-safe render profile\n --image-size <px> Exact square output size (default: 720)\n --module-size <px> Legacy pixels-per-module sizing\n --quiet-zone <modules> Quiet zone in modules (default: 4)\n --debug Emit scanner diagnostics to stderr on decode\n -h, --help Show help\n`);
|
|
18
|
+
console.log(`QuadQR CLI\n\nUsage:\n quadqr encode <text> [-o file.png|file.svg] [--ecc M] [--version auto|1..40] [--high-density]\n quadqr encode <text> [--compression auto]\n quadqr encode <text> --sign-key signing-key.json [--key-id issuer-main]\n quadqr encode <text> --password <password> [-o file.png|file.svg]\n quadqr decode <file.png> [--password <password> | --key <64-hex-key>] [--verify-key signing-key.json] [--debug]\n quadqr keygen\n quadqr signkeygen [-o signing-key.json]\n\nOptions:\n -o, --output <file> Output PNG/SVG path, or signing-key JSON for signkeygen\n --ecc <L|M|Q|H> ECC profile (default: M)\n --version <auto|1..40> Symbol version (default: auto)\n --compression <mode> none|auto|smart|brotli|deflate|lz (default: auto)\n --compression-level <n> Explicit LZ/DEFLATE 1..9 or Brotli 0..11 level\n --high-density Enable experimental Triangle16 High Density Mode\n --sign-key <file> Sign using a signkeygen JSON bundle\n --key-id <id> Override the signing key ID stored in the QuadQR\n --embed-public-key Also embed the public key for untrusted/self-contained checks\n --verify-key <file> Verify a signed QuadQR with a trusted key bundle\n --password <text> Encrypt/decrypt with password mode\n --key <hex> Encrypt/decrypt with raw 256-bit key mode\n --print Use the print-safe render profile\n --image-size <px> Exact square output size (default: 720)\n --module-size <px> Legacy pixels-per-module sizing\n --quiet-zone <modules> Quiet zone in modules (default: 4)\n --debug Emit scanner diagnostics to stderr on decode\n -h, --help Show help\n`);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function parse(argv) {
|
|
@@ -31,6 +31,7 @@ function parse(argv) {
|
|
|
31
31
|
else if (token === "--password") flags.password = argv[++i];
|
|
32
32
|
else if (token === "--key") flags.key = argv[++i];
|
|
33
33
|
else if (token === "--compression") flags.compression = argv[++i];
|
|
34
|
+
else if (token === "--compression-level") flags.compressionLevel = Number(argv[++i]);
|
|
34
35
|
else if (token === "--sign-key") flags.signKey = argv[++i];
|
|
35
36
|
else if (token === "--key-id") flags.keyId = argv[++i];
|
|
36
37
|
else if (token === "--embed-public-key") flags.embedPublicKey = true;
|
|
@@ -104,6 +105,7 @@ async function main() {
|
|
|
104
105
|
ecc: flags.ecc || "M",
|
|
105
106
|
highDensity: Boolean(flags.highDensity),
|
|
106
107
|
compression: flags.compression || "auto",
|
|
108
|
+
...(Number.isFinite(flags.compressionLevel) ? { compressionLevel: flags.compressionLevel } : {}),
|
|
107
109
|
...(flags.version && flags.version !== "auto" ? { version: Number(flags.version) } : {})
|
|
108
110
|
};
|
|
109
111
|
const signingBundle = flags.signKey ? await readSigningBundle(flags.signKey) : null;
|
|
@@ -140,7 +142,7 @@ async function main() {
|
|
|
140
142
|
const saved = output.toLowerCase().endsWith(".svg")
|
|
141
143
|
? await saveSVG(code, output, renderOptions)
|
|
142
144
|
: await savePNG(code, output, renderOptions);
|
|
143
|
-
console.log(`Saved ${output} (${saved.bytes} bytes, v${code.version}, ${code.size}x${code.size}, ${code.highDensity ? "High Density experimental" : "Normal RGBW"}, ECC ${code.eccLevel}).`);
|
|
145
|
+
console.log(`Saved ${output} (${saved.bytes} bytes, v${code.version}, ${code.size}x${code.size}, ${code.highDensity ? "High Density experimental" : "Normal RGBW"}, ECC ${code.eccLevel}${code.compressed ? `, ${code.compression}${code.compressionLevel != null ? ` level ${code.compressionLevel}` : ""}` : ""}).`);
|
|
144
146
|
return;
|
|
145
147
|
}
|
|
146
148
|
|
package/dist/esm/benchmark.js
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
decodeMatrix,
|
|
13
13
|
getVersionInfo,
|
|
14
14
|
compressPayload,
|
|
15
|
+
compressDeflatePayload,
|
|
16
|
+
compressBrotliPayload,
|
|
15
17
|
MAX_VERSION
|
|
16
18
|
} from "./quadqr.js";
|
|
17
19
|
|
|
@@ -130,29 +132,57 @@ export function calculateCapacityPlan(options = {}) {
|
|
|
130
132
|
else if (typeof options.payload === "string") sourceBytes = new TextEncoder().encode(options.payload);
|
|
131
133
|
else sourceBytes = new Uint8Array(Math.max(0, Math.floor(options.payloadBytes ?? 0)));
|
|
132
134
|
|
|
133
|
-
const requestedCompression = options.compression ?? "none";
|
|
135
|
+
const requestedCompression = String(options.compression ?? "none").toLowerCase();
|
|
136
|
+
if (!["none", "auto", "smart", "lz", "deflate", "brotli"].includes(requestedCompression)) {
|
|
137
|
+
throw new Error("compression must be none, auto, smart, lz, deflate, or brotli.");
|
|
138
|
+
}
|
|
134
139
|
const signed = Boolean(options.signed);
|
|
135
140
|
const keyIdBytes = options.keyId ? new TextEncoder().encode(String(options.keyId)).length : 0;
|
|
136
141
|
const envelopeHeaderBytes = 16;
|
|
137
142
|
const signingBytes = signed ? 64 + keyIdBytes + (options.embedPublicKey ? 32 : 0) : 0;
|
|
138
143
|
let compression = requestedCompression;
|
|
144
|
+
let compressionLevel = null;
|
|
139
145
|
let storedBytes = sourceBytes.length;
|
|
140
146
|
let compressed = false;
|
|
141
147
|
|
|
142
148
|
if (requestedCompression !== "none" && hasConcretePayload) {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
149
|
+
const candidates = [];
|
|
150
|
+
const explicitLevel = options.compressionLevel;
|
|
151
|
+
if (requestedCompression === "auto" || requestedCompression === "smart" || requestedCompression === "lz") {
|
|
152
|
+
const level = requestedCompression === "lz" ? (explicitLevel ?? options.lzLevel ?? 6) : 6;
|
|
153
|
+
candidates.push({ compression: "lz", bytes: compressPayload(sourceBytes, { level }).length, level });
|
|
154
|
+
}
|
|
155
|
+
if (requestedCompression === "auto") {
|
|
156
|
+
candidates.push({ compression: "deflate", bytes: compressDeflatePayload(sourceBytes, { level: 6 }).length, level: 6 });
|
|
157
|
+
candidates.push({ compression: "brotli", bytes: compressBrotliPayload(sourceBytes, { quality: 6 }).length, level: 6 });
|
|
158
|
+
} else if (requestedCompression === "smart") {
|
|
159
|
+
for (const level of [6, 8, 9]) candidates.push({ compression: "deflate", bytes: compressDeflatePayload(sourceBytes, { level }).length, level });
|
|
160
|
+
for (const level of [6, 9, 11]) candidates.push({ compression: "brotli", bytes: compressBrotliPayload(sourceBytes, { quality: level }).length, level });
|
|
161
|
+
} else if (requestedCompression === "deflate") {
|
|
162
|
+
const level = explicitLevel ?? options.deflateLevel ?? 6;
|
|
163
|
+
candidates.push({ compression: "deflate", bytes: compressDeflatePayload(sourceBytes, { level }).length, level });
|
|
164
|
+
} else if (requestedCompression === "brotli") {
|
|
165
|
+
const level = explicitLevel ?? options.brotliQuality ?? 11;
|
|
166
|
+
candidates.push({ compression: "brotli", bytes: compressBrotliPayload(sourceBytes, { quality: level }).length, level });
|
|
167
|
+
}
|
|
168
|
+
if (!candidates.length) throw new Error("compression must be none, auto, smart, lz, deflate, or brotli.");
|
|
169
|
+
candidates.sort((a, b) => a.bytes - b.bytes);
|
|
170
|
+
const best = candidates[0];
|
|
171
|
+
const compressionOverhead = signed ? 0 : envelopeHeaderBytes;
|
|
172
|
+
if (!["auto", "smart"].includes(requestedCompression) || best.bytes + compressionOverhead < sourceBytes.length) {
|
|
173
|
+
storedBytes = best.bytes;
|
|
146
174
|
compressed = true;
|
|
147
|
-
compression =
|
|
175
|
+
compression = best.compression;
|
|
176
|
+
compressionLevel = best.level ?? null;
|
|
148
177
|
} else {
|
|
149
178
|
compression = "none";
|
|
179
|
+
compressionLevel = null;
|
|
150
180
|
}
|
|
151
181
|
} else if (requestedCompression !== "none" && !hasConcretePayload) {
|
|
152
182
|
compression = "unknown";
|
|
153
183
|
}
|
|
154
184
|
|
|
155
|
-
const needsEnvelope = signed || compressed || requestedCompression === "lz";
|
|
185
|
+
const needsEnvelope = signed || compressed || requestedCompression === "lz" || requestedCompression === "deflate" || requestedCompression === "brotli";
|
|
156
186
|
const encodedBytes = storedBytes + (needsEnvelope ? envelopeHeaderBytes + signingBytes : 0);
|
|
157
187
|
const extensionOverheadBytes = encodedBytes - storedBytes;
|
|
158
188
|
|
|
@@ -185,6 +215,7 @@ export function calculateCapacityPlan(options = {}) {
|
|
|
185
215
|
storedBytes,
|
|
186
216
|
extensionOverheadBytes,
|
|
187
217
|
compression,
|
|
218
|
+
compressionLevel,
|
|
188
219
|
compressed,
|
|
189
220
|
signed,
|
|
190
221
|
quadqrVersion,
|