Last active
December 24, 2024 04:27
-
-
Save md-riaz/6b4cd9fe1f8a68e5ad98f8ce6e766bfc to your computer and use it in GitHub Desktop.
const qr = new QRCode(); const modules = qr.generate("Hello, World!"); console.log(qr.toSVG());
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// QR Code Generator Library | |
class QRCode { | |
constructor(version = 1, errorCorrection = 'M') { | |
this.version = version; | |
this.errorCorrection = errorCorrection; | |
this.moduleCount = 21 + (version - 1) * 4; | |
this.modules = Array(this.moduleCount).fill().map(() => Array(this.moduleCount).fill(null)); | |
this.errorCorrectionLevels = { | |
'L': 0x01, // 7% error correction | |
'M': 0x00, // 15% error correction | |
'Q': 0x03, // 25% error correction | |
'H': 0x02 // 30% error correction | |
}; | |
} | |
// Generate QR code from text | |
generate(text) { | |
const data = this.encodeData(text); | |
const blocks = this.addErrorCorrection(data); | |
const bitStream = this.interleaveBlocks(blocks); | |
this.placeModules(bitStream); | |
return this.modules; | |
} | |
// Encode data into bit sequence | |
encodeData(text) { | |
const mode = 0x04; // Byte mode | |
const length = text.length; | |
// Calculate available capacity | |
const maxDataBits = this.getDataCapacity(); | |
const requiredBits = 4 + 8 + (length * 8); | |
if (requiredBits > maxDataBits) { | |
throw new Error("Input data exceeds QR code capacity."); | |
} | |
let bits = []; | |
// Add mode indicator (4 bits) | |
bits.push(...this.toBits(mode, 4)); | |
// Add length indicator (8 bits for version 1-9 in byte mode) | |
bits.push(...this.toBits(length, 8)); | |
// Add data | |
for (let i = 0; i < length; i++) { | |
bits.push(...this.toBits(text.charCodeAt(i), 8)); | |
} | |
// Add terminator and padding | |
const remainingBits = maxDataBits - bits.length; | |
const terminatorLength = Math.min(4, remainingBits); | |
bits.push(...Array(terminatorLength).fill(0)); | |
while (bits.length % 8 !== 0) { | |
bits.push(0); | |
} | |
// Add pad bytes | |
const padBytes = [0xEC, 0x11]; | |
let padIndex = 0; | |
while (bits.length < maxDataBits) { | |
bits.push(...this.toBits(padBytes[padIndex], 8)); | |
padIndex = (padIndex + 1) % 2; | |
} | |
return bits; | |
} | |
// Add error correction codes | |
addErrorCorrection(dataBits) { | |
const blocks = this.splitIntoBlocks(dataBits); | |
const result = []; | |
for (const block of blocks) { | |
const ecBytes = this.calculateReedSolomon(block); | |
result.push({ | |
data: block, | |
correction: ecBytes | |
}); | |
} | |
return result; | |
} | |
// Reed-Solomon error correction calculation | |
calculateReedSolomon(data) { | |
const generator = this.getGeneratorPolynomial(this.getECCodewordsPerBlock()); | |
const message = [...data, ...Array(generator.length - 1).fill(0)]; | |
for (let i = 0; i < data.length; i++) { | |
const factor = message[i]; | |
if (factor !== 0) { | |
for (let j = 0; j < generator.length; j++) { | |
message[i + j] ^= this.galoisMultiply(generator[j], factor); | |
} | |
} | |
} | |
return message.slice(data.length); | |
} | |
// Galois field multiplication | |
galoisMultiply(x, y) { | |
let result = 0; | |
while (y > 0) { | |
if (y & 1) { | |
result ^= x; | |
} | |
x <<= 1; | |
if (x > 0xFF) { | |
x ^= 0x11D; // x^8 + x^4 + x^3 + x^2 + 1 | |
} | |
y >>= 1; | |
} | |
return result; | |
} | |
// Get generator polynomial for Reed-Solomon error correction | |
getGeneratorPolynomial(degree) { | |
let polynomial = [1]; | |
for (let i = 0; i < degree; i++) { | |
polynomial = this.multiplyPolynomials(polynomial, [1, this.galoisExp(i)]); | |
} | |
return polynomial; | |
} | |
// Multiply two polynomials in GF(256) | |
multiplyPolynomials(p1, p2) { | |
const result = Array(p1.length + p2.length - 1).fill(0); | |
for (let i = 0; i < p1.length; i++) { | |
for (let j = 0; j < p2.length; j++) { | |
result[i + j] ^= this.galoisMultiply(p1[i], p2[j]); | |
} | |
} | |
return result; | |
} | |
// Galois field exponentiation | |
galoisExp(exp) { | |
return this.galoisExpTable[exp % 255]; | |
} | |
// Interleave data and error correction blocks | |
interleaveBlocks(blocks) { | |
const result = []; | |
// Interleave data blocks | |
const maxDataLength = Math.max(...blocks.map(b => b.data.length)); | |
for (let i = 0; i < maxDataLength; i++) { | |
for (const block of blocks) { | |
if (i < block.data.length) { | |
result.push(block.data[i]); | |
} | |
} | |
} | |
// Interleave error correction blocks | |
const maxECLength = Math.max(...blocks.map(b => b.correction.length)); | |
for (let i = 0; i < maxECLength; i++) { | |
for (const block of blocks) { | |
if (i < block.correction.length) { | |
result.push(block.correction[i]); | |
} | |
} | |
} | |
return result; | |
} | |
// Place modules in the QR code matrix | |
placeModules(bitStream) { | |
this.addFinderPatterns(); | |
this.addAlignmentPatterns(); | |
this.addTimingPatterns(); | |
this.addFormatInfo(); | |
this.addVersionInfo(); | |
// Place data bits | |
let bitIndex = 0; | |
for (let right = this.moduleCount - 1; right >= 1; right -= 2) { | |
if (right <= 6) right--; // Skip vertical timing pattern | |
for (let vert = 0; vert < this.moduleCount; vert++) { | |
for (let i = 0; i < 2; i++) { | |
const col = right - i; | |
const upward = ((right + 1) & 2) === 0; | |
const row = upward ? this.moduleCount - 1 - vert : vert; | |
if (this.modules[row][col] === null && bitIndex < bitStream.length) { | |
this.modules[row][col] = bitStream[bitIndex]; | |
bitIndex++; | |
} | |
} | |
} | |
} | |
this.addMask(0); // Apply mask pattern 0 | |
} | |
// Add finder patterns (three large squares in corners) | |
addFinderPatterns() { | |
const addPattern = (row, col) => { | |
for (let r = -1; r <= 7; r++) { | |
for (let c = -1; c <= 7; c++) { | |
if (row + r < 0 || col + c < 0 || | |
row + r >= this.moduleCount || col + c >= this.moduleCount) continue; | |
const inBound = (r >= 0 && r <= 6 && (c === 0 || c === 6)) || | |
(c >= 0 && c <= 6 && (r === 0 || r === 6)) || | |
(r >= 2 && r <= 4 && c >= 2 && c <= 4); | |
this.modules[row + r][col + c] = inBound; | |
} | |
} | |
}; | |
addPattern(0, 0); // Top-left | |
addPattern(0, this.moduleCount - 7); // Top-right | |
addPattern(this.moduleCount - 7, 0); // Bottom-left | |
} | |
// Add alignment patterns | |
addAlignmentPatterns() { | |
if (this.version === 1) return; // Version 1 has no alignment patterns | |
const positions = this.getAlignmentPatternPositions(); | |
for (const row of positions) { | |
for (const col of positions) { | |
// Skip positions that overlap with finder patterns | |
if ((row < 8 && col < 8) || | |
(row < 8 && col > this.moduleCount - 9) || | |
(row > this.moduleCount - 9 && col < 8)) continue; | |
// Add 5x5 alignment pattern | |
for (let r = -2; r <= 2; r++) { | |
for (let c = -2; c <= 2; c++) { | |
this.modules[row + r][col + c] = | |
r === -2 || r === 2 || c === -2 || c === 2 || | |
(r === 0 && c === 0); | |
} | |
} | |
} | |
} | |
} | |
// Add timing patterns | |
addTimingPatterns() { | |
for (let i = 8; i < this.moduleCount - 8; i++) { | |
if (this.modules[6][i] === null) { | |
this.modules[6][i] = i % 2 === 0; | |
} | |
if (this.modules[i][6] === null) { | |
this.modules[i][6] = i % 2 === 0; | |
} | |
} | |
} | |
// Add format information | |
addFormatInfo() { | |
const formatInfo = this.getFormatInfo(); | |
// Top-left format info | |
for (let i = 0; i <= 8; i++) { | |
if (i !== 6) { // Skip timing pattern | |
this.modules[i][8] = formatInfo[i]; | |
} | |
} | |
for (let i = 7; i >= 0; i--) { | |
if (i !== 6) { // Skip timing pattern | |
this.modules[8][i] = formatInfo[14 - i]; | |
} | |
} | |
// Top-right and bottom-left format info | |
for (let i = this.moduleCount - 1; i >= this.moduleCount - 8; i--) { | |
this.modules[8][i] = formatInfo[this.moduleCount - 1 - i]; | |
} | |
for (let i = this.moduleCount - 7; i < this.moduleCount; i++) { | |
this.modules[i][8] = formatInfo[i - (this.moduleCount - 8)]; | |
} | |
} | |
// Add version information (versions 7 and above) | |
addVersionInfo() { | |
if (this.version < 7) return; | |
const versionInfo = this.getVersionInfo(); | |
let index = 0; | |
// Bottom-right version info | |
for (let i = 0; i < 6; i++) { | |
for (let j = this.moduleCount - 11; j < this.moduleCount - 8; j++) { | |
this.modules[i][j] = versionInfo[index]; | |
index++; | |
} | |
} | |
// Top-right version info | |
index = 0; | |
for (let i = this.moduleCount - 11; i < this.moduleCount - 8; i++) { | |
for (let j = 0; j < 6; j++) { | |
this.modules[i][j] = versionInfo[index]; | |
index++; | |
} | |
} | |
} | |
// Apply mask pattern | |
addMask(pattern) { | |
for (let row = 0; row < this.moduleCount; row++) { | |
for (let col = 0; col < this.moduleCount; col++) { | |
if (this.modules[row][col] === null) continue; | |
let mask; | |
switch (pattern) { | |
case 0: mask = (row + col) % 2 === 0; break; | |
case 1: mask = row % 2 === 0; break; | |
case 2: mask = col % 3 === 0; break; | |
case 3: mask = (row + col) % 3 === 0; break; | |
case 4: mask = (Math.floor(row / 2) + Math.floor(col / 3)) % 2 === 0; break; | |
case 5: mask = (row * col) % 2 + (row * col) % 3 === 0; break; | |
case 6: mask = ((row * col) % 2 + (row * col) % 3) % 2 === 0; break; | |
case 7: mask = ((row + col) % 2 + (row * col) % 3) % 2 === 0; break; | |
} | |
this.modules[row][col] = this.modules[row][col] ^ mask; | |
} | |
} | |
} | |
// Convert number to binary array | |
toBits(num, length) { | |
const result = []; | |
for (let i = length - 1; i >= 0; i--) { | |
result.push((num >> i) & 1); | |
} | |
return result; | |
} | |
// Utility methods | |
getDataCapacity() { | |
// Simplified - actual capacity depends on version and EC level | |
return (this.moduleCount - 8) * (this.moduleCount - 8) - 3 * 64 - 2 * 36; | |
} | |
getECCodewordsPerBlock() { | |
// Simplified - actual values depend on version and EC level | |
return 10; | |
} | |
splitIntoBlocks(data) { | |
// Simplified - actual block structure depends on version and EC level | |
return [data]; | |
} | |
getAlignmentPatternPositions() { | |
if (this.version === 1) return []; | |
const interval = Math.floor(this.version / 7) + 2; | |
const positions = [6]; | |
let pos = this.moduleCount - 7; | |
while (pos > 6) { | |
positions.unshift(pos); | |
pos -= interval; | |
} | |
return positions; | |
} | |
getFormatInfo() { | |
// Simplified - actual format info depends on EC level and mask pattern | |
return Array(15).fill(0); | |
} | |
getVersionInfo() { | |
// Simplified - actual version info depends on version | |
return Array(18).fill(0); | |
} | |
// Initialize Galois field tables | |
galoisExpTable = (() => { | |
const table = new Array(256); | |
let x = 1; | |
for (let i = 0; i < 256; i++) { | |
table[i] = x; | |
x <<= 1; | |
if (x > 0xFF) { | |
x ^= 0x11D; | |
} | |
} | |
return table; | |
})(); | |
// Convert QR code to SVG | |
toSVG(moduleSize = 5) { | |
const size = this.moduleCount * moduleSize; | |
let svg = `<svg viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">`; | |
for (let row = 0; row < this.moduleCount; row++) { | |
for (let col = 0; col < this.moduleCount; col++) { | |
if (this.modules[row][col]) { | |
svg += `<rect x="${col * moduleSize}" y="${row * moduleSize}" width="${moduleSize}" height="${moduleSize}" fill="black"/>`; | |
} | |
} | |
} | |
svg += `</svg>`; | |
return svg; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment