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 +21 -0
- package/README.md +51 -0
- package/package.json +20 -0
- package/src/QRCodeGenerator.js +455 -0
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
|
+
[](https://www.repostatus.org/#active)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.npmjs.com/package/qrcode-pack)
|
|
6
|
+
[](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
|
+
}
|