Spec & internals
Rendering a QR code from a matrix
Rendering starts from a boolean matrix — for each dark module, draw a square of moduleSize pixels at (column + quietZone, row + quietZone) × moduleSize. Add four modules of quiet zone on every side, multiply canvas dimensions by devicePixelRatio to stay sharp, and export PNG at final display size or larger.
The hand-off point
Everything covered in how QR codes are generated —
modes, Reed–Solomon, masking — ends at a data structure, not an image: a square boolean
matrix, true for dark, typically row-major. UseQR's core returns exactly this
(modules: boolean[][], with size counting modules per side, quiet zone not
included). Rendering is the separate, dumber, easier-to-get-wrong job of turning that
matrix into pixels or vectors.
The core loop
Three numbers govern the geometry:
- moduleSize — pixels per module (UseQR defaults to 8);
- quietZone — blank modules on every side (default 4, the spec minimum);
- total — the output edge:
(size + 2 × quietZone) × moduleSize.
The loop itself:
total = (size + 2*quietZone) * moduleSize
fill entire total × total area with background // opaque, not assumed
for row in 0..size-1:
for col in 0..size-1:
if matrix[row][col]:
x = (col + quietZone) * moduleSize
y = (row + quietZone) * moduleSize
fillRect(x, y, moduleSize, moduleSize) // foreground
Note col maps to x and row to y — transposing them mirrors the code, which
often still scans and therefore survives testing until it
ships. Keep moduleSize an integer: fractional module widths force modules onto
fractional pixel boundaries, and anti-aliasing turns crisp edges into grey gradients that
cost contrast exactly where the decoder samples.
Painting the background explicitly matters too. A transparent-background PNG dropped onto a dark page becomes a polarity accident; if you want transparency, treat it as a deliberate feature with known placement, per transparent background QR codes.
Canvas and the device pixel ratio
On the web, a canvas has two sizes: its CSS layout size and its pixel buffer. On a 2× or 3× display, drawing a 320 px code into a 320-buffer canvas displayed at 320 CSS pixels renders soft — the browser upscales your buffer. The standard correction:
dpr = window.devicePixelRatio || 1
canvas.style.width = cssSize + "px"
canvas.style.height = cssSize + "px"
canvas.width = cssSize * dpr
canvas.height = cssSize * dpr
ctx.scale(dpr, dpr)
// …then draw in CSS pixel coordinates as normal
A blurry on-screen QR code usually still scans — phone cameras are forgiving of soft edges at screen sizes — but it looks broken, and screenshots of it inherit the blur.
PNG or SVG
The loop above describes raster thinking, but the same traversal emits vectors: instead of
fillRect, append a rectangle sub-path per dark module and fill once — the approach
dissected in QR code SVG path structure.
The decision rule is destination, not preference. Fixed-size screen display: PNG (or canvas directly). Anything that will be scaled, printed or handed to a designer: SVG, because raster scaling is where codes go blurry or pixelated. If you must ship raster for print, render at the final physical size at 300 DPI or more — the arithmetic lives on vector vs raster.
However rendered, the output is a claim, not a fact, until a decoder reads it back — /validate does render-then-decode in one step.
FAQ
How do I draw a QR code from a matrix of booleans?
Iterate rows and columns; for each true cell, fill a square of moduleSize pixels at ((column + quietZone) × moduleSize, (row + quietZone) × moduleSize). Paint the background first and include four quiet-zone modules on every side.
Why is my canvas QR code blurry on Retina screens?
The canvas pixel buffer matches CSS size, so high-DPI displays upscale it. Multiply canvas.width and canvas.height by devicePixelRatio, keep the CSS size unchanged, and scale the drawing context by the same factor before drawing.
Do I add the quiet zone before or after rendering?
During. Offset every module by the quiet-zone width and size the output as (modules + 2 × quiet zone) × moduleSize, so the margin is baked into the file. Margins left to whoever places the image have a way of disappearing.
Should moduleSize be an integer?
Yes, for raster output. Fractional module sizes put module edges between pixels, and the resulting anti-aliasing greys out edges and shrinks effective contrast. Pick an integer pixel count per module and let overall image size follow from it.
Try it — free, no signup
Related
- QR code anatomy — every region of the symbol — A labelled tour of a QR code: finder patterns, separators, timing, alignment, format and version information, the data region and the quiet zone.
- QR code SVG path structure — How a module matrix becomes SVG — one merged path versus thousands of rects, crisp-edges rendering, integer coordinates, and why seams appear.
- How a QR code is generated, step by step — Encoding runs in eight steps: choose the mode, choose the version, build the bit stream, split into codewords, compute Reed–Solomon error correction,…
- Generate a QR code in JavaScript and React — For a QR code in a browser or React app, either point an <img> at the keyless API — one line, no dependency — or use a client-side library such as qrcode…