# 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.

Source: https://useqr.app/docs/spec/rendering-a-qr-code-from-a-matrix · Last reviewed 2026-08-21 · UseQR is free forever, MIT licensed, no signup.

---

## The hand-off point

Everything covered in [how QR codes are generated](/docs/spec/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](/glossary/quiet-zone));
- **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](/docs/spec/mirrored-qr-codes) 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](/docs/spec/qr-code-color-inversion-and-polarity);
if you want transparency, treat it as a deliberate feature with known placement, per
[transparent background QR codes](/docs/design/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](/docs/spec/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](/docs/troubleshooting/qr-code-pixelated-when-enlarged). 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](/docs/spec/vector-vs-raster-qr-codes).

However rendered, the output is a claim, not a fact, until a decoder reads it back —
[/validate](/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

- https://useqr.app/url
- https://useqr.app/validate
- https://useqr.app/scan
