quadqr-js 1.2.0 → 1.3.0
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 +29 -3
- package/README.md +34 -9
- package/SPECIFICATION.md +8 -3
- package/bin/quadqr.js +4 -2
- package/dist/esm/benchmark.js +34 -9
- package/dist/esm/quadqr.js +878 -226
- package/dist/esm/vision.js +452 -14
- package/dist/quadqr.js +1330 -242
- package/dist/quadqr.min.js +1328 -242
- package/docs/API.md +10 -4
- package/docs/CLI.md +6 -0
- package/docs/HIGH_DENSITY_MODE.md +100 -0
- package/docs/README.md +2 -1
- package/docs/TRIANGLE16.md +108 -0
- package/package.json +4 -2
- package/types/benchmark.d.ts +3 -3
- package/types/index.d.ts +29 -1
package/FORMAT.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Experimental custom symbology. It is not ISO QR Code.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Normal mode uses the default 4-state RGBW data cells. An optional experimental **High Density Mode** uses the 16-state Triangle16 physical layout for payload/body cells. Earlier RGB/ternary formats are intentionally not supported.
|
|
8
8
|
|
|
9
9
|
## Matrix sizes
|
|
10
10
|
|
|
@@ -33,6 +33,17 @@ Four data states:
|
|
|
33
33
|
|
|
34
34
|
Structural white and data white intentionally share the same visible/internal value. Reserved-position geometry distinguishes their roles.
|
|
35
35
|
|
|
36
|
+
### High Density Mode (Triangle16)
|
|
37
|
+
|
|
38
|
+
High Density Mode is identified by header flag bit 6 and implemented with Triangle16 cells. Each non-reserved payload module is split by a fixed `/` diagonal into an upper-left triangle and a lower-right triangle. Each triangle independently uses the RGBW 2-bit alphabet. The packed internal value is:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
cell = (upperLeftColor << 2) | lowerRightColor
|
|
42
|
+
range = 0..15
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Therefore Triangle16 has 16 visible states and carries 4 raw bits per body data cell. Same-color pairs such as R/R or B/B render as a visually solid module. Structural modules never use Triangle16 packing.
|
|
46
|
+
|
|
36
47
|
## Finder structures
|
|
37
48
|
|
|
38
49
|
Three 7×7 black/white finder structures are placed at top-left, top-right, and bottom-left, with white separator cells where they fit inside the matrix.
|
|
@@ -99,6 +110,18 @@ Therefore:
|
|
|
99
110
|
1 RGBW data cell = exactly 2 raw bits
|
|
100
111
|
```
|
|
101
112
|
|
|
113
|
+
For Triangle16 ECC/body bytes:
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
bits 7..4 -> Triangle16 cell 0
|
|
117
|
+
bits 3..0 -> Triangle16 cell 1
|
|
118
|
+
|
|
119
|
+
1 byte = exactly 2 Triangle16 body cells
|
|
120
|
+
1 Triangle16 body cell = exactly 4 raw bits
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The protected header remains encoded as four solid-color RGBW-equivalent cells per byte even when Triangle16 is selected. Internally those solid header cells are represented as R/R, G/G, B/B, or W/W. This keeps the bootstrap/header substantially easier to recover from blur and perspective distortion.
|
|
124
|
+
|
|
102
125
|
## Masks
|
|
103
126
|
|
|
104
127
|
Four masks are defined. Each returns a 2-bit value 0..3:
|
|
@@ -117,7 +140,7 @@ visible = raw XOR mask
|
|
|
117
140
|
raw = visible XOR mask
|
|
118
141
|
```
|
|
119
142
|
|
|
120
|
-
The encoder evaluates all four masks using run-length and
|
|
143
|
+
The encoder evaluates all four masks using run-length and color-balance penalties. In Triangle16 body cells, two deterministic 2-bit masks are packed into one 4-bit XOR mask; protected solid-color header cells use the normal RGBW mask on both halves so they remain solid. The mask ID is not serialized. The decoder tries all four and accepts only a path whose protected header, ECC, and CRC validate.
|
|
121
144
|
|
|
122
145
|
## Header
|
|
123
146
|
|
|
@@ -167,7 +190,10 @@ Flags for both header forms:
|
|
|
167
190
|
bit 0 UTF-8 text flag
|
|
168
191
|
bits 1..2 ECC profile id
|
|
169
192
|
bit 3 Secure Payload envelope flag
|
|
170
|
-
|
|
193
|
+
bit 4 internal payload-extension metadata present
|
|
194
|
+
bit 5 signed-payload hint
|
|
195
|
+
bit 6 High Density Mode flag
|
|
196
|
+
bit 7 reserved
|
|
171
197
|
```
|
|
172
198
|
|
|
173
199
|
ECC ids:
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>
|
|
8
|
+
<strong>Normal RGBW mode by default, with an optional experimental High Density Mode using Triangle16 split cells.</strong>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
<a href="SPECIFICATION.md">Technical Specification</a>
|
|
27
27
|
</p>
|
|
28
28
|
|
|
29
|
-
**QuadQR** is an experimental open-source 2D matrix code that uses
|
|
29
|
+
**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.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Default RGBW mapping:
|
|
32
32
|
|
|
33
33
|
| Color | Bits |
|
|
34
34
|
|---|---|
|
|
@@ -39,6 +39,28 @@ Each QuadQR data cell represents exactly **2 bits**:
|
|
|
39
39
|
|
|
40
40
|
That gives QuadQR a four-symbol alphabet and a raw density of **2 bits per data cell**.
|
|
41
41
|
|
|
42
|
+
### Experimental High Density Mode
|
|
43
|
+
|
|
44
|
+
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:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
4 colors × 4 colors = 16 states
|
|
48
|
+
log2(16) = 4 bits per data cell
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
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.
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
const code = encodeText("High-density QuadQR", {
|
|
55
|
+
ecc: "M",
|
|
56
|
+
highDensity: true
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
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
|
+
|
|
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.
|
|
63
|
+
|
|
42
64
|
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.
|
|
43
65
|
|
|
44
66
|
> **Important:** QuadQR is an experimental custom format. It is **not ISO QR Code**, and normal QR scanner apps cannot decode it.
|
|
@@ -131,7 +153,8 @@ So at the raw data-cell level:
|
|
|
131
153
|
| Format | States per data cell | Raw information |
|
|
132
154
|
|---|---:|---:|
|
|
133
155
|
| Binary QR | 2 | 1 bit |
|
|
134
|
-
| QuadQR | 4 | 2 bits |
|
|
156
|
+
| QuadQR RGBW | 4 | 2 bits |
|
|
157
|
+
| QuadQR High Density Mode (Triangle16) | 16 | 4 bits |
|
|
135
158
|
|
|
136
159
|
This is a **2× raw symbol-density advantage**.
|
|
137
160
|
|
|
@@ -280,7 +303,7 @@ Therefore:
|
|
|
280
303
|
|
|
281
304
|
> The capacity benchmark is a same-dimension and same-label comparison, not yet an equal-damage-tolerance comparison.
|
|
282
305
|
|
|
283
|
-
The
|
|
306
|
+
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.
|
|
284
307
|
|
|
285
308
|
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.
|
|
286
309
|
|
|
@@ -442,7 +465,7 @@ White → (255, 255, 255)
|
|
|
442
465
|
|
|
443
466
|
Real camera input is not expected to match those exact values.
|
|
444
467
|
|
|
445
|
-
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. Only after
|
|
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.
|
|
446
469
|
|
|
447
470
|
---
|
|
448
471
|
|
|
@@ -504,9 +527,11 @@ version hypothesis
|
|
|
504
527
|
↓
|
|
505
528
|
primary alignment search
|
|
506
529
|
↓
|
|
507
|
-
homography / perspective correction
|
|
530
|
+
initial homography / perspective correction
|
|
508
531
|
↓
|
|
509
532
|
distributed alignment-grid validation
|
|
533
|
+
↓ (when geometry is plausible but imperfect)
|
|
534
|
+
secondary alignment multi-point homography refinement
|
|
510
535
|
↓
|
|
511
536
|
module-grid reconstruction
|
|
512
537
|
↓
|
|
@@ -1034,7 +1059,7 @@ Returns a standalone SVG string using the same exact `imageSize`, render styles,
|
|
|
1034
1059
|
|
|
1035
1060
|
### `scanImageData(imageData, options?)`
|
|
1036
1061
|
|
|
1037
|
-
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. 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.
|
|
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.
|
|
1038
1063
|
|
|
1039
1064
|
### `scanFile(file, options?)`
|
|
1040
1065
|
|
|
@@ -1046,7 +1071,7 @@ Scans one frame from an HTML video element. By default, if the video is displaye
|
|
|
1046
1071
|
|
|
1047
1072
|
### `startCameraScanner(video, options?)`
|
|
1048
1073
|
|
|
1049
|
-
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
|
|
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.
|
|
1050
1075
|
|
|
1051
1076
|
### `getVersionInfo(version, options?)`
|
|
1052
1077
|
|
package/SPECIFICATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Status
|
|
4
4
|
|
|
5
|
-
QuadQR is an experimental
|
|
5
|
+
QuadQR is an experimental RGBW matrix symbology. Normal mode uses 4-state RGBW cells, while the optional **High Density Mode** is experimental and uses the 16-state Triangle16 physical layout. It is not ISO/IEC QR Code and is not intended to be decoded by standard QR readers.
|
|
6
6
|
|
|
7
7
|
The physical matrix format remains **QuadQR Format v5**. Normal application data is always treated simply as UTF-8 text or arbitrary bytes. Compression, signatures, encryption, rendering, and diagnostics are optional features layered around that stable matrix codec.
|
|
8
8
|
|
|
@@ -22,7 +22,7 @@ Application bytes / UTF-8 text
|
|
|
22
22
|
├─ CRC-32
|
|
23
23
|
├─ GF(256) Reed-Solomon Spectrum ECC
|
|
24
24
|
├─ spectral-spatial interleaving
|
|
25
|
-
└─ RGBW matrix
|
|
25
|
+
└─ normal RGBW or High Density Triangle16 matrix
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
There is deliberately **no public payload-type registry**. Applications do not select URL, JSON, contact, Wi-Fi, or other semantic types. They encode text or bytes and interpret that data themselves.
|
|
@@ -40,6 +40,10 @@ Data modules use exactly four states:
|
|
|
40
40
|
|
|
41
41
|
Structural black is separate from the data alphabet. One encoded byte maps to exactly four RGBW data cells.
|
|
42
42
|
|
|
43
|
+
### High Density Mode
|
|
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.
|
|
46
|
+
|
|
43
47
|
## 3. Matrix sizing
|
|
44
48
|
|
|
45
49
|
```text
|
|
@@ -59,7 +63,8 @@ bits 1..2 ECC profile id
|
|
|
59
63
|
bit 3 Secure Payload v1 envelope
|
|
60
64
|
bit 4 internal payload-extension metadata present
|
|
61
65
|
bit 5 signed-payload hint
|
|
62
|
-
|
|
66
|
+
bit 6 High Density Mode flag
|
|
67
|
+
bit 7 reserved
|
|
63
68
|
```
|
|
64
69
|
|
|
65
70
|
Bit 4 is an implementation/interoperability hint used only when compression or signing requires metadata. It is not a user-selectable payload mode.
|
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]\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 --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|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`);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function parse(argv) {
|
|
@@ -27,6 +27,7 @@ function parse(argv) {
|
|
|
27
27
|
else if (token === "-o" || token === "--output") flags.output = argv[++i];
|
|
28
28
|
else if (token === "--ecc") flags.ecc = argv[++i];
|
|
29
29
|
else if (token === "--version") flags.version = argv[++i];
|
|
30
|
+
else if (token === "--high-density") flags.highDensity = true;
|
|
30
31
|
else if (token === "--password") flags.password = argv[++i];
|
|
31
32
|
else if (token === "--key") flags.key = argv[++i];
|
|
32
33
|
else if (token === "--compression") flags.compression = argv[++i];
|
|
@@ -101,6 +102,7 @@ async function main() {
|
|
|
101
102
|
|
|
102
103
|
const options = {
|
|
103
104
|
ecc: flags.ecc || "M",
|
|
105
|
+
highDensity: Boolean(flags.highDensity),
|
|
104
106
|
compression: flags.compression || "auto",
|
|
105
107
|
...(flags.version && flags.version !== "auto" ? { version: Number(flags.version) } : {})
|
|
106
108
|
};
|
|
@@ -138,7 +140,7 @@ async function main() {
|
|
|
138
140
|
const saved = output.toLowerCase().endsWith(".svg")
|
|
139
141
|
? await saveSVG(code, output, renderOptions)
|
|
140
142
|
: await savePNG(code, output, renderOptions);
|
|
141
|
-
console.log(`Saved ${output} (${saved.bytes} bytes, v${code.version}, ${code.size}x${code.size}, ECC ${code.eccLevel}).`);
|
|
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}).`);
|
|
142
144
|
return;
|
|
143
145
|
}
|
|
144
146
|
|
package/dist/esm/benchmark.js
CHANGED
|
@@ -30,6 +30,16 @@ function normalizeEcc(ecc = "M") {
|
|
|
30
30
|
return value;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function resolveHighDensity(options = {}) {
|
|
34
|
+
if (typeof options.highDensity === "boolean") return options.highDensity;
|
|
35
|
+
// Backward compatibility for the first experimental Triangle16 branch.
|
|
36
|
+
return options.cellEncoding === "triangle16";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function encodingForHighDensity(highDensity) {
|
|
40
|
+
return highDensity ? "triangle16" : "rgbw";
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
function nowMs() {
|
|
34
44
|
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
35
45
|
return performance.now();
|
|
@@ -76,9 +86,11 @@ export function getStandardQrByteCapacity(version, ecc = "M") {
|
|
|
76
86
|
return STANDARD_QR_BYTE_CAPACITY[normalizeEcc(ecc)][version - 1];
|
|
77
87
|
}
|
|
78
88
|
|
|
79
|
-
export function compareCapacity(version, ecc = "M") {
|
|
89
|
+
export function compareCapacity(version, ecc = "M", options = {}) {
|
|
80
90
|
const level = normalizeEcc(ecc);
|
|
81
|
-
const
|
|
91
|
+
const highDensity = resolveHighDensity(options);
|
|
92
|
+
const cellEncoding = encodingForHighDensity(highDensity);
|
|
93
|
+
const quadqr = getVersionInfo(version, { ecc: level, highDensity });
|
|
82
94
|
const standardQrBytes = getStandardQrByteCapacity(version, level);
|
|
83
95
|
const quadqrBytes = quadqr.capacityBytes;
|
|
84
96
|
const differenceBytes = quadqrBytes - standardQrBytes;
|
|
@@ -94,6 +106,8 @@ export function compareCapacity(version, ecc = "M") {
|
|
|
94
106
|
differenceBytes,
|
|
95
107
|
ratio,
|
|
96
108
|
gainPercent,
|
|
109
|
+
highDensity,
|
|
110
|
+
cellEncoding: quadqr.cellEncoding,
|
|
97
111
|
quadqrBitsPerDataCell: quadqr.bitsPerDataCell,
|
|
98
112
|
quadqrPayloadEfficiencyPercent: quadqr.theoreticalBits > 0
|
|
99
113
|
? (quadqrBytes * 8 / quadqr.theoreticalBits) * 100
|
|
@@ -108,6 +122,8 @@ export function compareCapacity(version, ecc = "M") {
|
|
|
108
122
|
/** Calculate the smallest QuadQR and standard QR versions for a payload size. */
|
|
109
123
|
export function calculateCapacityPlan(options = {}) {
|
|
110
124
|
const ecc = normalizeEcc(options.ecc ?? "M");
|
|
125
|
+
const highDensity = resolveHighDensity(options);
|
|
126
|
+
const cellEncoding = encodingForHighDensity(highDensity);
|
|
111
127
|
let sourceBytes;
|
|
112
128
|
const hasConcretePayload = options.payload instanceof Uint8Array || typeof options.payload === "string";
|
|
113
129
|
if (options.payload instanceof Uint8Array) sourceBytes = options.payload;
|
|
@@ -143,7 +159,7 @@ export function calculateCapacityPlan(options = {}) {
|
|
|
143
159
|
let quadqrVersion = null;
|
|
144
160
|
let quadqrInfo = null;
|
|
145
161
|
for (let version = 1; version <= MAX_VERSION; version++) {
|
|
146
|
-
const info = getVersionInfo(version, { ecc });
|
|
162
|
+
const info = getVersionInfo(version, { ecc, highDensity });
|
|
147
163
|
if (encodedBytes <= info.capacityBytes) {
|
|
148
164
|
quadqrVersion = version;
|
|
149
165
|
quadqrInfo = info;
|
|
@@ -162,6 +178,8 @@ export function calculateCapacityPlan(options = {}) {
|
|
|
162
178
|
|
|
163
179
|
return {
|
|
164
180
|
ecc,
|
|
181
|
+
highDensity,
|
|
182
|
+
cellEncoding,
|
|
165
183
|
sourceBytes: sourceBytes.length,
|
|
166
184
|
encodedBytes,
|
|
167
185
|
storedBytes,
|
|
@@ -185,12 +203,15 @@ export function calculateCapacityPlan(options = {}) {
|
|
|
185
203
|
|
|
186
204
|
export function buildCapacityComparison(options = {}) {
|
|
187
205
|
const ecc = normalizeEcc(options.ecc ?? "M");
|
|
206
|
+
const highDensity = resolveHighDensity(options);
|
|
188
207
|
const versions = options.versions ?? Array.from({ length: MAX_VERSION }, (_, i) => i + 1);
|
|
189
|
-
return versions.map((version) => compareCapacity(version, ecc));
|
|
208
|
+
return versions.map((version) => compareCapacity(version, ecc, { highDensity }));
|
|
190
209
|
}
|
|
191
210
|
|
|
192
211
|
export function benchmarkCodec(options = {}) {
|
|
193
212
|
const ecc = normalizeEcc(options.ecc ?? "M");
|
|
213
|
+
const highDensity = resolveHighDensity(options);
|
|
214
|
+
const cellEncoding = encodingForHighDensity(highDensity);
|
|
194
215
|
const iterations = Math.max(1, Math.floor(options.iterations ?? 30));
|
|
195
216
|
const warmup = Math.max(0, Math.floor(options.warmup ?? Math.min(5, iterations)));
|
|
196
217
|
const requestedSizes = options.payloadSizes ?? [24, 32, 128, 512, 1024, 2048];
|
|
@@ -202,14 +223,14 @@ export function benchmarkCodec(options = {}) {
|
|
|
202
223
|
|
|
203
224
|
let probe;
|
|
204
225
|
try {
|
|
205
|
-
probe = encodeBytes(payload, { ecc });
|
|
226
|
+
probe = encodeBytes(payload, { ecc, highDensity });
|
|
206
227
|
} catch (error) {
|
|
207
228
|
results.push({ payloadBytes, skipped: true, reason: error.message });
|
|
208
229
|
continue;
|
|
209
230
|
}
|
|
210
231
|
|
|
211
232
|
for (let i = 0; i < warmup; i++) {
|
|
212
|
-
const encoded = encodeBytes(payload, { ecc, version: probe.version });
|
|
233
|
+
const encoded = encodeBytes(payload, { ecc, highDensity, version: probe.version });
|
|
213
234
|
decodeMatrix(encoded.matrix);
|
|
214
235
|
}
|
|
215
236
|
|
|
@@ -219,7 +240,7 @@ export function benchmarkCodec(options = {}) {
|
|
|
219
240
|
|
|
220
241
|
for (let i = 0; i < iterations; i++) {
|
|
221
242
|
let start = nowMs();
|
|
222
|
-
encoded = encodeBytes(payload, { ecc, version: probe.version });
|
|
243
|
+
encoded = encodeBytes(payload, { ecc, highDensity, version: probe.version });
|
|
223
244
|
encodeSamples.push(nowMs() - start);
|
|
224
245
|
|
|
225
246
|
start = nowMs();
|
|
@@ -231,7 +252,7 @@ export function benchmarkCodec(options = {}) {
|
|
|
231
252
|
}
|
|
232
253
|
}
|
|
233
254
|
|
|
234
|
-
const versionInfo = getVersionInfo(encoded.version, { ecc });
|
|
255
|
+
const versionInfo = getVersionInfo(encoded.version, { ecc, highDensity });
|
|
235
256
|
results.push({
|
|
236
257
|
payloadBytes,
|
|
237
258
|
skipped: false,
|
|
@@ -250,6 +271,8 @@ export function benchmarkCodec(options = {}) {
|
|
|
250
271
|
return {
|
|
251
272
|
format: "QuadQR",
|
|
252
273
|
ecc,
|
|
274
|
+
highDensity,
|
|
275
|
+
cellEncoding,
|
|
253
276
|
iterations,
|
|
254
277
|
warmup,
|
|
255
278
|
generatedAt: new Date().toISOString(),
|
|
@@ -259,11 +282,13 @@ export function benchmarkCodec(options = {}) {
|
|
|
259
282
|
|
|
260
283
|
export function benchmarkReport(options = {}) {
|
|
261
284
|
const ecc = normalizeEcc(options.ecc ?? "M");
|
|
285
|
+
const highDensity = resolveHighDensity(options);
|
|
262
286
|
const versions = options.versions ?? [1, 2, 5, 10, 20, 30, 40];
|
|
263
287
|
return {
|
|
264
|
-
capacity: buildCapacityComparison({ ecc, versions }),
|
|
288
|
+
capacity: buildCapacityComparison({ ecc, versions, highDensity }),
|
|
265
289
|
performance: benchmarkCodec({
|
|
266
290
|
ecc,
|
|
291
|
+
highDensity,
|
|
267
292
|
iterations: options.iterations ?? 30,
|
|
268
293
|
warmup: options.warmup,
|
|
269
294
|
payloadSizes: options.payloadSizes
|