qr-kit 2.1.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/index.js ADDED
@@ -0,0 +1,40 @@
1
+ // index.js — main entry point for qr-kit
2
+ //
3
+ // Three layers, zero dependencies:
4
+ // Layer 1 — Pure computation (works in Node, Deno, Edge Runtime, browser, Worker)
5
+ // Layer 2 — Rendering adapters (browser or Node with canvas)
6
+ // Layer 3 — Browser actions / integrations (import via deep path for tree-shaking)
7
+
8
+ // ─── Layer 1: Core QR engine ──────────────────────────────────────────────────
9
+ export { makeQr, getModule, isFunctionModule, QrInputTooLongError } from './qr/qr-core.js';
10
+ export { computeLayout } from './utils/layout.js';
11
+
12
+ // ─── Layer 1: URL & link utilities ───────────────────────────────────────────
13
+ export { sanitizeUrlForQR, utf8ByteLen } from './utils/url.js';
14
+ export { buildQrLink } from './utils/link.js';
15
+
16
+ // ─── Layer 2: Rendering adapters ─────────────────────────────────────────────
17
+ export { makeQrPath, makeQrPathSplit, makeQrSvgString } from './renderers/svg.js';
18
+ export { renderQrToCanvas, makeQrCanvas } from './renderers/canvas.js';
19
+
20
+ // ─── Layer 2: Logo overlay (zero-DOM, works in Node/Worker/browser) ──────────
21
+ export {
22
+ makeQrWithLogoSvg,
23
+ getLogoConstraints,
24
+ loadLogoAsDataUrl,
25
+ buildQrWithLogoSvgAsync,
26
+ LOGO_MAX_COVERAGE_ECC_M,
27
+ LOGO_MAX_COVERAGE_ECC_L,
28
+ } from './utils/logo.js';
29
+
30
+ // ─── Layer 2: React component (requires React ≥ 17 as peer dep) ──────────────
31
+ export { default } from './components/QRCodeGenerator.jsx';
32
+ export { useQrCode } from './components/useQrCode.js';
33
+ export { useQrWorker } from './components/useQrWorker.js';
34
+
35
+ // ─── Layer 3: Browser-only actions (prefer deep imports for tree-shaking) ─────
36
+ // import { downloadQrPng } from 'qr-kit/utils/raster'
37
+ // import { downloadQrJpeg } from 'qr-kit/utils/jpegQr'
38
+ // import { buildQrCompositeBlob, downloadQrComposite } from 'qr-kit/utils/poster'
39
+ // import { buildQrPdfBytes, downloadQrPdf } from 'qr-kit/utils/pdf'
40
+ // import { buildPdfWithTemplateBytes, downloadPdfWithTemplateImage } from 'qr-kit/utils/pdf'
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "qr-kit",
3
+ "version": "2.1.0",
4
+ "description": "Complete QR code toolkit. Zero dependencies. Logo overlay, PDF export, link optimizer. 5.4 kB core.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "module": "index.js",
8
+ "types": "types/index.d.ts",
9
+ "exports": {
10
+ ".": "./index.js",
11
+ "./qr/qr-core": "./qr/qr-core.js",
12
+ "./utils/layout": "./utils/layout.js",
13
+ "./utils/url": "./utils/url.js",
14
+ "./utils/link": "./utils/link.js",
15
+ "./utils/logo": "./utils/logo.js",
16
+ "./renderers/svg": "./renderers/svg.js",
17
+ "./renderers/canvas": "./renderers/canvas.js",
18
+ "./utils/raster": "./utils/raster.js",
19
+ "./utils/jpegQr": "./utils/jpegQr.js",
20
+ "./utils/poster": "./utils/poster.js",
21
+ "./utils/pdf": "./utils/pdf.js",
22
+ "./worker/qr.worker": "./worker/qr.worker.js"
23
+ },
24
+ "scripts": {
25
+ "test": "node tests/run.js",
26
+ "test:watch": "node --watch tests/run.js",
27
+ "size": "node scripts/size.js"
28
+ },
29
+ "sideEffects": false,
30
+ "keywords": [
31
+ "qr",
32
+ "qrcode",
33
+ "qr-code",
34
+ "generator",
35
+ "zero-dependencies",
36
+ "logo",
37
+ "pdf",
38
+ "svg",
39
+ "react",
40
+ "typescript",
41
+ "browser",
42
+ "nodejs"
43
+ ],
44
+ "peerDependencies": {
45
+ "react": ">=17"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "react": {
49
+ "optional": true
50
+ }
51
+ },
52
+ "files": [
53
+ "index.js",
54
+ "qr/",
55
+ "components/",
56
+ "renderers/",
57
+ "utils/",
58
+ "worker/",
59
+ "types/",
60
+ "CHANGELOG.md"
61
+ ],
62
+ "license": "MIT",
63
+ "repository": {
64
+ "type": "git",
65
+ "url": "https://github.com/Yaroslav3991/qr-kit.git"
66
+ },
67
+ "bugs": {
68
+ "url": "https://github.com/Yaroslav3991/qr-kit/issues"
69
+ },
70
+ "homepage": "https://github.com/Yaroslav3991/qr-kit#readme"
71
+ }
package/qr/qr-core.js ADDED
@@ -0,0 +1,408 @@
1
+ // qr/qr-core.js
2
+ // QR code generator: Byte mode, versions 1–12, ECC L/M.
3
+ // Implements interleaving, remainder bits and the dark module per the QR standard.
4
+ //
5
+ // Key design decisions — see DECISIONS.md for full rationale:
6
+ // • modules is a flat Uint8Array (size²) — ~8× faster iteration than nested arrays
7
+ // • functionMask is exported so renderers can style finder/timing differently
8
+ // • QrInputTooLongError is a typed class — catch-by-type instead of string parsing
9
+
10
+ // ─── Typed error ──────────────────────────────────────────────────────────────
11
+
12
+ export class QrInputTooLongError extends Error {
13
+ constructor(byteLength, maxBytes, maxVersion, eccLevel) {
14
+ super(
15
+ `Input is ${byteLength} bytes but max for v1–${maxVersion} at EC level ${eccLevel} is ${maxBytes} bytes.`,
16
+ );
17
+ this.name = 'QrInputTooLongError';
18
+ this.byteLength = byteLength;
19
+ this.maxBytes = maxBytes;
20
+ this.maxVersion = maxVersion;
21
+ this.eccLevel = eccLevel;
22
+ }
23
+ }
24
+
25
+ // ─── Public API ───────────────────────────────────────────────────────────────
26
+
27
+ export function makeQr(text, opts = {}) {
28
+ const { eccLevel = 'M', maxVersion = 6 } = opts;
29
+ const bytes = utf8Encode(text);
30
+ const spec = pickVersion(bytes.length, eccLevel, maxVersion);
31
+ const { version, size, dataCodewords, ecPerBlock, blocks } = spec;
32
+
33
+ const bits = [];
34
+ const ccb = ccBitsByte(version);
35
+ pushBits(bits, 0x4, 4);
36
+ pushBits(bits, bytes.length, ccb);
37
+ for (const b of bytes) pushBits(bits, b, 8);
38
+
39
+ const maxDataBits = dataCodewords * 8;
40
+ const remain = maxDataBits - bits.length;
41
+ pushBits(bits, 0, Math.min(4, Math.max(remain, 0)));
42
+ while (bits.length % 8 !== 0) pushBits(bits, 0, 1);
43
+
44
+ const data = [];
45
+ for (let i = 0; i < bits.length; i += 8) data.push(bitSliceToByte(bits, i));
46
+ const pad = [0xec, 0x11];
47
+ for (let i = 0; data.length < dataCodewords; i++) data.push(pad[i & 1]);
48
+
49
+ const blocksArr = [];
50
+ let p = 0;
51
+ for (const g of blocks) {
52
+ for (let k = 0; k < g.count; k++) {
53
+ const chunk = data.slice(p, p + g.data);
54
+ p += g.data;
55
+ blocksArr.push({ data: chunk, ec: rsCompute(chunk, ecPerBlock) });
56
+ }
57
+ }
58
+
59
+ const interleaved = [];
60
+ const maxLen = Math.max(...blocksArr.map(b => b.data.length));
61
+ for (let i = 0; i < maxLen; i++)
62
+ for (const b of blocksArr) if (i < b.data.length) interleaved.push(b.data[i]);
63
+ for (let i = 0; i < ecPerBlock; i++)
64
+ for (const b of blocksArr) interleaved.push(b.ec[i]);
65
+
66
+ const finalBits = bytesToBits(interleaved);
67
+ const rem = remainderBits(version);
68
+ for (let i = 0; i < rem; i++) finalBits.push(0);
69
+
70
+ // Flat Uint8Array: 0xFF = unset sentinel
71
+ const modules = new Uint8Array(size * size).fill(0xFF);
72
+ placeFunctionPatterns(modules, size, version);
73
+ placeDataBits(modules, size, finalBits);
74
+
75
+ // Build function-pattern mask once; reuse across all 8 mask evaluations
76
+ const funcMask = new Uint8Array(size * size);
77
+ markFunctionModules(funcMask, size, version);
78
+
79
+ let bestMask = 0, bestScore = Infinity, best = null;
80
+ for (let mask = 0; mask < 8; mask++) {
81
+ const clone = modules.slice();
82
+ applyMask(clone, size, mask, funcMask);
83
+ const score = penaltyScore(clone, size);
84
+ if (score < bestScore) { bestScore = score; bestMask = mask; best = clone; }
85
+ }
86
+
87
+ writeFormatInfo(best, size, eccLevelBits(eccLevel), bestMask);
88
+ writeVersionInfo(best, size, version);
89
+
90
+ // Replace any remaining sentinels (guard against placement bugs)
91
+ for (let i = 0; i < best.length; i++) if (best[i] === 0xFF) best[i] = 0;
92
+
93
+ return { version, size, modules: best, functionMask: funcMask, eccLevel };
94
+ }
95
+
96
+ export function getModule(model, x, y) {
97
+ return model.modules[y * model.size + x];
98
+ }
99
+
100
+ export function isFunctionModule(model, x, y) {
101
+ return model.functionMask[y * model.size + x] === 1;
102
+ }
103
+
104
+ // ─── Version tables ───────────────────────────────────────────────────────────
105
+
106
+ const EC_TABLE = {
107
+ L: {
108
+ 1: { ecPerBlock: 7, blocks: [{ count: 1, data: 19 }] },
109
+ 2: { ecPerBlock: 10, blocks: [{ count: 1, data: 34 }] },
110
+ 3: { ecPerBlock: 15, blocks: [{ count: 1, data: 55 }] },
111
+ 4: { ecPerBlock: 20, blocks: [{ count: 1, data: 80 }] },
112
+ 5: { ecPerBlock: 26, blocks: [{ count: 1, data: 108 }] },
113
+ 6: { ecPerBlock: 18, blocks: [{ count: 2, data: 68 }] },
114
+ 7: { ecPerBlock: 20, blocks: [{ count: 2, data: 78 }] },
115
+ 8: { ecPerBlock: 24, blocks: [{ count: 2, data: 97 }] },
116
+ 9: { ecPerBlock: 30, blocks: [{ count: 2, data: 116 }] },
117
+ 10: { ecPerBlock: 18, blocks: [{ count: 2, data: 68 }, { count: 2, data: 69 }] },
118
+ 11: { ecPerBlock: 20, blocks: [{ count: 4, data: 81 }] },
119
+ 12: { ecPerBlock: 24, blocks: [{ count: 2, data: 92 }, { count: 2, data: 93 }] },
120
+ },
121
+ M: {
122
+ 1: { ecPerBlock: 10, blocks: [{ count: 1, data: 16 }] },
123
+ 2: { ecPerBlock: 16, blocks: [{ count: 1, data: 28 }] },
124
+ 3: { ecPerBlock: 26, blocks: [{ count: 1, data: 44 }] },
125
+ 4: { ecPerBlock: 18, blocks: [{ count: 2, data: 32 }] },
126
+ 5: { ecPerBlock: 24, blocks: [{ count: 2, data: 43 }] },
127
+ 6: { ecPerBlock: 16, blocks: [{ count: 4, data: 27 }] },
128
+ 7: { ecPerBlock: 18, blocks: [{ count: 4, data: 31 }] },
129
+ 8: { ecPerBlock: 22, blocks: [{ count: 2, data: 38 }, { count: 2, data: 39 }] },
130
+ 9: { ecPerBlock: 22, blocks: [{ count: 3, data: 36 }, { count: 2, data: 37 }] },
131
+ 10: { ecPerBlock: 26, blocks: [{ count: 4, data: 43 }, { count: 1, data: 44 }] },
132
+ 11: { ecPerBlock: 30, blocks: [{ count: 1, data: 50 }, { count: 4, data: 51 }] },
133
+ 12: { ecPerBlock: 22, blocks: [{ count: 6, data: 36 }, { count: 2, data: 37 }] },
134
+ },
135
+ };
136
+
137
+ function remainderBits(v) {
138
+ if (v === 1) return 0;
139
+ if (v >= 2 && v <= 6) return 7;
140
+ if (v >= 7 && v <= 13) return 0;
141
+ if (v >= 14 && v <= 20) return 3;
142
+ if (v >= 21 && v <= 27) return 4;
143
+ if (v >= 28 && v <= 34) return 3;
144
+ return 0;
145
+ }
146
+
147
+ function pickVersion(byteLen, eccLevel, maxVersion) {
148
+ let maxCap = 0;
149
+ for (let v = 1; v <= maxVersion; v++) {
150
+ const row = EC_TABLE[eccLevel]?.[v];
151
+ if (!row) continue;
152
+ const dataCW = row.blocks.reduce((s, g) => s + g.count * g.data, 0);
153
+ const header = 4 + ccBitsByte(v);
154
+ const cap = Math.floor((dataCW * 8 - header - 4) / 8);
155
+ maxCap = Math.max(maxCap, cap);
156
+ if (header + byteLen * 8 + 4 <= dataCW * 8)
157
+ return { version: v, size: 17 + 4 * v, dataCodewords: dataCW, ecPerBlock: row.ecPerBlock, blocks: row.blocks };
158
+ }
159
+ throw new QrInputTooLongError(byteLen, maxCap, maxVersion, eccLevel);
160
+ }
161
+
162
+ function ccBitsByte(version) { return version <= 9 ? 8 : 16; }
163
+
164
+ // ─── Matrix helpers ───────────────────────────────────────────────────────────
165
+
166
+ function setMod(M, size, x, y, dark) {
167
+ if (x >= 0 && x < size && y >= 0 && y < size) M[y * size + x] = dark ? 1 : 0;
168
+ }
169
+ function setReserve(M, size, x, y) {
170
+ if (x >= 0 && x < size && y >= 0 && y < size && M[y * size + x] === 0xFF)
171
+ M[y * size + x] = 0;
172
+ }
173
+
174
+ function placeFinder(M, size, x, y) {
175
+ for (let dy = 0; dy < 7; dy++) {
176
+ for (let dx = 0; dx < 7; dx++) {
177
+ const outer = dx === 0 || dx === 6 || dy === 0 || dy === 6;
178
+ const inner = dx >= 2 && dx <= 4 && dy >= 2 && dy <= 4;
179
+ setMod(M, size, x + dx, y + dy, outer || inner);
180
+ }
181
+ }
182
+ for (let i = -1; i <= 7; i++) {
183
+ setReserve(M, size, x - 1, y + i);
184
+ setReserve(M, size, x + 7, y + i);
185
+ setReserve(M, size, x + i, y - 1);
186
+ setReserve(M, size, x + i, y + 7);
187
+ }
188
+ }
189
+
190
+ function placeAlignment(M, size, x, y) {
191
+ for (let dy = 0; dy < 5; dy++) {
192
+ for (let dx = 0; dx < 5; dx++) {
193
+ setMod(M, size, x + dx, y + dy,
194
+ (dx === 0 || dx === 4 || dy === 0 || dy === 4) || (dx === 2 && dy === 2));
195
+ }
196
+ }
197
+ }
198
+
199
+ const ALIGNMENT = {
200
+ 1: [],
201
+ 2: [6,18], 3: [6,22], 4: [6,26], 5: [6,30], 6: [6,34],
202
+ 7: [6,22,38], 8: [6,24,42], 9: [6,26,46], 10:[6,28,50], 11:[6,30,54], 12:[6,32,58],
203
+ };
204
+
205
+ function placeFunctionPatterns(M, size, version) {
206
+ placeFinder(M, size, 0, 0);
207
+ placeFinder(M, size, size - 7, 0);
208
+ placeFinder(M, size, 0, size - 7);
209
+ for (let i = 8; i < size - 8; i++) {
210
+ setMod(M, size, 6, i, i % 2 === 0);
211
+ setMod(M, size, i, 6, i % 2 === 0);
212
+ }
213
+ if (version >= 2) {
214
+ const c = ALIGNMENT[version] || [];
215
+ for (const cy of c) for (const cx of c) {
216
+ if ((cx <= 8 && cy <= 8) || (cx >= size-9 && cy <= 8) || (cx <= 8 && cy >= size-9)) continue;
217
+ placeAlignment(M, size, cx - 2, cy - 2);
218
+ }
219
+ }
220
+ setMod(M, size, 8, 4 * version + 9, 1);
221
+ if (version >= 7) {
222
+ for (let y = 0; y < 6; y++) for (let x = 0; x < 3; x++) setReserve(M, size, size-11+x, y);
223
+ for (let y = 0; y < 3; y++) for (let x = 0; x < 6; x++) setReserve(M, size, x, size-11+y);
224
+ }
225
+ for (let i = 0; i < 9; i++) { setReserve(M, size, 8, i); setReserve(M, size, i, 8); }
226
+ for (let i = 0; i < 8; i++) setReserve(M, size, 8, size-1-i);
227
+ for (let i = 0; i < 7; i++) setReserve(M, size, size-1-i, 8);
228
+ }
229
+
230
+ function markFunctionModules(mask, size, version) {
231
+ const mark = (x0, y0, w, h) => {
232
+ for (let y = Math.max(0,y0); y < Math.min(size,y0+h); y++)
233
+ for (let x = Math.max(0,x0); x < Math.min(size,x0+w); x++)
234
+ mask[y * size + x] = 1;
235
+ };
236
+ mark(-1,-1,9,9); mark(size-8,-1,9,9); mark(-1,size-8,9,9);
237
+ for (let i = 8; i < size-8; i++) { mask[6*size+i]=1; mask[i*size+6]=1; }
238
+ mask[(4*version+9)*size+8] = 1;
239
+ if (version >= 2) {
240
+ const c = ALIGNMENT[version] || [];
241
+ for (const cy of c) for (const cx of c) {
242
+ if ((cx<=8&&cy<=8)||(cx>=size-9&&cy<=8)||(cx<=8&&cy>=size-9)) continue;
243
+ for (let dy=-2;dy<=2;dy++) for (let dx=-2;dx<=2;dx++) mask[(cy+dy)*size+(cx+dx)]=1;
244
+ }
245
+ }
246
+ for (let i=0;i<=8;i++) { mask[8*size+i]=1; mask[i*size+8]=1; }
247
+ for (let i=0;i<8;i++) { mask[(size-1-i)*size+8]=1; mask[8*size+(size-1-i)]=1; }
248
+ if (version >= 7) {
249
+ for (let y=0;y<6;y++) for (let x=0;x<3;x++) mask[y*size+(size-11+x)]=1;
250
+ for (let y=0;y<3;y++) for (let x=0;x<6;x++) mask[(size-11+y)*size+x]=1;
251
+ }
252
+ }
253
+
254
+ function placeDataBits(M, size, dataBits) {
255
+ let bitIdx = 0, upward = true;
256
+ for (let x = size-1; x > 0; x -= 2) {
257
+ if (x === 6) x--;
258
+ for (let yOff = 0; yOff < size; yOff++) {
259
+ const y = upward ? size-1-yOff : yOff;
260
+ for (let dx = 0; dx < 2; dx++) {
261
+ const xx = x - dx, i = y*size+xx;
262
+ if (M[i] !== 0xFF) continue;
263
+ M[i] = bitIdx < dataBits.length ? (dataBits[bitIdx++] ? 1 : 0) : 0;
264
+ }
265
+ }
266
+ upward = !upward;
267
+ }
268
+ }
269
+
270
+ function applyMask(M, size, maskId, funcMask) {
271
+ for (let y = 0; y < size; y++)
272
+ for (let x = 0; x < size; x++) {
273
+ const i = y*size+x;
274
+ if (!funcMask[i] && maskFn(maskId, x, y)) M[i] ^= 1;
275
+ }
276
+ }
277
+
278
+ function maskFn(id, x, y) {
279
+ switch (id) {
280
+ case 0: return (x+y)%2===0;
281
+ case 1: return y%2===0;
282
+ case 2: return x%3===0;
283
+ case 3: return (x+y)%3===0;
284
+ case 4: return (Math.floor(y/2)+Math.floor(x/3))%2===0;
285
+ case 5: return ((x*y)%2)+((x*y)%3)===0;
286
+ case 6: return (((x*y)%2)+((x*y)%3))%2===0;
287
+ case 7: return (((x+y)%2)+((x*y)%3))%2===0;
288
+ default: return false;
289
+ }
290
+ }
291
+
292
+ function penaltyScore(M, size) {
293
+ let score = 0;
294
+ const lp = arr => {
295
+ let s=0, run=arr[0], len=1;
296
+ for (let i=1;i<arr.length;i++) {
297
+ if (arr[i]===run) len++;
298
+ else { if (len>=5) s+=3+(len-5); run=arr[i]; len=1; }
299
+ }
300
+ if (len>=5) s+=3+(len-5);
301
+ return s;
302
+ };
303
+ const row=new Uint8Array(size), col=new Uint8Array(size);
304
+ for (let y=0;y<size;y++) { for (let x=0;x<size;x++) row[x]=M[y*size+x]; score+=lp(row); }
305
+ for (let x=0;x<size;x++) { for (let y=0;y<size;y++) col[y]=M[y*size+x]; score+=lp(col); }
306
+ for (let y=0;y<size-1;y++) for (let x=0;x<size-1;x++) {
307
+ const c=M[y*size+x]+M[y*size+x+1]+M[(y+1)*size+x]+M[(y+1)*size+x+1];
308
+ if (c===0||c===4) score+=3;
309
+ }
310
+ const pat=[0,0,0,0,1,0,1,1,1,0,1,0,0,0,0];
311
+ const hasPat=arr=>{
312
+ outer:for(let i=0;i<=arr.length-pat.length;i++){
313
+ for(let j=0;j<pat.length;j++) if(arr[i+j]!==pat[j]) continue outer; return true;
314
+ } return false;
315
+ };
316
+ for(let y=0;y<size;y++){for(let x=0;x<size;x++) row[x]=M[y*size+x]; if(hasPat(row)) score+=40;}
317
+ for(let x=0;x<size;x++){for(let y=0;y<size;y++) col[y]=M[y*size+x]; if(hasPat(col)) score+=40;}
318
+ let black=0; for(let i=0;i<M.length;i++) if(M[i]) black++;
319
+ score+=Math.floor(Math.abs((black*100)/M.length-50)/5)*10;
320
+ return score;
321
+ }
322
+
323
+ function eccLevelBits(l) { return l==='L'?1:l==='M'?0:l==='Q'?3:2; }
324
+
325
+ function formatBCH(d) {
326
+ let v=d<<10; const g=0x537;
327
+ for(let i=14;i>=10;i--) if((v>>i)&1) v^=g<<(i-10);
328
+ return v&0x3ff;
329
+ }
330
+
331
+ function writeFormatInfo(M, size, ecc2, maskId) {
332
+ const val=(((ecc2<<3)|(maskId&7))<<10|formatBCH((ecc2<<3)|(maskId&7)))^0x5412;
333
+ const b=(x,y,bit)=>{M[y*size+x]=bit?1:0;};
334
+ for(let i=0;i<6;i++){b(8,i,(val>>i)&1);b(i,8,(val>>i)&1);}
335
+ b(8,7,(val>>6)&1);b(8,8,(val>>7)&1);b(7,8,(val>>8)&1);
336
+ for(let i=9;i<15;i++) b(14-i,8,(val>>i)&1);
337
+ for(let i=0;i<8;i++) b(size-1-i,8,(val>>i)&1);
338
+ for(let i=8;i<15;i++) b(8,size-15+i,(val>>i)&1);
339
+ }
340
+
341
+ function versionBCH(ver) {
342
+ let v=(ver&0x3f)<<12; const g=0x1f25;
343
+ for(let i=17;i>=12;i--) if((v>>i)&1) v^=g<<(i-12);
344
+ return v&0xfff;
345
+ }
346
+
347
+ function writeVersionInfo(M, size, version) {
348
+ if (version<7) return;
349
+ const bits=((version&0x3f)<<12)|versionBCH(version&0x3f);
350
+ for(let y=0;y<3;y++) for(let x=0;x<6;x++) M[(size-11+y)*size+x]=(bits>>(y+3*x))&1;
351
+ for(let y=0;y<6;y++) for(let x=0;x<3;x++) M[y*size+(size-11+x)]=(bits>>(y*3+x))&1;
352
+ }
353
+
354
+ // ─── Bit / byte utilities ─────────────────────────────────────────────────────
355
+
356
+ function pushBits(bits, val, len) {
357
+ for (let i=len-1;i>=0;i--) bits.push((val>>i)&1);
358
+ }
359
+ function bitSliceToByte(bits, i) {
360
+ let v=0; for(let b=0;b<8;b++) v=(v<<1)|(bits[i+b]||0); return v&0xff;
361
+ }
362
+ function bytesToBits(arr) {
363
+ const out=[]; for(const b of arr) pushBits(out,b,8); return out;
364
+ }
365
+ function utf8Encode(str) {
366
+ const enc=[];
367
+ for(let i=0;i<str.length;i++){
368
+ const c=str.charCodeAt(i);
369
+ if(c<0x80) enc.push(c);
370
+ else if(c<0x800) enc.push(0xc0|(c>>6),0x80|(c&0x3f));
371
+ else if(c>=0xd800&&c<=0xdbff){
372
+ const hi=c,lo=str.charCodeAt(++i),cp=(hi-0xd800)*0x400+(lo-0xdc00)+0x10000;
373
+ enc.push(0xf0|(cp>>18),0x80|((cp>>12)&0x3f),0x80|((cp>>6)&0x3f),0x80|(cp&0x3f));
374
+ } else enc.push(0xe0|(c>>12),0x80|((c>>6)&0x3f),0x80|(c&0x3f));
375
+ }
376
+ return enc;
377
+ }
378
+
379
+ // ─── GF(256) and Reed-Solomon ─────────────────────────────────────────────────
380
+
381
+ const GF = (() => {
382
+ const exp=new Uint8Array(512),log=new Uint8Array(256);
383
+ let x=1;
384
+ for(let i=0;i<255;i++){exp[i]=x;log[x]=i;x<<=1;if(x&0x100)x^=0x11d;}
385
+ for(let i=255;i<512;i++) exp[i]=exp[i-255];
386
+ return {exp,log,mul:(a,b)=>a&&b?exp[log[a]+log[b]]:0};
387
+ })();
388
+
389
+ function rsGeneratorPoly(deg) {
390
+ let poly=[1];
391
+ for(let i=0;i<deg;i++){
392
+ const next=new Array(poly.length+1).fill(0);
393
+ for(let j=0;j<poly.length;j++){next[j]^=GF.mul(poly[j],1);next[j+1]^=GF.mul(poly[j],GF.exp[i]);}
394
+ poly=next;
395
+ }
396
+ return poly;
397
+ }
398
+
399
+ function rsCompute(data,ecCount){
400
+ const gen=rsGeneratorPoly(ecCount),res=new Array(ecCount).fill(0);
401
+ for(const d of data){
402
+ const f=d^res[0];
403
+ for(let i=0;i<ecCount-1;i++) res[i]=res[i+1];
404
+ res[ecCount-1]=0;
405
+ for(let j=0;j<ecCount;j++) res[j]^=GF.mul(gen[j+1],f);
406
+ }
407
+ return res;
408
+ }
@@ -0,0 +1,96 @@
1
+ // renderers/canvas.js
2
+ // Pure canvas rendering adapter. Draws a QR model onto any HTMLCanvasElement.
3
+ // No DOM creation, no side effects — the caller owns the canvas.
4
+
5
+ import { computeLayout } from '../utils/layout.js';
6
+
7
+ /**
8
+ * Renders a QR model onto an existing canvas element.
9
+ *
10
+ * By separating rendering from canvas creation the caller can:
11
+ * - reuse an existing canvas (avoids DOM churn in animations)
12
+ * - use OffscreenCanvas in a Web Worker
13
+ * - set their own canvas dimensions before calling
14
+ *
15
+ * @param {import('../qr/qr-core.js').QRModel} model
16
+ * @param {HTMLCanvasElement|OffscreenCanvas} canvas
17
+ * @param {object} [opts]
18
+ * @param {number} [opts.size=256] - Target outer size in pixels.
19
+ * @param {number} [opts.margin=16] - Quiet-zone padding in pixels.
20
+ * @param {string} [opts.fg='#000'] - Dark module colour.
21
+ * @param {string} [opts.bg='#fff'] - Background colour.
22
+ * @param {number} [opts.scale=1] - Device pixel ratio / export scale.
23
+ * @param {string} [opts.fnColor=null] - Optional separate colour for function modules.
24
+ * If null, fg is used for all dark modules.
25
+ */
26
+ export function renderQrToCanvas(model, canvas, {
27
+ size = 256,
28
+ margin = 16,
29
+ fg = '#000',
30
+ bg = '#fff',
31
+ scale = 1,
32
+ fnColor = null,
33
+ } = {}) {
34
+ const { outer, moduleSize, quietLeft, quietTop } = computeLayout(model.size, size, margin);
35
+ const { modules, functionMask, size: n } = model;
36
+
37
+ const w = Math.round(outer * scale);
38
+ canvas.width = w;
39
+ canvas.height = w;
40
+
41
+ const ctx = canvas.getContext('2d');
42
+ ctx.imageSmoothingEnabled = false;
43
+ ctx.scale(scale, scale);
44
+
45
+ // Background
46
+ ctx.fillStyle = bg;
47
+ ctx.fillRect(0, 0, outer, outer);
48
+
49
+ const ms = moduleSize;
50
+
51
+ if (!fnColor) {
52
+ // Fast path: all dark modules same colour
53
+ ctx.fillStyle = fg;
54
+ for (let y = 0; y < n; y++) {
55
+ for (let x = 0; x < n; x++) {
56
+ if (modules[y * n + x]) {
57
+ ctx.fillRect(quietLeft + x * ms, quietTop + y * ms, ms, ms);
58
+ }
59
+ }
60
+ }
61
+ } else {
62
+ // Two-pass: function modules get fnColor, data modules get fg
63
+ ctx.fillStyle = fg;
64
+ for (let y = 0; y < n; y++) {
65
+ for (let x = 0; x < n; x++) {
66
+ const i = y * n + x;
67
+ if (!modules[i] || functionMask[i]) continue;
68
+ ctx.fillRect(quietLeft + x * ms, quietTop + y * ms, ms, ms);
69
+ }
70
+ }
71
+ ctx.fillStyle = fnColor;
72
+ for (let y = 0; y < n; y++) {
73
+ for (let x = 0; x < n; x++) {
74
+ const i = y * n + x;
75
+ if (!modules[i] || !functionMask[i]) continue;
76
+ ctx.fillRect(quietLeft + x * ms, quietTop + y * ms, ms, ms);
77
+ }
78
+ }
79
+ }
80
+
81
+ ctx.setTransform(1, 0, 0, 1, 0, 0); // reset scale
82
+ }
83
+
84
+ /**
85
+ * Creates a new canvas, renders the QR model onto it, and returns it.
86
+ * Convenience wrapper around renderQrToCanvas.
87
+ *
88
+ * @param {import('../qr/qr-core.js').QRModel} model
89
+ * @param {object} [opts] - Same options as renderQrToCanvas.
90
+ * @returns {HTMLCanvasElement}
91
+ */
92
+ export function makeQrCanvas(model, opts = {}) {
93
+ const canvas = document.createElement('canvas');
94
+ renderQrToCanvas(model, canvas, opts);
95
+ return canvas;
96
+ }