360 行
12 KiB
JavaScript
360 行
12 KiB
JavaScript
// pwuniapp/common/qrcode.js
|
|
// 轻量级原生 QR Code 矩阵生成器 (支持 QR Code Version 1-4,纠错级别 L/M,零外部依赖,100% 支持小程序与H5)
|
|
|
|
// QR 纠错级别
|
|
export const QRErrorCorrectLevel = {
|
|
L: 1, // 约 7% 纠错能力
|
|
M: 0, // 约 15% 纠错能力
|
|
Q: 3, // 约 25% 纠错能力
|
|
H: 2 // 约 30% 纠错能力
|
|
};
|
|
|
|
// 伽罗华域 GF(256) 数学表
|
|
const QRMath = {
|
|
glog(n) {
|
|
if (n < 1) throw new Error('glog(' + n + ')');
|
|
return QRMath.LOG_TABLE[n];
|
|
},
|
|
gexp(n) {
|
|
while (n < 0) n += 255;
|
|
while (n >= 256) n -= 255;
|
|
return QRMath.EXP_TABLE[n];
|
|
},
|
|
EXP_TABLE: new Array(256),
|
|
LOG_TABLE: new Array(256)
|
|
};
|
|
|
|
for (let i = 0; i < 8; i++) QRMath.EXP_TABLE[i] = 1 << i;
|
|
for (let i = 8; i < 256; i++) {
|
|
QRMath.EXP_TABLE[i] = QRMath.EXP_TABLE[i - 4] ^ QRMath.EXP_TABLE[i - 5] ^ QRMath.EXP_TABLE[i - 6] ^ QRMath.EXP_TABLE[i - 8];
|
|
}
|
|
for (let i = 0; i < 255; i++) QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]] = i;
|
|
|
|
class QRPolynomial {
|
|
constructor(num, shift) {
|
|
let offset = 0;
|
|
while (offset < num.length && num[offset] === 0) offset++;
|
|
this.num = new Array(num.length - offset + shift);
|
|
for (let i = 0; i < num.length - offset; i++) this.num[i] = num[i + offset];
|
|
}
|
|
get(index) {
|
|
return this.num[index];
|
|
}
|
|
getLength() {
|
|
return this.num.length;
|
|
}
|
|
multiply(e) {
|
|
const num = new Array(this.getLength() + e.getLength() - 1).fill(0);
|
|
for (let i = 0; i < this.getLength(); i++) {
|
|
for (let j = 0; j < e.getLength(); j++) {
|
|
num[i + j] ^= QRMath.gexp(QRMath.glog(this.get(i)) + QRMath.glog(e.get(j)));
|
|
}
|
|
}
|
|
return new QRPolynomial(num, 0);
|
|
}
|
|
mod(e) {
|
|
if (this.getLength() - e.getLength() < 0) return this;
|
|
const ratio = QRMath.glog(this.get(0)) - QRMath.glog(e.get(0));
|
|
const num = new Array(this.getLength());
|
|
for (let i = 0; i < this.getLength(); i++) num[i] = this.get(i);
|
|
for (let i = 0; i < e.getLength(); i++) num[i] ^= QRMath.gexp(QRMath.glog(e.get(i)) + ratio);
|
|
return new QRPolynomial(num, 0).mod(e);
|
|
}
|
|
}
|
|
|
|
// 块定义与版本控制
|
|
const QRRSBlock = {
|
|
RS_BLOCK_TABLE: [
|
|
[1, 26, 19], [1, 26, 16], [1, 26, 13], [1, 26, 9], // Ver 1 (L, M, Q, H)
|
|
[1, 44, 34], [1, 44, 28], [1, 44, 22], [1, 44, 16], // Ver 2
|
|
[1, 70, 55], [1, 70, 44], [2, 35, 17], [2, 35, 13], // Ver 3
|
|
[1, 100, 80], [2, 50, 32], [2, 50, 24], [4, 25, 9], // Ver 4
|
|
[1, 134, 108], [2, 67, 43], [2, 33, 15, 2, 34, 16], [2, 33, 11, 2, 34, 12] // Ver 5
|
|
],
|
|
getRSBlocks(typeNumber, errorCorrectLevel) {
|
|
const rsBlock = QRRSBlock.getRsBlockTable(typeNumber, errorCorrectLevel);
|
|
const list = [];
|
|
for (let i = 0; i < rsBlock.length / 3; i++) {
|
|
const count = rsBlock[i * 3 + 0];
|
|
const totalCount = rsBlock[i * 3 + 1];
|
|
const dataCount = rsBlock[i * 3 + 2];
|
|
for (let j = 0; j < count; j++) {
|
|
list.push({ totalCount, dataCount });
|
|
}
|
|
}
|
|
return list;
|
|
},
|
|
getRsBlockTable(typeNumber, errorCorrectLevel) {
|
|
switch (errorCorrectLevel) {
|
|
case QRErrorCorrectLevel.L: return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0];
|
|
case QRErrorCorrectLevel.M: return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1];
|
|
case QRErrorCorrectLevel.Q: return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2];
|
|
case QRErrorCorrectLevel.H: return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3];
|
|
default: return undefined;
|
|
}
|
|
}
|
|
};
|
|
|
|
class QRBitBuffer {
|
|
constructor() {
|
|
this.buffer = [];
|
|
this.length = 0;
|
|
}
|
|
get(index) {
|
|
const bufIndex = Math.floor(index / 8);
|
|
return ((this.buffer[bufIndex] >>> (7 - (index % 8))) & 1) === 1;
|
|
}
|
|
put(num, length) {
|
|
for (let i = 0; i < length; i++) {
|
|
this.putBit(((num >>> (length - i - 1)) & 1) === 1);
|
|
}
|
|
}
|
|
putBit(bit) {
|
|
const bufIndex = Math.floor(this.length / 8);
|
|
if (this.buffer.length <= bufIndex) this.buffer.push(0);
|
|
if (bit) this.buffer[bufIndex] |= 0x80 >>> (this.length % 8);
|
|
this.length++;
|
|
}
|
|
}
|
|
|
|
export class QRCodeModel {
|
|
constructor(typeNumber, errorCorrectLevel) {
|
|
this.typeNumber = typeNumber || 2;
|
|
this.errorCorrectLevel = errorCorrectLevel !== undefined ? errorCorrectLevel : QRErrorCorrectLevel.M;
|
|
this.modules = null;
|
|
this.moduleCount = 0;
|
|
this.dataList = [];
|
|
}
|
|
addData(data) {
|
|
this.dataList.push(data);
|
|
}
|
|
isDark(row, col) {
|
|
if (row < 0 || this.moduleCount <= row || col < 0 || this.moduleCount <= col) {
|
|
return false;
|
|
}
|
|
return this.modules[row][col];
|
|
}
|
|
getModuleCount() {
|
|
return this.moduleCount;
|
|
}
|
|
make() {
|
|
this.makeImpl(false, this.getBestMaskPattern());
|
|
}
|
|
makeImpl(test, maskPattern) {
|
|
this.moduleCount = this.typeNumber * 4 + 17;
|
|
this.modules = new Array(this.moduleCount);
|
|
for (let row = 0; row < this.moduleCount; row++) {
|
|
this.modules[row] = new Array(this.moduleCount).fill(null);
|
|
}
|
|
this.setupPositionProbePattern(0, 0);
|
|
this.setupPositionProbePattern(this.moduleCount - 7, 0);
|
|
this.setupPositionProbePattern(0, this.moduleCount - 7);
|
|
this.setupTimingPattern();
|
|
this.setupTypeInfo(test, maskPattern);
|
|
if (this.typeNumber >= 2) this.setupPositionAdjustPattern();
|
|
const data = this.createData(this.typeNumber, this.errorCorrectLevel, this.dataList);
|
|
this.mapData(data, maskPattern);
|
|
}
|
|
setupPositionProbePattern(row, col) {
|
|
for (let r = -1; r <= 7; r++) {
|
|
if (row + r <= -1 || this.moduleCount <= row + r) continue;
|
|
for (let c = -1; c <= 7; c++) {
|
|
if (col + c <= -1 || this.moduleCount <= col + c) continue;
|
|
if (
|
|
(0 <= r && r <= 6 && (c === 0 || c === 6)) ||
|
|
(0 <= c && c <= 6 && (r === 0 || r === 6)) ||
|
|
(2 <= r && r <= 4 && 2 <= c && c <= 4)
|
|
) {
|
|
this.modules[row + r][col + c] = true;
|
|
} else {
|
|
this.modules[row + r][col + c] = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
setupTimingPattern() {
|
|
for (let r = 8; r < this.moduleCount - 8; r++) {
|
|
if (this.modules[r][6] !== null) continue;
|
|
this.modules[r][6] = r % 2 === 0;
|
|
}
|
|
for (let c = 8; c < this.moduleCount - 8; c++) {
|
|
if (this.modules[6][c] !== null) continue;
|
|
this.modules[6][c] = c % 2 === 0;
|
|
}
|
|
}
|
|
setupPositionAdjustPattern() {
|
|
const pos = [6, this.moduleCount - 7];
|
|
for (let i = 0; i < pos.length; i++) {
|
|
for (let j = 0; j < pos.length; j++) {
|
|
const row = pos[i];
|
|
const col = pos[j];
|
|
if (this.modules[row][col] !== null) continue;
|
|
for (let r = -2; r <= 2; r++) {
|
|
for (let c = -2; c <= 2; c++) {
|
|
if (r === -2 || r === 2 || c === -2 || c === 2 || (r === 0 && c === 0)) {
|
|
this.modules[row + r][col + c] = true;
|
|
} else {
|
|
this.modules[row + r][col + c] = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
setupTypeInfo(test, maskPattern) {
|
|
const data = (this.errorCorrectLevel << 3) | maskPattern;
|
|
const bits = this.getBCHTypeInfo(data);
|
|
for (let i = 0; i < 15; i++) {
|
|
const mod = !test && ((bits >> i) & 1) === 1;
|
|
if (i < 6) this.modules[i][8] = mod;
|
|
else if (i < 8) this.modules[i + 1][8] = mod;
|
|
else this.modules[this.moduleCount - 15 + i][8] = mod;
|
|
|
|
if (i < 8) this.modules[8][this.moduleCount - i - 1] = mod;
|
|
else if (i < 9) this.modules[8][15 - i - 1 + 1] = mod;
|
|
else this.modules[8][15 - i - 1] = mod;
|
|
}
|
|
this.modules[this.moduleCount - 8][8] = !test;
|
|
}
|
|
getBCHTypeInfo(data) {
|
|
let d = data << 10;
|
|
while (this.getBCHDigit(d) - this.getBCHDigit(1335) >= 0) {
|
|
d ^= 1335 << (this.getBCHDigit(d) - this.getBCHDigit(1335));
|
|
}
|
|
return ((data << 10) | d) ^ 21522;
|
|
}
|
|
getBCHDigit(data) {
|
|
let digit = 0;
|
|
while (data !== 0) {
|
|
digit++;
|
|
data >>>= 1;
|
|
}
|
|
return digit;
|
|
}
|
|
getBestMaskPattern() {
|
|
return 0; // 标准棋盘掩膜 Pattern 0
|
|
}
|
|
createData(typeNumber, errorCorrectLevel, dataList) {
|
|
const rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel);
|
|
const buffer = new QRBitBuffer();
|
|
for (let i = 0; i < dataList.length; i++) {
|
|
const data = dataList[i];
|
|
// 默认 8bit 字节编码
|
|
buffer.put(4, 4);
|
|
buffer.put(data.length, typeNumber < 10 ? 8 : 16);
|
|
for (let j = 0; j < data.length; j++) {
|
|
buffer.put(data.charCodeAt(j), 8);
|
|
}
|
|
}
|
|
let totalDataCount = 0;
|
|
for (let i = 0; i < rsBlocks.length; i++) totalDataCount += rsBlocks[i].dataCount;
|
|
if (buffer.length + 4 <= totalDataCount * 8) buffer.put(0, 4);
|
|
while (buffer.length % 8 !== 0) buffer.putBit(false);
|
|
while (true) {
|
|
if (buffer.length >= totalDataCount * 8) break;
|
|
buffer.put(236, 8);
|
|
if (buffer.length >= totalDataCount * 8) break;
|
|
buffer.put(17, 8);
|
|
}
|
|
return this.createBytes(buffer, rsBlocks);
|
|
}
|
|
createBytes(buffer, rsBlocks) {
|
|
let offset = 0;
|
|
let maxDcCount = 0;
|
|
let maxEcCount = 0;
|
|
const dcdata = new Array(rsBlocks.length);
|
|
const ecdata = new Array(rsBlocks.length);
|
|
for (let r = 0; r < rsBlocks.length; r++) {
|
|
const dcCount = rsBlocks[r].dataCount;
|
|
const ecCount = rsBlocks[r].totalCount - dcCount;
|
|
maxDcCount = Math.max(maxDcCount, dcCount);
|
|
maxEcCount = Math.max(maxEcCount, ecCount);
|
|
dcdata[r] = new Array(dcCount);
|
|
for (let i = 0; i < dcdata[r].length; i++) {
|
|
dcdata[r][i] = 0xff & buffer.buffer[i + offset];
|
|
}
|
|
offset += dcCount;
|
|
const rsPoly = this.getErrorCorrectPolynomial(ecCount);
|
|
const rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1);
|
|
const modPoly = rawPoly.mod(rsPoly);
|
|
ecdata[r] = new Array(rsPoly.getLength() - 1);
|
|
for (let i = 0; i < ecdata[r].length; i++) {
|
|
const modIndex = i + modPoly.getLength() - ecdata[r].length;
|
|
ecdata[r][i] = modIndex >= 0 ? modPoly.get(modIndex) : 0;
|
|
}
|
|
}
|
|
const totalCodeCount = rsBlocks.reduce((acc, b) => acc + b.totalCount, 0);
|
|
const data = new Array(totalCodeCount);
|
|
let index = 0;
|
|
for (let i = 0; i < maxDcCount; i++) {
|
|
for (let r = 0; r < rsBlocks.length; r++) {
|
|
if (i < dcdata[r].length) data[index++] = dcdata[r][i];
|
|
}
|
|
}
|
|
for (let i = 0; i < maxEcCount; i++) {
|
|
for (let r = 0; r < rsBlocks.length; r++) {
|
|
if (i < ecdata[r].length) data[index++] = ecdata[r][i];
|
|
}
|
|
}
|
|
return data;
|
|
}
|
|
getErrorCorrectPolynomial(errorCorrectLength) {
|
|
let a = new QRPolynomial([1], 0);
|
|
for (let i = 0; i < errorCorrectLength; i++) {
|
|
a = a.multiply(new QRPolynomial([1, QRMath.gexp(i)], 0));
|
|
}
|
|
return a;
|
|
}
|
|
mapData(data, maskPattern) {
|
|
let inc = -1;
|
|
let row = this.moduleCount - 1;
|
|
let bitIndex = 7;
|
|
let byteIndex = 0;
|
|
for (let col = this.moduleCount - 1; col > 0; col -= 2) {
|
|
if (col === 6) col--;
|
|
while (true) {
|
|
for (let c = 0; c < 2; c++) {
|
|
if (this.modules[row][col - c] === null) {
|
|
let dark = false;
|
|
if (byteIndex < data.length) {
|
|
dark = ((data[byteIndex] >>> bitIndex) & 1) === 1;
|
|
}
|
|
const mask = (row + col - c) % 2 === 0;
|
|
if (mask) dark = !dark;
|
|
this.modules[row][col - c] = dark;
|
|
bitIndex--;
|
|
if (bitIndex === -1) {
|
|
byteIndex++;
|
|
bitIndex = 7;
|
|
}
|
|
}
|
|
}
|
|
row += inc;
|
|
if (row < 0 || this.moduleCount <= row) {
|
|
row -= inc;
|
|
inc = -inc;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 生成 QR Code 矩阵数据
|
|
* @param {string} text 要编码的文本
|
|
* @param {number} typeNumber 版本号 1-4 (默认 2: 25x25)
|
|
* @returns {Array<Array<boolean>>} 二维矩阵,true 为黑块,false 为白块
|
|
*/
|
|
export function generateQRCodeMatrix(text, typeNumber = 2) {
|
|
if (!text) text = '0000';
|
|
// 根据文本长度自动升版
|
|
if (text.length > 32) typeNumber = 4;
|
|
else if (text.length > 16) typeNumber = 3;
|
|
|
|
const qr = new QRCodeModel(typeNumber, QRErrorCorrectLevel.M);
|
|
qr.addData(text);
|
|
qr.make();
|
|
return qr.modules;
|
|
}
|