The Challenge: QR Codes Without Libraries

Most developers reach for npm install qrcode and move on. But one developer decided to implement the full QR Code Model 2 algorithm in vanilla JavaScript—zero external dependencies, no server, just a single HTML file. The result is a free, client-side QR code generator that runs entirely in the browser.

Technical Deep Dive: How QR Codes Actually Work

The ISO spec for QR codes is 126 pages. The implementation covers six major steps:

1. Data Encoding

Three modes based on content:

  • Numeric (0-9): packs 3 digits into 10 bits
  • Alphanumeric (0-9 A-Z $%*+-./:space): 2 chars into 11 bits
  • Byte (everything else): UTF-8, one byte per 8 bits

The encoder auto-detects the mode and finds the minimum QR version (1–40) that fits:

function detectMode(text) {
  if (/^\d+$/.test(text)) return NUMERIC_MODE;
  if (text.split('').every(c => ALPHANUMS.includes(c))) return ALPHANUM_MODE;
  return BYTE_MODE;
}

2. Reed-Solomon Error Correction

QR codes survive up to 30% damage (at ECL H) using Reed-Solomon codes—the same algorithm used in CDs and deep-space transmissions. The implementation uses Galois Field GF(256) with precomputed EXP and LOG tables for fast multiplication:

const GF = (() => {
  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 <<= 1;
    if (x & 0x100) x ^= 0x11d; // reduce mod primitive polynomial
  }
  for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255];
  return {
    mul(a, b) { return (a === 0 || b === 0) ? 0 : EXP[LOG[a] + LOG[b]]; },
    // ...
  };
})();

3. Data Interleaving

For larger versions, data splits into multiple blocks, each with its own RS codewords. Blocks are interleaved byte-by-byte so physical damage hits different blocks and can be recovered.

4. Matrix Construction

A QR code matrix (21×21 for version 1, up to 177×177 for version 40) includes:

  • Finder patterns (three 7×7 squares in corners)
  • Alignment patterns (smaller squares for versions 2+)
  • Timing patterns (alternating dark/light rows/columns)
  • Format information (ECL and mask pattern, BCH(15,5) encoded)
  • Version information (for versions 7+, BCH(18,6) encoded)
  • Data modules (placed in a specific zigzag path)

5. Masking

Eight mask functions are evaluated (e.g., (row + col) % 2 === 0). The encoder tries all 8, scores each using a 4-rule penalty system, and picks the lowest-penalty mask:

const MASK_FNS = [
  (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
];

6. Format Info Placement

The chosen ECL and mask index are BCH-encoded into a 15-bit format string. The developer precomputed all 32 possible values to keep runtime code simple.

The Hardest Bug

Format info placement had a subtle off-by-one. The spec places bits 0–5 of the 15-bit format word in rows 0–5 of column 8, bit 7 in row 7, and bit 8 in row 8. The bit ordering was reversed for horizontal vs vertical copies, producing valid-looking but wrong format areas. The fix: explicitly write each position separately rather than looping symmetrically.

Testing: 202 Cases, No Test Framework

The test suite covers every layer with 202 tests—no Jest, no Mocha, just a 30-line inline test runner. Key test categories:

  • GF(256) arithmetic: 14 tests
  • RS generator: 8 tests
  • Mode detection: 12 tests
  • UTF-8 encoding: 10 tests
  • Data capacity table: 10 tests (versions 1–40, all 4 ECL levels)
  • charCountBits: 9 tests (bit width transitions at v9/v26)
  • encodeData version selection: 12 tests
  • BitStream: 8 tests
  • generateQR shape: 12 tests
  • Module values: 10 tests
  • Version sizes: 8 tests (size = version × 4 + 17)
  • Mask patterns: 8 tests
  • Version info BCH: 6 tests (known spec values v7–v10, v40)
  • Format strings: 6 tests (all ECL levels)
  • Alignment positions: 6 tests (v1, v2, v7, v40)
  • Codeword interleaving: 6 tests
  • ECL level comparisons: 8 tests
  • Known RS vector: 6 tests (exact byte match from QR spec)
  • Finder pattern modules: 8 tests
  • Edge cases: 6 tests (emoji, long URLs, whitespace)
  • ECL stress: 6 tests
  • Null input handling: 4 tests

The Tool

The generator offers:

  • Real-time generation (debounced at 80ms)
  • Size selector: 128×128, 256×256, or 512×512 pixels
  • Error correction levels: L (7%), M (15%), Q (25%), H (30%)
  • Color picker for foreground and background
  • PNG download via canvas
  • SVG download with crisp vector output

Try it: https://qr-code-generator-e83.pages.dev

The full source is a single index.html—open DevTools to read through. No build step, no framework, no minification. Part of devnestio—a growing collection of zero-dependency developer tools.

Why This Matters

Building a QR code generator from scratch teaches you about error-correcting codes, Galois field arithmetic, and matrix encoding—concepts that apply to many areas of software engineering. Plus, having a zero-dependency tool means it works offline, loads instantly, and is auditable in minutes.