Skip to content

Instantly share code, notes, and snippets.

@md-riaz
Last active December 24, 2024 04:27
Show Gist options
  • Save md-riaz/6b4cd9fe1f8a68e5ad98f8ce6e766bfc to your computer and use it in GitHub Desktop.
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());
// 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