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.
- package/FORMAT.md +40 -3
- package/README.md +199 -20
- package/SPECIFICATION.md +337 -0
- package/bin/quadqr.js +105 -14
- package/dist/esm/benchmark.js +113 -8
- package/dist/esm/node.js +13 -1
- package/dist/esm/quadqr.js +2312 -299
- package/dist/esm/vision.js +452 -14
- package/dist/quadqr.js +2746 -299
- package/dist/quadqr.min.js +2736 -297
- package/docs/API.md +230 -8
- package/docs/BROWSER_CDN.md +51 -6
- package/docs/CLI.md +156 -76
- package/docs/GETTING_STARTED.md +80 -7
- package/docs/HIGH_DENSITY_MODE.md +100 -0
- package/docs/NODE.md +137 -123
- package/docs/README.md +60 -41
- package/docs/TRIANGLE16.md +108 -0
- package/docs/WASM.md +2 -2
- package/package.json +10 -3
- package/types/benchmark.d.ts +4 -3
- package/types/index.d.ts +144 -2
- package/types/node.d.ts +2 -0
package/dist/esm/benchmark.js
CHANGED
|
@@ -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
|
|
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");
|