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.
@@ -11,6 +11,7 @@ import {
11
11
  encodeBytes,
12
12
  decodeMatrix,
13
13
  getVersionInfo,
14
+ compressPayload,
14
15
  MAX_VERSION
15
16
  } from "./quadqr.js";
16
17
 
@@ -29,6 +30,16 @@ function normalizeEcc(ecc = "M") {
29
30
  return value;
30
31
  }
31
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
+
32
43
  function nowMs() {
33
44
  if (typeof performance !== "undefined" && typeof performance.now === "function") {
34
45
  return performance.now();
@@ -75,9 +86,11 @@ export function getStandardQrByteCapacity(version, ecc = "M") {
75
86
  return STANDARD_QR_BYTE_CAPACITY[normalizeEcc(ecc)][version - 1];
76
87
  }
77
88
 
78
- export function compareCapacity(version, ecc = "M") {
89
+ export function compareCapacity(version, ecc = "M", options = {}) {
79
90
  const level = normalizeEcc(ecc);
80
- const quadqr = getVersionInfo(version, { ecc: level });
91
+ const highDensity = resolveHighDensity(options);
92
+ const cellEncoding = encodingForHighDensity(highDensity);
93
+ const quadqr = getVersionInfo(version, { ecc: level, highDensity });
81
94
  const standardQrBytes = getStandardQrByteCapacity(version, level);
82
95
  const quadqrBytes = quadqr.capacityBytes;
83
96
  const differenceBytes = quadqrBytes - standardQrBytes;
@@ -93,6 +106,8 @@ export function compareCapacity(version, ecc = "M") {
93
106
  differenceBytes,
94
107
  ratio,
95
108
  gainPercent,
109
+ highDensity,
110
+ cellEncoding: quadqr.cellEncoding,
96
111
  quadqrBitsPerDataCell: quadqr.bitsPerDataCell,
97
112
  quadqrPayloadEfficiencyPercent: quadqr.theoreticalBits > 0
98
113
  ? (quadqrBytes * 8 / quadqr.theoreticalBits) * 100
@@ -103,14 +118,100 @@ export function compareCapacity(version, ecc = "M") {
103
118
  };
104
119
  }
105
120
 
121
+
122
+ /** Calculate the smallest QuadQR and standard QR versions for a payload size. */
123
+ export function calculateCapacityPlan(options = {}) {
124
+ const ecc = normalizeEcc(options.ecc ?? "M");
125
+ const highDensity = resolveHighDensity(options);
126
+ const cellEncoding = encodingForHighDensity(highDensity);
127
+ let sourceBytes;
128
+ const hasConcretePayload = options.payload instanceof Uint8Array || typeof options.payload === "string";
129
+ if (options.payload instanceof Uint8Array) sourceBytes = options.payload;
130
+ else if (typeof options.payload === "string") sourceBytes = new TextEncoder().encode(options.payload);
131
+ else sourceBytes = new Uint8Array(Math.max(0, Math.floor(options.payloadBytes ?? 0)));
132
+
133
+ const requestedCompression = options.compression ?? "none";
134
+ const signed = Boolean(options.signed);
135
+ const keyIdBytes = options.keyId ? new TextEncoder().encode(String(options.keyId)).length : 0;
136
+ const envelopeHeaderBytes = 16;
137
+ const signingBytes = signed ? 64 + keyIdBytes + (options.embedPublicKey ? 32 : 0) : 0;
138
+ let compression = requestedCompression;
139
+ let storedBytes = sourceBytes.length;
140
+ let compressed = false;
141
+
142
+ if (requestedCompression !== "none" && hasConcretePayload) {
143
+ const candidate = compressPayload(sourceBytes);
144
+ if (requestedCompression === "lz" || candidate.length < sourceBytes.length - 2) {
145
+ storedBytes = candidate.length;
146
+ compressed = true;
147
+ compression = "lz";
148
+ } else {
149
+ compression = "none";
150
+ }
151
+ } else if (requestedCompression !== "none" && !hasConcretePayload) {
152
+ compression = "unknown";
153
+ }
154
+
155
+ const needsEnvelope = signed || compressed || requestedCompression === "lz";
156
+ const encodedBytes = storedBytes + (needsEnvelope ? envelopeHeaderBytes + signingBytes : 0);
157
+ const extensionOverheadBytes = encodedBytes - storedBytes;
158
+
159
+ let quadqrVersion = null;
160
+ let quadqrInfo = null;
161
+ for (let version = 1; version <= MAX_VERSION; version++) {
162
+ const info = getVersionInfo(version, { ecc, highDensity });
163
+ if (encodedBytes <= info.capacityBytes) {
164
+ quadqrVersion = version;
165
+ quadqrInfo = info;
166
+ break;
167
+ }
168
+ }
169
+
170
+ const standard = STANDARD_QR_BYTE_CAPACITY[ecc];
171
+ let standardQrVersion = null;
172
+ for (let version = 1; version <= MAX_VERSION; version++) {
173
+ if (sourceBytes.length <= standard[version - 1]) {
174
+ standardQrVersion = version;
175
+ break;
176
+ }
177
+ }
178
+
179
+ return {
180
+ ecc,
181
+ highDensity,
182
+ cellEncoding,
183
+ sourceBytes: sourceBytes.length,
184
+ encodedBytes,
185
+ storedBytes,
186
+ extensionOverheadBytes,
187
+ compression,
188
+ compressed,
189
+ signed,
190
+ quadqrVersion,
191
+ quadqrSize: quadqrInfo?.size ?? null,
192
+ quadqrCapacityBytes: quadqrInfo?.capacityBytes ?? null,
193
+ remainingBytes: quadqrInfo ? quadqrInfo.capacityBytes - encodedBytes : null,
194
+ utilizationPercent: quadqrInfo ? encodedBytes / quadqrInfo.capacityBytes * 100 : null,
195
+ standardQrVersion,
196
+ standardQrSize: standardQrVersion ? 21 + 4 * (standardQrVersion - 1) : null,
197
+ matrixWidthSavingsPercent: quadqrInfo && standardQrVersion
198
+ ? (1 - quadqrInfo.size / (21 + 4 * (standardQrVersion - 1))) * 100
199
+ : null,
200
+ note: "Compression is measured only when concrete payload bytes are provided. Byte-count-only estimates cannot predict compression gain."
201
+ };
202
+ }
203
+
106
204
  export function buildCapacityComparison(options = {}) {
107
205
  const ecc = normalizeEcc(options.ecc ?? "M");
206
+ const highDensity = resolveHighDensity(options);
108
207
  const versions = options.versions ?? Array.from({ length: MAX_VERSION }, (_, i) => i + 1);
109
- return versions.map((version) => compareCapacity(version, ecc));
208
+ return versions.map((version) => compareCapacity(version, ecc, { highDensity }));
110
209
  }
111
210
 
112
211
  export function benchmarkCodec(options = {}) {
113
212
  const ecc = normalizeEcc(options.ecc ?? "M");
213
+ const highDensity = resolveHighDensity(options);
214
+ const cellEncoding = encodingForHighDensity(highDensity);
114
215
  const iterations = Math.max(1, Math.floor(options.iterations ?? 30));
115
216
  const warmup = Math.max(0, Math.floor(options.warmup ?? Math.min(5, iterations)));
116
217
  const requestedSizes = options.payloadSizes ?? [24, 32, 128, 512, 1024, 2048];
@@ -122,14 +223,14 @@ export function benchmarkCodec(options = {}) {
122
223
 
123
224
  let probe;
124
225
  try {
125
- probe = encodeBytes(payload, { ecc });
226
+ probe = encodeBytes(payload, { ecc, highDensity });
126
227
  } catch (error) {
127
228
  results.push({ payloadBytes, skipped: true, reason: error.message });
128
229
  continue;
129
230
  }
130
231
 
131
232
  for (let i = 0; i < warmup; i++) {
132
- const encoded = encodeBytes(payload, { ecc, version: probe.version });
233
+ const encoded = encodeBytes(payload, { ecc, highDensity, version: probe.version });
133
234
  decodeMatrix(encoded.matrix);
134
235
  }
135
236
 
@@ -139,7 +240,7 @@ export function benchmarkCodec(options = {}) {
139
240
 
140
241
  for (let i = 0; i < iterations; i++) {
141
242
  let start = nowMs();
142
- encoded = encodeBytes(payload, { ecc, version: probe.version });
243
+ encoded = encodeBytes(payload, { ecc, highDensity, version: probe.version });
143
244
  encodeSamples.push(nowMs() - start);
144
245
 
145
246
  start = nowMs();
@@ -151,7 +252,7 @@ export function benchmarkCodec(options = {}) {
151
252
  }
152
253
  }
153
254
 
154
- const versionInfo = getVersionInfo(encoded.version, { ecc });
255
+ const versionInfo = getVersionInfo(encoded.version, { ecc, highDensity });
155
256
  results.push({
156
257
  payloadBytes,
157
258
  skipped: false,
@@ -170,6 +271,8 @@ export function benchmarkCodec(options = {}) {
170
271
  return {
171
272
  format: "QuadQR",
172
273
  ecc,
274
+ highDensity,
275
+ cellEncoding,
173
276
  iterations,
174
277
  warmup,
175
278
  generatedAt: new Date().toISOString(),
@@ -179,11 +282,13 @@ export function benchmarkCodec(options = {}) {
179
282
 
180
283
  export function benchmarkReport(options = {}) {
181
284
  const ecc = normalizeEcc(options.ecc ?? "M");
285
+ const highDensity = resolveHighDensity(options);
182
286
  const versions = options.versions ?? [1, 2, 5, 10, 20, 30, 40];
183
287
  return {
184
- capacity: buildCapacityComparison({ ecc, versions }),
288
+ capacity: buildCapacityComparison({ ecc, versions, highDensity }),
185
289
  performance: benchmarkCodec({
186
290
  ecc,
291
+ highDensity,
187
292
  iterations: options.iterations ?? 30,
188
293
  warmup: options.warmup,
189
294
  payloadSizes: options.payloadSizes
package/dist/esm/node.js CHANGED
@@ -10,7 +10,7 @@ import { readFile, writeFile } from "node:fs/promises";
10
10
  import { deflateSync, inflateSync } from "node:zlib";
11
11
  import { webcrypto } from "node:crypto";
12
12
 
13
- import { renderToImageData, scanImageData } from "./quadqr.js";
13
+ import { renderToImageData, renderToSVG, scanImageData } from "./quadqr.js";
14
14
 
15
15
  if (!globalThis.crypto) globalThis.crypto = webcrypto;
16
16
 
@@ -242,6 +242,18 @@ export async function savePNG(codeOrMatrix, filename, options = {}) {
242
242
  return { filename, bytes: png.length };
243
243
  }
244
244
 
245
+ /** Render a QuadQR code/matrix directly to an SVG string. */
246
+ export function toSVG(codeOrMatrix, options = {}) {
247
+ return renderToSVG(codeOrMatrix, options);
248
+ }
249
+
250
+ /** Render a QuadQR code/matrix and save it as SVG. */
251
+ export async function saveSVG(codeOrMatrix, filename, options = {}) {
252
+ const svg = toSVG(codeOrMatrix, options);
253
+ await writeFile(filename, svg, "utf8");
254
+ return { filename, bytes: Buffer.byteLength(svg, "utf8") };
255
+ }
256
+
245
257
  async function decodeWithOptionalSharp(buffer) {
246
258
  try {
247
259
  const { default: sharp } = await import("sharp");