Skip to main content

QR codes: choosing a library and two small pitfalls

· 2 min read

A QR tool looks like the simplest thing: type text, draw black and white squares. Building it, I hit one pitfall choosing a library and another handling Chinese text.

Choosing: look at dependencies first​

The most popular package is qrcode, which is feature-complete but depends on pngjs, yargs, and dijkstrajs — far too heavy for "draw squares on a canvas".

The other option is qrcode-generator: zero dependencies, written by the author of the QR specification book, still maintained in 2025. It only computes the matrix and leaves rendering to me, which is exactly right.

Pitfall 1: the exports field blocks the way​

I wanted its UTF-8 variant and instinctively wrote:

import {stringToBytes} from 'qrcode-generator/dist/qrcode_UTF8.mjs';

The build failed immediately: Package subpath './dist/qrcode_UTF8.mjs' is not defined by "exports". The package's exports only allows importing from the root.

The fix turned out cleaner: import the root, then implement UTF-8 bytes myself:

import qrcode from 'qrcode-generator';
qrcode.stringToBytes = (s) => Array.from(new TextEncoder().encode(s));

TextEncoder is browser-native — cleaner than pulling in a variant file.

Pitfall 2: garbled Chinese​

Without overriding stringToBytes, the library treats each character as a single byte and Chinese comes out garbled. After the override, length is counted in UTF-8 bytes and the QR content matches the input.

One constraint to note: the longer the content, the denser the code, and exceeding capacity throws. The tool wraps it in try/catch and reports "content too long".

Rendering and export​

Once the matrix is available, draw it yourself:

const count = qr.getModuleCount();
const cell = Math.floor(size / (count + margin * 2));

Using an integer cell avoids half-pixel blur, and the margin is applied on all sides. Export is just canvas.toBlob('image/png') — no extra encoder needed.

Choosing an error-correction level​

L/M/Q/H: higher levels resist damage but get denser for the same content. M is enough for everyday use; go to Q or H for printed material that wears easily.

Lesson​

Small libraries' exports restrictions often block the way, but a few lines of native code usually get around them. When choosing a library, the dependency count matters more than the feature count.

Loading comments…