ste-canvas-poster 1.0.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.
@@ -0,0 +1,533 @@
1
+ /**
2
+ * qrcodeGenerator.js - 轻量级 QR 码生成器
3
+ * 版本:v0.0.1
4
+ *
5
+ * 基于 Nayuki QR Code generator 算法原理实现,纯 JS,无 DOM 依赖。
6
+ *
7
+ * 用法:
8
+ * import { generateQRMatrix } from './qrcodeGenerator.js';
9
+ * const matrix = generateQRMatrix('https://example.com');
10
+ * // matrix[r][c] => 1=黑, 0=白
11
+ */
12
+
13
+ // ─────────────────────────────────────────────
14
+ // 工具:UTF-8 编码
15
+ // ─────────────────────────────────────────────
16
+
17
+ function stringToUtf8Bytes(text) {
18
+ const encoded = encodeURIComponent(text);
19
+ const bytes = [];
20
+ for (let i = 0; i < encoded.length; ) {
21
+ if (encoded[i] === '%') {
22
+ bytes.push(parseInt(encoded.slice(i + 1, i + 3), 16));
23
+ i += 3;
24
+ } else {
25
+ bytes.push(encoded.charCodeAt(i));
26
+ i++;
27
+ }
28
+ }
29
+ return bytes;
30
+ }
31
+
32
+ // ─────────────────────────────────────────────
33
+ // 伽罗华域 GF(2^8) 运算
34
+ // ─────────────────────────────────────────────
35
+
36
+ const GF_EXP = new Array(512);
37
+ const GF_LOG = new Array(256);
38
+ (function () {
39
+ let x = 1;
40
+ for (let i = 0; i < 255; i++) {
41
+ GF_EXP[i] = x;
42
+ GF_LOG[x] = i;
43
+ x = x << 1;
44
+ if (x > 255) x ^= 0x11d;
45
+ }
46
+ for (let i = 255; i < 512; i++) {
47
+ GF_EXP[i] = GF_EXP[i - 255];
48
+ }
49
+ })();
50
+
51
+ function gfMul(x, y) {
52
+ if (x === 0 || y === 0) return 0;
53
+ return GF_EXP[(GF_LOG[x] + GF_LOG[y]) % 255];
54
+ }
55
+
56
+ function gfPow(x, p) {
57
+ return GF_EXP[(GF_LOG[x] * p) % 255];
58
+ }
59
+
60
+ function rsGeneratorPoly(count) {
61
+ let g = [1];
62
+ for (let i = 0; i < count; i++) {
63
+ const factor = [1, gfPow(2, i)];
64
+ const result = new Array(g.length + factor.length - 1).fill(0);
65
+ for (let j = 0; j < g.length; j++) {
66
+ for (let k = 0; k < factor.length; k++) {
67
+ result[j + k] ^= gfMul(g[j], factor[k]);
68
+ }
69
+ }
70
+ g = result;
71
+ }
72
+ return g;
73
+ }
74
+
75
+ function rsCalcErrorCorrection(data, eccCount) {
76
+ const gen = rsGeneratorPoly(eccCount);
77
+ const msg = [...data, ...new Array(gen.length - 1).fill(0)];
78
+ for (let i = 0; i < data.length; i++) {
79
+ const coeff = msg[i];
80
+ if (coeff !== 0) {
81
+ for (let j = 1; j < gen.length; j++) {
82
+ msg[i + j] ^= gfMul(gen[j], coeff);
83
+ }
84
+ }
85
+ }
86
+ return msg.slice(data.length);
87
+ }
88
+
89
+ // ─────────────────────────────────────────────
90
+ // QR 码版本参数(版本 1-40,纠错级别 M)
91
+ // 每项: [totalCodewords, ecCodewordsPerBlock, numBlocks, dataCodewordsPerBlock]
92
+ // ─────────────────────────────────────────────
93
+
94
+ const EC_PARAMS = {
95
+ // version: [totalCodewords, ecPerBlock, numBlocks, dataPerBlock]
96
+ 1: [26, 10, 1, 16],
97
+ 2: [44, 16, 1, 28],
98
+ 3: [70, 26, 1, 44],
99
+ 4: [100, 18, 2, 32],
100
+ 5: [134, 24, 2, 43],
101
+ 6: [172, 16, 4, 27],
102
+ 7: [196, 18, 4, 31],
103
+ 8: [242, 22, 4, 38],
104
+ 9: [292, 22, 5, 36],
105
+ 10: [346, 26, 5, 43],
106
+ 11: [404, 30, 5, 50],
107
+ 12: [466, 22, 8, 36],
108
+ 13: [532, 22, 9, 37],
109
+ 14: [581, 24, 9, 38],
110
+ 15: [655, 24, 10, 40],
111
+ 16: [733, 28, 10, 45],
112
+ 17: [815, 28, 11, 46],
113
+ 18: [901, 26, 13, 43],
114
+ 19: [991, 26, 14, 44],
115
+ 20: [1085, 26, 16, 41],
116
+ };
117
+
118
+ // 数据容量(Byte 模式,纠错级别 M)
119
+ const BYTE_CAPACITY = [
120
+ 0, 14, 26, 42, 62, 84, 106, 122, 152, 180, 213, 251, 287, 331, 362, 412, 450, 504, 560, 624, 666,
121
+ ];
122
+
123
+ function selectVersion(dataLen) {
124
+ for (let v = 1; v <= 20; v++) {
125
+ if (dataLen <= (BYTE_CAPACITY[v] || 0)) return v;
126
+ }
127
+ return 20;
128
+ }
129
+
130
+ // ─────────────────────────────────────────────
131
+ // 数据编码(Byte 模式)
132
+ // ─────────────────────────────────────────────
133
+
134
+ function encodeData(text, version) {
135
+ const bytes = stringToUtf8Bytes(text);
136
+ const bits = [];
137
+
138
+ // 模式指示符:Byte = 0100
139
+ bits.push(0, 1, 0, 0);
140
+
141
+ // 字符计数指示符
142
+ const lenBits = version <= 9 ? 8 : 16;
143
+ for (let i = lenBits - 1; i >= 0; i--) {
144
+ bits.push((bytes.length >> i) & 1);
145
+ }
146
+
147
+ // 数据位
148
+ for (const b of bytes) {
149
+ for (let i = 7; i >= 0; i--) {
150
+ bits.push((b >> i) & 1);
151
+ }
152
+ }
153
+
154
+ // 终止符(最多4个0)
155
+ const params = EC_PARAMS[version];
156
+ const totalDataBits = (params[2] * params[3] + (params[2] > 1 ? 0 : 0)) * 8;
157
+ // 简化:用总数据码字 * 8
158
+ const dataCodewords = Math.floor(params[0] * params[3] / (params[3] + params[1]));
159
+ const totalBits = dataCodewords * 8;
160
+
161
+ for (let i = 0; i < 4 && bits.length < totalBits; i++) bits.push(0);
162
+
163
+ // 字节对齐
164
+ while (bits.length % 8) bits.push(0);
165
+
166
+ // 转码字
167
+ const codewords = [];
168
+ for (let i = 0; i < bits.length; i += 8) {
169
+ let val = 0;
170
+ for (let j = 0; j < 8; j++) val = (val << 1) | (bits[i + j] || 0);
171
+ codewords.push(val);
172
+ }
173
+
174
+ // 填充码字
175
+ const pad = [0xec, 0x11];
176
+ while (codewords.length < dataCodewords) {
177
+ codewords.push(pad[codewords.length % 2]);
178
+ }
179
+
180
+ return codewords;
181
+ }
182
+
183
+ // ─────────────────────────────────────────────
184
+ // 矩阵操作
185
+ // ─────────────────────────────────────────────
186
+
187
+ function createMatrix(size) {
188
+ const mat = [];
189
+ for (let i = 0; i < size; i++) {
190
+ mat.push(new Array(size).fill(-1)); // -1 = 未占用
191
+ }
192
+ return mat;
193
+ }
194
+
195
+ // 放置 Finder Pattern(7x7 定位图案 + 1格间隔带)
196
+ function placeFinder(mat, row, col) {
197
+ const pattern = [
198
+ [1, 1, 1, 1, 1, 1, 1],
199
+ [1, 0, 0, 0, 0, 0, 1],
200
+ [1, 0, 1, 1, 1, 0, 1],
201
+ [1, 0, 1, 1, 1, 0, 1],
202
+ [1, 0, 1, 1, 1, 0, 1],
203
+ [1, 0, 0, 0, 0, 0, 1],
204
+ [1, 1, 1, 1, 1, 1, 1],
205
+ ];
206
+ for (let r = -1; r <= 7; r++) {
207
+ for (let c = -1; c <= 7; c++) {
208
+ const mr = row + r;
209
+ const mc = col + c;
210
+ if (mr < 0 || mc < 0 || mr >= mat.length || mc >= mat.length) continue;
211
+ if (r >= 0 && r <= 6 && c >= 0 && c <= 6) {
212
+ mat[mr][mc] = pattern[r][c];
213
+ } else {
214
+ mat[mr][mc] = 0; // 间隔带为白
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ // 对齐图案位置表
221
+ const ALIGNMENT_POSITIONS = [
222
+ null,
223
+ null, // v1
224
+ [6, 18], // v2
225
+ [6, 22], // v3
226
+ [6, 26], // v4
227
+ [6, 30], // v5
228
+ [6, 34], // v6
229
+ [6, 22, 38], // v7
230
+ [6, 24, 42], // v8
231
+ [6, 26, 46], // v9
232
+ [6, 28, 50], // v10
233
+ [6, 30, 54], // v11
234
+ [6, 32, 58], // v12
235
+ [6, 34, 62], // v13
236
+ [6, 26, 46, 66], // v14
237
+ [6, 26, 48, 70], // v15
238
+ [6, 26, 50, 74], // v16
239
+ [6, 30, 54, 78], // v17
240
+ [6, 30, 56, 82], // v18
241
+ [6, 30, 58, 86], // v19
242
+ [6, 34, 62, 90], // v20
243
+ ];
244
+
245
+ function placeAlignment(mat, version) {
246
+ if (version < 2) return;
247
+ const positions = ALIGNMENT_POSITIONS[version];
248
+ if (!positions) return;
249
+
250
+ for (const r of positions) {
251
+ for (const c of positions) {
252
+ // 跳过与 finder 重叠的位置
253
+ if (mat[r][c] !== -1) continue;
254
+ for (let dr = -2; dr <= 2; dr++) {
255
+ for (let dc = -2; dc <= 2; dc++) {
256
+ const val = Math.abs(dr) === 2 || Math.abs(dc) === 2 || (dr === 0 && dc === 0) ? 1 : 0;
257
+ mat[r + dr][c + dc] = val;
258
+ }
259
+ }
260
+ }
261
+ }
262
+ }
263
+
264
+ function placeTiming(mat, size) {
265
+ for (let i = 8; i < size - 8; i++) {
266
+ if (mat[6][i] === -1) mat[6][i] = i % 2 === 0 ? 1 : 0;
267
+ if (mat[i][6] === -1) mat[i][6] = i % 2 === 0 ? 1 : 0;
268
+ }
269
+ }
270
+
271
+ function placeDarkModule(mat, version) {
272
+ mat[4 * version + 9][8] = 1;
273
+ }
274
+
275
+ // 格式信息编码
276
+ function placeFormatInfo(mat, size, maskPattern) {
277
+ // 纠错级别 M = 00
278
+ const ecl = 0b00;
279
+ let data = (ecl << 3) | maskPattern;
280
+ let rem = data;
281
+ for (let i = 0; i < 10; i++) {
282
+ rem = (rem << 1) ^ ((rem >> 9) * 0b10100110111);
283
+ }
284
+ const bits = ((data << 10) | rem) ^ 0b101010000010010;
285
+
286
+ // 第一组位置
287
+ const positions1 = [
288
+ [8, 0], [8, 1], [8, 2], [8, 3], [8, 4], [8, 5],
289
+ [8, 7], [8, 8], [7, 8], [5, 8],
290
+ [4, 8], [3, 8], [2, 8], [1, 8], [0, 8],
291
+ ];
292
+ // 第二组位置
293
+ const positions2 = [
294
+ [size - 1, 8], [size - 2, 8], [size - 3, 8], [size - 4, 8],
295
+ [size - 5, 8], [size - 6, 8], [size - 7, 8],
296
+ [8, size - 8], [8, size - 7], [8, size - 6], [8, size - 5],
297
+ [8, size - 4], [8, size - 3], [8, size - 2], [8, size - 1],
298
+ ];
299
+
300
+ for (let i = 0; i < 15; i++) {
301
+ const bit = (bits >> (14 - i)) & 1;
302
+ if (positions1[i]) mat[positions1[i][0]][positions1[i][1]] = bit;
303
+ if (positions2[i]) mat[positions2[i][0]][positions2[i][1]] = bit;
304
+ }
305
+ }
306
+
307
+ // 版本信息(版本 >= 7)
308
+ function placeVersionInfo(mat, size, version) {
309
+ if (version < 7) return;
310
+ let rem = version;
311
+ for (let i = 0; i < 12; i++) {
312
+ rem = (rem << 1) ^ ((rem >> 11) * 0b1111100100101);
313
+ }
314
+ const bits = (version << 12) | rem;
315
+
316
+ for (let i = 0; i < 18; i++) {
317
+ const bit = (bits >> i) & 1;
318
+ const r = Math.floor(i / 3);
319
+ const c = size - 11 + (i % 3);
320
+ mat[r][c] = bit;
321
+ mat[c][r] = bit;
322
+ }
323
+ }
324
+
325
+ // 放置数据位
326
+ function placeDataBits(mat, size, dataBits) {
327
+ let bitIdx = 0;
328
+ let upward = true;
329
+
330
+ for (let colPair = size - 1; colPair >= 1; colPair -= 2) {
331
+ if (colPair === 6) colPair = 5; // 跳过时序列列
332
+
333
+ for (let rowOffset = 0; rowOffset < size; rowOffset++) {
334
+ const row = upward ? size - 1 - rowOffset : rowOffset;
335
+
336
+ for (let colOffset = 0; colOffset <= 1; colOffset++) {
337
+ const col = colPair - colOffset;
338
+ if (mat[row][col] !== -1) continue; // 跳过已占用的功能区域
339
+ mat[row][col] = bitIdx < dataBits.length ? dataBits[bitIdx++] : 0;
340
+ }
341
+ }
342
+ upward = !upward;
343
+ }
344
+ }
345
+
346
+ // ─────────────────────────────────────────────
347
+ // 掩码评估与应用
348
+ // ─────────────────────────────────────────────
349
+
350
+ const MASK_FNS = [
351
+ (r, c) => (r + c) % 2 === 0,
352
+ (r, c) => r % 2 === 0,
353
+ (r, c) => c % 3 === 0,
354
+ (r, c) => (r + c) % 3 === 0,
355
+ (r, c) => (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0,
356
+ (r, c) => ((r * c) % 2 + (r * c) % 3) === 0,
357
+ (r, c) => ((r * c) % 2 + (r * c) % 3) % 2 === 0,
358
+ (r, c) => ((r + c) % 2 + (r * c) % 3) % 2 === 0,
359
+ ];
360
+
361
+ // 只对数据区域应用掩码(功能区域保持不变)
362
+ function applyMask(mat, size, maskPattern) {
363
+ const fn = MASK_FNS[maskPattern];
364
+ const result = mat.map(row => [...row]);
365
+
366
+ for (let r = 0; r < size; r++) {
367
+ for (let c = 0; c < size; c++) {
368
+ if (isDataArea(size, r, c)) {
369
+ result[r][c] = fn(r, c) ? (mat[r][c] ^ 1) : mat[r][c];
370
+ }
371
+ }
372
+ }
373
+ return result;
374
+ }
375
+
376
+ // 判断是否为数据区域(非功能区域)
377
+ function isDataArea(size, r, c) {
378
+ // Finder + 分隔带区域
379
+ if (r <= 8 && c <= 8) return false; // 左上
380
+ if (r <= 8 && c >= size - 8) return false; // 右上
381
+ if (r >= size - 8 && c <= 8) return false; // 左下
382
+
383
+ // 时序图案
384
+ if (r === 6 || c === 6) return false;
385
+
386
+ // 暗模块
387
+ if (r === size - 8 && c === 8) return false;
388
+
389
+ return true;
390
+ }
391
+
392
+ // 评估掩码惩罚分(简化版,选择惩罚最小的掩码)
393
+ function evaluatePenalty(mat, size) {
394
+ let penalty = 0;
395
+
396
+ // 规则1:同行/列连续同色5个以上
397
+ for (let r = 0; r < size; r++) {
398
+ let count = 1;
399
+ for (let c = 1; c < size; c++) {
400
+ if (mat[r][c] === mat[r][c - 1]) {
401
+ count++;
402
+ } else {
403
+ if (count >= 5) penalty += count - 2;
404
+ count = 1;
405
+ }
406
+ }
407
+ if (count >= 5) penalty += count - 2;
408
+ }
409
+ for (let c = 0; c < size; c++) {
410
+ let count = 1;
411
+ for (let r = 1; r < size; r++) {
412
+ if (mat[r][c] === mat[r - 1][c]) {
413
+ count++;
414
+ } else {
415
+ if (count >= 5) penalty += count - 2;
416
+ count = 1;
417
+ }
418
+ }
419
+ if (count >= 5) penalty += count - 2;
420
+ }
421
+
422
+ // 规则2:2x2同色块
423
+ for (let r = 0; r < size - 1; r++) {
424
+ for (let c = 0; c < size - 1; c++) {
425
+ const v = mat[r][c];
426
+ if (v === mat[r][c + 1] && v === mat[r + 1][c] && v === mat[r + 1][c + 1]) {
427
+ penalty += 3;
428
+ }
429
+ }
430
+ }
431
+
432
+ return penalty;
433
+ }
434
+
435
+ // ─────────────────────────────────────────────
436
+ // 核心:生成 QR 矩阵
437
+ // ─────────────────────────────────────────────
438
+
439
+ /**
440
+ * 生成 QR 码模块矩阵
441
+ * @param {string} text 要编码的文本
442
+ * @returns {number[][]} 二维数组,1=黑,0=白
443
+ */
444
+ export function generateQRMatrix(text) {
445
+ const byteLen = stringToUtf8Bytes(text).length;
446
+ if (byteLen > BYTE_CAPACITY[20]) {
447
+ throw new Error(`[qrcodeGenerator] 数据过长(${byteLen}字节),最大支持${BYTE_CAPACITY[20]}字节`);
448
+ }
449
+ const version = selectVersion(byteLen);
450
+ const size = 17 + 4 * version;
451
+
452
+ // 编码数据 + 纠错
453
+ const codewords = encodeData(text, version);
454
+ const params = EC_PARAMS[version];
455
+ const numBlocks = params[2];
456
+ const ecPerBlock = params[1];
457
+
458
+ // 分块计算纠错码
459
+ const dataPerBlock = params[3];
460
+ const shortBlockLen = Math.floor(codewords.length / numBlocks);
461
+ const longBlocks = codewords.length % numBlocks;
462
+
463
+ const blocks = [];
464
+ const ecBlocks = [];
465
+ let offset = 0;
466
+ for (let i = 0; i < numBlocks; i++) {
467
+ const blockLen = shortBlockLen + (i >= numBlocks - longBlocks ? 1 : 0);
468
+ const blockData = codewords.slice(offset, offset + blockLen);
469
+ offset += blockLen;
470
+ blocks.push(blockData);
471
+ ecBlocks.push(rsCalcErrorCorrection(blockData, ecPerBlock));
472
+ }
473
+
474
+ // 交错排列数据和纠错码字
475
+ const interleaved = [];
476
+ const maxDataLen = Math.max(...blocks.map(b => b.length));
477
+ for (let i = 0; i < maxDataLen; i++) {
478
+ for (let j = 0; j < numBlocks; j++) {
479
+ if (i < blocks[j].length) interleaved.push(blocks[j][i]);
480
+ }
481
+ }
482
+ for (let i = 0; i < ecPerBlock; i++) {
483
+ for (let j = 0; j < numBlocks; j++) {
484
+ interleaved.push(ecBlocks[j][i]);
485
+ }
486
+ }
487
+
488
+ // 转为位流
489
+ const dataBits = [];
490
+ for (const byte of interleaved) {
491
+ for (let i = 7; i >= 0; i--) {
492
+ dataBits.push((byte >> i) & 1);
493
+ }
494
+ }
495
+
496
+ // 尝试所有8种掩码,选择惩罚最小的
497
+ let bestMask = 0;
498
+ let bestPenalty = Infinity;
499
+ let bestMatrix = null;
500
+
501
+ for (let mask = 0; mask < 8; mask++) {
502
+ const mat = createMatrix(size);
503
+
504
+ // 放置功能图案
505
+ placeFinder(mat, 0, 0);
506
+ placeFinder(mat, 0, size - 7);
507
+ placeFinder(mat, size - 7, 0);
508
+ placeAlignment(mat, version);
509
+ placeTiming(mat, size);
510
+ placeDarkModule(mat, version);
511
+ placeFormatInfo(mat, size, mask);
512
+ placeVersionInfo(mat, size, version);
513
+
514
+ // 放置数据
515
+ placeDataBits(mat, size, dataBits);
516
+
517
+ // 应用掩码(只对数据区域)
518
+ const masked = applyMask(mat, size, mask);
519
+
520
+ // 重新放置格式信息(掩码后)
521
+ placeFormatInfo(masked, size, mask);
522
+
523
+ const penalty = evaluatePenalty(masked, size);
524
+ if (penalty < bestPenalty) {
525
+ bestPenalty = penalty;
526
+ bestMask = mask;
527
+ bestMatrix = masked;
528
+ }
529
+ }
530
+
531
+ return bestMatrix;
532
+ }
533
+