qrcode-pack 0.1.2 → 0.1.3

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.
@@ -0,0 +1,45 @@
1
+ import QRCodeData from './src/QRCodeData.js';
2
+ import QRCodeMatrix from './src/QRCodeMatrix.js';
3
+ import QRCodeRenderer from './src/QRCodeRenderer.js';
4
+
5
+ export default class QRCodeGenerator {
6
+ constructor(text, opts = {}) {
7
+ this.text = text;
8
+ this.ecLevel = (opts.ecLevel || 'H').toUpperCase();
9
+ this.data = new QRCodeData(this.text, { ecLevel: this.ecLevel });
10
+ this.matrix = new QRCodeMatrix(this.data.version, 17 + 4 * this.data.version);
11
+ this.alignPositions = this.data.versionTable[this.data.version].align;
12
+ this.renderer = null;
13
+ }
14
+
15
+ buildMatrix(maskNum = 0) {
16
+ const allCodewords = this.data.buildDataBits();
17
+ const fullCodewords = this.data.makeECC(allCodewords);
18
+
19
+ // Place finder patterns
20
+ this.matrix.placeFinderPattern(0, 0);
21
+ this.matrix.placeFinderPattern(this.matrix.size - 7, 0);
22
+ this.matrix.placeFinderPattern(0, this.matrix.size - 7);
23
+
24
+ // Place separators, timing patterns, and alignment patterns
25
+ this.matrix.placeSeparators();
26
+ this.matrix.placeTimingPatterns();
27
+ this.matrix.placeAlignmentPatterns(this.alignPositions);
28
+ this.matrix.reserveFormatAndVersionAreas();
29
+
30
+ // Place data bits and apply mask
31
+ this.matrix.placeDataBits(fullCodewords);
32
+ this.matrix.applyMask(maskNum);
33
+ this.matrix.writeFormatBits(this.ecLevel, maskNum);
34
+
35
+ return this.matrix.modules;
36
+ }
37
+
38
+ // Render the QR code to a canvas element
39
+ renderToCanvas(canvasId) {
40
+ this.renderer = new QRCodeRenderer(this.matrix.modules);
41
+ this.renderer.renderToCanvas(canvasId);
42
+ }
43
+
44
+
45
+ }
package/README.md CHANGED
@@ -18,16 +18,13 @@ To install run:
18
18
  ## Usage
19
19
 
20
20
  ````
21
- import QRCodeGenerator from 'qrcode-generator-js';
21
+ import QRCodeGenerator from 'qrcode-pack';
22
22
 
23
23
  const qr = new QRCodeGenerator('Hello World');
24
24
  qr.buildMatrix();
25
- qr.renderToCanvas('myCanvas', 512);
25
+ qr.renderToCanvas('canvasID');
26
26
 
27
27
  ````
28
-
29
- ## Props
30
-
31
28
  ## Props
32
29
 
33
30
  | Prop / Method | Type | Default | Description |
@@ -40,9 +37,6 @@ qr.renderToCanvas('myCanvas', 512);
40
37
  | `buildMatrix()` | `function` | — | Generates the internal QR Code matrix based on `text` and `ecLevel`. |
41
38
  | `renderToCanvas(canvasId, size)` | `function` | — | Renders the QR Code to an HTML `<canvas>`. `canvasId` = ID of the canvas, `size` = width/height in pixels. |
42
39
 
43
- > **Optional note:**
44
- > Additional options may be added in future releases, such as custom colors, padding, or different rendering outputs.
45
-
46
40
  ## Contributing
47
41
 
48
42
  Contributions are welcome.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qrcode-pack",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "",
5
5
  "homepage": "https://github.com/sarahcssiqueira/qrcode-pack#readme",
6
6
  "bugs": {
@@ -13,8 +13,5 @@
13
13
  "license": "MIT",
14
14
  "author": "Sarah C. Siqueira",
15
15
  "type": "module",
16
- "main": "src/QRCodeGenerator.js",
17
- "scripts": {
18
- "test": "echo \"Error: no test specified\" && exit 1"
19
- }
16
+ "main": "/QRCodeGenerator.js"
20
17
  }
package/src/GFMath.js ADDED
@@ -0,0 +1,53 @@
1
+ export default class GFMath {
2
+ constructor() {
3
+ this.initGF();
4
+ }
5
+
6
+ // GF init + helpers
7
+ initGF() {
8
+ const exp = new Array(512);
9
+ const log = new Array(256);
10
+ let x = 1;
11
+ for (let i = 0; i < 255; i++) {
12
+ exp[i] = x;
13
+ log[x] = i;
14
+ x = (x << 1) ^ (x & 0x80 ? 0x11d : 0);
15
+ x &= 0xff;
16
+ }
17
+ for (let i = 255; i < 512; i++) exp[i] = exp[i - 255];
18
+ this.gfExp = exp;
19
+ this.gfLog = log;
20
+ }
21
+
22
+ // Addition in GF(256) is just XOR
23
+ gfAdd(a,b){ return a ^ b; }
24
+
25
+ // Multiplication in GF(256) using log/exp tables for efficiency
26
+ gfMul(a,b){ if(a===0||b===0) return 0; return this.gfExp[(this.gfLog[a]+this.gfLog[b])%255]; }
27
+
28
+ // Generate the Reed-Solomon generator polynomial for the given error correction length
29
+ makeGenerator(ecLen){
30
+ let gen=[1];
31
+ for(let i=0;i<ecLen;i++){ gen = this.polyMul(gen,[1,this.gfExp[i]]); }
32
+ return gen;
33
+ }
34
+
35
+ // Polynomial multiplication in GF(256), used for generator creation and ECC calculation
36
+ polyMul(a,b){
37
+ const res = new Array(a.length + b.length -1).fill(0);
38
+ 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]);
39
+ return res;
40
+ }
41
+
42
+ // Polynomial division in GF(256), used to calculate ECC codewords from the message and generator
43
+ polyDiv(message, generator){
44
+ const msg = message.slice();
45
+ for(let i=0;i<=message.length - generator.length;i++){
46
+ const coef = msg[i];
47
+ if(coef !== 0) {
48
+ for (let j = 1; j < generator.length; j++) msg[i+j] ^= this.gfMul(generator[j], coef);
49
+ }
50
+ }
51
+ return msg.slice(message.length - (generator.length - 1));
52
+ }
53
+ }
@@ -0,0 +1,212 @@
1
+ import GFMath from './GFMath.js';
2
+
3
+ const RS_BLOCK_TABLE = {
4
+ 1: { L: [1, 26, 19], M: [1, 26, 16], Q: [1, 26, 13], H: [1, 26, 9] },
5
+ 2: { L: [1, 44, 34], M: [1, 44, 28], Q: [1, 44, 22], H: [1, 44, 16] },
6
+ 3: { L: [1, 70, 55], M: [1, 70, 44], Q: [2, 35, 17], H: [2, 35, 13] },
7
+ 4: { L: [1, 100, 80], M: [2, 50, 32], Q: [2, 50, 24], H: [4, 25, 9] },
8
+ 5: { L: [1, 134, 108], M: [2, 67, 43], Q: [2, 33, 15, 2, 34, 16], H: [2, 33, 11, 2, 34, 12] },
9
+ 6: { L: [2, 86, 68], M: [4, 43, 27], Q: [4, 43, 19], H: [4, 43, 15] },
10
+ 7: { L: [2, 98, 78], M: [4, 49, 31], Q: [2, 32, 14, 4, 33, 15], H: [4, 39, 13, 1, 40, 14] },
11
+ 8: { L: [2, 121, 97], M: [2, 60, 38, 2, 61, 39], Q: [4, 40, 18, 2, 41, 19], H: [4, 40, 14, 2, 41, 15] },
12
+ 9: { L: [2, 146, 116], M: [3, 58, 36, 2, 59, 37], Q: [4, 36, 16, 4, 37, 17], H: [4, 36, 12, 4, 37, 13] },
13
+ 10: { L: [2, 86, 68, 2, 87, 69], M: [4, 69, 43, 1, 70, 44], Q: [6, 43, 19, 2, 44, 20], H: [6, 43, 15, 2, 44, 16] },
14
+ 11: { L: [4, 101, 81], M: [1, 80, 50, 4, 81, 51], Q: [4, 50, 22, 4, 51, 23], H: [3, 36, 12, 8, 37, 13] },
15
+ 12: { L: [2, 116, 92, 2, 117, 93], M: [6, 58, 36, 2, 59, 37], Q: [4, 46, 20, 6, 47, 21], H: [7, 42, 14, 4, 43, 15] },
16
+ 13: { L: [4, 133, 107], M: [8, 59, 37, 1, 60, 38], Q: [8, 44, 20, 4, 45, 21], H: [12, 33, 11, 4, 34, 12] },
17
+ 14: { L: [3, 145, 115, 1, 146, 116], M: [4, 64, 40, 5, 65, 41], Q: [11, 36, 16, 5, 37, 17], H: [11, 36, 12, 5, 37, 13] },
18
+ 15: { L: [5, 109, 87, 1, 110, 88], M: [5, 65, 41, 5, 66, 42], Q: [5, 54, 24, 7, 55, 25], H: [11, 36, 12, 7, 37, 13] },
19
+ };
20
+
21
+ export default class QRCodeData {
22
+ constructor(text, opts = {}) {
23
+ this.text = text;
24
+ this.ecLevel = (opts.ecLevel || 'H').toUpperCase();
25
+ this.dataBytes = this.toUtf8Bytes(this.text);
26
+ this.version = null;
27
+ this.modules = null;
28
+ this.reserved = null;
29
+ this.versionTable = {
30
+ 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} },
31
+ 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} },
32
+ 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} },
33
+ 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} },
34
+ 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} },
35
+ 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} },
36
+ 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} },
37
+ 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} },
38
+ 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} },
39
+ 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} },
40
+ 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} },
41
+ 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} },
42
+ 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} },
43
+ 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} },
44
+ 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} },
45
+ };
46
+ this.chooseVersion();
47
+ this.size = 17 + 4 * this.version;
48
+ this.modules = Array.from({ length: this.size }, () => Array(this.size).fill(null));
49
+ this.reserved = Array.from({ length: this.size }, () => Array(this.size).fill(false));
50
+
51
+ // Initialize GF(256) math for ECC calculations
52
+ this.gfmath = new GFMath();
53
+
54
+ }
55
+
56
+ // Convert input text to UTF-8 byte array
57
+ toUtf8Bytes(str){
58
+ return Array.from(new TextEncoder().encode(str));
59
+ }
60
+
61
+ // Determine the smallest QR code version that can accommodate the input data and error correction level
62
+ chooseVersion(){
63
+ for(let v=1; v<=15; v++){
64
+ const verInfo = this.versionTable[v];
65
+ const capacityBits = verInfo[this.ecLevel] * 8;
66
+ const lengthBits = v <= 9 ? 8 : 16;
67
+ const requiredBits = 4 + lengthBits + (this.dataBytes.length * 8);
68
+
69
+ if(requiredBits <= capacityBits){
70
+ this.version = v;
71
+ return;
72
+ }
73
+ }
74
+ throw new Error('Data too long for versions 1..15');
75
+ }
76
+
77
+ // Build the data bits array according to QR code specification,
78
+ // including mode indicator, length, data, terminator, padding, and interleaving
79
+ buildDataBits() {
80
+ const bits = [];
81
+
82
+ bits.push(...this.numToBits(0b0100, 4));
83
+
84
+ const lengthBits = this.version <= 9 ? 8 : 16;
85
+ bits.push(...this.numToBits(this.dataBytes.length, lengthBits));
86
+
87
+ for (const b of this.dataBytes) bits.push(...this.numToBits(b, 8));
88
+
89
+ const capacityBits = this.getDataCapacityBits();
90
+ const remaining = capacityBits - bits.length;
91
+ if (remaining > 0) {
92
+ const term = Math.min(4, remaining);
93
+ for (let i = 0; i < term; i++) bits.push(0);
94
+ }
95
+ while (bits.length % 8 !== 0) bits.push(0);
96
+ const dataBytes = [];
97
+ for (let i = 0; i < bits.length; i += 8) {
98
+ dataBytes.push(parseInt(bits.slice(i, i + 8).join(''), 2));
99
+ }
100
+
101
+ // 7) Pad with alternating bytes 0xec and 0x11 until we reach the total data capacity in bytes for the version and error correction level
102
+ const totalDataBytes = this.getDataCapacityBytes();
103
+ const pads = [0xec, 0x11];
104
+ let padIndex = 0;
105
+ while (dataBytes.length < totalDataBytes) {
106
+ dataBytes.push(pads[padIndex % 2]);
107
+ padIndex++;
108
+ }
109
+
110
+ return dataBytes;
111
+ }
112
+
113
+ // Return data capacity in bits for the current version and error correction level
114
+ getDataCapacityBits() {
115
+ const verInfo = this.versionTable[this.version];
116
+ return verInfo[this.ecLevel] * 8;
117
+ }
118
+
119
+ // Return data capacity in bytes for the current version and error correction level
120
+ getDataCapacityBytes() {
121
+ const verInfo = this.versionTable[this.version];
122
+ return verInfo[this.ecLevel];
123
+ }
124
+
125
+ numToBits(num,len){
126
+ const arr = [];
127
+ for(let i=len-1;i>=0;i--)
128
+ arr.push((num >> i) & 1);
129
+ return arr;
130
+ }
131
+
132
+ getRSBlocks(){
133
+ const rsEntry = RS_BLOCK_TABLE[this.version]?.[this.ecLevel];
134
+
135
+ if(!rsEntry)
136
+ throw new Error(`Missing RS block data for version ${this.version} ${this.ecLevel}`);
137
+
138
+ const blocks = [];
139
+
140
+ for(let i = 0; i < rsEntry.length; i += 3){
141
+ const count = rsEntry[i];
142
+ const totalCount = rsEntry[i + 1];
143
+ const dataCount = rsEntry[i + 2];
144
+
145
+ for(let blockIndex = 0; blockIndex < count; blockIndex++){
146
+ blocks.push({
147
+ totalCount,
148
+ dataCount,
149
+ ecCount: totalCount - dataCount,
150
+ });
151
+ }
152
+ }
153
+
154
+ return blocks;
155
+ }
156
+
157
+ makeECC(dataBytes){
158
+ const rsBlocks = this.getRSBlocks();
159
+ const blockData = [];
160
+ let offset = 0;
161
+
162
+ for(const block of rsBlocks){
163
+ const chunk = dataBytes.slice(offset, offset + block.dataCount);
164
+
165
+ if(chunk.length !== block.dataCount)
166
+ throw new Error(`Invalid data block layout for version ${this.version} ${this.ecLevel}`);
167
+
168
+ blockData.push(chunk);
169
+ offset += block.dataCount;
170
+ }
171
+
172
+ if(offset !== dataBytes.length)
173
+ throw new Error(`Unassigned data codewords for version ${this.version} ${this.ecLevel}`);
174
+
175
+ const eccBlocks = [];
176
+ const generators = new Map();
177
+
178
+ for(let b=0;b<rsBlocks.length;b++){
179
+ const ecLen = rsBlocks[b].ecCount;
180
+ let generator = generators.get(ecLen);
181
+
182
+ if(!generator){
183
+ generator = this.gfmath.makeGenerator(ecLen);
184
+ generators.set(ecLen, generator);
185
+ }
186
+
187
+ const msg = blockData[b].concat(new Array(ecLen).fill(0));
188
+ const ecc = this.gfmath.polyDiv(msg,generator);
189
+ eccBlocks.push(ecc.slice(-ecLen));
190
+ }
191
+
192
+ // Interleave data bytes
193
+ const interleaved = [];
194
+ const maxDataLen = Math.max(...blockData.map(b=>b.length));
195
+ for(let i=0;i<maxDataLen;i++){
196
+ for(let b=0;b<blockData.length;b++){
197
+ if(i < blockData[b].length)
198
+ interleaved.push(blockData[b][i]);
199
+ }
200
+ }
201
+ // Interleave ECC bytes
202
+ const maxEcLen = Math.max(...eccBlocks.map(block => block.length));
203
+ for(let i=0;i<maxEcLen;i++){
204
+ for(let b=0;b<eccBlocks.length;b++){
205
+ if(i < eccBlocks[b].length)
206
+ interleaved.push(eccBlocks[b][i]);
207
+ }
208
+ }
209
+ return interleaved;
210
+ }
211
+
212
+ }
@@ -0,0 +1,164 @@
1
+ export default class QRCodeMatrix {
2
+ constructor(version, size) {
3
+ this.version = version;
4
+ this.size = size;
5
+ this.modules = Array.from({ length: size }, () => Array(size).fill(null));
6
+ this.reserved = Array.from({ length: size }, () => Array(size).fill(false));
7
+ }
8
+
9
+ // Functions to place patterns and data in the matrix according to QR code specification
10
+ placeFinderPattern(x, y) {
11
+ const p = [
12
+ [1,1,1,1,1,1,1],
13
+ [1,0,0,0,0,0,1],
14
+ [1,0,1,1,1,0,1],
15
+ [1,0,1,1,1,0,1],
16
+ [1,0,1,1,1,0,1],
17
+ [1,0,0,0,0,0,1],
18
+ [1,1,1,1,1,1,1],
19
+ ];
20
+ for (let dy = 0; dy < 7; dy++) for (let dx = 0; dx < 7; dx++) {
21
+ this.modules[y + dy][x + dx] = p[dy][dx];
22
+ this.reserved[y + dy][x + dx] = true;
23
+ }
24
+ }
25
+
26
+ // Place separators (white modules) around finder patterns
27
+ placeSeparators() {
28
+ const s = this.size;
29
+ // top-left
30
+ 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; }
31
+ // top-right
32
+ 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; }
33
+ // bottom-left
34
+ 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; }
35
+ }
36
+
37
+ // Place timing patterns (alternating black and white modules) between finder patterns
38
+ placeTimingPatterns() {
39
+ for (let i = 8; i < this.size - 8; i++) {
40
+ if (!this.reserved[6][i]) { this.modules[6][i] = i % 2; this.reserved[6][i] = true; }
41
+ if (!this.reserved[i][6]) { this.modules[i][6] = i % 2; this.reserved[i][6] = true; }
42
+ }
43
+ }
44
+
45
+ // Place alignment patterns based on version-specific positions, avoiding overlaps with finder patterns
46
+ placeAlignmentPatterns(alignPositions) {
47
+ if (!alignPositions || alignPositions.length === 0) return;
48
+ for (let ay = 0; ay < alignPositions.length; ay++) {
49
+ for (let ax = 0; ax < alignPositions.length; ax++) {
50
+ const x = alignPositions[ax], y = alignPositions[ay];
51
+ const overlapsFinder = (x <= 8 && y <= 8) || (x >= this.size - 9 && y <= 8) || (x <= 8 && y >= this.size - 9);
52
+ if (overlapsFinder) continue;
53
+ this.placeAlignmentPatternAt(x - 2, y - 2);
54
+ }
55
+ }
56
+ }
57
+
58
+ // Place a single 5x5 alignment pattern at the specified coordinates
59
+ placeAlignmentPatternAt(x, y) {
60
+ const pat = [
61
+ [1,1,1,1,1],
62
+ [1,0,0,0,1],
63
+ [1,0,1,0,1],
64
+ [1,0,0,0,1],
65
+ [1,1,1,1,1],
66
+ ];
67
+ for (let dy = 0; dy < 5; dy++) for (let dx = 0; dx < 5; dx++) {
68
+ this.modules[y + dy][x + dx] = pat[dy][dx];
69
+ this.reserved[y + dy][x + dx] = true;
70
+ }
71
+ }
72
+
73
+ // Reserve areas for format and version information, which will be filled later
74
+ reserveFormatAndVersionAreas() {
75
+ const n = this.size;
76
+ for (let i = 0; i < 9; i++) {
77
+ if (i !== 6) { this.reserved[8][i] = true; this.reserved[i][8] = true; }
78
+ }
79
+ for (let i = 0; i < 8; i++) {
80
+ this.reserved[n - 1 - i][8] = true; // bottom-left
81
+ this.reserved[8][n - 1 - i] = true; // top-right
82
+ }
83
+ this.modules[8][n - 8] = 1;
84
+ this.reserved[8][n - 8] = true;
85
+ }
86
+
87
+ // Place data bits in the matrix in a zig-zag pattern, skipping reserved areas, and applying the mask
88
+ placeDataBits(allBytes){
89
+ const bits = [];
90
+ for(const b of allBytes) for(let i=7;i>=0;i--) bits.push((b>>i)&1);
91
+ let bitIndex = 0;
92
+ let dirUp = true;
93
+ let x = this.size - 1;
94
+ let y;
95
+ while(x > 0){
96
+ if(x === 6) x--; // skip vertical timing
97
+ for(let i=0;i<this.size;i++){
98
+ const row = dirUp ? (this.size - 1 - i) : i;
99
+ for(let colOffset=0; colOffset<2; colOffset++){
100
+ const cx = x - colOffset;
101
+ const cy = row;
102
+ if(!this.reserved[cy][cx]){
103
+ const bit = bitIndex < bits.length ? bits[bitIndex++] : 0;
104
+ this.modules[cy][cx] = bit;
105
+ }
106
+ }
107
+ }
108
+ x -= 2;
109
+ dirUp = !dirUp;
110
+ }
111
+ }
112
+
113
+ // Apply the specified mask pattern to the data modules, skipping reserved areas
114
+ applyMask(maskNum) {
115
+ const n = this.size;
116
+ const out = Array.from({ length: n }, (_, y) => Array.from({ length: n }, (_, x) => this.modules[y][x]));
117
+ for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) {
118
+ if (this.reserved[y][x]) continue;
119
+ let mask;
120
+ switch(maskNum){
121
+ case 0: mask = (y + x) % 2 === 0; break;
122
+ case 1: mask = y % 2 === 0; break;
123
+ case 2: mask = x % 3 === 0; break;
124
+ case 3: mask = (y + x) % 3 === 0; break;
125
+ case 4: mask = (Math.floor(y/2) + Math.floor(x/3)) % 2 === 0; break;
126
+ case 5: mask = ((y*x) % 2) + ((y*x) % 3) === 0; break;
127
+ case 6: mask = (((y*x) % 2) + ((y*x) % 3)) % 2 === 0; break;
128
+ case 7: mask = (((y + x) % 2) + ((y*x) % 3)) % 2 === 0; break;
129
+ default: mask = false;
130
+ }
131
+ if(mask) out[y][x] ^= 1;
132
+ }
133
+ this.modules = out;
134
+ }
135
+
136
+ // Format bits are calculated based on error correction level and mask pattern, then placed in reserved areas
137
+ getFormatBits(ecLevel, maskNum) {
138
+ const ecMap = {L:1, M:0, Q:3, H:2};
139
+ const formatInfo = ((ecMap[ecLevel] & 0x3) << 3) | (maskNum & 0x7);
140
+ let data = formatInfo << 10;
141
+ const generator = 0x537;
142
+ for (let i = 14; i >= 10; i--) if ((data >> i) & 1) data ^= (generator << (i - 10));
143
+ const formatBits = ((formatInfo << 10) | (data & 0x3FF)) ^ 0x5412;
144
+ const bits = [];
145
+ for (let i = 14; i >= 0; i--) bits.push((formatBits >> i) & 1);
146
+ return bits;
147
+ }
148
+
149
+ // Write the format bits to their reserved positions in the matrix, ensuring they are marked as reserved
150
+ writeFormatBits(ecLevel, maskNum) {
151
+ const bits = this.getFormatBits(ecLevel, maskNum);
152
+ const n = this.size;
153
+
154
+ // first copy top-left
155
+ const order1 = [[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]];
156
+ for (let i = 0; i < 15; i++) { const [r,c] = order1[i]; this.modules[r][c] = bits[i]; this.reserved[r][c] = true; }
157
+
158
+ // second copy top-right and bottom-left
159
+ const order2 = [];
160
+ for (let i = 0; i < 7; i++) order2.push([n-1-i, 8]);
161
+ for (let i = 7; i < 15; i++) order2.push([8, n-15+i]);
162
+ for (let i = 0; i < 15; i++) { const [r,c] = order2[i]; this.modules[r][c] = bits[i]; this.reserved[r][c] = true; }
163
+ }
164
+ }
@@ -0,0 +1,31 @@
1
+ export default class QRCodeRenderer {
2
+ constructor(matrix) { this.modules = matrix; this.size = matrix.length; }
3
+
4
+ // Render the QR code matrix to a canvas element with specified pixel size and margin,
5
+ // ensuring crisp rendering on high-DPI displays
6
+ renderToCanvas(canvasId, pixelSize = 512, marginModules = 4) {
7
+ const canvas = document.getElementById(canvasId);
8
+ if (!canvas) throw new Error('Canvas not found');
9
+ const moduleCount = this.size;
10
+ const totalModules = moduleCount + marginModules * 2;
11
+ const cell = Math.floor(pixelSize / totalModules);
12
+ const actualSize = cell * totalModules;
13
+ const dpr = (typeof window !== 'undefined' && window.devicePixelRatio) ? window.devicePixelRatio : 1;
14
+ canvas.style.width = `${actualSize}px`;
15
+ canvas.style.height = `${actualSize}px`;
16
+ canvas.width = Math.max(1, Math.floor(actualSize * dpr));
17
+ canvas.height = Math.max(1, Math.floor(actualSize * dpr));
18
+ const ctx = canvas.getContext('2d');
19
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
20
+ if (typeof ctx.imageSmoothingEnabled !== 'undefined') ctx.imageSmoothingEnabled = false;
21
+ try { canvas.style.imageRendering = 'pixelated'; } catch (e) {}
22
+
23
+ ctx.fillStyle = '#fff';
24
+ ctx.fillRect(0, 0, actualSize, actualSize);
25
+ const offset = marginModules * cell;
26
+ for (let y = 0; y < moduleCount; y++) for (let x = 0; x < moduleCount; x++) {
27
+ ctx.fillStyle = this.modules[y][x] ? '#000' : '#fff';
28
+ ctx.fillRect(offset + x * cell, offset + y * cell, cell, cell);
29
+ }
30
+ }
31
+ }
@@ -1,455 +0,0 @@
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
- }