UNPKG

gtht-miniapp-sdk

Version:

gtht-miniapp-sdk 是一套基于 Uniapp + Vue3 框架开发的兼容多端的 UI 组件库

617 lines (616 loc) 20.6 kB
/* |------------------------------------------------------------------------------- | Error Correction Coding |------------------------------------------------------------------------------- | https://dev.to/maxart2501/let-s-develop-a-qr-code-generator-part-iii-error-correction-1kbm | */ const LOG = new Uint8Array(256); const EXP = new Uint8Array(256); (() => { for (let exponent = 1, value = 1; exponent < 256; exponent++) { value = value > 127 ? (value << 1) ^ 285 : value << 1; LOG[value] = exponent % 255; EXP[exponent % 255] = value; } })(); function mul(a, b) { return a && b ? EXP[(LOG[a] + LOG[b]) % 255] : 0; } function div(a, b) { return EXP[(LOG[a] + LOG[b] * 254) % 255]; } function polyMul(poly1, poly2) { const coeffs = new Uint8Array(poly1.length + poly2.length - 1); for (let index = 0; index < coeffs.length; index++) { let coeff = 0; for (let p1index = 0; p1index <= index; p1index++) { const p2index = index - p1index; coeff ^= mul(poly1[p1index], poly2[p2index]); } coeffs[index] = coeff; } return coeffs; } function polyRest(dividend, divisor) { const quotientLength = dividend.length - divisor.length + 1; let rest = new Uint8Array(dividend); for (let count = 0; count < quotientLength; count++) { if (rest[0]) { const factor = div(rest[0], divisor[0]); const subtr = new Uint8Array(rest.length); subtr.set(polyMul(divisor, new Uint8Array([factor])), 0); rest = rest.map((value, index) => value ^ subtr[index]).slice(1); } else { rest = rest.slice(1); } } return rest; } const cacheGeneratorPoly = {}; function getGeneratorPoly(degree) { if (cacheGeneratorPoly[degree]) { return cacheGeneratorPoly[degree]; } let lastPoly = new Uint8Array([1]); for (let index = 0; index < degree; index++) { lastPoly = polyMul(lastPoly, new Uint8Array([1, EXP[index]])); } return (cacheGeneratorPoly[degree] = lastPoly); } function getECC(data, degree) { const messagePoly = new Uint8Array(data.length + degree); messagePoly.set(data, 0); return polyRest(messagePoly, getGeneratorPoly(degree)); } /* |------------------------------------------------------------------------------- | QR Code Generator |------------------------------------------------------------------------------- | https://www.thonky.com/qr-code-tutorial/ | */ const ECCodewordsPerBlock = [ [ 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, ], [ 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, ], [ 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, ], [ 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, ], ]; const ECBlocks = [ [ 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25, ], [ 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49, ], [ 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68, ], [ 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81, ], ]; function appendBits(num, length, bits) { for (let i = length - 1; i >= 0; i--) { bits.push((num >>> i) & 1); } return bits; } var Mode; (function (Mode) { Mode[Mode["Numeric"] = 0] = "Numeric"; Mode[Mode["Alphanumeric"] = 1] = "Alphanumeric"; Mode[Mode["Byte"] = 2] = "Byte"; })(Mode || (Mode = {})); function getMode(text) { if (/^[0-9]*$/.test(text)) { return Mode.Numeric; } if (/^[0-9A-Z $%*+-./:]*$/.test(text)) { return Mode.Alphanumeric; } return Mode.Byte; } function getModeIndicator(mode) { switch (mode) { case Mode.Numeric: return 0b0001; case Mode.Alphanumeric: return 0b0010; default: return 0b0100; } } function encodeNumeric(text) { const bits = []; (text.match(/.{1,3}/g) || []).forEach((item) => { appendBits(+item, (item.length - 1) * 3 + 4, bits); }); return bits; } export const ALPHANUMERIC_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'; function encodeAlphaNumeric(text) { const bits = []; (text.match(/.{1,2}/g) || []).forEach((item) => { const i1 = ALPHANUMERIC_CHARS.indexOf(item[0]); const i2 = ALPHANUMERIC_CHARS.indexOf(item[1]); if (i2 === -1) { appendBits(i1, 6, bits); } else { appendBits(i1 * 45 + i2, 11, bits); } }); return bits; } function toUtf8ByteArray(text) { text = encodeURI(text); const bytes = []; for (let i = 0; i < text.length; i++) { if (text.charAt(i) !== '%') { bytes.push(text.charCodeAt(i)); } else { bytes.push(parseInt(text.slice(i + 1, i + 3), 16)); i += 2; } } return bytes; } function encodeByte(text) { const bits = []; toUtf8ByteArray(text).forEach((item) => { appendBits(item, 8, bits); }); return bits; } function encodeData(mode, text) { switch (mode) { case Mode.Numeric: return encodeNumeric(text); case Mode.Alphanumeric: return encodeAlphaNumeric(text); default: return encodeByte(text); } } function getFunctionModules(version) { let count = 225 + 8 * version; if (version > 6) { count += 36; } if (version > 1) { const alignCount = ~~(version / 7) + 2; count += (25 * alignCount - 10) * alignCount - 55; } return count; } function getSize(version) { return version * 4 + 17; } function getTotalDataModules(version) { return Math.pow(getSize(version), 2) - getFunctionModules(version); } function getECCodewords(eclIndex, version) { const codewords = ECCodewordsPerBlock[eclIndex][version - 1]; const blocks = ECBlocks[eclIndex][version - 1]; return codewords * blocks; } function getRawDataCodewords(eclIndex, version) { return getTotalDataModules(version) / 8 - getECCodewords(eclIndex, version); } const charCountIndicatorTable = { [Mode.Numeric]: [10, 12, 14], [Mode.Alphanumeric]: [9, 11, 13], [Mode.Byte]: [8, 16, 16], }; function getCharCountIndicator(mode, version) { return charCountIndicatorTable[mode][~~((version + 7) / 17)]; } function getVersion(mode, eclIndex, codewords) { for (let version = 1; version <= 40; version++) { const rawDataCodewords = getRawDataCodewords(eclIndex, version); if (rawDataCodewords >= codewords + (4 + getCharCountIndicator(mode, version)) / 8) { return version; } } return 40; } function addFinderPatterns(map, size) { for (let i = 0; i < 3; i++) { const originX = i === 1 ? size - 7 : 0; const originY = i === 2 ? size - 7 : 0; for (let y = 0; y < 7; y++) { for (let x = 0; x < 7; x++) { const bit = x === 0 || x === 6 || y === 0 || y === 6 || (x > 1 && x < 5 && y > 1 && y < 5) ? 1 : 0; map[originY + y][originX + x] = bit; } } } } function addSeparatorPatterns(map, size) { for (let i = 0; i < 8; i++) { map[i][7] = 0; map[i][size - 8] = 0; map[size - 8 + i][7] = 0; map[7][i] = 0; map[7][size - 8 + i] = 0; map[size - 8][i] = 0; } } function getAlignmentPatternLocations(version) { if (version == 1) { return []; } const alignCount = ~~(version / 7) + 2; const step = version === 32 ? 26 : Math.ceil((version * 4 + 4) / (alignCount * 2 - 2)) * 2; const locations = [6]; for (let location = getSize(version) - 7; locations.length < alignCount; location -= step) { locations.splice(1, 0, location); } return locations; } function addAlignmentPatterns(map, version) { const locations = getAlignmentPatternLocations(version); for (const locY of locations) { for (const locX of locations) { if (map[locY][locX] !== null) { continue; } for (let y = 0; y < 5; y++) { for (let x = 0; x < 5; x++) { map[locY - 2 + y][locX - 2 + x] = x === 0 || x === 4 || y === 0 || y === 4 || (x === 2 && y === 2) ? 1 : 0; } } } } } function addTimingPatterns(map, size) { const length = size - 16; for (let i = 0; i < length; i++) { const n = i % 2 === 0 ? 1 : 0; map[6][8 + i] = n; map[8 + i][6] = n; } } function addDarkPattern(map, size) { map[size - 8][8] = 1; } function addFormatInformationPatterns(map, size, fillBit) { for (let i = 0; i < 15; i++) { const bit = fillBit(15 - 1 - i); // 横条 map[8][i < 6 ? i : i < 7 ? i + 1 : size - 15 + i] = bit; // 竖条 map[size - 1 - (i < 7 ? i : i < 9 ? size - 16 + i : size - 15 + i)][8] = bit; } } function addVersionInformationPatterns(map, size, fillBit) { for (let i = 0; i < 18; i++) { // 左下 map[size - 11 + (i % 3)][~~(i / 3)] = fillBit(i); // 右上 map[~~(i / 3)][size - 11 + ~~(i % 3)] = fillBit(i); } } function placeDataBits(map, size, dataBits) { const length = size * size; let b = 0; for (let i = 0; i < length; i++) { const rest = ~~(i / 2) % size; const quo = ~~(i / (size * 2)); const y = quo % 2 === 0 ? size - 1 - rest : rest; let x = size - 2 - (quo * 2 - (i % 2 === 0 ? 1 : 0)); if (x < 7) { x--; } if (map[y][x] === null) { map[y][x] = dataBits[b++] + 2; } } } const maskModes = [ (row, col) => (row + col) % 2 === 0, (row) => row % 2 === 0, (_, col) => col % 3 === 0, (row, col) => (row + col) % 3 === 0, (row, col) => (~~(row / 2) + ~~(col / 3)) % 2 === 0, (row, col) => ((row * col) % 2) + ((row * col) % 3) === 0, (row, col) => (((row * col) % 2) + ((row * col) % 3)) % 2 === 0, (row, col) => (((row + col) % 2) + ((row * col) % 3)) % 2 === 0, ]; function getLeastFiveSameColoredPenalty(getEach, size) { let score = 0; for (let i = 0; i < size; i++) { let sum = 0; let last = -1; for (let j = 0; j < size; j++) { const bit = getEach(i, j); if (sum === 0 || last === bit) { sum++; last = bit; } if (last !== bit || j === size - 1) { if (sum >= 5) { score += sum - 2; } sum = 1; last = bit; } } } return score; } function getTwoByTwoSameColoredPenalty(map, size) { let score = 0; for (let y = 0; y < size - 2; y++) { for (let x = 0; x < size - 2; x++) { if ((map[y][x] + map[y][x + 1] + map[y + 1][x] + map[y + 1][x + 1]) % 4 === 0) { score += 3; } } } return score; } function getFinderLikePenalty(map, size) { let score = 0; for (let i = 0; i < size - 11; i++) { for (let j = 0; j < size - 11; j++) { let rowBits = 0; let colBits = 0; for (let k = 0; k < 11; k++) { rowBits = (rowBits << 1) + map[i][j + k]; colBits = (colBits << 1) + map[j + k][i]; } if (rowBits === 0b10111010000 || rowBits === 0b00001011101) { score += 40; } if (colBits === 0b10111010000 || colBits === 0b00001011101) { score += 40; } } } return score; } function getBalancePenalty(map) { const darkModules = map.reduce((sum, row) => sum + row.reduce((sum, bit) => sum + bit, 0), 0); const percent = (darkModules / (map.length * map.length)) * 100; const score = Math.min(Math.abs(Math.floor(percent / 5) * 5 - 50) / 5, Math.abs(Math.ceil(percent / 5) * 5 - 50) / 5) * 10; return score; } const errorCorrectionBit = [0b01, 0b00, 0b11, 0b10]; function getFormatInformationBits(eclIndex, maskNum) { const ecMaskBits = (errorCorrectionBit[eclIndex] << 3) | maskNum; let rest = ecMaskBits; for (let i = 0; i < 10; i++) { rest = (rest << 1) ^ ((rest >>> 9) * 0b10100110111); } return ((ecMaskBits << 10) | rest) ^ 0b101010000010010; } function getVersionInformationBits(version) { let rest = version; for (let i = 0; i < 12; i++) { rest = (rest << 1) ^ ((rest >>> 11) * 0b1111100100101); } return (version << 12) | rest; } export const ECLList = ['L', 'M', 'Q', 'H']; export function qrcode(text, options = {}) { const { ecl = 'M' } = options; let eclIndex = ECLList.indexOf(ecl); if (eclIndex < 0) { eclIndex = 1; } const mode = getMode(text); const modeIndicator = getModeIndicator(mode); let version = 0; let size = 0; let dataBits = []; const groups = []; let map = []; let maskNum = 0; let maskMap = []; // 数据编码 { // 使用选定模式进行编码 dataBits = encodeData(mode, text); const codewords = dataBits.length / 8; const charLength = mode === Mode.Byte ? codewords : text.length; // 确定数据的最小版本 version = getVersion(mode, eclIndex, codewords); size = getSize(version); // 添加模式指示器、字符计数指示器 dataBits = [ ...appendBits(modeIndicator, 4, []), ...appendBits(charLength, getCharCountIndicator(mode, version), []), ...dataBits, ]; // 确定此QR码所需的数据位数 const rawDataBits = getRawDataCodewords(eclIndex, version) * 8; // 如有必要,添加0结束符 appendBits(0, Math.min(rawDataBits - dataBits.length, 4), dataBits); // 添加更多的0使长度成为8的倍数 appendBits(0, (8 - (dataBits.length % 8)) % 8, dataBits); // 如果字符串仍然太短,添加填充字节 for (let padByte = 0b11101100; dataBits.length < rawDataBits; padByte ^= 0b11101100 ^ 0b00010001) { appendBits(padByte, 8, dataBits); } } // 纠错编码 { const codewords = []; dataBits.forEach((bit, i) => { codewords[i >> 3] |= bit << (7 - (i & 7)); }); const blocks = ECBlocks[eclIndex][version - 1]; const g2Blocks = ~~(getTotalDataModules(version) / 8) % blocks; const g1Blocks = blocks - g2Blocks; const totalDC = getRawDataCodewords(eclIndex, version); const g1DCPerBlock = ~~(totalDC / blocks); const g2DCPerBlock = g2Blocks === 0 ? 0 : (totalDC - g1DCPerBlock * g1Blocks) / g2Blocks; const eccNum = ECCodewordsPerBlock[eclIndex][version - 1]; let currentIndex = 0; for (const [blocks, DCPerBlock] of [ [g1Blocks, g1DCPerBlock], [g2Blocks, g2DCPerBlock], ]) { for (let b = 0; b < blocks; b++) { const dataCodewords = new Uint8Array(codewords.slice(currentIndex, currentIndex + DCPerBlock)); groups.push({ data: dataCodewords, ecc: getECC(dataCodewords, eccNum), }); currentIndex += DCPerBlock; } } } // 构造最后信息 { // 将块进行交叉 const dataCodewords = []; const ecCodewords = []; let i = 0; while (true) { const result = groups.map((block) => { const dataCodeWord = block.data[i]; if (dataCodeWord !== undefined) { dataCodewords.push(dataCodeWord); } const ecCodeWord = block.ecc[i]; if (ecCodeWord !== undefined) { ecCodewords.push(ecCodeWord); } return dataCodeWord === undefined && ecCodeWord === undefined; }); if (result.every((done) => done)) { break; } i++; } // 将交错纠错码字放在交错数据码字之后 const interleavedCodewords = dataCodewords.concat(ecCodewords); dataBits = []; interleavedCodewords.forEach((codewords) => { appendBits(codewords, 8, dataBits); }); // 如果有必要,添加剩余位 appendBits(0, getTotalDataModules(version) - dataBits.length, dataBits); } // 模块在矩阵中的放置 { map = Array(size) .fill(null) .map(() => Array(size).fill(null)); // 添加查找器图案 addFinderPatterns(map, size); // 添加分隔符 addSeparatorPatterns(map, size); // 添加对齐图案 addAlignmentPatterns(map, version); // 添加时序图案 addTimingPatterns(map, size); // 添加黑暗图案 addDarkPattern(map, size); // 添加格式信息占位图案 addFormatInformationPatterns(map, size, () => 0); // 添加版本信息占位图案 if (version >= 7) { addVersionInformationPatterns(map, size, () => 0); } // 放置数据位 placeDataBits(map, size, dataBits); } // 数据掩码 { let lowerScore = Infinity; for (let i = 0; i < 8; i++) { const maskMode = maskModes[i]; // 生成掩码图案 const seedMap = map.map((row, rowIndex) => row.map((col, colIndex) => { if (col < 2) { return col; } col = col - 2; return maskMode(rowIndex, colIndex) ? col ^ 1 : col; })); // 计算罚分 let score = 0; // 评估条件1 // 行 score += getLeastFiveSameColoredPenalty((i, j) => seedMap[i][j], size); // 列 score += getLeastFiveSameColoredPenalty((i, j) => seedMap[j][i], size); // 评估条件2 score += getTwoByTwoSameColoredPenalty(seedMap, size); // 评估条件3 score += getFinderLikePenalty(seedMap, size); // 评估条件4 score += getBalancePenalty(seedMap); // 选择八种掩码图案的最低罚分 if (score < lowerScore) { lowerScore = score; maskNum = i; maskMap = seedMap; } } } // 格式和版本信息 { // 添加格式信息图案 const formatBits = getFormatInformationBits(eclIndex, maskNum); addFormatInformationPatterns(map, size, (i) => (formatBits >> i) & 1); { // 添加版本信息图案 if (version > 6) { const versionBits = getVersionInformationBits(version); addVersionInformationPatterns(map, size, (i) => (versionBits >> i) & 1); } } } // 输出最终矩阵 { maskMap.forEach((row, y) => { row.forEach((col, x) => { const mapCol = map[y][x]; if (mapCol !== 0 && mapCol !== 1) { map[y][x] = col; } }); }); return map; } }