Files

601 lines
18 KiB
JavaScript

const ECL = { L: 0, M: 1, Q: 2, H: 3 };
const MODE = {
Numeric: 0b0001,
Alphanumeric: 0b0010,
Byte: 0b0100,
Kanji: 0b1000
};
const ALPHANUM = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';
const CAPACITIES = [
[17, 14, 11, 7],
[32, 26, 20, 14],
[53, 42, 32, 24],
[78, 62, 46, 34],
[106, 84, 60, 44],
[134, 106, 74, 58],
[154, 122, 86, 64],
[192, 152, 108, 84],
[230, 180, 130, 98],
[271, 213, 151, 119],
[321, 251, 177, 137],
[367, 287, 203, 155],
[425, 331, 241, 177],
[458, 362, 258, 194],
[520, 412, 292, 220],
[586, 450, 322, 250],
[644, 504, 364, 280],
[718, 560, 394, 310],
[792, 624, 442, 338],
[858, 666, 482, 382],
[929, 711, 509, 403],
[1003, 779, 565, 439],
[1091, 857, 611, 461],
[1171, 911, 661, 511],
[1273, 997, 715, 535],
[1367, 1059, 751, 593],
[1465, 1125, 805, 625],
[1528, 1190, 868, 658],
[1628, 1264, 908, 698],
[1732, 1370, 982, 742],
[1840, 1452, 1030, 790],
[1952, 1538, 1112, 842],
[2068, 1628, 1168, 898],
[2188, 1722, 1228, 958],
[2303, 1809, 1283, 983],
[2431, 1911, 1351, 1051],
[2563, 1989, 1423, 1093],
[2699, 2099, 1499, 1139],
[2809, 2213, 1579, 1219],
[2953, 2331, 1663, 1273]
];
const EC_PARAMS = [
[[7, 19, 1, 0, 0], [10, 16, 1, 0, 0], [13, 13, 1, 0, 0], [17, 9, 1, 0, 0]],
[[10, 34, 1, 0, 0], [16, 28, 1, 0, 0], [22, 22, 1, 0, 0], [28, 16, 1, 0, 0]],
[[15, 55, 1, 0, 0], [26, 44, 1, 0, 0], [18, 17, 2, 0, 0], [22, 13, 2, 0, 0]],
[[20, 80, 1, 0, 0], [18, 32, 2, 0, 0], [26, 24, 2, 0, 0], [16, 9, 4, 0, 0]],
[[26, 108, 1, 0, 0], [24, 43, 2, 0, 0], [18, 15, 2, 16, 2], [22, 11, 2, 12, 2]],
[[18, 68, 2, 0, 0], [16, 27, 4, 0, 0], [24, 19, 4, 0, 0], [28, 15, 4, 0, 0]],
[[20, 78, 2, 0, 0], [18, 31, 4, 0, 0], [18, 14, 2, 15, 4], [26, 13, 4, 14, 1]],
[[24, 97, 2, 0, 0], [22, 38, 2, 39, 2], [22, 18, 4, 19, 2], [26, 14, 4, 15, 2]],
[[30, 116, 2, 0, 0], [22, 36, 3, 37, 2], [20, 16, 4, 17, 4], [24, 12, 4, 13, 4]],
[[18, 68, 2, 69, 2], [26, 43, 4, 44, 1], [24, 19, 6, 20, 2], [28, 15, 6, 16, 2]],
[[20, 81, 4, 0, 0], [30, 50, 1, 51, 4], [28, 22, 4, 23, 4], [24, 12, 3, 13, 8]],
[[24, 92, 2, 93, 2], [22, 36, 6, 37, 2], [26, 20, 4, 21, 6], [28, 14, 7, 15, 4]],
[[26, 107, 4, 0, 0], [22, 37, 8, 38, 1], [24, 20, 8, 21, 4], [22, 11, 12, 12, 4]],
[[30, 115, 3, 116, 1], [24, 40, 4, 41, 5], [20, 16, 11, 17, 5], [24, 12, 11, 13, 5]],
[[22, 87, 5, 88, 1], [24, 41, 5, 42, 5], [30, 24, 5, 25, 7], [24, 12, 11, 13, 7]],
[[24, 98, 5, 99, 1], [28, 45, 7, 46, 3], [24, 19, 15, 20, 2], [30, 15, 3, 16, 13]],
[[28, 107, 1, 108, 5], [28, 46, 10, 47, 1], [28, 22, 1, 23, 15], [28, 14, 2, 15, 17]],
[[30, 120, 5, 121, 1], [26, 43, 9, 44, 4], [28, 22, 17, 23, 1], [28, 14, 2, 15, 19]],
[[28, 113, 3, 114, 4], [26, 44, 3, 45, 11], [26, 21, 17, 22, 4], [26, 13, 9, 14, 16]],
[[28, 107, 3, 108, 5], [26, 41, 3, 42, 13], [30, 24, 15, 25, 5], [28, 15, 15, 16, 10]],
[[28, 116, 4, 117, 4], [26, 42, 17, 0, 0], [28, 22, 17, 23, 6], [30, 16, 19, 17, 6]],
[[28, 111, 2, 112, 7], [28, 46, 17, 0, 0], [30, 24, 7, 25, 16], [24, 13, 34, 0, 0]],
[[30, 121, 4, 122, 5], [28, 47, 4, 48, 14], [30, 24, 11, 25, 14], [30, 15, 16, 16, 14]],
[[30, 117, 6, 118, 4], [28, 45, 6, 46, 14], [30, 24, 11, 25, 16], [30, 16, 30, 17, 2]],
[[26, 106, 8, 107, 4], [28, 47, 8, 48, 13], [30, 24, 7, 25, 22], [30, 15, 22, 16, 13]],
[[28, 114, 10, 115, 2], [28, 46, 19, 47, 4], [28, 22, 28, 23, 6], [30, 16, 33, 17, 4]],
[[30, 122, 8, 123, 4], [28, 45, 22, 46, 3], [30, 23, 8, 24, 26], [30, 15, 12, 16, 28]],
[[30, 117, 3, 118, 10], [28, 45, 3, 46, 23], [30, 24, 4, 25, 31], [30, 15, 11, 16, 31]],
[[30, 116, 7, 117, 7], [28, 45, 21, 46, 7], [30, 23, 1, 24, 37], [30, 15, 19, 16, 26]],
[[30, 115, 5, 116, 10], [28, 47, 19, 48, 10], [30, 24, 15, 25, 25], [30, 15, 23, 16, 25]],
[[30, 115, 13, 116, 3], [28, 46, 2, 47, 29], [30, 24, 42, 25, 1], [30, 15, 23, 16, 28]],
[[30, 115, 17, 0, 0], [28, 46, 10, 47, 23], [30, 24, 10, 25, 35], [30, 15, 19, 16, 35]],
[[30, 115, 17, 116, 1], [28, 46, 14, 47, 21], [30, 24, 29, 25, 19], [30, 15, 11, 16, 46]],
[[30, 115, 13, 116, 6], [28, 46, 14, 47, 23], [30, 24, 44, 25, 7], [30, 16, 59, 17, 1]],
[[30, 121, 12, 122, 7], [28, 47, 12, 48, 26], [30, 24, 39, 25, 14], [30, 15, 22, 16, 41]],
[[30, 121, 6, 122, 14], [28, 47, 6, 48, 34], [30, 24, 46, 25, 10], [30, 15, 2, 16, 64]],
[[30, 122, 17, 123, 4], [28, 46, 29, 47, 14], [30, 24, 49, 25, 10], [30, 15, 24, 16, 46]],
[[30, 122, 4, 123, 18], [28, 46, 13, 47, 32], [30, 24, 48, 25, 14], [30, 15, 42, 16, 32]],
[[30, 117, 20, 118, 4], [28, 47, 40, 48, 7], [30, 24, 43, 25, 22], [30, 15, 10, 16, 67]],
[[30, 118, 19, 119, 6], [28, 47, 18, 48, 31], [30, 24, 34, 25, 34], [30, 15, 20, 16, 61]]
];
const ALIGN_POS = [
[],
[6, 18],
[6, 22],
[6, 26],
[6, 30],
[6, 34],
[6, 22, 38],
[6, 24, 42],
[6, 26, 46],
[6, 28, 50],
[6, 30, 54],
[6, 32, 58],
[6, 34, 62],
[6, 26, 46, 66],
[6, 26, 48, 70],
[6, 26, 50, 74],
[6, 30, 54, 78],
[6, 30, 56, 82],
[6, 30, 58, 86],
[6, 34, 62, 90],
[6, 28, 50, 72, 94],
[6, 26, 50, 74, 98],
[6, 30, 54, 78, 102],
[6, 28, 54, 80, 106],
[6, 32, 58, 84, 110],
[6, 30, 58, 86, 114],
[6, 34, 62, 90, 118],
[6, 26, 50, 74, 98, 122],
[6, 30, 54, 78, 102, 126],
[6, 26, 52, 78, 104, 130],
[6, 30, 56, 82, 108, 134],
[6, 34, 60, 86, 112, 138],
[6, 30, 58, 86, 114, 142],
[6, 34, 62, 90, 118, 146],
[6, 30, 54, 78, 102, 126, 150],
[6, 24, 50, 76, 102, 128, 154],
[6, 28, 54, 80, 106, 132, 158],
[6, 32, 58, 84, 110, 136, 162],
[6, 26, 54, 82, 110, 138, 166],
[6, 30, 58, 86, 114, 142, 170]
];
const FORMAT_BITS = [
0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976,
0x5412, 0x5125, 0x5e7c, 0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0,
0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183, 0x2eda, 0x2bed,
0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b
];
const VERSION_BITS = [
0x07c94, 0x085bc, 0x09a99, 0x0a4d3, 0x0bbf6, 0x0c762, 0x0d847, 0x0e60d,
0x0f928, 0x10b78, 0x1145d, 0x12a17, 0x13532, 0x149a6, 0x15683, 0x168c9,
0x177ec, 0x18ec4, 0x191e1, 0x1afab, 0x1b08e, 0x1cc1a, 0x1d33f, 0x1ed75,
0x1f250, 0x209d5, 0x216f0, 0x228ba, 0x2379f, 0x24b0b, 0x2542e, 0x26a64,
0x27541, 0x28c69
];
const EXP = new Uint8Array(512);
const LOG = new Uint8Array(256);
(() => {
let x = 1;
for (let i = 0; i < 255; i++) {
EXP[i] = x;
LOG[x] = i;
x = (x << 1) ^ (x >= 128 ? 0x11d : 0);
}
for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255];
})();
function rsEncode(data, ecLen) {
const gen = new Uint8Array(ecLen + 1);
gen[0] = 1;
for (let i = 0; i < ecLen; i++) {
for (let j = i + 1; j >= 1; j--) {
gen[j] = gen[j] ? EXP[LOG[gen[j]] + i] ^ gen[j - 1] : gen[j - 1];
}
gen[0] = EXP[LOG[gen[0]] + i];
}
const result = new Uint8Array(ecLen);
for (let i = 0; i < data.length; i++) {
const coef = data[i] ^ result[0];
result.copyWithin(0, 1);
result[ecLen - 1] = 0;
if (coef) {
for (let j = 0; j < ecLen; j++) {
result[j] ^= EXP[LOG[gen[ecLen - 1 - j]] + LOG[coef]];
}
}
}
return result;
}
function getMode(text) {
if (/^\d+$/.test(text)) return MODE.Numeric;
if (/^[0-9A-Z $%*+\-./:]+$/.test(text)) return MODE.Alphanumeric;
return MODE.Byte;
}
function getCharCountBits(ver, mode) {
const idx = ver < 10 ? 0 : ver < 27 ? 1 : 2;
return [[10, 9, 8, 8], [12, 11, 16, 10], [14, 13, 16, 12]][idx][[MODE.Numeric, MODE.Alphanumeric, MODE.Byte, MODE.Kanji].indexOf(mode)];
}
function toUtf8(str) {
const bytes = [];
for (let i = 0; i < str.length; i++) {
let c = str.charCodeAt(i);
if (c < 0x80) {
bytes.push(c);
} else if (c < 0x800) {
bytes.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f));
} else if (c >= 0xd800 && c < 0xdc00 && i + 1 < str.length) {
const c2 = str.charCodeAt(++i);
c = 0x10000 + ((c & 0x3ff) << 10) + (c2 & 0x3ff);
bytes.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
} else {
bytes.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
}
}
return bytes;
}
function getMinVersion(text, ecl) {
const mode = getMode(text);
const len = mode === MODE.Byte ? toUtf8(text).length : text.length;
for (let v = 1; v <= 40; v++) {
if (len <= CAPACITIES[v - 1][ecl]) return v;
}
return -1;
}
function encodeData(text, ver, ecl) {
const mode = getMode(text);
const bits = [];
const write = (val, len) => {
for (let i = len - 1; i >= 0; i--) bits.push((val >> i) & 1);
};
write(mode, 4);
const utf8 = mode === MODE.Byte ? toUtf8(text) : null;
const charCount = utf8 ? utf8.length : text.length;
write(charCount, getCharCountBits(ver, mode));
if (mode === MODE.Numeric) {
for (let i = 0; i < text.length; i += 3) {
const chunk = text.substr(i, 3);
write(parseInt(chunk, 10), chunk.length * 3 + 1);
}
} else if (mode === MODE.Alphanumeric) {
for (let i = 0; i < text.length; i += 2) {
if (i + 1 < text.length) {
write(ALPHANUM.indexOf(text[i]) * 45 + ALPHANUM.indexOf(text[i + 1]), 11);
} else {
write(ALPHANUM.indexOf(text[i]), 6);
}
}
} else {
for (const b of utf8) write(b, 8);
}
const params = EC_PARAMS[ver - 1][ecl];
const [ecPerBlock, dc1, bc1, dc2, bc2] = params;
const totalDC = dc1 * bc1 + dc2 * bc2;
const capacity = totalDC * 8;
const termLen = Math.min(4, capacity - bits.length);
for (let i = 0; i < termLen; i++) bits.push(0);
while (bits.length % 8) bits.push(0);
const pads = [0xec, 0x11];
let padIdx = 0;
while (bits.length < capacity) {
write(pads[padIdx++ % 2], 8);
}
const bytes = [];
for (let i = 0; i < bits.length; i += 8) {
let b = 0;
for (let j = 0; j < 8; j++) b = (b << 1) | bits[i + j];
bytes.push(b);
}
const blocks = [];
const ecBlocks = [];
let offset = 0;
for (let i = 0; i < bc1; i++) {
const block = bytes.slice(offset, offset + dc1);
blocks.push(block);
ecBlocks.push(rsEncode(new Uint8Array(block), ecPerBlock));
offset += dc1;
}
for (let i = 0; i < bc2; i++) {
const block = bytes.slice(offset, offset + dc2);
blocks.push(block);
ecBlocks.push(rsEncode(new Uint8Array(block), ecPerBlock));
offset += dc2;
}
const result = [];
const maxDC = Math.max(dc1, dc2);
for (let i = 0; i < maxDC; i++) {
for (const block of blocks) {
if (i < block.length) result.push(block[i]);
}
}
for (let i = 0; i < ecPerBlock; i++) {
for (const ec of ecBlocks) {
result.push(ec[i]);
}
}
return result;
}
function createMatrix(ver) {
const size = ver * 4 + 17;
const matrix = [];
const reserved = [];
for (let i = 0; i < size; i++) {
matrix.push(new Array(size).fill(0));
reserved.push(new Array(size).fill(false));
}
const mark = (r, c) => {
if (r >= 0 && r < size && c >= 0 && c < size) reserved[r][c] = true;
};
const placeFinder = (r, c) => {
for (let dr = -1; dr <= 7; dr++) {
for (let dc = -1; dc <= 7; dc++) {
const nr = r + dr;
const nc = c + dc;
if (nr < 0 || nr >= size || nc < 0 || nc >= size) continue;
mark(nr, nc);
if (dr >= 0 && dr <= 6 && dc >= 0 && dc <= 6) {
const isBlack = dr === 0 || dr === 6 || dc === 0 || dc === 6 || (dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4);
matrix[nr][nc] = isBlack ? 1 : 0;
} else {
matrix[nr][nc] = 0;
}
}
}
};
placeFinder(0, 0);
placeFinder(0, size - 7);
placeFinder(size - 7, 0);
if (ver >= 2) {
const positions = ALIGN_POS[ver - 1];
for (const r of positions) {
for (const c of positions) {
if (reserved[r][c]) continue;
for (let dr = -2; dr <= 2; dr++) {
for (let dc = -2; dc <= 2; dc++) {
mark(r + dr, c + dc);
const isBlack = Math.abs(dr) === 2 || Math.abs(dc) === 2 || (dr === 0 && dc === 0);
matrix[r + dr][c + dc] = isBlack ? 1 : 0;
}
}
}
}
}
for (let i = 8; i < size - 8; i++) {
const v = i % 2 === 0 ? 1 : 0;
if (!reserved[6][i]) {
matrix[6][i] = v;
mark(6, i);
}
if (!reserved[i][6]) {
matrix[i][6] = v;
mark(i, 6);
}
}
matrix[size - 8][8] = 1;
mark(size - 8, 8);
for (let i = 0; i < 9; i++) {
mark(8, i);
mark(i, 8);
}
for (let i = 0; i < 8; i++) {
mark(8, size - 1 - i);
mark(size - 1 - i, 8);
}
if (ver >= 7) {
for (let i = 0; i < 6; i++) {
for (let j = 0; j < 3; j++) {
mark(i, size - 11 + j);
mark(size - 11 + j, i);
}
}
}
return { matrix, reserved, size };
}
function placeData(matrix, reserved, data) {
const size = matrix.length;
let bitIdx = 0;
let upward = true;
for (let col = size - 1; col >= 1; col -= 2) {
if (col === 6) col = 5;
for (let i = 0; i < size; i++) {
const row = upward ? size - 1 - i : i;
for (let dc = 0; dc < 2; dc++) {
const c = col - dc;
if (!reserved[row][c]) {
const bit = bitIdx < data.length * 8 ? (data[Math.floor(bitIdx / 8)] >> (7 - bitIdx % 8)) & 1 : 0;
matrix[row][c] = bit;
bitIdx++;
}
}
}
upward = !upward;
}
}
function applyMask(matrix, reserved, mask) {
const size = matrix.length;
const result = matrix.map((row) => [...row]);
const masks = [
(r, c) => (r + c) % 2 === 0,
(r, c) => r % 2 === 0,
(r, c) => c % 3 === 0,
(r, c) => (r + c) % 3 === 0,
(r, c) => (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0,
(r, c) => (r * c) % 2 + (r * c) % 3 === 0,
(r, c) => ((r * c) % 2 + (r * c) % 3) % 2 === 0,
(r, c) => ((r + c) % 2 + (r * c) % 3) % 2 === 0
];
for (let r = 0; r < size; r++) {
for (let c = 0; c < size; c++) {
if (!reserved[r][c] && masks[mask](r, c)) {
result[r][c] ^= 1;
}
}
}
return result;
}
function placeFormatInfo(matrix, ecl, mask) {
const size = matrix.length;
const bits = FORMAT_BITS[ecl * 8 + mask];
for (let i = 0; i <= 5; i++) matrix[8][i] = (bits >> (14 - i)) & 1;
matrix[8][7] = (bits >> 8) & 1;
matrix[8][8] = (bits >> 7) & 1;
matrix[7][8] = (bits >> 6) & 1;
for (let i = 0; i <= 5; i++) matrix[i][8] = (bits >> i) & 1;
for (let i = 0; i <= 7; i++) matrix[8][size - 1 - i] = (bits >> i) & 1;
for (let i = 0; i <= 6; i++) matrix[size - 1 - i][8] = (bits >> (14 - i)) & 1;
}
function placeVersionInfo(matrix, ver) {
if (ver < 7) return;
const size = matrix.length;
const bits = VERSION_BITS[ver - 7];
for (let i = 0; i < 6; i++) {
for (let j = 0; j < 3; j++) {
const bit = (bits >> (i * 3 + j)) & 1;
matrix[i][size - 11 + j] = bit;
matrix[size - 11 + j][i] = bit;
}
}
}
function calcPenalty(matrix) {
const size = matrix.length;
let penalty = 0;
for (let r = 0; r < size; r++) {
let cnt = 1;
for (let c = 1; c < size; c++) {
if (matrix[r][c] === matrix[r][c - 1]) cnt++;
else {
if (cnt >= 5) penalty += cnt - 2;
cnt = 1;
}
}
if (cnt >= 5) penalty += cnt - 2;
}
for (let c = 0; c < size; c++) {
let cnt = 1;
for (let r = 1; r < size; r++) {
if (matrix[r][c] === matrix[r - 1][c]) cnt++;
else {
if (cnt >= 5) penalty += cnt - 2;
cnt = 1;
}
}
if (cnt >= 5) penalty += cnt - 2;
}
for (let r = 0; r < size - 1; r++) {
for (let c = 0; c < size - 1; c++) {
const v = matrix[r][c];
if (v === matrix[r][c + 1] && v === matrix[r + 1][c] && v === matrix[r + 1][c + 1]) {
penalty += 3;
}
}
}
const p1 = [1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0];
const p2 = [0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1];
for (let r = 0; r < size; r++) {
for (let c = 0; c <= size - 11; c++) {
let m1 = true;
let m2 = true;
for (let i = 0; i < 11; i++) {
if (matrix[r][c + i] !== p1[i]) m1 = false;
if (matrix[r][c + i] !== p2[i]) m2 = false;
}
if (m1 || m2) penalty += 40;
}
}
for (let c = 0; c < size; c++) {
for (let r = 0; r <= size - 11; r++) {
let m1 = true;
let m2 = true;
for (let i = 0; i < 11; i++) {
if (matrix[r + i][c] !== p1[i]) m1 = false;
if (matrix[r + i][c] !== p2[i]) m2 = false;
}
if (m1 || m2) penalty += 40;
}
}
let dark = 0;
for (let r = 0; r < size; r++) {
for (let c = 0; c < size; c++) {
if (matrix[r][c]) dark++;
}
}
const ratio = dark / (size * size);
penalty += Math.floor(Math.abs(ratio - 0.5) / 0.05) * 10;
return penalty;
}
function selectMask(matrix, reserved, ecl, ver) {
let bestMask = 0;
let bestPenalty = Infinity;
for (let mask = 0; mask < 8; mask++) {
const masked = applyMask(matrix, reserved, mask);
placeFormatInfo(masked, ecl, mask);
placeVersionInfo(masked, ver);
const p = calcPenalty(masked);
if (p < bestPenalty) {
bestPenalty = p;
bestMask = mask;
}
}
return bestMask;
}
function generate(text, options = {}) {
const eclName = String(options.errorCorrectionLevel || 'M').toUpperCase();
const ecl = ECL[eclName] !== undefined ? ECL[eclName] : ECL.M;
let ver = options.version || getMinVersion(text, ecl);
if (ver < 1) throw new Error('数据过长');
if (ver > 40) ver = 40;
const data = encodeData(text, ver, ecl);
const { matrix, reserved, size } = createMatrix(ver);
placeData(matrix, reserved, data);
const mask = selectMask(matrix, reserved, ecl, ver);
const final = applyMask(matrix, reserved, mask);
placeFormatInfo(final, ecl, mask);
placeVersionInfo(final, ver);
return {
version: ver,
size,
modules: final,
errorCorrectionLevel: eclName
};
}
export default { generate, ECL, MODE };
export { generate, ECL, MODE };