Articles
July 10, 20268 min read

How QR codes actually work

QR codes are everywhere — on restaurant menus, product labels, business cards. But what's actually happening when your phone's camera decodes one in a fraction of a second? This article walks through the full encoding pipeline, from raw bytes to the black-and-white matrix you see printed.

A matrix of structured data

A QR code is a two-dimensional barcode divided into a grid of square modules, each either dark or light. The grid size depends on the version: version 1 is 21×21, and each increment adds 4 modules per side, up to version 40 at 177×177.

Not every module carries user data. Large areas are reserved for function patterns:

  • Finder patterns — the three identical 7×7 squares in three corners that let a scanner orient the symbol regardless of rotation or perspective.
  • Timing patterns — alternating dark/light stripes along row 6 and column 6 that establish the module grid scale.
  • Alignment patterns — smaller squares placed in higher versions to correct for image distortion.
  • Format information — two copies of a 15-bit strip encoding error-correction level and mask pattern.

Only after reserving all of those is the remaining area available for encoded data.

Encoding the data

Before anything is written to the grid, the raw input goes through several encoding steps.

Mode selection

QR supports multiple encoding modes. Numeric mode uses 10 bits per 3 digits, making it ultra-compact for phone numbers. Alphanumeric mode handles uppercase letters and a handful of symbols. Byte mode encodes arbitrary bytes — including full UTF-8 URLs — at 8 bits each. Kanji mode compresses two-byte Japanese characters into 13 bits.

A URL like https://example.com is typically encoded in byte mode.

Building the bitstream

The bitstream starts with a 4-bit mode indicator (0100 for byte mode), followed by a length field, then the raw bytes. After the data, a 4-bit 0000 terminator is appended, and the stream is padded to fill the full data capacity of the chosen version with alternating 0xEC and 0x11 padding codewords.

The total number of 8-bit data codewords available depends on version and error-correction level. A version 6 symbol with level L provides 86 data codewords.

Reed–Solomon error correction

Here is where QR codes become remarkably resilient. After the data codewords are ready, the encoder computes error-correction codewords using Reed–Solomon coding over GF(256) — the finite field of 256 elements.

Reed–Solomon works by treating the data as a polynomial and computing its remainder when divided by a fixed generator polynomial. The result is a set of redundant codewords that allow a decoder to:

  • Detect errors (without knowing where they are), and
  • Correct them, as long as the number of errors doesn't exceed $\lfloor e / 2 \rfloor$, where $e$ is the error-correction capacity.

The four QR error-correction levels are:

| Level | Can recover up to | |-------|------------------| | L | ~7% of codewords | | M | ~15% | | Q | ~25% | | H | ~30% |

This is why QR codes can still scan when partly obscured by a logo.

Interleaving

For larger symbols, data and EC codewords from multiple blocks are interleaved before being written to the grid. This spreads any burst of physical damage (a tear, a smudge) across multiple blocks rather than concentrating it in one, increasing the practical chance of recovery.

Writing data to the grid

Data codewords are written into the grid in an upward-then-downward zigzag pattern, two columns wide, starting from the bottom-right corner and working left. Function pattern areas are skipped.

Each module's dark/light state comes from XOR-ing the corresponding bit with a mask pattern.

Masking

Eight mask patterns are defined by simple mathematical formulas applied to each module's row and column indices. For example, mask 0 flips a module if (row + col) % 2 == 0.

The spec requires the encoder to evaluate all eight masks and pick the one that minimises a penalty score computed from four criteria:

  1. Long runs of same-color modules in rows/columns.
  2. 2×2 blocks of the same color.
  3. Patterns that resemble finder patterns.
  4. The ratio of dark to light modules being far from 50%.

Good masking prevents the module grid from having large uniform regions that confuse scanners. It's also the lever that QArt-style tools exploit: by choosing which mask makes the most data modules match a target image, they achieve stylistic designs while staying scannable.

Format information

The chosen mask index (0–7) and error-correction level are encoded together into a 15-bit string, protected by a BCH code for robustness, and written into two locations in the symbol so a scanner can read them even if one copy is damaged.

Decoding in reverse

A phone camera does all of this backwards: locate the finder patterns, correct for perspective, read format information, dequantize modules to bits, undo the mask, extract codewords, run Reed–Solomon to fix any errors, and finally decode the bitstream back into text. Modern implementations can do this in a single video frame.

Why this matters for design

Understanding the pipeline reveals what is and isn't flexible. The function patterns are fixed — they cannot change without breaking scannability. But the free data and EC modules are determined by a linear algebra problem over GF(256). That's what makes artistic QR codes tractable: if you treat the data codewords as unknowns and the desired pixel pattern as a system of equations, you can often find a valid URL encoding that matches a target image to a surprising degree — which is exactly what the QR Studio solver does.