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.
- package/QRCodeGenerator.js +45 -0
- package/README.md +2 -8
- package/package.json +2 -5
- package/src/GFMath.js +53 -0
- package/src/QRCodeData.js +212 -0
- package/src/QRCodeMatrix.js +164 -0
- package/src/QRCodeRenderer.js +31 -0
- package/src/QRCodeGenerator.js +0 -455
|
@@ -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-
|
|
21
|
+
import QRCodeGenerator from 'qrcode-pack';
|
|
22
22
|
|
|
23
23
|
const qr = new QRCodeGenerator('Hello World');
|
|
24
24
|
qr.buildMatrix();
|
|
25
|
-
qr.renderToCanvas('
|
|
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.
|
|
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": "
|
|
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
|
+
}
|
package/src/QRCodeGenerator.js
DELETED
|
@@ -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
|
-
}
|