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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  Experimental custom symbology. It is not ISO QR Code.
6
6
 
7
- Only this RGBW format is implemented. Earlier RGB/ternary formats are intentionally not supported.
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 four-state balance penalties. The mask ID is not serialized. The decoder tries all four and accepts only a path whose protected header, ECC, and CRC validate.
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
- bits 4..7 reserved
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>Four visible states. Two bits per data cell. A modern experiment in high-density matrix codes.</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 four visible data states instead of the two states used by a traditional black-and-white QR module.
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
- Each QuadQR data cell represents exactly **2 bits**:
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 raw QuadQR data alphabet is exactly **2 bits per data cell**. Ratios approaching ~3× in the current usable-payload benchmark are caused by differences in total structural and ECC overhead between the two formats, not because a QuadQR cell contains 3 bits.
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 that fails does it 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. 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.
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 that 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 crops run only after a miss. Finder-only recovery also tries multiple center-weighted Auto Color histograms before threshold bracketing. `cameraAutoColorEvery` defaults to 1 so the same-frame Auto Color recovery is attempted immediately after each fast miss. 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.
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 four-state RGBW matrix symbology. It is not ISO/IEC QR Code and is not intended to be decoded by standard QR readers.
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
- bits 6..7 reserved
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
 
@@ -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 quadqr = getVersionInfo(version, { ecc: level });
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