Skip to content
UseQR
ESC

Jump to

MOVEOPEN50 places

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.

View as MarkdownPaste this page into any AI assistant. It is plain, portable Markdown.

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