quadqr-js 1.4.2 → 1.5.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.
@@ -1,170 +1,170 @@
1
- # Compression 3.0
2
-
3
- QuadQR keeps compression as an internal transport detail around normal text or byte payloads. It does not introduce a payload type system, and the decompressor does not need to know which compression level was used.
4
-
5
- ## Modes
6
-
7
- ```js
8
- encodeText(text, { compression: "none" });
9
- encodeText(text, { compression: "auto" });
10
- encodeText(text, { compression: "smart" });
11
- encodeText(text, { compression: "brotli" });
12
- encodeText(text, { compression: "deflate" });
13
- encodeText(text, { compression: "lz" });
14
- ```
15
-
16
- - `none` stores the application payload directly.
17
- - `auto` is the fast balanced mode. It performs one pass with LZ level 6, DEFLATE level 6, and Brotli quality 6, compares the complete stored sizes, and keeps the smallest result.
18
- - `smart` is the opt-in CPU-heavy mode. It starts with the same balanced candidates as `auto`, checks the resulting QuadQR version, and only spends more CPU on stronger DEFLATE/Brotli passes when a smaller physical QuadQR is realistically reachable.
19
- - `brotli` forces the bundled Brotli codec.
20
- - `deflate` forces the portable raw-DEFLATE codec.
21
- - `lz` forces the original QuadQR LZSS-style stream for compatibility and testing.
22
-
23
- For unsigned payloads, `auto` and `smart` include the 16-byte internal extension-envelope cost when deciding whether compression helps. If compression would not make the complete stored representation smaller, the original payload is kept with no compression envelope. Signed payloads already require the extension envelope, so their comparison includes the fixed signing metadata automatically. Secure payload planning also includes the fixed encryption-envelope overhead when Smart evaluates version boundaries.
24
-
25
- ## Auto versus Smart
26
-
27
- `auto` intentionally avoids expensive maximum-quality passes:
28
-
29
- ```text
30
- Raw
31
- LZ level 6
32
- DEFLATE level 6
33
- Brotli quality 6
34
- ↓
35
- smallest final representation
36
- ```
37
-
38
- `smart` begins the same way, then considers the next smaller QuadQR version. When that boundary is close enough to be plausible, it escalates in stages:
39
-
40
- ```text
41
- Balanced pass
42
- LZ 6
43
- DEFLATE 6
44
- Brotli 6
45
- ↓
46
- Is a smaller QuadQR version realistically reachable?
47
- ↓ yes
48
- Strong pass
49
- DEFLATE 8
50
- Brotli 9
51
- ↓
52
- Still close to another/same smaller-version boundary?
53
- ↓ yes
54
- Maximum pass
55
- DEFLATE 9
56
- Brotli 11
57
- ```
58
-
59
- Smart is deliberately CPU-heavy and should be used when minimizing the physical matrix matters more than generation time. If an exact `version` is requested and the balanced pass does not fit, Smart may also escalate to stronger levels when that can plausibly make the requested version fit. The current plausibility gates are intentionally conservative: the strong stage is considered when the next boundary is within 30% of the current stored size or 192 bytes, and the maximum stage when it is within 16% or 96 bytes. The demo runs Smart in a Web Worker so the browser UI remains responsive.
60
-
61
- The generated code object includes generation-only diagnostics such as `compressionLevel`, `compressionStrategy`, and `smartCompression`. Compression level is not stored in the QuadQR because LZ, DEFLATE, and Brotli decoders do not need it.
62
-
63
- ## Explicit compression levels
64
-
65
- Use the generic `compressionLevel` option when you explicitly select LZ, DEFLATE, or Brotli:
66
-
67
- ```js
68
- const lz = encodeText(text, {
69
- compression: "lz",
70
- compressionLevel: 9
71
- });
72
-
73
- const deflated = encodeText(text, {
74
- compression: "deflate",
75
- compressionLevel: 9
76
- });
77
-
78
- const brotlied = encodeText(text, {
79
- compression: "brotli",
80
- compressionLevel: 11
81
- });
82
- ```
83
-
84
- Ranges and defaults:
85
-
86
- | Algorithm | Range | Explicit default |
87
- | --- | ---: | ---: |
88
- | Legacy LZ | 1..9 | 6 |
89
- | DEFLATE | 1..9 | 6 |
90
- | Brotli | 0..11 | 11 |
91
-
92
- Algorithm-specific aliases are also accepted: `lzLevel`, `deflateLevel`, and `brotliQuality`. `compressionLevel` is the recommended public option.
93
-
94
- Levels only affect encoder CPU/search effort and output size. They do not change the compression ID or decoder behavior.
95
-
96
- ## Legacy LZ profile
97
-
98
- Compression ID `1` keeps the original QuadQR LZSS-style wire format. Levels `1..9` only control how much encoder work is spent walking candidate history and, at stronger levels, bounded lazy-match lookahead. Level 6 preserves the historical 32-candidate QuadQR search depth, so existing callers that do not pass a level keep the same default behavior.
99
-
100
- The direct helpers are:
101
-
102
- ```js
103
- compressPayload(bytes, { level: 9 });
104
- decompressPayload(compressedBytes, originalLength);
105
- ```
106
-
107
- The LZ window remains 4095 bytes and each back-reference still represents a 3..18 byte match. The compressed stream format is identical at every level, so the level is never serialized and old/new LZ decoders remain compatible. Auto and Smart use LZ level 6; Smart does not escalate LZ because its additional staged CPU budget is reserved for the stronger DEFLATE/Brotli candidates.
108
-
109
- ## Brotli profile
110
-
111
- Compression ID `3` is a standard Brotli stream. QuadQR vendors the codec into the library so Brotli compression and decompression are synchronous and available in browser ESM, the classic browser bundle, Web Workers, and server-side Node.js without a runtime package or native binding.
112
-
113
- The direct helpers are:
114
-
115
- ```js
116
- compressBrotliPayload(bytes, { quality: 9 });
117
- decompressBrotliPayload(compressedBytes, originalLength);
118
- ```
119
-
120
- Brotli quality accepts integers `0..11`. Explicit `compression: "brotli"` defaults to quality 11 for backward compatibility. `auto` starts at quality 6. `smart` starts at 6 and may test 9 and 11 when a smaller QuadQR version is plausible.
121
-
122
- ## Portable DEFLATE profile
123
-
124
- Compression ID `2` is a raw RFC 1951 DEFLATE stream. QuadQR's bundled encoder uses a fixed-Huffman block with level-dependent LZ77 match-search effort. It supports:
125
-
126
- - a 32 KiB LZ77 window;
127
- - match distances up to 32768 bytes;
128
- - match lengths up to 258 bytes;
129
- - DEFLATE levels `1..9`;
130
- - progressively deeper candidate search and lazy matching at stronger levels;
131
- - deterministic synchronous output;
132
- - no runtime dependency.
133
-
134
- The direct helpers are:
135
-
136
- ```js
137
- compressDeflatePayload(bytes, { level: 9 });
138
- decompressDeflatePayload(compressedBytes, originalLength);
139
- ```
140
-
141
- The stream remains standard raw DEFLATE at every level. The level is not serialized.
142
-
143
- ## Node.js and browser portability
144
-
145
- All payload compression paths are bundled JavaScript. The library does not import `node:zlib`, use browser `CompressionStream`, depend on DOM APIs, or fetch a codec at runtime.
146
-
147
- ```js
148
- import { encodeText, decodeMatrix } from "quadqr-js";
149
-
150
- const code = encodeText("hello ".repeat(1000), {
151
- compression: "smart"
152
- });
153
-
154
- const decoded = decodeMatrix(code.matrix);
155
- console.log(code.compression, code.compressionLevel);
156
- console.log(decoded.compression); // decoder only needs the algorithm ID
157
- ```
158
-
159
- ## Compression IDs and compatibility
160
-
161
- The internal envelope keeps the stable IDs:
162
-
163
- ```text
164
- 0 = none
165
- 1 = legacy LZ
166
- 2 = raw DEFLATE
167
- 3 = Brotli
168
- ```
169
-
170
- Compression level is intentionally not part of the envelope. Existing LZ, DEFLATE, and Brotli decoders remain compatible with streams produced at any supported level.
1
+ # Compression 3.0
2
+
3
+ QuadQR keeps compression as an internal transport detail around normal text or byte payloads. It does not introduce a payload type system, and the decompressor does not need to know which compression level was used.
4
+
5
+ ## Modes
6
+
7
+ ```js
8
+ encodeText(text, { compression: "none" });
9
+ encodeText(text, { compression: "auto" });
10
+ encodeText(text, { compression: "smart" });
11
+ encodeText(text, { compression: "brotli" });
12
+ encodeText(text, { compression: "deflate" });
13
+ encodeText(text, { compression: "lz" });
14
+ ```
15
+
16
+ - `none` stores the application payload directly.
17
+ - `auto` is the fast balanced mode. It performs one pass with LZ level 6, DEFLATE level 6, and Brotli quality 6, compares the complete stored sizes, and keeps the smallest result.
18
+ - `smart` is the opt-in CPU-heavy mode. It starts with the same balanced candidates as `auto`, checks the resulting QuadQR version, and only spends more CPU on stronger DEFLATE/Brotli passes when a smaller physical QuadQR is realistically reachable.
19
+ - `brotli` forces the bundled Brotli codec.
20
+ - `deflate` forces the portable raw-DEFLATE codec.
21
+ - `lz` forces the original QuadQR LZSS-style stream for compatibility and testing.
22
+
23
+ For unsigned payloads, `auto` and `smart` include the 16-byte internal extension-envelope cost when deciding whether compression helps. If compression would not make the complete stored representation smaller, the original payload is kept with no compression envelope. Signed payloads already require the extension envelope, so their comparison includes the fixed signing metadata automatically. Secure payload planning also includes the fixed encryption-envelope overhead when Smart evaluates version boundaries.
24
+
25
+ ## Auto versus Smart
26
+
27
+ `auto` intentionally avoids expensive maximum-quality passes:
28
+
29
+ ```text
30
+ Raw
31
+ LZ level 6
32
+ DEFLATE level 6
33
+ Brotli quality 6
34
+ ↓
35
+ smallest final representation
36
+ ```
37
+
38
+ `smart` begins the same way, then considers the next smaller QuadQR version. When that boundary is close enough to be plausible, it escalates in stages:
39
+
40
+ ```text
41
+ Balanced pass
42
+ LZ 6
43
+ DEFLATE 6
44
+ Brotli 6
45
+ ↓
46
+ Is a smaller QuadQR version realistically reachable?
47
+ ↓ yes
48
+ Strong pass
49
+ DEFLATE 8
50
+ Brotli 9
51
+ ↓
52
+ Still close to another/same smaller-version boundary?
53
+ ↓ yes
54
+ Maximum pass
55
+ DEFLATE 9
56
+ Brotli 11
57
+ ```
58
+
59
+ Smart is deliberately CPU-heavy and should be used when minimizing the physical matrix matters more than generation time. If an exact `version` is requested and the balanced pass does not fit, Smart may also escalate to stronger levels when that can plausibly make the requested version fit. The current plausibility gates are intentionally conservative: the strong stage is considered when the next boundary is within 30% of the current stored size or 192 bytes, and the maximum stage when it is within 16% or 96 bytes. The demo runs Smart in a Web Worker so the browser UI remains responsive.
60
+
61
+ The generated code object includes generation-only diagnostics such as `compressionLevel`, `compressionStrategy`, and `smartCompression`. Compression level is not stored in the QuadQR because LZ, DEFLATE, and Brotli decoders do not need it.
62
+
63
+ ## Explicit compression levels
64
+
65
+ Use the generic `compressionLevel` option when you explicitly select LZ, DEFLATE, or Brotli:
66
+
67
+ ```js
68
+ const lz = encodeText(text, {
69
+ compression: "lz",
70
+ compressionLevel: 9
71
+ });
72
+
73
+ const deflated = encodeText(text, {
74
+ compression: "deflate",
75
+ compressionLevel: 9
76
+ });
77
+
78
+ const brotlied = encodeText(text, {
79
+ compression: "brotli",
80
+ compressionLevel: 11
81
+ });
82
+ ```
83
+
84
+ Ranges and defaults:
85
+
86
+ | Algorithm | Range | Explicit default |
87
+ | --- | ---: | ---: |
88
+ | Legacy LZ | 1..9 | 6 |
89
+ | DEFLATE | 1..9 | 6 |
90
+ | Brotli | 0..11 | 11 |
91
+
92
+ Algorithm-specific aliases are also accepted: `lzLevel`, `deflateLevel`, and `brotliQuality`. `compressionLevel` is the recommended public option.
93
+
94
+ Levels only affect encoder CPU/search effort and output size. They do not change the compression ID or decoder behavior.
95
+
96
+ ## Legacy LZ profile
97
+
98
+ Compression ID `1` keeps the original QuadQR LZSS-style wire format. Levels `1..9` only control how much encoder work is spent walking candidate history and, at stronger levels, bounded lazy-match lookahead. Level 6 preserves the historical 32-candidate QuadQR search depth, so existing callers that do not pass a level keep the same default behavior.
99
+
100
+ The direct helpers are:
101
+
102
+ ```js
103
+ compressPayload(bytes, { level: 9 });
104
+ decompressPayload(compressedBytes, originalLength);
105
+ ```
106
+
107
+ The LZ window remains 4095 bytes and each back-reference still represents a 3..18 byte match. The compressed stream format is identical at every level, so the level is never serialized and old/new LZ decoders remain compatible. Auto and Smart use LZ level 6; Smart does not escalate LZ because its additional staged CPU budget is reserved for the stronger DEFLATE/Brotli candidates.
108
+
109
+ ## Brotli profile
110
+
111
+ Compression ID `3` is a standard Brotli stream. QuadQR vendors the codec into the library so Brotli compression and decompression are synchronous and available in browser ESM, the classic browser bundle, Web Workers, and server-side Node.js without a runtime package or native binding.
112
+
113
+ The direct helpers are:
114
+
115
+ ```js
116
+ compressBrotliPayload(bytes, { quality: 9 });
117
+ decompressBrotliPayload(compressedBytes, originalLength);
118
+ ```
119
+
120
+ Brotli quality accepts integers `0..11`. Explicit `compression: "brotli"` defaults to quality 11 for backward compatibility. `auto` starts at quality 6. `smart` starts at 6 and may test 9 and 11 when a smaller QuadQR version is plausible.
121
+
122
+ ## Portable DEFLATE profile
123
+
124
+ Compression ID `2` is a raw RFC 1951 DEFLATE stream. QuadQR's bundled encoder uses a fixed-Huffman block with level-dependent LZ77 match-search effort. It supports:
125
+
126
+ - a 32 KiB LZ77 window;
127
+ - match distances up to 32768 bytes;
128
+ - match lengths up to 258 bytes;
129
+ - DEFLATE levels `1..9`;
130
+ - progressively deeper candidate search and lazy matching at stronger levels;
131
+ - deterministic synchronous output;
132
+ - no runtime dependency.
133
+
134
+ The direct helpers are:
135
+
136
+ ```js
137
+ compressDeflatePayload(bytes, { level: 9 });
138
+ decompressDeflatePayload(compressedBytes, originalLength);
139
+ ```
140
+
141
+ The stream remains standard raw DEFLATE at every level. The level is not serialized.
142
+
143
+ ## Node.js and browser portability
144
+
145
+ All payload compression paths are bundled JavaScript. The library does not import `node:zlib`, use browser `CompressionStream`, depend on DOM APIs, or fetch a codec at runtime.
146
+
147
+ ```js
148
+ import { encodeText, decodeMatrix } from "quadqr-js";
149
+
150
+ const code = encodeText("hello ".repeat(1000), {
151
+ compression: "smart"
152
+ });
153
+
154
+ const decoded = decodeMatrix(code.matrix);
155
+ console.log(code.compression, code.compressionLevel);
156
+ console.log(decoded.compression); // decoder only needs the algorithm ID
157
+ ```
158
+
159
+ ## Compression IDs and compatibility
160
+
161
+ The internal envelope keeps the stable IDs:
162
+
163
+ ```text
164
+ 0 = none
165
+ 1 = legacy LZ
166
+ 2 = raw DEFLATE
167
+ 3 = Brotli
168
+ ```
169
+
170
+ Compression level is intentionally not part of the envelope. Existing LZ, DEFLATE, and Brotli decoders remain compatible with streams produced at any supported level.
@@ -163,7 +163,7 @@ PNG generation/decoding and SVG generation are built into the Node.js adapter.
163
163
 
164
164
  ```html
165
165
  <canvas id="qr"></canvas>
166
- <script src="https://cdn.jsdelivr.net/npm/quadqr-js@1.4.1/dist/quadqr.min.js"></script>
166
+ <script src="https://cdn.jsdelivr.net/npm/quadqr-js@1.5.1/dist/quadqr.min.js"></script>
167
167
  <script>
168
168
  const code = QuadQR.encodeText("No build step");
169
169
  QuadQR.renderToCanvas(code, document.querySelector("#qr"));
@@ -1,100 +1,100 @@
1
- # High Density Mode (Experimental)
2
-
3
- High Density Mode is an experimental QuadQR option implemented with the Triangle16 cell layout. It keeps the existing QuadQR geometry, finder patterns, timing pattern, alignment patterns, calibration, Spectrum ECC, CRC, security envelope, compression, and signing behavior, but changes how payload data cells are represented.
4
-
5
- ## Physical cell
6
-
7
- A Triangle16 payload cell is split by one fixed `/` diagonal:
8
-
9
- ```text
10
- +---------+
11
- | AAAAAA /|
12
- | AAAAA /B|
13
- | AAAA /BB|
14
- | AAA /BBB|
15
- | AA /BBBB|
16
- | A /BBBBB|
17
- | /BBBBBBB|
18
- +---------+
19
- ```
20
-
21
- `A` is the upper-left triangle and `B` is the lower-right triangle. Each triangle independently uses the existing RGBW alphabet:
22
-
23
- ```text
24
- R = 00
25
- G = 01
26
- B = 10
27
- W = 11
28
- ```
29
-
30
- The pair therefore has 16 states:
31
-
32
- ```text
33
- R/R R/G R/B R/W
34
- G/R G/G G/B G/W
35
- B/R B/G B/B B/W
36
- W/R W/G W/B W/W
37
- ```
38
-
39
- The first triangle carries the high 2 bits and the second triangle carries the low 2 bits. One Triangle16 body cell therefore carries 4 raw bits, so one byte occupies two body cells instead of four RGBW cells.
40
-
41
- ## Protected header stays solid
42
-
43
- The protected bootstrap/header intentionally does not use mixed-color triangles. Its normal RGBW header cells are represented as same-color pairs such as `R/R`, `G/G`, `B/B`, and `W/W`.
44
-
45
- This uses four cells per protected header byte, exactly like RGBW, but makes it much easier for the decoder to recover the mode flag when the image is blurred, skewed, resized, or color-shifted. Header flag bit 6 declares Triangle16 for the ECC-protected body.
46
-
47
- ## Scanner sampling
48
-
49
- The scanner does not sample the diagonal or the exact module center. Each triangle now uses a small **three-anchor sampling cluster** positioned safely inside its region. The cluster is robustly aggregated, and the variation between the three anchors is retained as a spatial-stability signal. This is less sensitive to blur, resampling, small homography offsets, and diagonal color bleed than relying on one point per triangle.
50
-
51
- Each triangle is classified independently against the calibrated RGBW palette. The cell confidence combines the weaker color-classification confidence with the measured within-triangle sample stability. The scanner also retains the most plausible alternate Triangle16 state by changing the less reliable of the two regions first. Those confidence and alternate-state signals feed Spectrum ECC 2.0 for erasure and bounded soft-decision recovery.
52
-
53
- The normal image and camera scanner automatically detects High Density Mode and attempts Triangle16 dual-region sampling when needed. No separate scan mode is required. If ordinary finder/alignment geometry is good enough to locate the symbol but not precise enough to decode the half-cell regions, the scanner performs one bounded **precise-alignment recovery** pass. That pass uses denser alignment-pattern probes and a finer sub-module search, then retries the same dual-triangle classifier.
54
-
55
- ## Rendering rules
56
-
57
- High Density Mode uses exact hard-edged Triangle16 payload cells. Decorative payload styles are intentionally bypassed in this experimental mode because rounded, inset, soft, or depth effects can reduce the usable sampling area or contaminate the diagonal boundary. Structural modules keep the normal QuadQR rendering behavior.
58
-
59
- PNG/ImageData, Canvas, SVG, browser, and Node rendering all support Triangle16.
60
-
61
- ## API
62
-
63
- ```js
64
- import { encodeText } from "quadqr-js";
65
-
66
- const code = encodeText("High-density QuadQR", {
67
- ecc: "M",
68
- highDensity: true
69
- });
70
- ```
71
-
72
- High Density Mode is disabled by default. Existing callers remain on normal RGBW mode unless `highDensity: true` is explicitly supplied.
73
-
74
- ## Capacity
75
-
76
- Raw body density is:
77
-
78
- ```text
79
- RGBW 4 states 2 bits/body cell
80
- Triangle16 16 states 4 bits/body cell
81
- ```
82
-
83
- The protected header remains RGBW-equivalent, and ECC/CRC overhead is unchanged at the byte level, so usable payload capacity is close to but not exactly 2x for a fixed matrix version.
84
-
85
- Use `getVersionInfo(version, { ecc, highDensity: true })` or the demo capacity calculator for the exact value.
86
-
87
- ## Reliability caveat
88
-
89
- Triangle16 doubles raw body bits per cell, but it also halves the spatial area available to each independently classified color. Its meaningful real-world metric is not only bits per matrix cell. It is reliable payload bytes at a fixed physical size, camera distance, angle, lighting condition, resize/compression pipeline, and print quality.
90
-
91
- The branch should therefore be stress-tested before treating High Density Mode as stable. Important cases include:
92
-
93
- - perspective and partial finder degradation;
94
- - defocus and motion blur;
95
- - low camera pixel coverage per module;
96
- - JPEG compression and repeated image resizing;
97
- - shadows, glare, white-balance shifts, and saturation changes;
98
- - cheap printing, ink spread, paper tint, and camera recapture.
99
-
100
- If full 16-state reliability becomes the limiting factor, a restricted triangle alphabet can be explored later without changing the basic physical-cell experiment.
1
+ # High Density Mode (Experimental)
2
+
3
+ High Density Mode is an experimental QuadQR option implemented with the Triangle16 cell layout. It keeps the existing QuadQR geometry, finder patterns, timing pattern, alignment patterns, calibration, Spectrum ECC, CRC, security envelope, compression, and signing behavior, but changes how payload data cells are represented.
4
+
5
+ ## Physical cell
6
+
7
+ A Triangle16 payload cell is split by one fixed `/` diagonal:
8
+
9
+ ```text
10
+ +---------+
11
+ | AAAAAA /|
12
+ | AAAAA /B|
13
+ | AAAA /BB|
14
+ | AAA /BBB|
15
+ | AA /BBBB|
16
+ | A /BBBBB|
17
+ | /BBBBBBB|
18
+ +---------+
19
+ ```
20
+
21
+ `A` is the upper-left triangle and `B` is the lower-right triangle. Each triangle independently uses the existing RGBW alphabet:
22
+
23
+ ```text
24
+ R = 00
25
+ G = 01
26
+ B = 10
27
+ W = 11
28
+ ```
29
+
30
+ The pair therefore has 16 states:
31
+
32
+ ```text
33
+ R/R R/G R/B R/W
34
+ G/R G/G G/B G/W
35
+ B/R B/G B/B B/W
36
+ W/R W/G W/B W/W
37
+ ```
38
+
39
+ The first triangle carries the high 2 bits and the second triangle carries the low 2 bits. One Triangle16 body cell therefore carries 4 raw bits, so one byte occupies two body cells instead of four RGBW cells.
40
+
41
+ ## Protected header stays solid
42
+
43
+ The protected bootstrap/header intentionally does not use mixed-color triangles. Its normal RGBW header cells are represented as same-color pairs such as `R/R`, `G/G`, `B/B`, and `W/W`.
44
+
45
+ This uses four cells per protected header byte, exactly like RGBW, but makes it much easier for the decoder to recover the mode flag when the image is blurred, skewed, resized, or color-shifted. Header flag bit 6 declares Triangle16 for the ECC-protected body.
46
+
47
+ ## Scanner sampling
48
+
49
+ The scanner does not sample the diagonal or the exact module center. Each triangle now uses a small **three-anchor sampling cluster** positioned safely inside its region. The cluster is robustly aggregated, and the variation between the three anchors is retained as a spatial-stability signal. This is less sensitive to blur, resampling, small homography offsets, and diagonal color bleed than relying on one point per triangle.
50
+
51
+ Each triangle is classified independently against the calibrated RGBW palette. The cell confidence combines the weaker color-classification confidence with the measured within-triangle sample stability. The scanner also retains the most plausible alternate Triangle16 state by changing the less reliable of the two regions first. Those confidence and alternate-state signals feed Spectrum ECC 2.0 for erasure and bounded soft-decision recovery.
52
+
53
+ The normal image and camera scanner automatically detects High Density Mode and attempts Triangle16 dual-region sampling when needed. No separate scan mode is required. If ordinary finder/alignment geometry is good enough to locate the symbol but not precise enough to decode the half-cell regions, the scanner performs one bounded **precise-alignment recovery** pass. That pass uses denser alignment-pattern probes and a finer sub-module search, then retries the same dual-triangle classifier.
54
+
55
+ ## Rendering rules
56
+
57
+ High Density Mode uses exact hard-edged Triangle16 payload cells. Decorative payload styles are intentionally bypassed in this experimental mode because rounded, inset, soft, or depth effects can reduce the usable sampling area or contaminate the diagonal boundary. Structural modules keep the normal QuadQR rendering behavior.
58
+
59
+ PNG/ImageData, Canvas, SVG, browser, and Node rendering all support Triangle16.
60
+
61
+ ## API
62
+
63
+ ```js
64
+ import { encodeText } from "quadqr-js";
65
+
66
+ const code = encodeText("High-density QuadQR", {
67
+ ecc: "M",
68
+ highDensity: true
69
+ });
70
+ ```
71
+
72
+ High Density Mode is disabled by default. Existing callers remain on normal RGBW mode unless `highDensity: true` is explicitly supplied.
73
+
74
+ ## Capacity
75
+
76
+ Raw body density is:
77
+
78
+ ```text
79
+ RGBW 4 states 2 bits/body cell
80
+ Triangle16 16 states 4 bits/body cell
81
+ ```
82
+
83
+ The protected header remains RGBW-equivalent, and ECC/CRC overhead is unchanged at the byte level, so usable payload capacity is close to but not exactly 2x for a fixed matrix version.
84
+
85
+ Use `getVersionInfo(version, { ecc, highDensity: true })` or the demo capacity calculator for the exact value.
86
+
87
+ ## Reliability caveat
88
+
89
+ Triangle16 doubles raw body bits per cell, but it also halves the spatial area available to each independently classified color. Its meaningful real-world metric is not only bits per matrix cell. It is reliable payload bytes at a fixed physical size, camera distance, angle, lighting condition, resize/compression pipeline, and print quality.
90
+
91
+ The branch should therefore be stress-tested before treating High Density Mode as stable. Important cases include:
92
+
93
+ - perspective and partial finder degradation;
94
+ - defocus and motion blur;
95
+ - low camera pixel coverage per module;
96
+ - JPEG compression and repeated image resizing;
97
+ - shadows, glare, white-balance shifts, and saturation changes;
98
+ - cheap printing, ink spread, paper tint, and camera recapture.
99
+
100
+ If full 16-state reliability becomes the limiting factor, a restricted triangle alphabet can be explored later without changing the basic physical-cell experiment.