quadqr-js 1.0.2 → 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.
@@ -0,0 +1,337 @@
1
+ # QuadQR Technical Specification
2
+
3
+ ## Status
4
+
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
+
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
+
9
+ For exact matrix geometry and Reed-Solomon framing, see [`FORMAT.md`](./FORMAT.md).
10
+
11
+ ## 1. Layer model
12
+
13
+ ```text
14
+ Application bytes / UTF-8 text
15
+ │
16
+ ├─ optional compression metadata (internal)
17
+ ├─ optional Ed25519 signature metadata (internal)
18
+ ├─ optional Secure Payload v1 (AES-256-GCM)
19
+ │
20
+ └─ QuadQR Format v5
21
+ ├─ protected header
22
+ ├─ CRC-32
23
+ ├─ GF(256) Reed-Solomon Spectrum ECC
24
+ ├─ spectral-spatial interleaving
25
+ └─ normal RGBW or High Density Triangle16 matrix
26
+ ```
27
+
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.
29
+
30
+ ## 2. Physical cell alphabet
31
+
32
+ Data modules use exactly four states:
33
+
34
+ | State | Bits | Internal value |
35
+ |---|---|---:|
36
+ | Red | `00` | 0 |
37
+ | Green | `01` | 1 |
38
+ | Blue | `10` | 2 |
39
+ | White | `11` | 3 |
40
+
41
+ Structural black is separate from the data alphabet. One encoded byte maps to exactly four RGBW data cells.
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
+
47
+ ## 3. Matrix sizing
48
+
49
+ ```text
50
+ size = 21 + 4 × (version - 1)
51
+ version = 1..40
52
+ ```
53
+
54
+ Three 7×7 finder patterns are always present. Version 1 has the legacy QuadQR 5×5 bottom-right alignment marker. Versions 2–40 use the distributed alignment schedule defined in `FORMAT.md`.
55
+
56
+ ## 4. Format v5 header flags
57
+
58
+ The protected Format v5 header uses:
59
+
60
+ ```text
61
+ bit 0 UTF-8 text flag
62
+ bits 1..2 ECC profile id
63
+ bit 3 Secure Payload v1 envelope
64
+ bit 4 internal payload-extension metadata present
65
+ bit 5 signed-payload hint
66
+ bit 6 High Density Mode flag
67
+ bit 7 reserved
68
+ ```
69
+
70
+ Bit 4 is an implementation/interoperability hint used only when compression or signing requires metadata. It is not a user-selectable payload mode.
71
+
72
+ ## 5. Spectrum ECC
73
+
74
+ The field remains:
75
+
76
+ ```text
77
+ GF(2^8) = GF(256)
78
+ primitive polynomial = 0x11d
79
+ ```
80
+
81
+ Version 2+ parity profiles:
82
+
83
+ | Profile | Parity bytes/block | Correctable unknown byte errors/block |
84
+ |---|---:|---:|
85
+ | L | 12 | 6 |
86
+ | M | 24 | 12 |
87
+ | Q | 36 | 18 |
88
+ | H | 48 | 24 |
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.
91
+
92
+ ## 6. Internal payload extension envelope
93
+
94
+ Compression and signing require a small amount of metadata. QuadQR stores that metadata in an **internal extension envelope** only when needed. Applications should normally use `encodeText()`, `encodeBytes()`, `encodeSignedText()`, or `encodeSignedBytes()` and never construct this envelope themselves.
95
+
96
+ Current envelope magic:
97
+
98
+ ```text
99
+ QPX1
100
+ ```
101
+
102
+ Fixed header size: **16 bytes**.
103
+
104
+ | Offset | Size | Field |
105
+ |---:|---:|---|
106
+ | 0 | 4 | ASCII magic `QPX1` |
107
+ | 4 | 1 | Extension version (`2`; decoder also accepts legacy `1`) |
108
+ | 5 | 1 | Flags |
109
+ | 6 | 1 | Compression ID |
110
+ | 7 | 1 | Signature algorithm ID |
111
+ | 8 | 4 | Original application payload length, big-endian |
112
+ | 12 | 1 | Signing key-ID length (v2); legacy signer-label length in v1 |
113
+ | 13 | 1 | Optional embedded public-key length |
114
+ | 14 | 1 | Signature length |
115
+ | 15 | 1 | Reserved (`0`) |
116
+
117
+ Variable bytes follow as:
118
+
119
+ ```text
120
+ keyId || optionalPublicKey || signature || storedPayload
121
+ ```
122
+
123
+ Envelope flags currently use:
124
+
125
+ ```text
126
+ bit 0 signed
127
+ bit 1 compressed
128
+ bit 2 public key embedded
129
+ ```
130
+
131
+ The envelope contains no semantic content type. The Format v5 text flag still determines whether the recovered application payload should be decoded as UTF-8 text.
132
+
133
+ ## 7. Compression
134
+
135
+ Compression IDs:
136
+
137
+ ```text
138
+ 0 = none
139
+ 1 = QuadQR portable LZ
140
+ ```
141
+
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:
143
+
144
+ ```text
145
+ 12-bit offset: 1..4095 bytes
146
+ 4-bit length: stored value + 3, therefore 3..18 bytes
147
+ ```
148
+
149
+ Public compression modes are:
150
+
151
+ ```text
152
+ none
153
+ auto
154
+ lz
155
+ ```
156
+
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**.
158
+
159
+ Compression occurs before signing, encryption, and Format v5 ECC.
160
+
161
+ ## 8. Signed QuadQR
162
+
163
+ Signature algorithm ID `1` is **Ed25519**.
164
+
165
+ Signed payloads in extension v2 store:
166
+
167
+ ```text
168
+ optional compact key ID
169
+ 64-byte Ed25519 signature
170
+ optional 32-byte raw Ed25519 public key only when explicitly requested
171
+ ```
172
+
173
+ The normal production profile does **not** embed the public key. The signature covers:
174
+
175
+ ```text
176
+ 16-byte extension header
177
+ || key ID
178
+ || optional embedded public key
179
+ || stored payload bytes
180
+ ```
181
+
182
+ The signature field itself is excluded from the signed message.
183
+
184
+ The private Ed25519 key is used only by the issuer to create signatures and must never be embedded in a QuadQR. Verification uses a trusted public key supplied externally by the application, server, certificate, or trusted-key registry. The optional key ID is only an identifier that helps the verifier select the correct trusted public key.
185
+
186
+ For compatibility or self-contained integrity checks, implementations may set `embedPublicKey: true`. A signature verified only against a key embedded in the same QuadQR proves integrity and key possession, but does not establish a trusted signer identity. Such verification should be reported separately from verification against an external trust anchor.
187
+
188
+ Legacy extension v1 symbols used a signer label and embedded public key. Decoders may continue to read and verify those symbols for backward compatibility.
189
+
190
+ ## 9. Secure + signed composition
191
+
192
+ When compression, signing, and encryption are combined, QuadQR uses:
193
+
194
+ ```text
195
+ application payload
196
+ → optional compression
197
+ → optional Ed25519 signature metadata
198
+ → AES-256-GCM Secure Payload v1
199
+ → Format v5 ECC/matrix
200
+ ```
201
+
202
+ After scanning:
203
+
204
+ ```text
205
+ Format v5 decode
206
+ → AES-GCM authentication/decryption
207
+ → internal compression/signature metadata processing
208
+ → application payload
209
+ → optional Ed25519 signature verification
210
+ ```
211
+
212
+ This keeps signature metadata confidential when encryption is enabled while preserving offline verification after decryption.
213
+
214
+ ## 10. Text and binary APIs
215
+
216
+ Text and bytes are first-class without semantic payload types:
217
+
218
+ - `encodeText()` / `decodeMatrix().text` for UTF-8 text;
219
+ - `encodeBytes()` for arbitrary bytes;
220
+ - `encodeUint8Array()` / `decodeUint8Array()` as explicit byte-oriented convenience APIs.
221
+
222
+ Compression works on both text and byte payloads through the same `compression` option.
223
+
224
+ ## 11. Rendering profiles
225
+
226
+ Rendering does not modify the encoded matrix.
227
+
228
+ ### Screen mode
229
+
230
+ Uses the normal RGBW palette and permits:
231
+
232
+ ```text
233
+ classic
234
+ soft
235
+ depth
236
+ inset
237
+ ```
238
+
239
+ ### Print mode
240
+
241
+ `mode: "print"` applies conservative defaults:
242
+
243
+ - minimum quiet zone of 4 modules unless explicitly overridden;
244
+ - print-safe darker RGB primaries;
245
+ - Classic solid-module rendering by default;
246
+ - physical-size guidance through `getPrintGuidance()`.
247
+
248
+ Recommended general-purpose starting module size is **0.40 mm/module**. Real printer, paper, ink/toner, lamination, lighting, and camera validation remains necessary for production deployments.
249
+
250
+ ## 12. Logo safety
251
+
252
+ Logos are rendering overlays and never modify Format v5 data structures.
253
+
254
+ `size: "auto"` estimates a conservative logo ratio from:
255
+
256
+ - ECC profile;
257
+ - encoded utilization;
258
+ - version 1 compact-profile penalty;
259
+ - clear-background usage;
260
+ - screen vs print mode.
261
+
262
+ `findMaxSafeLogoSize()` can empirically search the largest decodable logo size when an ImageData-like logo is available. Generated symbols should still be verified after final rendering.
263
+
264
+ ## 13. Scanner diagnostics
265
+
266
+ Successful image scans expose normalized diagnostic fields including:
267
+
268
+ ```text
269
+ confidence
270
+ geometryConfidence
271
+ calibrationConfidence
272
+ structureConfidence
273
+ eccUtilization
274
+ correctedErrors
275
+ erasureSymbols
276
+ ```
277
+
278
+ With `debug: true`, the scanner can also report stage state, geometry candidates, finder/vision passes, latest sampled matrix, confidence matrix, color-normalization method, and the failed stage when decoding does not complete.
279
+
280
+ Debug output is diagnostic evidence, not a cryptographic assurance score.
281
+
282
+ ## 14. Scanability score and torture testing
283
+
284
+ `runImageStressTest()` and `assessScanability()` use deterministic synthetic distortions to estimate robustness against:
285
+
286
+ - blur;
287
+ - low brightness;
288
+ - high exposure;
289
+ - uneven shadow;
290
+ - contrast loss;
291
+ - perspective distortion;
292
+ - JPEG-like quantization/block artifacts;
293
+ - downscaling.
294
+
295
+ Current rating bands:
296
+
297
+ ```text
298
+ 90–100 Excellent
299
+ 75–89 Good
300
+ 50–74 Risky
301
+ 0–49 Likely unscannable
302
+ ```
303
+
304
+ These scores are regression/testing aids. They do not replace validation with real phone cameras, printers, displays, paper stocks, lighting conditions, and physical damage.
305
+
306
+ ## 15. Capacity planning
307
+
308
+ Capacity is determined from the actual Format v5 layout, protected header, CRC, and Spectrum ECC plan. Compression can reduce stored payload bytes, while signatures and encryption add metadata bytes before the matrix codec.
309
+
310
+ The benchmark helper can report:
311
+
312
+ - minimum QuadQR version;
313
+ - encoded bytes;
314
+ - remaining capacity;
315
+ - utilization;
316
+ - approximate same-letter standard QR byte-mode version.
317
+
318
+ When only a payload byte count is known, `compression: "auto"` cannot predict the gain because compressibility depends on the actual bytes.
319
+
320
+ Standard QR comparisons use the same **nominal ECC letter only**. QuadQR and ISO QR recovery strengths are not equivalent and should not be presented as such.
321
+
322
+ ## 16. Compatibility principles
323
+
324
+ Implementations should follow these rules:
325
+
326
+ 1. Keep RGBW mapping exactly `R=00, G=01, B=10, W=11`.
327
+ 2. Keep GF(256) Spectrum ECC and its errors+erasures behavior.
328
+ 3. Preserve Format v5 decoding for normal and Secure Payload symbols.
329
+ 4. Keep application semantics outside the QuadQR codec. Do not require a growing content-type registry.
330
+ 5. Treat compression/signature metadata as internal transport metadata, not a separate user payload mode.
331
+ 6. Treat `keyId` only as an identifier. Signer trust comes from an external trusted public-key binding.
332
+ 7. Keep rendering effects out of structural finder/timing/alignment/calibration modules.
333
+ 8. Validate public format changes with both matrix and rendered-image scanner tests.
334
+
335
+ ## 17. Reference implementation
336
+
337
+ The JavaScript implementation in this repository is the current reference implementation. Public entry points are documented under `docs/` and exercised by `tests/self-test.js` and `tests/package-test.js`.
package/bin/quadqr.js CHANGED
@@ -1,18 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readFile } from "node:fs/promises";
3
+ import { readFile, writeFile } from "node:fs/promises";
4
4
  import process from "node:process";
5
5
  import {
6
6
  bytesToHex,
7
7
  decryptDecoded,
8
8
  encodeSecureText,
9
+ encodeSignedText,
9
10
  encodeText,
10
- generateRaw256Key
11
+ generateRaw256Key,
12
+ generateSigningKeyPair,
13
+ verifyDecodedSignature
11
14
  } from "../dist/index.js";
12
- import { savePNG, scanFile } from "../dist/node.js";
15
+ import { savePNG, saveSVG, scanFile } from "../dist/node.js";
13
16
 
14
17
  function help() {
15
- console.log(`QuadQR CLI\n\nUsage:\n quadqr encode <text> [-o file.png] [--ecc M] [--version auto|1..40]\n quadqr encode <text> --password <password> [-o file.png]\n quadqr encode <text> --key <64-hex-key> [-o file.png]\n quadqr decode <file.png> [--password <password> | --key <64-hex-key>]\n quadqr keygen\n\nOptions:\n -o, --output <file> Output PNG path (default: quadqr.png)\n --ecc <L|M|Q|H> ECC profile (default: M)\n --version <auto|1..40> Symbol version (default: auto)\n --password <text> Encrypt/decrypt with password mode\n --key <hex> Encrypt/decrypt with raw 256-bit key mode\n --module-size <px> PNG module size (default: 12)\n --quiet-zone <modules> PNG quiet zone (default: 4)\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`);
16
19
  }
17
20
 
18
21
  function parse(argv) {
@@ -24,8 +27,17 @@ function parse(argv) {
24
27
  else if (token === "-o" || token === "--output") flags.output = argv[++i];
25
28
  else if (token === "--ecc") flags.ecc = argv[++i];
26
29
  else if (token === "--version") flags.version = argv[++i];
30
+ else if (token === "--high-density") flags.highDensity = true;
27
31
  else if (token === "--password") flags.password = argv[++i];
28
32
  else if (token === "--key") flags.key = argv[++i];
33
+ else if (token === "--compression") flags.compression = argv[++i];
34
+ else if (token === "--sign-key") flags.signKey = argv[++i];
35
+ else if (token === "--key-id") flags.keyId = argv[++i];
36
+ else if (token === "--embed-public-key") flags.embedPublicKey = true;
37
+ else if (token === "--verify-key") flags.verifyKey = argv[++i];
38
+ else if (token === "--print") flags.print = true;
39
+ else if (token === "--debug") flags.debug = true;
40
+ else if (token === "--image-size") flags.imageSize = Number(argv[++i]);
29
41
  else if (token === "--module-size") flags.moduleSize = Number(argv[++i]);
30
42
  else if (token === "--quiet-zone") flags.quietZone = Number(argv[++i]);
31
43
  else args.push(token);
@@ -33,6 +45,25 @@ function parse(argv) {
33
45
  return { args, flags };
34
46
  }
35
47
 
48
+ function bytesToBase64(bytes) {
49
+ return Buffer.from(bytes).toString("base64");
50
+ }
51
+
52
+ function base64ToBytes(value, label) {
53
+ if (typeof value !== "string" || !value) throw new Error(`${label} is missing from signing key bundle.`);
54
+ return new Uint8Array(Buffer.from(value, "base64"));
55
+ }
56
+
57
+ async function readSigningBundle(filename) {
58
+ const parsed = JSON.parse(await readFile(filename, "utf8"));
59
+ if (parsed.algorithm && parsed.algorithm !== "Ed25519") throw new Error(`Unsupported signing algorithm ${parsed.algorithm}.`);
60
+ return {
61
+ privateKey: parsed.privateKeyPkcs8 ? base64ToBytes(parsed.privateKeyPkcs8, "privateKeyPkcs8") : null,
62
+ publicKey: base64ToBytes(parsed.publicKeyRaw || parsed.publicKeyBytes, "publicKeyRaw"),
63
+ keyId: parsed.keyId || null
64
+ };
65
+ }
66
+
36
67
  async function main() {
37
68
  const { args, flags } = parse(process.argv.slice(2));
38
69
  if (flags.help || !args.length) {
@@ -46,6 +77,24 @@ async function main() {
46
77
  return;
47
78
  }
48
79
 
80
+ if (command === "signkeygen") {
81
+ const pair = await generateSigningKeyPair();
82
+ const bundle = JSON.stringify({
83
+ format: "quadqr-ed25519-key-v1",
84
+ algorithm: pair.algorithm,
85
+ privateKeyPkcs8: bytesToBase64(pair.privateKeyPkcs8),
86
+ publicKeyRaw: bytesToBase64(pair.publicKeyBytes),
87
+ keyId: pair.keyId
88
+ }, null, 2) + "\n";
89
+ if (flags.output) {
90
+ await writeFile(flags.output, bundle, { mode: 0o600 });
91
+ console.log(`Saved ${flags.output}. Keep the private key file secret.`);
92
+ } else {
93
+ process.stdout.write(bundle);
94
+ }
95
+ return;
96
+ }
97
+
49
98
  if (command === "encode") {
50
99
  const text = args.join(" ");
51
100
  if (!text) throw new Error("encode requires text.");
@@ -53,27 +102,52 @@ async function main() {
53
102
 
54
103
  const options = {
55
104
  ecc: flags.ecc || "M",
105
+ highDensity: Boolean(flags.highDensity),
106
+ compression: flags.compression || "auto",
56
107
  ...(flags.version && flags.version !== "auto" ? { version: Number(flags.version) } : {})
57
108
  };
58
- const code = flags.password
59
- ? await encodeSecureText(text, { ...options, security: { mode: "password", password: flags.password } })
109
+ const signingBundle = flags.signKey ? await readSigningBundle(flags.signKey) : null;
110
+ const signing = signingBundle ? {
111
+ privateKey: signingBundle.privateKey,
112
+ keyId: flags.keyId || signingBundle.keyId || undefined,
113
+ ...(flags.embedPublicKey ? { publicKey: signingBundle.publicKey, embedPublicKey: true } : {})
114
+ } : null;
115
+ const security = flags.password
116
+ ? { mode: "password", password: flags.password }
60
117
  : flags.key
61
- ? await encodeSecureText(text, { ...options, security: { mode: "raw-key", key: flags.key } })
62
- : encodeText(text, options);
118
+ ? { mode: "raw-key", key: flags.key }
119
+ : null;
120
+
121
+ let code;
122
+ if (security) {
123
+ code = await encodeSecureText(text, { ...options, security, ...(signing ? { signing } : {}) });
124
+ } else if (signing) {
125
+ code = await encodeSignedText(text, { ...options, ...signing });
126
+ } else {
127
+ code = encodeText(text, options);
128
+ }
63
129
 
64
130
  const output = flags.output || "quadqr.png";
65
- const saved = await savePNG(code, output, {
66
- moduleSize: flags.moduleSize || 12,
67
- quietZone: Number.isFinite(flags.quietZone) ? flags.quietZone : 4
68
- });
69
- console.log(`Saved ${output} (${saved.bytes} bytes, v${code.version}, ${code.size}x${code.size}, ECC ${code.eccLevel}).`);
131
+ const renderOptions = {
132
+ quietZone: Number.isFinite(flags.quietZone) ? flags.quietZone : 4,
133
+ ...(flags.print ? { mode: "print" } : {}),
134
+ ...(Number.isFinite(flags.imageSize)
135
+ ? { imageSize: flags.imageSize }
136
+ : Number.isFinite(flags.moduleSize)
137
+ ? { moduleSize: flags.moduleSize }
138
+ : { imageSize: 720 })
139
+ };
140
+ const saved = output.toLowerCase().endsWith(".svg")
141
+ ? await saveSVG(code, output, renderOptions)
142
+ : 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}).`);
70
144
  return;
71
145
  }
72
146
 
73
147
  if (command === "decode") {
74
148
  const filename = args[0];
75
149
  if (!filename) throw new Error("decode requires an image filename.");
76
- let result = await scanFile(filename);
150
+ let result = await scanFile(filename, flags.debug ? { debug: true } : {});
77
151
  if (result.secure) {
78
152
  if (!flags.password && !flags.key) {
79
153
  console.log(JSON.stringify({
@@ -88,6 +162,23 @@ async function main() {
88
162
  }
89
163
  result = await decryptDecoded(result, flags.password ? { password: flags.password } : { key: flags.key });
90
164
  }
165
+ if (result.signed && flags.verifyKey) {
166
+ const verifier = await readSigningBundle(flags.verifyKey);
167
+ result = await verifyDecodedSignature(result, { publicKey: verifier.publicKey });
168
+ }
169
+ if (flags.debug) {
170
+ console.error(JSON.stringify({
171
+ confidence: result.confidence ?? null,
172
+ geometryConfidence: result.geometryConfidence ?? null,
173
+ calibrationConfidence: result.calibrationConfidence ?? null,
174
+ eccUtilization: result.eccUtilization ?? null,
175
+ signed: Boolean(result.signed),
176
+ signatureVerified: result.signatureVerified ?? null,
177
+ signatureTrusted: result.signatureTrusted ?? null,
178
+ signingKeyId: result.signingKeyId ?? null,
179
+ diagnostics: result.diagnostics ?? null
180
+ }, null, 2));
181
+ }
91
182
  if (result.text != null) console.log(result.text);
92
183
  else process.stdout.write(Buffer.from(result.payload));
93
184
  return;