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 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
- ## Confidence-aware error/erasure decoding
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 module also receives a confidence score derived from the separation between its nearest and second-nearest calibrated palette states.
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
- One GF(256) symbol corresponds to four 2-bit data cells. The symbol confidence is the minimum confidence of those four constituent cells because an error in any one cell changes the reconstructed byte.
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. Valid correction requires syndrome verification and the complete QuadQR payload still must pass CRC-32.
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
- -> nearest calibrated RGBW classification + confidence
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/docs-site/"><strong>Documentation Site</strong></a>
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="docs/README.md">Markdown Docs</a>
22
+ <a href="https://akanshsirohi.github.io/QuadQR/documentation/format.html">Format Reference</a>
23
23
  ·
24
- <a href="FORMAT.md">Matrix Format</a>
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 [`docs/HIGH_DENSITY_MODE.md`](docs/HIGH_DENSITY_MODE.md) for the physical cell mapping, protected-header strategy, scanner sampling rules, and reliability caveats.
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-v2-L.png?raw=true" alt="QuadQR example using ECC profile L" width="320"><br>
76
- <sub><strong>ECC L</strong> · Higher payload capacity</sub>
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-v2-M.png?raw=true" alt="QuadQR example using ECC profile M" width="320"><br>
80
- <sub><strong>ECC M</strong> · More error-correction redundancy</sub>
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: QuadQR-specific reliability without capacity loss
159
+ ### Spectrum ECC 2.0: confidence-aware + soft decoding
162
160
 
163
- QuadQR now uses a second idea that is possible because the scanner already measures calibrated color rather than only black/white state: **confidence-aware Reed-Solomon recovery**.
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
- For every sampled RGBW data module, the scanner keeps both the selected state and a confidence score based on the distance to the nearest and second-nearest calibrated color states. Four module confidences are combined into the confidence of their GF(256) byte symbol.
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
- If normal hard-decision Reed-Solomon decoding fails, the least-confident byte symbols can be promoted to **known erasures**. Reed-Solomon can spend parity more efficiently on known erasure locations than on completely unknown errors. CRC-32 remains the final acceptance check.
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 original payload untouched when compression would not save space. 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.
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 documented in [`SPECIFICATION.md`](./SPECIFICATION.md).
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
- nearest-color classification + confidence scoring
564
+ RGBW/Triangle16 classification + confidence + second hypothesis
549
565
 
550
- four-state unmasking
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.0/dist/quadqr.min.js"></script>
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 [`docs/CLI.md`](docs/CLI.md) for all CLI options, including compression, raw 256-bit key mode, signing, print mode, and scanner diagnostics.
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
- docs-site/
903
- index.html Standalone documentation website
904
- app.js
905
- styles.css
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 Photoshop-style 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 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 Auto Color histograms before threshold bracketing. `cameraHighResolutionMaxDimension` defaults to 1600, `cameraHighResolutionEvery` defaults to 2, and `cameraAutoColorEvery` defaults to 1. Multi-frame voting remains enabled by default with a four-frame history. 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 Auto Color was used, the enhanced recovery pixels and their crop rectangle, so UIs can keep the frozen frame and finder overlay aligned.
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 classification voting;
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 two points well inside the triangles and excludes the diagonal boundary.
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 (`2`; decoder also accepts legacy `1`) |
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
- The portable LZ stream is an LZSS-style format 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:
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"` uses compression only when it meaningfully reduces payload size. When it does not help and the payload is not signed, QuadQR stores the original payload directly with **no extension-envelope overhead**.
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
 
@@ -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 candidate = compressPayload(sourceBytes);
144
- if (requestedCompression === "lz" || candidate.length < sourceBytes.length - 2) {
145
- storedBytes = candidate.length;
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 = "lz";
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,