quadqr-js 0.7.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 +390 -0
- package/LICENSE +661 -0
- package/README.md +1042 -0
- package/bin/quadqr.js +102 -0
- package/dist/benchmark.cjs +1 -0
- package/dist/benchmark.js +1 -0
- package/dist/browser.js +2 -0
- package/dist/esm/benchmark.js +193 -0
- package/dist/esm/geometry.js +149 -0
- package/dist/esm/node.js +275 -0
- package/dist/esm/quadqr.js +1962 -0
- package/dist/esm/reed-solomon.js +371 -0
- package/dist/esm/security.js +402 -0
- package/dist/esm/vision.js +752 -0
- package/dist/esm/wasm.js +98 -0
- package/dist/index.cjs +1 -0
- package/dist/index.js +2 -0
- package/dist/node.cjs +1 -0
- package/dist/node.js +1 -0
- package/dist/quadqr.js +3651 -0
- package/dist/quadqr.min.js +3593 -0
- package/dist/wasm/quadqr-core.wasm +0 -0
- package/docs/API.md +188 -0
- package/docs/BROWSER_CDN.md +83 -0
- package/docs/GETTING_STARTED.md +91 -0
- package/docs/NODE.md +92 -0
- package/docs/PUBLISHING.md +115 -0
- package/docs/README.md +26 -0
- package/docs/SECURITY.md +78 -0
- package/docs/WASM.md +36 -0
- package/package.json +95 -0
- package/types/benchmark.d.ts +7 -0
- package/types/index.d.ts +139 -0
- package/types/node.d.ts +9 -0
package/dist/esm/node.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js adapters for QuadQR.
|
|
3
|
+
*
|
|
4
|
+
* Core encoding/decoding remains shared with the browser. This module adds
|
|
5
|
+
* dependency-free PNG file/buffer helpers and can optionally use `sharp`, when
|
|
6
|
+
* already installed by the host application, for JPEG/WebP/AVIF input.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
10
|
+
import { deflateSync, inflateSync } from "node:zlib";
|
|
11
|
+
import { webcrypto } from "node:crypto";
|
|
12
|
+
|
|
13
|
+
import { renderToImageData, scanImageData } from "./quadqr.js";
|
|
14
|
+
|
|
15
|
+
if (!globalThis.crypto) globalThis.crypto = webcrypto;
|
|
16
|
+
|
|
17
|
+
export * from "./quadqr.js";
|
|
18
|
+
export { initWasm, getWasmState, disableWasm } from "./wasm.js";
|
|
19
|
+
|
|
20
|
+
const PNG_SIGNATURE = Uint8Array.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
21
|
+
|
|
22
|
+
function assert(condition, message) {
|
|
23
|
+
if (!condition) throw new Error(message);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function asUint8Array(input) {
|
|
27
|
+
if (input instanceof Uint8Array) return input;
|
|
28
|
+
if (ArrayBuffer.isView(input)) return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
29
|
+
if (input instanceof ArrayBuffer) return new Uint8Array(input);
|
|
30
|
+
return new Uint8Array(input);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function u32be(value) {
|
|
34
|
+
return Uint8Array.from([(value >>> 24) & 255, (value >>> 16) & 255, (value >>> 8) & 255, value & 255]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readU32(bytes, offset) {
|
|
38
|
+
return (((bytes[offset] << 24) >>> 0) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]) >>> 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let PNG_CRC_TABLE = null;
|
|
42
|
+
function pngCrc32(bytes) {
|
|
43
|
+
if (!PNG_CRC_TABLE) {
|
|
44
|
+
PNG_CRC_TABLE = new Uint32Array(256);
|
|
45
|
+
for (let n = 0; n < 256; n++) {
|
|
46
|
+
let c = n;
|
|
47
|
+
for (let k = 0; k < 8; k++) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
|
|
48
|
+
PNG_CRC_TABLE[n] = c >>> 0;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
let crc = 0xffffffff;
|
|
52
|
+
for (const byte of bytes) crc = PNG_CRC_TABLE[(crc ^ byte) & 255] ^ (crc >>> 8);
|
|
53
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function concat(...parts) {
|
|
57
|
+
const total = parts.reduce((sum, part) => sum + part.length, 0);
|
|
58
|
+
const out = new Uint8Array(total);
|
|
59
|
+
let offset = 0;
|
|
60
|
+
for (const part of parts) {
|
|
61
|
+
out.set(part, offset);
|
|
62
|
+
offset += part.length;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function pngChunk(type, data = new Uint8Array(0)) {
|
|
68
|
+
const typeBytes = Uint8Array.from(type.split("").map((char) => char.charCodeAt(0)));
|
|
69
|
+
const body = concat(typeBytes, data);
|
|
70
|
+
return concat(u32be(data.length), body, u32be(pngCrc32(body)));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function paeth(a, b, c) {
|
|
74
|
+
const p = a + b - c;
|
|
75
|
+
const pa = Math.abs(p - a);
|
|
76
|
+
const pb = Math.abs(p - b);
|
|
77
|
+
const pc = Math.abs(p - c);
|
|
78
|
+
return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function unfilterRow(filter, row, previous, bpp) {
|
|
82
|
+
const out = new Uint8Array(row.length);
|
|
83
|
+
for (let x = 0; x < row.length; x++) {
|
|
84
|
+
const left = x >= bpp ? out[x - bpp] : 0;
|
|
85
|
+
const up = previous ? previous[x] : 0;
|
|
86
|
+
const upLeft = previous && x >= bpp ? previous[x - bpp] : 0;
|
|
87
|
+
const raw = row[x];
|
|
88
|
+
switch (filter) {
|
|
89
|
+
case 0: out[x] = raw; break;
|
|
90
|
+
case 1: out[x] = (raw + left) & 255; break;
|
|
91
|
+
case 2: out[x] = (raw + up) & 255; break;
|
|
92
|
+
case 3: out[x] = (raw + Math.floor((left + up) / 2)) & 255; break;
|
|
93
|
+
case 4: out[x] = (raw + paeth(left, up, upLeft)) & 255; break;
|
|
94
|
+
default: throw new Error(`Unsupported PNG filter type ${filter}.`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function samplePacked(row, index, bitDepth) {
|
|
101
|
+
if (bitDepth === 8) return row[index];
|
|
102
|
+
const perByte = 8 / bitDepth;
|
|
103
|
+
const byte = row[Math.floor(index / perByte)];
|
|
104
|
+
const shift = (perByte - 1 - (index % perByte)) * bitDepth;
|
|
105
|
+
const mask = (1 << bitDepth) - 1;
|
|
106
|
+
return (byte >>> shift) & mask;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Decode a non-interlaced PNG into QuadQR's RGBA ImageData shape. */
|
|
110
|
+
export function decodePNG(input) {
|
|
111
|
+
const bytes = asUint8Array(input);
|
|
112
|
+
assert(bytes.length >= 33, "PNG input is too short.");
|
|
113
|
+
assert(PNG_SIGNATURE.every((value, index) => bytes[index] === value), "Input is not a PNG image.");
|
|
114
|
+
|
|
115
|
+
let offset = 8;
|
|
116
|
+
let width = 0;
|
|
117
|
+
let height = 0;
|
|
118
|
+
let bitDepth = 0;
|
|
119
|
+
let colorType = -1;
|
|
120
|
+
let interlace = 0;
|
|
121
|
+
let palette = null;
|
|
122
|
+
let transparency = null;
|
|
123
|
+
const idat = [];
|
|
124
|
+
|
|
125
|
+
while (offset + 12 <= bytes.length) {
|
|
126
|
+
const length = readU32(bytes, offset);
|
|
127
|
+
const type = String.fromCharCode(...bytes.slice(offset + 4, offset + 8));
|
|
128
|
+
const start = offset + 8;
|
|
129
|
+
const end = start + length;
|
|
130
|
+
assert(end + 4 <= bytes.length, "PNG chunk is truncated.");
|
|
131
|
+
const data = bytes.slice(start, end);
|
|
132
|
+
|
|
133
|
+
if (type === "IHDR") {
|
|
134
|
+
width = readU32(data, 0);
|
|
135
|
+
height = readU32(data, 4);
|
|
136
|
+
bitDepth = data[8];
|
|
137
|
+
colorType = data[9];
|
|
138
|
+
assert(data[10] === 0 && data[11] === 0, "Unsupported PNG compression/filter method.");
|
|
139
|
+
interlace = data[12];
|
|
140
|
+
} else if (type === "PLTE") {
|
|
141
|
+
palette = data;
|
|
142
|
+
} else if (type === "tRNS") {
|
|
143
|
+
transparency = data;
|
|
144
|
+
} else if (type === "IDAT") {
|
|
145
|
+
idat.push(data);
|
|
146
|
+
} else if (type === "IEND") {
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
offset = end + 4;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
assert(width > 0 && height > 0, "PNG has no valid IHDR dimensions.");
|
|
153
|
+
assert(interlace === 0, "Interlaced PNG is not supported by the dependency-free Node decoder.");
|
|
154
|
+
assert([0, 2, 3, 4, 6].includes(colorType), `Unsupported PNG color type ${colorType}.`);
|
|
155
|
+
if (colorType === 3) assert([1, 2, 4, 8].includes(bitDepth), `Unsupported indexed PNG bit depth ${bitDepth}.`);
|
|
156
|
+
else assert(bitDepth === 8, `Only 8-bit PNG is supported for color type ${colorType}.`);
|
|
157
|
+
if (colorType === 3) assert(palette && palette.length >= 3, "Indexed PNG is missing its palette.");
|
|
158
|
+
|
|
159
|
+
const channels = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 }[colorType];
|
|
160
|
+
const rowBytes = Math.ceil(width * channels * bitDepth / 8);
|
|
161
|
+
const bpp = Math.max(1, Math.ceil(channels * bitDepth / 8));
|
|
162
|
+
const inflated = inflateSync(Buffer.from(concat(...idat)));
|
|
163
|
+
assert(inflated.length === height * (rowBytes + 1), "PNG decompressed data length is unexpected.");
|
|
164
|
+
|
|
165
|
+
const rgba = new Uint8ClampedArray(width * height * 4);
|
|
166
|
+
let sourceOffset = 0;
|
|
167
|
+
let previous = null;
|
|
168
|
+
|
|
169
|
+
for (let y = 0; y < height; y++) {
|
|
170
|
+
const filter = inflated[sourceOffset++];
|
|
171
|
+
const filtered = inflated.subarray(sourceOffset, sourceOffset + rowBytes);
|
|
172
|
+
sourceOffset += rowBytes;
|
|
173
|
+
const row = unfilterRow(filter, filtered, previous, bpp);
|
|
174
|
+
previous = row;
|
|
175
|
+
|
|
176
|
+
for (let x = 0; x < width; x++) {
|
|
177
|
+
const dest = (y * width + x) * 4;
|
|
178
|
+
if (colorType === 6) {
|
|
179
|
+
const src = x * 4;
|
|
180
|
+
rgba[dest] = row[src]; rgba[dest + 1] = row[src + 1]; rgba[dest + 2] = row[src + 2]; rgba[dest + 3] = row[src + 3];
|
|
181
|
+
} else if (colorType === 2) {
|
|
182
|
+
const src = x * 3;
|
|
183
|
+
rgba[dest] = row[src]; rgba[dest + 1] = row[src + 1]; rgba[dest + 2] = row[src + 2]; rgba[dest + 3] = 255;
|
|
184
|
+
} else if (colorType === 4) {
|
|
185
|
+
const src = x * 2;
|
|
186
|
+
rgba[dest] = row[src]; rgba[dest + 1] = row[src]; rgba[dest + 2] = row[src]; rgba[dest + 3] = row[src + 1];
|
|
187
|
+
} else if (colorType === 0) {
|
|
188
|
+
const gray = row[x];
|
|
189
|
+
rgba[dest] = gray; rgba[dest + 1] = gray; rgba[dest + 2] = gray; rgba[dest + 3] = 255;
|
|
190
|
+
} else {
|
|
191
|
+
const index = samplePacked(row, x, bitDepth);
|
|
192
|
+
const src = index * 3;
|
|
193
|
+
rgba[dest] = palette[src] ?? 0;
|
|
194
|
+
rgba[dest + 1] = palette[src + 1] ?? 0;
|
|
195
|
+
rgba[dest + 2] = palette[src + 2] ?? 0;
|
|
196
|
+
rgba[dest + 3] = transparency?.[index] ?? 255;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { width, height, data: rgba };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Encode an RGBA ImageData-like object as a PNG Buffer. */
|
|
205
|
+
export function encodePNG(imageData, options = {}) {
|
|
206
|
+
const { width, height, data } = imageData ?? {};
|
|
207
|
+
assert(Number.isInteger(width) && width > 0 && Number.isInteger(height) && height > 0, "Valid image dimensions are required.");
|
|
208
|
+
assert(data && data.length === width * height * 4, "RGBA image data has an invalid length.");
|
|
209
|
+
|
|
210
|
+
const raw = new Uint8Array(height * (1 + width * 4));
|
|
211
|
+
let cursor = 0;
|
|
212
|
+
for (let y = 0; y < height; y++) {
|
|
213
|
+
raw[cursor++] = 0;
|
|
214
|
+
const start = y * width * 4;
|
|
215
|
+
raw.set(data.subarray ? data.subarray(start, start + width * 4) : Uint8Array.from(data).subarray(start, start + width * 4), cursor);
|
|
216
|
+
cursor += width * 4;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const ihdr = new Uint8Array(13);
|
|
220
|
+
ihdr.set(u32be(width), 0);
|
|
221
|
+
ihdr.set(u32be(height), 4);
|
|
222
|
+
ihdr[8] = 8;
|
|
223
|
+
ihdr[9] = 6;
|
|
224
|
+
ihdr[10] = 0;
|
|
225
|
+
ihdr[11] = 0;
|
|
226
|
+
ihdr[12] = 0;
|
|
227
|
+
|
|
228
|
+
const level = Math.max(0, Math.min(9, options.compressionLevel ?? 9));
|
|
229
|
+
const compressed = new Uint8Array(deflateSync(Buffer.from(raw), { level }));
|
|
230
|
+
return Buffer.from(concat(PNG_SIGNATURE, pngChunk("IHDR", ihdr), pngChunk("IDAT", compressed), pngChunk("IEND")));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Render a QuadQR code/matrix directly to a PNG Buffer. */
|
|
234
|
+
export function toPNG(codeOrMatrix, options = {}) {
|
|
235
|
+
return encodePNG(renderToImageData(codeOrMatrix, options), options);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Render a QuadQR code/matrix and save it to disk. */
|
|
239
|
+
export async function savePNG(codeOrMatrix, filename, options = {}) {
|
|
240
|
+
const png = toPNG(codeOrMatrix, options);
|
|
241
|
+
await writeFile(filename, png);
|
|
242
|
+
return { filename, bytes: png.length };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function decodeWithOptionalSharp(buffer) {
|
|
246
|
+
try {
|
|
247
|
+
const { default: sharp } = await import("sharp");
|
|
248
|
+
const result = await sharp(buffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
249
|
+
return {
|
|
250
|
+
width: result.info.width,
|
|
251
|
+
height: result.info.height,
|
|
252
|
+
data: new Uint8ClampedArray(result.data.buffer, result.data.byteOffset, result.data.byteLength)
|
|
253
|
+
};
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (error?.code === "ERR_MODULE_NOT_FOUND" || /Cannot find package 'sharp'|Cannot find module 'sharp'/.test(error?.message ?? "")) {
|
|
256
|
+
throw new Error("This image format needs the optional 'sharp' package in Node.js. PNG works with no dependencies. Install sharp or pass RGBA data to scanImageData().");
|
|
257
|
+
}
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Scan an image Buffer/Uint8Array. PNG is dependency-free; other formats use optional sharp. */
|
|
263
|
+
export async function scanBuffer(input, options = {}) {
|
|
264
|
+
const bytes = asUint8Array(input);
|
|
265
|
+
const imageData = PNG_SIGNATURE.every((value, index) => bytes[index] === value)
|
|
266
|
+
? decodePNG(bytes)
|
|
267
|
+
: await decodeWithOptionalSharp(Buffer.from(bytes));
|
|
268
|
+
return scanImageData(imageData, options);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Node.js file scanner. Overrides the browser File-based helper on the node subpath. */
|
|
272
|
+
export async function scanFile(filename, options = {}) {
|
|
273
|
+
assert(typeof filename === "string" || filename instanceof URL, "Node scanFile expects a file path or file URL.");
|
|
274
|
+
return scanBuffer(await readFile(filename), options);
|
|
275
|
+
}
|