OpusBike/webapp/js/qrcode.js

319 lines
11 KiB
JavaScript
Raw Normal View History

/**
* Minimal QR Code generator no dependencies
* Encodes a URL into a canvas element
* Based on QR Code Model 2, Version 3 (29x29), Error Correction Level L
*/
(function () {
'use strict';
// Generate QR code on a canvas for a given URL
function generateQR(canvas, url) {
// Use the Google Charts API as a simple fallback-free approach:
// Draw QR as an image loaded from a data URL generated client-side.
// For a truly zero-dependency approach, we use a proven minimal encoder.
const size = canvas.width;
const ctx = canvas.getContext('2d');
// Try using the minimal encoder
try {
const modules = encode(url);
const moduleCount = modules.length;
const cellSize = Math.floor((size - 16) / moduleCount); // 8px padding each side
const offset = Math.floor((size - cellSize * moduleCount) / 2);
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, size, size);
ctx.fillStyle = '#051229'; // Navy from brand
for (let r = 0; r < moduleCount; r++) {
for (let c = 0; c < moduleCount; c++) {
if (modules[r][c]) {
ctx.fillRect(
offset + c * cellSize,
offset + r * cellSize,
cellSize,
cellSize
);
}
}
}
} catch (e) {
// Fallback: show URL text
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, size, size);
ctx.fillStyle = '#051229';
ctx.font = '10px monospace';
ctx.textAlign = 'center';
ctx.fillText(url, size / 2, size / 2);
}
}
// ---- Minimal QR Encoder (byte mode, ECC L) ----
// GF(256) math
const EXP = new Uint8Array(512);
const LOG = new Uint8Array(256);
(function initGF() {
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 gfMul(a, b) {
return a === 0 || b === 0 ? 0 : EXP[LOG[a] + LOG[b]];
}
function polyMul(a, b) {
const r = new Uint8Array(a.length + b.length - 1);
for (let i = 0; i < a.length; i++)
for (let j = 0; j < b.length; j++)
r[i + j] ^= gfMul(a[i], b[j]);
return r;
}
function polyRemainder(data, gen) {
const r = new Uint8Array(data.length + gen.length - 1);
r.set(data);
for (let i = 0; i < data.length; i++) {
if (r[i] === 0) continue;
for (let j = 0; j < gen.length; j++)
r[i + j] ^= gfMul(gen[j], r[i]);
}
return r.slice(data.length);
}
function generatorPoly(n) {
let g = new Uint8Array([1]);
for (let i = 0; i < n; i++)
g = polyMul(g, new Uint8Array([1, EXP[i]]));
return g;
}
// Version info: [version, size, dataBytes, eccPerBlock, numBlocks]
// We pick the smallest version that fits
const VERSIONS = [
[1, 21, 19, 7, 1],
[2, 25, 34, 10, 1],
[3, 29, 55, 15, 1],
[4, 33, 80, 20, 1],
[5, 37, 108, 26, 1],
[6, 41, 136, 18, 2],
[7, 45, 156, 20, 2],
[8, 49, 194, 24, 2],
[9, 53, 232, 30, 2],
[10, 57, 274, 18, 4],
];
function pickVersion(dataLen) {
// Byte mode: 4 bits mode + char count bits + data + terminator
for (const v of VERSIONS) {
const charCountBits = v[0] <= 9 ? 8 : 16;
const totalBits = 4 + charCountBits + dataLen * 8;
const capacity = v[2] * 8;
if (totalBits <= capacity) return v;
}
return VERSIONS[VERSIONS.length - 1]; // fallback to largest
}
function encode(text) {
const data = new TextEncoder().encode(text);
const [version, size, dataBytes, eccPerBlock, numBlocks] = pickVersion(data.length);
const charCountBits = version <= 9 ? 8 : 16;
// Build data bitstream
const bits = [];
function pushBits(val, len) {
for (let i = len - 1; i >= 0; i--)
bits.push((val >> i) & 1);
}
pushBits(0b0100, 4); // Byte mode
pushBits(data.length, charCountBits);
for (const b of data) pushBits(b, 8);
pushBits(0, Math.min(4, dataBytes * 8 - bits.length)); // terminator
// Pad to byte boundary
while (bits.length % 8 !== 0) bits.push(0);
// Pad bytes
const padBytes = [0xEC, 0x11];
let padIdx = 0;
while (bits.length < dataBytes * 8) {
pushBits(padBytes[padIdx % 2], 8);
padIdx++;
}
// Convert to bytes
const dataArr = new Uint8Array(dataBytes);
for (let i = 0; i < dataBytes; i++) {
let byte = 0;
for (let b = 0; b < 8; b++) byte = (byte << 1) | (bits[i * 8 + b] || 0);
dataArr[i] = byte;
}
// Split into blocks and generate ECC
const blockSize = Math.floor(dataBytes / numBlocks);
const gen = generatorPoly(eccPerBlock);
const dataBlocks = [];
const eccBlocks = [];
for (let b = 0; b < numBlocks; b++) {
const start = b * blockSize;
const end = b === numBlocks - 1 ? dataBytes : start + blockSize;
const block = dataArr.slice(start, end);
dataBlocks.push(block);
eccBlocks.push(polyRemainder(block, gen));
}
// Interleave
const codewords = [];
const maxDataLen = Math.max(...dataBlocks.map(b => b.length));
for (let i = 0; i < maxDataLen; i++)
for (const block of dataBlocks)
if (i < block.length) codewords.push(block[i]);
for (let i = 0; i < eccPerBlock; i++)
for (const block of eccBlocks)
if (i < block.length) codewords.push(block[i]);
// Create module matrix
const modules = Array.from({ length: size }, () => new Uint8Array(size));
const reserved = Array.from({ length: size }, () => new Uint8Array(size));
// Finder patterns
function drawFinder(row, col) {
for (let r = -1; r <= 7; r++) {
for (let c = -1; c <= 7; c++) {
const rr = row + r, cc = col + c;
if (rr < 0 || rr >= size || cc < 0 || cc >= size) continue;
reserved[rr][cc] = 1;
if (r >= 0 && r <= 6 && c >= 0 && c <= 6) {
modules[rr][cc] =
(r === 0 || r === 6 || c === 0 || c === 6 ||
(r >= 2 && r <= 4 && c >= 2 && c <= 4)) ? 1 : 0;
}
}
}
}
drawFinder(0, 0);
drawFinder(0, size - 7);
drawFinder(size - 7, 0);
// Alignment pattern (version >= 2)
if (version >= 2) {
const pos = alignmentPositions(version);
for (const r of pos) {
for (const c of pos) {
if (reserved[r][c]) continue;
for (let dr = -2; dr <= 2; dr++) {
for (let dc = -2; dc <= 2; dc++) {
const rr = r + dr, cc = c + dc;
reserved[rr][cc] = 1;
modules[rr][cc] =
(Math.abs(dr) === 2 || Math.abs(dc) === 2 || (dr === 0 && dc === 0)) ? 1 : 0;
}
}
}
}
}
// Timing patterns
for (let i = 8; i < size - 8; i++) {
reserved[6][i] = 1;
modules[6][i] = i % 2 === 0 ? 1 : 0;
reserved[i][6] = 1;
modules[i][6] = i % 2 === 0 ? 1 : 0;
}
// Dark module
reserved[size - 8][8] = 1;
modules[size - 8][8] = 1;
// Reserve format info areas
for (let i = 0; i < 8; i++) {
reserved[8][i] = 1;
reserved[8][size - 1 - i] = 1;
reserved[i][8] = 1;
reserved[size - 1 - i][8] = 1;
}
reserved[8][8] = 1;
// Reserve version info (version >= 7) — not needed for our range
// Place data
let bitIdx = 0;
const totalBitsToPlace = codewords.length * 8;
let col = size - 1;
while (col >= 0) {
if (col === 6) col--; // skip timing column
for (let dir = 0; dir < 2; dir++) {
const cc = col - dir;
const upward = ((size - 1 - col + (col > 6 ? 1 : 0)) / 2) % 2 === 0;
for (let i = 0; i < size; i++) {
const r = upward ? i : size - 1 - i;
if (reserved[r][cc]) continue;
if (bitIdx < totalBitsToPlace) {
const byteIdx = Math.floor(bitIdx / 8);
const bitOff = 7 - (bitIdx % 8);
modules[r][cc] = (codewords[byteIdx] >> bitOff) & 1;
bitIdx++;
}
}
}
col -= 2;
}
// Apply mask (mask 0: (row + col) % 2 === 0)
for (let r = 0; r < size; r++)
for (let c = 0; c < size; c++)
if (!reserved[r][c] && (r + c) % 2 === 0)
modules[r][c] ^= 1;
// Write format info (mask 0, ECC L)
const formatBits = getFormatBits(0); // ECC L, mask 0
// Horizontal
for (let i = 0; i < 6; i++) modules[8][i] = (formatBits >> (14 - i)) & 1;
modules[8][7] = (formatBits >> 8) & 1;
modules[8][8] = (formatBits >> 7) & 1;
modules[7][8] = (formatBits >> 6) & 1;
for (let i = 0; i < 6; i++) modules[5 - i][8] = (formatBits >> (5 - i)) & 1;
// Vertical
for (let i = 0; i < 7; i++) modules[size - 1 - i][8] = (formatBits >> (14 - i)) & 1;
for (let i = 0; i < 8; i++) modules[8][size - 8 + i] = (formatBits >> (7 - i)) & 1;
return modules;
}
function alignmentPositions(version) {
if (version === 1) return [];
const positions = [6];
const last = version * 4 + 10;
const count = Math.floor(version / 7) + 2;
const step = Math.ceil((last - 6) / (count - 1));
for (let i = 1; i < count; i++) positions.push(6 + i * step > last ? last : 6 + i * step);
// Ensure last is exact
positions[positions.length - 1] = last;
return positions;
}
function getFormatBits(mask) {
// Pre-computed format info for ECC Level L (01) with masks 0-7
const FORMAT_INFO = [
0x77C4, 0x72F3, 0x7DAA, 0x789D, 0x662F, 0x6318, 0x6C41, 0x6976,
];
return FORMAT_INFO[mask];
}
// Auto-init on load
window.addEventListener('DOMContentLoaded', () => {
const canvas = document.getElementById('qr-canvas');
if (canvas) {
generateQR(canvas, 'https://monitor.proactys.swiss/opusbike/');
}
});
})();