文件
shanghaipeiwan/pwuniapp/common/qrcode.js

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;
}