qrcode-pack 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Sarah Siqueira
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # QR Code Pack
2
+
3
+ [![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![NPM Version](https://img.shields.io/npm/v/qrcode-pack)](https://www.npmjs.com/package/qrcode-pack)
6
+ [![Release Version](https://img.shields.io/github/release/sarahcssiqueira/qrcode-pack.svg?color)](https://github.com/sarahcssiqueira/qrcode-pack/releases/latest)
7
+
8
+
9
+ ## Installation
10
+
11
+ To install run:
12
+
13
+ `npm i qrcode-pack` or
14
+
15
+ `yarn add qrcode-pack`
16
+
17
+
18
+ ## Usage
19
+
20
+ ````
21
+ import QRCodeGenerator from 'qrcode-generator-js';
22
+
23
+ const qr = new QRCodeGenerator('Hello World');
24
+ qr.buildMatrix();
25
+ qr.renderToCanvas('myCanvas', 512);
26
+
27
+ ````
28
+
29
+ ## Props
30
+
31
+ TO DO
32
+
33
+ ## Contributing
34
+
35
+ Contributions are welcome.
36
+
37
+ - Fork the repository
38
+ - Create your feature branch (`git checkout -b feature/my-feature`)
39
+ - Commit your changes (`git commit -m 'commit message'`)
40
+ - Push to the branch (`git push origin feature/my-featuree`)
41
+ - Create a new Pull Request
42
+
43
+
44
+ ## License
45
+
46
+ This project is licensed under the MIT License - see the [LICENSE](./LICENSE.md) file for details.
47
+
48
+
49
+ ## Contact
50
+
51
+ Issues, suggestions, or feedback, create an [issue](https://github.com/sarahcssiqueira/qrcode-pack/issues).
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "qrcode-pack",
3
+ "version": "0.1.0",
4
+ "description": "",
5
+ "homepage": "https://github.com/sarahcssiqueira/qrcode-pack#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/sarahcssiqueira/qrcode-pack/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/sarahcssiqueira/qrcode-pack.git"
12
+ },
13
+ "license": "MIT",
14
+ "author": "Sarah C. Siqueira",
15
+ "type": "module",
16
+ "main": "src/QRCodeGenerator.js",
17
+ "scripts": {
18
+ "test": "echo \"Error: no test specified\" && exit 1"
19
+ }
20
+ }
@@ -0,0 +1,455 @@
1
+ export default class QRCodeGenerator {
2
+ constructor(text, opts = {}) {
3
+ this.text = text;
4
+ this.ecLevel = (opts.ecLevel || 'H').toUpperCase();
5
+ this.version = null;
6
+ this.modules = null;
7
+ this.reserved = null;
8
+ // corrected tables: L/M/Q/H are data codewords (totalCodewords - ecCodewordsPerBlock)
9
+ // Extended versionTable up to version 15 for even longer data support
10
+ this.versionTable = {
11
+ 1: { L: 19, M: 16, Q: 13, H: 9, align: [], totalCodewords: 26, ecCodewordsPerBlock: {L:7, M:10, Q:13, H:17}, blocks: {L:1, M:1, Q:1, H:1} },
12
+ 2: { L: 34, M: 28, Q: 22, H: 16, align: [6, 18], totalCodewords: 44, ecCodewordsPerBlock: {L:10, M:16, Q:22, H:28}, blocks: {L:1, M:1, Q:1, H:1} },
13
+ 3: { L: 55, M: 44, Q: 34, H: 26, align: [6, 22], totalCodewords: 70, ecCodewordsPerBlock: {L:15, M:26, Q:36, H:44}, blocks: {L:1, M:1, Q:2, H:2} },
14
+ 4: { L: 80, M: 64, Q: 48, H: 36, align: [6, 26], totalCodewords: 100, ecCodewordsPerBlock: {L:20, M:36, Q:52, H:64}, blocks: {L:1, M:2, Q:2, H:4} },
15
+ 5: { L: 108, M: 86, Q: 62, H: 46, align: [6, 30], totalCodewords: 134, ecCodewordsPerBlock: {L:26, M:48, Q:72, H:88}, blocks: {L:1, M:2, Q:2, H:2} },
16
+ 6: { L: 136, M: 108, Q: 76, H: 60, align: [6, 34], totalCodewords: 172, ecCodewordsPerBlock: {L:36, M:64, Q:96, H:112}, blocks: {L:2, M:4, Q:4, H:4} },
17
+ 7: { L: 156, M: 124, Q: 88, H: 66, align: [6, 22, 38], totalCodewords: 196, ecCodewordsPerBlock: {L:40, M:72, Q:108, H:130}, blocks: {L:2, M:4, Q:6, H:6} },
18
+ 8: { L: 194, M: 154, Q: 110, H: 86, align: [6, 24, 42], totalCodewords: 242, ecCodewordsPerBlock: {L:48, M:88, Q:132, H:156}, blocks: {L:2, M:4, Q:6, H:6} },
19
+ 9: { L: 232, M: 182, Q: 132, H: 100, align: [6, 26, 46], totalCodewords: 292, ecCodewordsPerBlock: {L:60, M:110, Q:160, H:192}, blocks: {L:2, M:5, Q:8, H:8} },
20
+ 10: { L: 274, M: 216, Q: 154, H: 122, align: [6, 28, 50], totalCodewords: 346, ecCodewordsPerBlock: {L:72, M:130, Q:192, H:224}, blocks: {L:4, M:5, Q:8, H:8} },
21
+ 11: { L: 324, M: 254, Q: 180, H: 140, align: [6, 30, 54], totalCodewords: 404, ecCodewordsPerBlock: {L:80, M:150, Q:224, H:264}, blocks: {L:4, M:5, Q:10, H:10} },
22
+ 12: { L: 370, M: 290, Q: 206, H: 158, align: [6, 32, 58], totalCodewords: 466, ecCodewordsPerBlock: {L:96, M:176, Q:260, H:308}, blocks: {L:4, M:8, Q:12, H:12} },
23
+ 13: { L: 428, M: 334, Q: 244, H: 180, align: [6, 34, 62], totalCodewords: 532, ecCodewordsPerBlock: {L:104, M:198, Q:288, H:352}, blocks: {L:4, M:9, Q:16, H:16} },
24
+ 14: { L: 461, M: 365, Q: 261, H: 197, align: [6, 26, 46, 66], totalCodewords: 581, ecCodewordsPerBlock: {L:120, M:216, Q:320, H:384}, blocks: {L:4, M:9, Q:16, H:16} },
25
+ 15: { L: 523, M: 415, Q: 295, H: 223, align: [6, 26, 48, 70], totalCodewords: 655, ecCodewordsPerBlock: {L:132, M:240, Q:360, H:432}, blocks: {L:6, M:10, Q:18, H:18} },
26
+ };
27
+ this.initGF();
28
+ this.dataBytes = this.toUtf8Bytes(this.text);
29
+ this.chooseVersion();
30
+ this.size = 17 + 4 * this.version;
31
+ this.modules = Array.from({ length: this.size }, () => Array(this.size).fill(null));
32
+ this.reserved = Array.from({ length: this.size }, () => Array(this.size).fill(false));
33
+ }
34
+
35
+ // GF init + helpers
36
+ initGF() {
37
+ const exp = new Array(512);
38
+ const log = new Array(256);
39
+ let x = 1;
40
+ for (let i = 0; i < 255; i++) {
41
+ exp[i] = x;
42
+ log[x] = i;
43
+ x = (x << 1) ^ (x & 0x80 ? 0x11d : 0);
44
+ x &= 0xff;
45
+ }
46
+ for (let i = 255; i < 512; i++) exp[i] = exp[i - 255];
47
+ this.gfExp = exp;
48
+ this.gfLog = log;
49
+ }
50
+ gfAdd(a,b){ return a ^ b; }
51
+ gfMul(a,b){ if(a===0||b===0) return 0; return this.gfExp[(this.gfLog[a]+this.gfLog[b])%255]; }
52
+ makeGenerator(ecLen){
53
+ let gen=[1];
54
+ for(let i=0;i<ecLen;i++){ gen = this.polyMul(gen,[1,this.gfExp[i]]); }
55
+ return gen;
56
+ }
57
+ polyMul(a,b){
58
+ const res = new Array(a.length + b.length -1).fill(0);
59
+ for(let i=0;i<a.length;i++) for(let j=0;j<b.length;j++) res[i+j] ^= this.gfMul(a[i], b[j]);
60
+ return res;
61
+ }
62
+ polyDiv(message, generator){
63
+ const msg = message.slice();
64
+ for(let i=0;i<=message.length - generator.length;i++){
65
+ const coef = msg[i];
66
+ if(coef !== 0) {
67
+ for (let j = 1; j < generator.length; j++) msg[i+j] ^= this.gfMul(generator[j], coef);
68
+ }
69
+ }
70
+ return msg.slice(message.length - (generator.length - 1));
71
+ }
72
+
73
+ toUtf8Bytes(str) { return Array.from(new TextEncoder().encode(str)); }
74
+
75
+ // Data bit building
76
+ buildDataBits(){
77
+ const bits = [];
78
+ bits.push(...this.numToBits(0b0100, 4)); // byte mode
79
+ const lengthBits = this.version <= 9 ? 8 : 16;
80
+ bits.push(...this.numToBits(this.dataBytes.length, lengthBits));
81
+ for(const b of this.dataBytes) bits.push(...this.numToBits(b,8));
82
+ const capacityBits = this.getDataCapacityBits();
83
+ const remaining = capacityBits - bits.length;
84
+ if(remaining > 0) {
85
+ const term = Math.min(4, remaining);
86
+ for(let i=0;i<term;i++) bits.push(0);
87
+ }
88
+ while(bits.length % 8 !== 0) bits.push(0);
89
+ const dataBytes = [];
90
+ for(let i=0;i<bits.length;i+=8) dataBytes.push(parseInt(bits.slice(i,i+8).join(''),2));
91
+ const totalDataBytes = this.getDataCapacityBytes();
92
+ const pads = [0xec, 0x11];
93
+ let padIndex = 0;
94
+ while(dataBytes.length < totalDataBytes) dataBytes.push(pads[padIndex++ % 2]);
95
+ return dataBytes;
96
+ }
97
+ numToBits(num,len){ const a=[]; for(let i=len-1;i>=0;i--) a.push((num>>i)&1); return a; }
98
+ // Use the capacity (bytes) precomputed in versionTable for byte mode
99
+ getDataCapacityBits(){ const verInfo = this.versionTable[this.version]; const dataBytes = verInfo[this.ecLevel]; return dataBytes * 8; }
100
+ getDataCapacityBytes(){ const verInfo = this.versionTable[this.version]; return verInfo[this.ecLevel]; }
101
+ chooseVersion(){
102
+ // Now supports up to version 15
103
+ for(let v=1; v<=15; v++){
104
+ const cap = this.versionTable[v][this.ecLevel];
105
+ if(this.dataBytes.length <= cap){
106
+ this.version = v;
107
+ return;
108
+ }
109
+ }
110
+ throw new Error('Data too long for versions 1..10');
111
+ }
112
+
113
+ // ECC
114
+ makeECC(dataBytes){
115
+ const ver = this.versionTable[this.version];
116
+ const total = ver.totalCodewords;
117
+ const blocks = ver.blocks[this.ecLevel];
118
+ const dataCodewords = dataBytes.length; // already padded to capacity
119
+ // split data bytes across blocks as evenly as spec requires (distribute remainders to first blocks)
120
+ const baseDataLen = Math.floor(dataCodewords / blocks);
121
+ const dataRemainder = dataCodewords % blocks;
122
+ const blockData = [];
123
+ let offset = 0;
124
+ for(let b=0;b<blocks;b++){
125
+ const len = baseDataLen + (b < dataRemainder ? 1 : 0);
126
+ blockData.push(dataBytes.slice(offset, offset + len));
127
+ offset += len;
128
+ }
129
+
130
+ // ECC per block is specified in the table (same for all blocks for v1..v4)
131
+ const eccBlocks = [];
132
+ const ecLen = ver.ecCodewordsPerBlock[this.ecLevel];
133
+ for(let b=0;b<blocks;b++){
134
+ const generator = this.makeGenerator(ecLen);
135
+ const msg = blockData[b].concat(new Array(generator.length - 1).fill(0));
136
+ const ecc = this.polyDiv(msg, generator);
137
+ eccBlocks.push(ecc.slice(-ecLen));
138
+ }
139
+
140
+ // interleave data bytes
141
+ const interleaved = [];
142
+ const maxDataLen = Math.max(...blockData.map(b=>b.length));
143
+ for(let i=0;i<maxDataLen;i++){
144
+ for(let b=0;b<blockData.length;b++){
145
+ if(i < blockData[b].length) interleaved.push(blockData[b][i]);
146
+ }
147
+ }
148
+ // interleave ecc bytes
149
+ const maxEcLen = Math.max(...eccBlocks.map(b=>b.length));
150
+ for(let i=0;i<maxEcLen;i++){
151
+ for(let b=0;b<eccBlocks.length;b++){
152
+ if(i < eccBlocks[b].length) interleaved.push(eccBlocks[b][i]);
153
+ }
154
+ }
155
+ return interleaved;
156
+ }
157
+
158
+ // Function patterns
159
+ placeFinderPattern(x,y){
160
+ const p = [
161
+ [1,1,1,1,1,1,1],
162
+ [1,0,0,0,0,0,1],
163
+ [1,0,1,1,1,0,1],
164
+ [1,0,1,1,1,0,1],
165
+ [1,0,1,1,1,0,1],
166
+ [1,0,0,0,0,0,1],
167
+ [1,1,1,1,1,1,1],
168
+ ];
169
+ for(let dy=0; dy<7; dy++) for(let dx=0; dx<7; dx++){
170
+ this.modules[y+dy][x+dx] = p[dy][dx];
171
+ this.reserved[y+dy][x+dx] = true;
172
+ }
173
+ }
174
+
175
+ // Separators: explicitly set to 0 and reserve them
176
+ placeSeparators(){
177
+ const s = this.size;
178
+ // top-left separator (row 7 col 0..7 and col 7 row 0..7)
179
+ for(let i=0;i<8;i++){ this.modules[7][i] = 0; this.reserved[7][i] = true; this.modules[i][7] = 0; this.reserved[i][7] = true; }
180
+ // top-right (row 7, cols s-8..s-1) and (cols s-8 row 0..7)
181
+ for(let i=0;i<8;i++){ this.modules[7][s-1-i] = 0; this.reserved[7][s-1-i] = true; this.modules[i][s-8] = 0; this.reserved[i][s-8] = true; }
182
+ // bottom-left
183
+ for(let i=0;i<8;i++){ this.modules[s-8][i] = 0; this.reserved[s-8][i] = true; this.modules[s-1-i][7] = 0; this.reserved[s-1-i][7] = true; }
184
+ }
185
+
186
+ placeTimingPatterns(){
187
+ for(let i=8;i<this.size-8;i++){
188
+ const v = (i % 2 === 0) ? 1 : 0;
189
+ if(!this.reserved[6][i]){ this.modules[6][i] = v; this.reserved[6][i] = true; }
190
+ if(!this.reserved[i][6]){ this.modules[i][6] = v; this.reserved[i][6] = true; }
191
+ }
192
+ }
193
+
194
+ placeAlignmentPatterns(){
195
+ const align = this.versionTable[this.version].align;
196
+ if(!align || align.length===0) return;
197
+ for(let ay=0; ay<align.length; ay++){
198
+ for(let ax=0; ax<align.length; ax++){
199
+ const x = align[ax], y = align[ay];
200
+ const overlapsFinder = (x <= 8 && y <= 8) || (x >= this.size - 9 && y <= 8) || (x <= 8 && y >= this.size - 9);
201
+ if(overlapsFinder) continue;
202
+ this.placeAlignmentPatternAt(x-2, y-2);
203
+ }
204
+ }
205
+ }
206
+ placeAlignmentPatternAt(x,y){
207
+ const pat = [
208
+ [1,1,1,1,1],
209
+ [1,0,0,0,1],
210
+ [1,0,1,0,1],
211
+ [1,0,0,0,1],
212
+ [1,1,1,1,1],
213
+ ];
214
+ for(let dy=0; dy<5; dy++) for(let dx=0; dx<5; dx++){
215
+ this.modules[y+dy][x+dx] = pat[dy][dx];
216
+ this.reserved[y+dy][x+dx] = true;
217
+ }
218
+ }
219
+
220
+ reserveFormatAndVersionAreas(){
221
+ const n = this.size;
222
+ // reserve the 9x9 top-left area lines (excluding timing at 6 which already reserved)
223
+ for(let i=0;i<9;i++){
224
+ if(i !== 6){ this.reserved[8][i] = true; this.reserved[i][8] = true; }
225
+ }
226
+ // reserve the format info near top-right and bottom-left
227
+ for(let i=0;i<8;i++){
228
+ this.reserved[n-1 - i][8] = true; // bottom-left vertical
229
+ this.reserved[8][n-1 - i] = true; // top-right horizontal
230
+ }
231
+ // dark module position (fixed dark module) per spec: (8, n-8) should be dark (1)
232
+ this.modules[8][n-8] = 1;
233
+ this.reserved[8][n-8] = true;
234
+ }
235
+
236
+ // Data placement
237
+ placeDataBits(allBytes){
238
+ const bits = [];
239
+ for(const b of allBytes) for(let i=7;i>=0;i--) bits.push((b>>i)&1);
240
+ let bitIndex = 0;
241
+ let dirUp = true;
242
+ let x = this.size - 1;
243
+ let y;
244
+ while(x > 0){
245
+ if(x === 6) x--; // skip vertical timing
246
+ for(let i=0;i<this.size;i++){
247
+ const row = dirUp ? (this.size - 1 - i) : i;
248
+ for(let colOffset=0; colOffset<2; colOffset++){
249
+ const cx = x - colOffset;
250
+ const cy = row;
251
+ if(!this.reserved[cy][cx]){
252
+ const bit = bitIndex < bits.length ? bits[bitIndex++] : 0;
253
+ this.modules[cy][cx] = bit;
254
+ }
255
+ }
256
+ }
257
+ x -= 2;
258
+ dirUp = !dirUp;
259
+ }
260
+ }
261
+
262
+ // Masking, scoring
263
+ applyMask(maskNum, matrixIn){
264
+ const n = this.size;
265
+ const out = Array.from({ length: n }, (_, y) => Array.from({ length: n }, (_, x) => matrixIn[y][x]));
266
+ for(let y=0;y<n;y++){
267
+ for(let x=0;x<n;x++){
268
+ if(this.reserved[y][x]) continue;
269
+ const v = out[y][x];
270
+ let mask;
271
+ switch(maskNum){
272
+ case 0: mask = (y + x) % 2 === 0; break;
273
+ case 1: mask = y % 2 === 0; break;
274
+ case 2: mask = x % 3 === 0; break;
275
+ case 3: mask = (y + x) % 3 === 0; break;
276
+ case 4: mask = (Math.floor(y/2) + Math.floor(x/3)) % 2 === 0; break;
277
+ case 5: mask = ((y*x) % 2) + ((y*x) % 3) === 0; break;
278
+ case 6: mask = (((y*x) % 2) + ((y*x) % 3)) % 2 === 0; break;
279
+ case 7: mask = (((y + x) % 2) + ((y*x) % 3)) % 2 === 0; break;
280
+ default: mask = false;
281
+ }
282
+ if(mask) out[y][x] = v ^ 1;
283
+ }
284
+ }
285
+ return out;
286
+ }
287
+ penaltyScore(mat){
288
+ const n = this.size;
289
+ let score = 0;
290
+ // Rule 1 (rows)
291
+ for(let y=0;y<n;y++){
292
+ let runColor = mat[y][0], runLen = 1;
293
+ for(let x=1;x<n;x++){
294
+ if(mat[y][x] === runColor) runLen++; else { if(runLen >= 5) score += 3 + (runLen - 5); runColor = mat[y][x]; runLen = 1; }
295
+ }
296
+ if(runLen >= 5) score += 3 + (runLen - 5);
297
+ }
298
+ // Rule 1 (cols)
299
+ for(let x=0;x<n;x++){
300
+ let runColor = mat[0][x], runLen = 1;
301
+ for(let y=1;y<n;y++){
302
+ if(mat[y][x] === runColor) runLen++; else { if(runLen >= 5) score += 3 + (runLen - 5); runColor = mat[y][x]; runLen = 1; }
303
+ }
304
+ if(runLen >= 5) score += 3 + (runLen - 5);
305
+ }
306
+ // Rule 2: 2x2 blocks
307
+ for(let y=0;y<n-1;y++) for(let x=0;x<n-1;x++){ const v = mat[y][x]; if(mat[y][x+1]===v && mat[y+1][x]===v && mat[y+1][x+1]===v) score += 3; }
308
+ // Rule 3: pattern 1011101 with 4 light modules either side (rows & cols)
309
+ const pattern1 = [1,0,1,1,1,0,1];
310
+ for(let y=0;y<n;y++){
311
+ for(let x=0;x<n-6;x++){
312
+ const slice = mat[y].slice(x,x+7);
313
+ if(this.arrEquals(slice, pattern1)){
314
+ const leftClear = x-4 >= 0 ? mat[y].slice(x-4,x).every(v=>v===0) : x===0;
315
+ const rightClear = x+7+4 <= n ? mat[y].slice(x+7,x+11).every(v=>v===0) : x+7===n;
316
+ if(leftClear || rightClear) score += 40;
317
+ }
318
+ }
319
+ }
320
+ for(let x=0;x<n;x++){
321
+ for(let y=0;y<n-6;y++){
322
+ const col=[]; for(let k=0;k<7;k++) col.push(mat[y+k][x]);
323
+ if(this.arrEquals(col, pattern1)){
324
+ const topClear = y-4 >=0 ? (()=>{ for(let k=y-4;k<y;k++) if(mat[k][x]!==0) return false; return true; })() : y===0;
325
+ const botClear = y+7+4 <= n ? (()=>{ for(let k=y+7;k<y+11;k++) if(mat[k][x]!==0) return false; return true; })() : y+7===n;
326
+ if(topClear || botClear) score += 40;
327
+ }
328
+ }
329
+ }
330
+ // Rule 4: dark module ratio
331
+ let dark = 0; for(let y=0;y<n;y++) for(let x=0;x<n;x++) if(mat[y][x]===1) dark++;
332
+ const total = n*n;
333
+ const percent = (dark*100)/total;
334
+ const prevMultipleOfFive = Math.abs(Math.round(percent/5)*5 - 50)/5;
335
+ score += prevMultipleOfFive * 10;
336
+ return score;
337
+ }
338
+ arrEquals(a,b){ if(a.length !== b.length) return false; for(let i=0;i<a.length;i++) if(a[i]!==b[i]) return false; return true; }
339
+
340
+ // Format bits: compute and write in spec positions correctly
341
+ getFormatBits(maskNum){
342
+ const ecMap = {L:1, M:0, Q:3, H:2};
343
+ const formatInfo = ((ecMap[this.ecLevel] & 0x3) << 3) | (maskNum & 0x7);
344
+ let data = formatInfo << 10;
345
+ const generator = 0x537;
346
+ for(let i=14;i>=10;i--){
347
+ if((data >> i) & 1) data ^= (generator << (i - 10));
348
+ }
349
+ const formatBits = ((formatInfo << 10) | (data & 0x3FF)) ^ 0x5412;
350
+ const bits = [];
351
+ for(let i=14;i>=0;i--) bits.push((formatBits >> i) & 1);
352
+ return bits;
353
+ }
354
+
355
+ writeFormatBits(mat, maskNum){
356
+ const bits = this.getFormatBits(maskNum);
357
+ const n = this.size;
358
+ // positions per spec (first copy around top-left)
359
+ const pos1 = [
360
+ [8,0],[8,1],[8,2],[8,3],[8,4],[8,5],[8,7],[8,8],
361
+ [7,8],[6,8],[5,8],[4,8],[3,8],[2,8],[1,8]
362
+ ];
363
+ // However the standard order for the 15 bits is specific: we'll follow
364
+ // spec order: (row,col) sequence mapped to bits[0..14]
365
+ const order1 = [
366
+ [8,0],[8,1],[8,2],[8,3],[8,4],[8,5],[8,7],[8,8],[7,8],[6,8],[5,8],[4,8],[3,8],[2,8],[1,8]
367
+ ];
368
+ for(let i=0;i<15;i++){
369
+ const [r,c] = order1[i];
370
+ mat[r][c] = bits[i];
371
+ this.reserved[r][c] = true;
372
+ }
373
+ // second copy: top-right and bottom-left (mirror)
374
+ const order2 = [];
375
+ // bottom-left vertical (from bottom up)
376
+ for(let i=0;i<7;i++) order2.push([n-1 - i, 8]); // 0..6
377
+ // top-right horizontal (from right to left)
378
+ for(let i=7;i<15;i++) order2.push([8, n - 15 + i]); // 7..14 mapping
379
+ // write second copy using same bits order
380
+ for(let i=0;i<15;i++){
381
+ const [r,c] = order2[i];
382
+ mat[r][c] = bits[i];
383
+ this.reserved[r][c] = true;
384
+ }
385
+ }
386
+
387
+ // Build matrix: orchestrates steps and mask selection
388
+ buildMatrix(){
389
+ const dataBytes = this.buildDataBits();
390
+ const allCodewords = this.makeECC(dataBytes);
391
+ // place function patterns
392
+ this.placeFinderPattern(0,0);
393
+ this.placeFinderPattern(this.size - 7, 0);
394
+ this.placeFinderPattern(0, this.size - 7);
395
+ this.placeSeparators();
396
+ this.placeTimingPatterns();
397
+ this.placeAlignmentPatterns();
398
+ this.reserveFormatAndVersionAreas();
399
+ // place data
400
+ this.placeDataBits(allCodewords);
401
+ // choose mask
402
+ let bestMask = 0, bestScore = Infinity, bestMat = null;
403
+ for(let m=0;m<8;m++){
404
+ const masked = this.applyMask(m, this.modules);
405
+ const copy = masked.map(r=>r.slice());
406
+ this.writeFormatBits(copy, m);
407
+ const score = this.penaltyScore(copy);
408
+ if(score < bestScore){ bestScore = score; bestMask = m; bestMat = copy; }
409
+ }
410
+ // finalize: fill nulls with 0
411
+ for(let y=0;y<this.size;y++) for(let x=0;x<this.size;x++) if(bestMat[y][x] === null || typeof bestMat[y][x] === 'undefined') bestMat[y][x] = 0;
412
+ this.modules = bestMat;
413
+ return this.modules;
414
+ }
415
+
416
+ // Crisp renderer: integer cell size, disable smoothing, enforce quiet zone
417
+ renderToCanvas(canvasId, pixelSize = 512, marginModules = 4){
418
+ const canvas = document.getElementById(canvasId);
419
+ if(!canvas) throw new Error('Canvas not found');
420
+ const moduleCount = this.size;
421
+ // Treat marginModules as a count of modules (per spec the quiet zone is 4 modules).
422
+ // total modules including margins on both sides
423
+ const totalModules = moduleCount + marginModules * 2;
424
+ // compute integer cell size (pixels per module)
425
+ const cell = Math.floor(pixelSize / totalModules);
426
+ if(cell < 1) throw new Error('pixelSize too small for QR code and margin');
427
+ const actualSize = cell * totalModules;
428
+ // handle high-DPI displays: set CSS size and backing store size, then scale
429
+ const dpr = (typeof window !== 'undefined' && window.devicePixelRatio) ? window.devicePixelRatio : 1;
430
+ // CSS size (unscaled) to keep layout consistent
431
+ canvas.style.width = `${actualSize}px`;
432
+ canvas.style.height = `${actualSize}px`;
433
+ // backing store size in device pixels
434
+ canvas.width = Math.max(1, Math.floor(actualSize * dpr));
435
+ canvas.height = Math.max(1, Math.floor(actualSize * dpr));
436
+ const ctx = canvas.getContext('2d');
437
+ // scale drawing operations so we can use CSS-pixel coordinates
438
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
439
+ // crisp pixels
440
+ if (typeof ctx.imageSmoothingEnabled !== 'undefined') ctx.imageSmoothingEnabled = false;
441
+ try { canvas.style.imageRendering = 'pixelated'; } catch (e) {}
442
+ // fill background white (use CSS pixel coords)
443
+ ctx.fillStyle = '#fff';
444
+ ctx.fillRect(0, 0, actualSize, actualSize);
445
+ // offset in CSS pixels (marginModules is modules, so multiply by cell)
446
+ const offset = marginModules * cell;
447
+ for(let y=0;y<moduleCount;y++){
448
+ for(let x=0;x<moduleCount;x++){
449
+ ctx.fillStyle = this.modules[y][x] ? '#000' : '#fff';
450
+ // draw using CSS-pixel coordinates; canvas is already scaled by DPR
451
+ ctx.fillRect(offset + x*cell, offset + y*cell, cell, cell);
452
+ }
453
+ }
454
+ }
455
+ }