// 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>} 二维矩阵,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; }