quadqr-js 1.2.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,8 @@ import {
12
12
  decodeMatrix,
13
13
  getVersionInfo,
14
14
  compressPayload,
15
+ compressDeflatePayload,
16
+ compressBrotliPayload,
15
17
  MAX_VERSION
16
18
  } from "./quadqr.js";
17
19
 
@@ -30,6 +32,16 @@ function normalizeEcc(ecc = "M") {
30
32
  return value;
31
33
  }
32
34
 
35
+ function resolveHighDensity(options = {}) {
36
+ if (typeof options.highDensity === "boolean") return options.highDensity;
37
+ // Backward compatibility for the first experimental Triangle16 branch.
38
+ return options.cellEncoding === "triangle16";
39
+ }
40
+
41
+ function encodingForHighDensity(highDensity) {
42
+ return highDensity ? "triangle16" : "rgbw";
43
+ }
44
+
33
45
  function nowMs() {
34
46
  if (typeof performance !== "undefined" && typeof performance.now === "function") {
35
47
  return performance.now();
@@ -76,9 +88,11 @@ export function getStandardQrByteCapacity(version, ecc = "M") {
76
88
  return STANDARD_QR_BYTE_CAPACITY[normalizeEcc(ecc)][version - 1];
77
89
  }
78
90
 
79
- export function compareCapacity(version, ecc = "M") {
91
+ export function compareCapacity(version, ecc = "M", options = {}) {
80
92
  const level = normalizeEcc(ecc);
81
- const quadqr = getVersionInfo(version, { ecc: level });
93
+ const highDensity = resolveHighDensity(options);
94
+ const cellEncoding = encodingForHighDensity(highDensity);
95
+ const quadqr = getVersionInfo(version, { ecc: level, highDensity });
82
96
  const standardQrBytes = getStandardQrByteCapacity(version, level);
83
97
  const quadqrBytes = quadqr.capacityBytes;
84
98
  const differenceBytes = quadqrBytes - standardQrBytes;
@@ -94,6 +108,8 @@ export function compareCapacity(version, ecc = "M") {
94
108
  differenceBytes,
95
109
  ratio,
96
110
  gainPercent,
111
+ highDensity,
112
+ cellEncoding: quadqr.cellEncoding,
97
113
  quadqrBitsPerDataCell: quadqr.bitsPerDataCell,
98
114
  quadqrPayloadEfficiencyPercent: quadqr.theoreticalBits > 0
99
115
  ? (quadqrBytes * 8 / quadqr.theoreticalBits) * 100
@@ -108,42 +124,72 @@ export function compareCapacity(version, ecc = "M") {
108
124
  /** Calculate the smallest QuadQR and standard QR versions for a payload size. */
109
125
  export function calculateCapacityPlan(options = {}) {
110
126
  const ecc = normalizeEcc(options.ecc ?? "M");
127
+ const highDensity = resolveHighDensity(options);
128
+ const cellEncoding = encodingForHighDensity(highDensity);
111
129
  let sourceBytes;
112
130
  const hasConcretePayload = options.payload instanceof Uint8Array || typeof options.payload === "string";
113
131
  if (options.payload instanceof Uint8Array) sourceBytes = options.payload;
114
132
  else if (typeof options.payload === "string") sourceBytes = new TextEncoder().encode(options.payload);
115
133
  else sourceBytes = new Uint8Array(Math.max(0, Math.floor(options.payloadBytes ?? 0)));
116
134
 
117
- const requestedCompression = options.compression ?? "none";
135
+ const requestedCompression = String(options.compression ?? "none").toLowerCase();
136
+ if (!["none", "auto", "smart", "lz", "deflate", "brotli"].includes(requestedCompression)) {
137
+ throw new Error("compression must be none, auto, smart, lz, deflate, or brotli.");
138
+ }
118
139
  const signed = Boolean(options.signed);
119
140
  const keyIdBytes = options.keyId ? new TextEncoder().encode(String(options.keyId)).length : 0;
120
141
  const envelopeHeaderBytes = 16;
121
142
  const signingBytes = signed ? 64 + keyIdBytes + (options.embedPublicKey ? 32 : 0) : 0;
122
143
  let compression = requestedCompression;
144
+ let compressionLevel = null;
123
145
  let storedBytes = sourceBytes.length;
124
146
  let compressed = false;
125
147
 
126
148
  if (requestedCompression !== "none" && hasConcretePayload) {
127
- const candidate = compressPayload(sourceBytes);
128
- if (requestedCompression === "lz" || candidate.length < sourceBytes.length - 2) {
129
- storedBytes = candidate.length;
149
+ const candidates = [];
150
+ const explicitLevel = options.compressionLevel;
151
+ if (requestedCompression === "auto" || requestedCompression === "smart" || requestedCompression === "lz") {
152
+ const level = requestedCompression === "lz" ? (explicitLevel ?? options.lzLevel ?? 6) : 6;
153
+ candidates.push({ compression: "lz", bytes: compressPayload(sourceBytes, { level }).length, level });
154
+ }
155
+ if (requestedCompression === "auto") {
156
+ candidates.push({ compression: "deflate", bytes: compressDeflatePayload(sourceBytes, { level: 6 }).length, level: 6 });
157
+ candidates.push({ compression: "brotli", bytes: compressBrotliPayload(sourceBytes, { quality: 6 }).length, level: 6 });
158
+ } else if (requestedCompression === "smart") {
159
+ for (const level of [6, 8, 9]) candidates.push({ compression: "deflate", bytes: compressDeflatePayload(sourceBytes, { level }).length, level });
160
+ for (const level of [6, 9, 11]) candidates.push({ compression: "brotli", bytes: compressBrotliPayload(sourceBytes, { quality: level }).length, level });
161
+ } else if (requestedCompression === "deflate") {
162
+ const level = explicitLevel ?? options.deflateLevel ?? 6;
163
+ candidates.push({ compression: "deflate", bytes: compressDeflatePayload(sourceBytes, { level }).length, level });
164
+ } else if (requestedCompression === "brotli") {
165
+ const level = explicitLevel ?? options.brotliQuality ?? 11;
166
+ candidates.push({ compression: "brotli", bytes: compressBrotliPayload(sourceBytes, { quality: level }).length, level });
167
+ }
168
+ if (!candidates.length) throw new Error("compression must be none, auto, smart, lz, deflate, or brotli.");
169
+ candidates.sort((a, b) => a.bytes - b.bytes);
170
+ const best = candidates[0];
171
+ const compressionOverhead = signed ? 0 : envelopeHeaderBytes;
172
+ if (!["auto", "smart"].includes(requestedCompression) || best.bytes + compressionOverhead < sourceBytes.length) {
173
+ storedBytes = best.bytes;
130
174
  compressed = true;
131
- compression = "lz";
175
+ compression = best.compression;
176
+ compressionLevel = best.level ?? null;
132
177
  } else {
133
178
  compression = "none";
179
+ compressionLevel = null;
134
180
  }
135
181
  } else if (requestedCompression !== "none" && !hasConcretePayload) {
136
182
  compression = "unknown";
137
183
  }
138
184
 
139
- const needsEnvelope = signed || compressed || requestedCompression === "lz";
185
+ const needsEnvelope = signed || compressed || requestedCompression === "lz" || requestedCompression === "deflate" || requestedCompression === "brotli";
140
186
  const encodedBytes = storedBytes + (needsEnvelope ? envelopeHeaderBytes + signingBytes : 0);
141
187
  const extensionOverheadBytes = encodedBytes - storedBytes;
142
188
 
143
189
  let quadqrVersion = null;
144
190
  let quadqrInfo = null;
145
191
  for (let version = 1; version <= MAX_VERSION; version++) {
146
- const info = getVersionInfo(version, { ecc });
192
+ const info = getVersionInfo(version, { ecc, highDensity });
147
193
  if (encodedBytes <= info.capacityBytes) {
148
194
  quadqrVersion = version;
149
195
  quadqrInfo = info;
@@ -162,11 +208,14 @@ export function calculateCapacityPlan(options = {}) {
162
208
 
163
209
  return {
164
210
  ecc,
211
+ highDensity,
212
+ cellEncoding,
165
213
  sourceBytes: sourceBytes.length,
166
214
  encodedBytes,
167
215
  storedBytes,
168
216
  extensionOverheadBytes,
169
217
  compression,
218
+ compressionLevel,
170
219
  compressed,
171
220
  signed,
172
221
  quadqrVersion,
@@ -185,12 +234,15 @@ export function calculateCapacityPlan(options = {}) {
185
234
 
186
235
  export function buildCapacityComparison(options = {}) {
187
236
  const ecc = normalizeEcc(options.ecc ?? "M");
237
+ const highDensity = resolveHighDensity(options);
188
238
  const versions = options.versions ?? Array.from({ length: MAX_VERSION }, (_, i) => i + 1);
189
- return versions.map((version) => compareCapacity(version, ecc));
239
+ return versions.map((version) => compareCapacity(version, ecc, { highDensity }));
190
240
  }
191
241
 
192
242
  export function benchmarkCodec(options = {}) {
193
243
  const ecc = normalizeEcc(options.ecc ?? "M");
244
+ const highDensity = resolveHighDensity(options);
245
+ const cellEncoding = encodingForHighDensity(highDensity);
194
246
  const iterations = Math.max(1, Math.floor(options.iterations ?? 30));
195
247
  const warmup = Math.max(0, Math.floor(options.warmup ?? Math.min(5, iterations)));
196
248
  const requestedSizes = options.payloadSizes ?? [24, 32, 128, 512, 1024, 2048];
@@ -202,14 +254,14 @@ export function benchmarkCodec(options = {}) {
202
254
 
203
255
  let probe;
204
256
  try {
205
- probe = encodeBytes(payload, { ecc });
257
+ probe = encodeBytes(payload, { ecc, highDensity });
206
258
  } catch (error) {
207
259
  results.push({ payloadBytes, skipped: true, reason: error.message });
208
260
  continue;
209
261
  }
210
262
 
211
263
  for (let i = 0; i < warmup; i++) {
212
- const encoded = encodeBytes(payload, { ecc, version: probe.version });
264
+ const encoded = encodeBytes(payload, { ecc, highDensity, version: probe.version });
213
265
  decodeMatrix(encoded.matrix);
214
266
  }
215
267
 
@@ -219,7 +271,7 @@ export function benchmarkCodec(options = {}) {
219
271
 
220
272
  for (let i = 0; i < iterations; i++) {
221
273
  let start = nowMs();
222
- encoded = encodeBytes(payload, { ecc, version: probe.version });
274
+ encoded = encodeBytes(payload, { ecc, highDensity, version: probe.version });
223
275
  encodeSamples.push(nowMs() - start);
224
276
 
225
277
  start = nowMs();
@@ -231,7 +283,7 @@ export function benchmarkCodec(options = {}) {
231
283
  }
232
284
  }
233
285
 
234
- const versionInfo = getVersionInfo(encoded.version, { ecc });
286
+ const versionInfo = getVersionInfo(encoded.version, { ecc, highDensity });
235
287
  results.push({
236
288
  payloadBytes,
237
289
  skipped: false,
@@ -250,6 +302,8 @@ export function benchmarkCodec(options = {}) {
250
302
  return {
251
303
  format: "QuadQR",
252
304
  ecc,
305
+ highDensity,
306
+ cellEncoding,
253
307
  iterations,
254
308
  warmup,
255
309
  generatedAt: new Date().toISOString(),
@@ -259,11 +313,13 @@ export function benchmarkCodec(options = {}) {
259
313
 
260
314
  export function benchmarkReport(options = {}) {
261
315
  const ecc = normalizeEcc(options.ecc ?? "M");
316
+ const highDensity = resolveHighDensity(options);
262
317
  const versions = options.versions ?? [1, 2, 5, 10, 20, 30, 40];
263
318
  return {
264
- capacity: buildCapacityComparison({ ecc, versions }),
319
+ capacity: buildCapacityComparison({ ecc, versions, highDensity }),
265
320
  performance: benchmarkCodec({
266
321
  ecc,
322
+ highDensity,
267
323
  iterations: options.iterations ?? 30,
268
324
  warmup: options.warmup,
269
325
  payloadSizes: options.payloadSizes