/* ============================================================ PIX — BR Code EMV + QR Code generator (offline, no deps) - buildPixPayload(opts) → string "Copia e Cola" - generateQRMatrix(text) → array of array of 0/1 (modules) ============================================================ */ (function () { /* ---------- BR Code EMV ---------- */ // CRC16/CCITT-FALSE (polynom 0x1021, init 0xFFFF) — utilisé par Pix function crc16(str) { let crc = 0xFFFF; for (let i = 0; i < str.length; i++) { crc ^= str.charCodeAt(i) << 8; for (let j = 0; j < 8; j++) { crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) : (crc << 1); crc &= 0xFFFF; } } return crc.toString(16).toUpperCase().padStart(4, "0"); } function tlv(id, value) { const v = String(value); const len = v.length.toString().padStart(2, "0"); return id + len + v; } function clean(str, maxLen) { const s = String(str || "") .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .replace(/[^\x20-\x7E]/g, "") .toUpperCase(); return maxLen ? s.slice(0, maxLen) : s; } function buildPixPayload(opts) { const key = String(opts.key || "").trim(); const name = clean(opts.name, 25); const city = clean(opts.city, 15); const amount = opts.amount != null ? Number(opts.amount).toFixed(2) : null; const txid = clean(opts.txid || "***", 25).replace(/[^A-Z0-9]/g, "") || "***"; const description = opts.description ? clean(opts.description, 50) : null; let merchantAccount = tlv("00", "BR.GOV.BCB.PIX") + tlv("01", key); if (description) merchantAccount += tlv("02", description); const mai = tlv("26", merchantAccount); const additional = tlv("62", tlv("05", txid)); let payload = ""; payload += tlv("00", "01"); payload += tlv("01", "12"); payload += mai; payload += tlv("52", "0000"); payload += tlv("53", "986"); if (amount) payload += tlv("54", amount); payload += tlv("58", "BR"); payload += tlv("59", name); payload += tlv("60", city); payload += additional; payload += "6304"; const crc = crc16(payload); return payload + crc; } /* ---------- QR Code generator (Reed-Solomon, byte mode, ECC level M) ---------- */ /* Implémentation minimale, support versions 1-10 (capacité ~213 chars en mode byte ECC-M) */ // Galois field tables (GF(256) avec primitif 0x011D) const EXP = new Array(256); const LOG = new Array(256); (function initGF() { let x = 1; for (let i = 0; i < 255; i++) { EXP[i] = x; LOG[x] = i; x <<= 1; if (x & 0x100) x ^= 0x011D; } EXP[255] = EXP[0]; })(); function gfMul(a, b) { if (a === 0 || b === 0) return 0; return EXP[(LOG[a] + LOG[b]) % 255]; } // Génère le polynôme générateur pour `degree` bits de correction function rsGenPoly(degree) { let poly = [1]; for (let i = 0; i < degree; i++) { const next = [0]; for (let j = 0; j < poly.length; j++) next.push(0); for (let j = 0; j < poly.length; j++) { next[j] ^= poly[j]; next[j + 1] ^= gfMul(poly[j], EXP[i]); } poly = next; } return poly; } function rsEncode(data, eccLen) { const gen = rsGenPoly(eccLen); const buf = data.slice(); for (let i = 0; i < eccLen; i++) buf.push(0); for (let i = 0; i < data.length; i++) { const factor = buf[i]; if (factor !== 0) { for (let j = 0; j < gen.length; j++) { buf[i + j] ^= gfMul(gen[j], factor); } } } return buf.slice(data.length); } // Tables capacité (version, ECC level M) — data codewords / blocks / ecc per block // [version-1]: { totalCw, dataCw, eccPerBlock, blocks: [[count, dataCw], ...] } const VERSIONS_M = { 1: { dataCw: 16, eccPerBlock: 10, blocks: [[1, 16]] }, 2: { dataCw: 28, eccPerBlock: 16, blocks: [[1, 28]] }, 3: { dataCw: 44, eccPerBlock: 26, blocks: [[1, 44]] }, 4: { dataCw: 64, eccPerBlock: 18, blocks: [[2, 32]] }, 5: { dataCw: 86, eccPerBlock: 24, blocks: [[2, 43]] }, 6: { dataCw: 108, eccPerBlock: 16, blocks: [[4, 27]] }, 7: { dataCw: 124, eccPerBlock: 18, blocks: [[4, 31]] }, 8: { dataCw: 154, eccPerBlock: 22, blocks: [[2, 38], [2, 39]] }, 9: { dataCw: 182, eccPerBlock: 22, blocks: [[3, 36], [2, 37]] }, 10: { dataCw: 216, eccPerBlock: 26, blocks: [[4, 43], [1, 44]] }, }; // Capacité max en mode byte (ECC-M) pour chaque version, en chars // = (dataCw * 8 - mode(4) - charCount(16) - terminator) / 8 function byteCapacity(v) { const info = VERSIONS_M[v]; const charCountBits = v <= 9 ? 8 : 16; return Math.floor((info.dataCw * 8 - 4 - charCountBits) / 8); } function pickVersion(textLen) { for (let v = 1; v <= 10; v++) { if (byteCapacity(v) >= textLen) return v; } throw new Error("QR: text too long for version 10"); } // Bit buffer function makeBitBuf() { const bits = []; return { put(val, n) { for (let i = n - 1; i >= 0; i--) bits.push((val >> i) & 1); }, length() { return bits.length; }, toBytes() { 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] || 0); } bytes.push(b); } return bytes; }, }; } function buildDataBytes(text, version) { const info = VERSIONS_M[version]; const charCountBits = version <= 9 ? 8 : 16; const buf = makeBitBuf(); // Mode indicator: byte = 0100 buf.put(0b0100, 4); buf.put(text.length, charCountBits); // Encode each byte (UTF-8) const bytes = []; for (let i = 0; i < text.length; i++) { const c = text.charCodeAt(i); if (c < 0x80) bytes.push(c); else if (c < 0x800) { bytes.push(0xC0 | (c >> 6)); bytes.push(0x80 | (c & 0x3F)); } else { bytes.push(0xE0 | (c >> 12)); bytes.push(0x80 | ((c >> 6) & 0x3F)); bytes.push(0x80 | (c & 0x3F)); } } // (mais EMV est ASCII pur, on limite quand même à text.length) for (let i = 0; i < text.length; i++) { buf.put(text.charCodeAt(i), 8); } // Terminator (up to 4 zero bits, but no more than capacity) const totalDataBits = info.dataCw * 8; const term = Math.min(4, totalDataBits - buf.length()); buf.put(0, term); // Pad to byte boundary while (buf.length() % 8 !== 0) buf.put(0, 1); // Get bytes, then pad with 0xEC, 0x11 alternating const dataBytes = buf.toBytes(); const padPattern = [0xEC, 0x11]; let pi = 0; while (dataBytes.length < info.dataCw) { dataBytes.push(padPattern[pi % 2]); pi++; } return dataBytes; } // Découper en blocs et calculer ECC, puis interleave function buildFinalCodewords(dataBytes, version) { const info = VERSIONS_M[version]; const dataBlocks = []; const eccBlocks = []; let offset = 0; for (const [count, blockDataLen] of info.blocks) { for (let i = 0; i < count; i++) { const block = dataBytes.slice(offset, offset + blockDataLen); offset += blockDataLen; dataBlocks.push(block); eccBlocks.push(rsEncode(block, info.eccPerBlock)); } } // Interleave data const result = []; let maxData = 0; for (const b of dataBlocks) maxData = Math.max(maxData, b.length); for (let i = 0; i < maxData; i++) { for (const b of dataBlocks) if (i < b.length) result.push(b[i]); } // Interleave ecc for (let i = 0; i < info.eccPerBlock; i++) { for (const b of eccBlocks) result.push(b[i]); } return result; } /* ---------- Matrix construction ---------- */ function size(v) { return 21 + 4 * (v - 1); } function makeEmptyMatrix(v) { const n = size(v); const m = []; const reserved = []; for (let i = 0; i < n; i++) { m.push(new Array(n).fill(null)); reserved.push(new Array(n).fill(false)); } return { m, reserved, n }; } function placeFinder(mat, r, c) { for (let dr = -1; dr <= 7; dr++) { for (let dc = -1; dc <= 7; dc++) { const rr = r + dr, cc = c + dc; if (rr < 0 || rr >= mat.n || cc < 0 || cc >= mat.n) continue; let v = 0; if (dr >= 0 && dr <= 6 && dc >= 0 && dc <= 6) { if (dr === 0 || dr === 6 || dc === 0 || dc === 6) v = 1; else if (dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4) v = 1; else v = 0; } else { v = 0; } mat.m[rr][cc] = v; mat.reserved[rr][cc] = true; } } } function placeTimings(mat) { for (let i = 8; i < mat.n - 8; i++) { const v = (i % 2 === 0) ? 1 : 0; if (mat.m[6][i] === null) { mat.m[6][i] = v; mat.reserved[6][i] = true; } if (mat.m[i][6] === null) { mat.m[i][6] = v; mat.reserved[i][6] = true; } } } // Tables alignement (versions 2-10) const ALIGN = { 2: [6, 18], 3: [6, 22], 4: [6, 26], 5: [6, 30], 6: [6, 34], 7: [6, 22, 38], 8: [6, 24, 42], 9: [6, 26, 46], 10: [6, 28, 50], }; function placeAlignment(mat, version) { const positions = ALIGN[version]; if (!positions) return; for (const r of positions) { for (const c of positions) { // skip si recouvre un finder if ((r <= 8 && c <= 8) || (r <= 8 && c >= mat.n - 9) || (r >= mat.n - 9 && c <= 8)) continue; for (let dr = -2; dr <= 2; dr++) { for (let dc = -2; dc <= 2; dc++) { const v = (Math.abs(dr) === 2 || Math.abs(dc) === 2 || (dr === 0 && dc === 0)) ? 1 : 0; mat.m[r + dr][c + dc] = v; mat.reserved[r + dr][c + dc] = true; } } } } } function reserveFormatAreas(mat) { // Format info: 9 bits autour finders for (let i = 0; i < 9; i++) { if (mat.m[8][i] === null) { mat.reserved[8][i] = true; } if (mat.m[i][8] === null) { mat.reserved[i][8] = true; } } for (let i = mat.n - 8; i < mat.n; i++) { mat.reserved[8][i] = true; mat.reserved[i][8] = true; } // Dark module (toujours noir) mat.m[mat.n - 8][8] = 1; mat.reserved[mat.n - 8][8] = true; } function placeData(mat, codewords) { const bits = []; for (const cw of codewords) { for (let i = 7; i >= 0; i--) bits.push((cw >> i) & 1); } let bitIdx = 0; let upward = true; let col = mat.n - 1; while (col > 0) { if (col === 6) col--; // skip timing column for (let i = 0; i < mat.n; i++) { const row = upward ? mat.n - 1 - i : i; for (let j = 0; j < 2; j++) { const c = col - j; if (!mat.reserved[row][c]) { mat.m[row][c] = (bitIdx < bits.length) ? bits[bitIdx] : 0; bitIdx++; } } } upward = !upward; col -= 2; } } // 8 patterns de masque 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, ]; function applyMask(matrix, reserved, n, maskFn) { const out = matrix.map(row => row.slice()); for (let r = 0; r < n; r++) { for (let c = 0; c < n; c++) { if (!reserved[r][c] && maskFn(r, c)) { out[r][c] = out[r][c] ^ 1; } } } return out; } // Format info — ECC-M (01) + maskNum (3 bits) + 10 bits ECC + XOR mask function formatBits(maskNum) { const data = (0b01 << 3) | maskNum; // 5 bits let rem = data << 10; const gen = 0b10100110111; for (let i = 14; i >= 10; i--) { if (rem & (1 << i)) rem ^= gen << (i - 10); } const bits = ((data << 10) | rem) ^ 0b101010000010010; return bits; // 15 bits } function placeFormat(matrix, n, maskNum) { const bits = formatBits(maskNum); // Premier set (autour finder TL) for (let i = 0; i < 6; i++) matrix[i][8] = (bits >> i) & 1; matrix[7][8] = (bits >> 6) & 1; matrix[8][8] = (bits >> 7) & 1; matrix[8][7] = (bits >> 8) & 1; for (let i = 0; i < 6; i++) matrix[8][5 - i] = (bits >> (9 + i)) & 1; // Second set (autour finders TR + BL) for (let i = 0; i < 8; i++) matrix[n - 1 - i][8] = (bits >> i) & 1; for (let i = 0; i < 7; i++) matrix[8][n - 7 + i] = (bits >> (8 + i)) & 1; } // Pénalités pour scoring (4 règles standard) function score(mat, n) { let p = 0; // Règle 1: runs de 5+ même couleur for (let i = 0; i < n; i++) { let runR = 1, runC = 1; for (let j = 1; j < n; j++) { if (mat[i][j] === mat[i][j - 1]) { runR++; if (runR === 5) p += 3; else if (runR > 5) p++; } else runR = 1; if (mat[j][i] === mat[j - 1][i]) { runC++; if (runC === 5) p += 3; else if (runC > 5) p++; } else runC = 1; } } // Règle 2: blocs 2x2 même couleur for (let r = 0; r < n - 1; r++) { for (let c = 0; c < n - 1; c++) { if (mat[r][c] === mat[r][c+1] && mat[r][c] === mat[r+1][c] && mat[r][c] === mat[r+1][c+1]) p += 3; } } return p; } function generateQRMatrix(text) { const v = pickVersion(text.length); const dataBytes = buildDataBytes(text, v); const codewords = buildFinalCodewords(dataBytes, v); const n = size(v); const mat = makeEmptyMatrix(v); placeFinder(mat, 0, 0); placeFinder(mat, 0, n - 7); placeFinder(mat, n - 7, 0); placeAlignment(mat, v); placeTimings(mat); reserveFormatAreas(mat); placeData(mat, codewords); // Choisir le meilleur masque let best = null, bestScore = Infinity; for (let mk = 0; mk < 8; mk++) { const masked = applyMask(mat.m, mat.reserved, n, MASKS[mk]); placeFormat(masked, n, mk); const s = score(masked, n); if (s < bestScore) { bestScore = s; best = masked; } } return best; } /* ---------- Config + exposition globale ---------- */ window.PIX_CONFIG = { key: "71b7d8f7-0272-4778-a6c4-33dcebdb4f2a", name: "HOTEL OMEGA", city: "OIAPOQUE", }; window.buildPixPayload = buildPixPayload; window.generateQRMatrix = generateQRMatrix; })();