- Pure JavaScript, zero native dependencies, runs in browsers and Node.js.
- Tree shakeable, side effect free, only bundle what you actually use.
- All QR code versions (1 - 40) and all error correction levels (L / M / Q / H).
- Mixed multi-segment encoding:
Numeric,Alphanumeric,Byte,KanjiandHanzi. - ECI (Extended Channel Interpretation) and FNC1 (GS1 / AIM) support.
- Structured append and mirror detection on the decode side.
- Built-in GIF encoder,
toDataURLworks without canvas. - Robust detection: perspective correction, timing line verification and mirror recovery.
npm install @nuintun/qrcode// ESM
import { Encoder } from '@nuintun/qrcode';
// CommonJS
const { Encoder } = require('@nuintun/qrcode');Encode a QR code to a GIF data URL in three lines of code:
import { Encoder } from '@nuintun/qrcode';
const encoder = new Encoder({ level: 'H' });
const qrcode = encoder.encode(new Byte('https://github.com/nuintun/qrcode'));
console.log(qrcode.toDataURL());Decode a QR code from an image:
import { binarize, Decoder, Detector, grayscale } from '@nuintun/qrcode';
// 1. Draw the image to a canvas and get its pixel data.
const imageData = context.getImageData(0, 0, width, height);
// 2. Convert to luminance and binarize to a black/white matrix.
const binarized = binarize(grayscale(imageData), width, height);
// 3. Detect QR code regions and decode each candidate.
const detector = new Detector();
for (const detect of detector.detect(binarized)) {
// See [Decoding Guide](#decoding-guide) for the full pattern.
const decoded = new Decoder().decode(detect.matrix);
console.log(decoded.content);
}The guides below explain each step in detail.
The encoder turns one or more segments into a matrix. Each segment declares its own optimised mode, and the encoder packs them into a single symbol, automatically selecting the smallest version that fits (or the version you fixed), choosing the best mask, and appending Reed-Solomon error correction codewords.
Error correction allows a damaged QR code to be read. Higher level means more redundancy, but also a larger symbol for the same data. Choose by scenario:
| Level | Recoverable Codewords | Recommended Scenario |
|---|---|---|
L |
~7% | Clean environments, maximum data capacity |
M |
~15% | Common default, good balance |
Q |
~25% | Outdoor, industrial, slightly dirty environments |
H |
~30% | Heavy damage, or overlaying a centered logo (WeChat style) |
const encoder = new Encoder({ level: 'H' });A segment is a piece of content encoded in its most efficient mode. Modes differ in how many bits per character they cost:
| Segment | Mode | Bits per Character | Allowed Characters |
|---|---|---|---|
Numeric |
Numeric | ~3.33 | Digits 0-9 |
Alphanumeric |
Alphanumeric | 5.5 | Digits, A-Z, space and $%*+-./: |
Byte |
Byte | 8 | Any text, encoded with a Charset |
Kanji |
Kanji | 13 | Shift-JIS double byte characters (Japanese) |
Hanzi |
Hanzi | 13 | GB2312 double byte characters (Chinese) |
Segments are combined in a single encode call and the decoder restores them in order:
import { Byte, Encoder, Hanzi, Kanji } from '@nuintun/qrcode';
const encoder = new Encoder({ level: 'H' });
const qrcode = encoder.encode(
// Hanzi
new Hanzi('你好世界'),
// Byte
new Byte('\nhello world\n'),
// Kanji
new Kanji('こんにちは世界')
);Use the most specific mode for each part: new Numeric('1234567890') costs far less
than new Byte('1234567890').
A version determines the symbol size, from 1 (21 x 21 modules) to 40 (177 x 177 modules). By default the encoder picks the smallest version that fits your data:
const encoder = new Encoder({ version: 'Auto' }); // defaultYou can also fix a version, useful when a scanner expects a specific symbol size.
The encoder throws data too big for requested version if your data does not fit:
const encoder = new Encoder({ version: 5 });A Charset identifies how Byte segment bytes should be interpreted by the scanner.
When you use a charset other than ISO_8859_1 (the default), the encoder automatically
appends an ECI segment so the scanner knows how to decode the bytes.
The built-in encode function only supports ASCII, ISO_8859_1 and UTF_8. For other
charsets, pass a custom encode function. In Node.js you can use iconv-lite:
import iconv from 'iconv-lite';
import { Byte, Charset, Encoder } from '@nuintun/qrcode';
const encoder = new Encoder({
level: 'H',
// Custom text encode function.
encode: (content, charset) => iconv.encode(content, charset.label)
});
const qrcode = encoder.encode(new Byte('你好世界', Charset.GBK));On the decode side, the built-in decode function uses the TextDecoder API, which covers
most labels the platform supports. For charsets the platform does not support, pass a custom
decode function:
import iconv from 'iconv-lite';
import { Decoder } from '@nuintun/qrcode';
const decoder = new Decoder({
// Custom text decode function.
decode: (bytes, charset) => iconv.decode(Buffer.from(bytes), charset.label)
});FNC1 marks a symbol as a GS1 or AIM application identifier, which changes how the
scanned content should be parsed. Enable it through hints, it is appended once,
before the first segment:
const encoder = new Encoder({
level: 'H',
hints: {
// GS1 application identifiers.
fnc1: ['GS1']
// Or AIM with an indicator byte:
// fnc1: ['AIM', 0x41]
}
});The decode side reports it back via decoded.fnc1.
Encoder#encode returns an Encoded object:
| Property / Method | Description |
|---|---|
size |
Symbol size in modules (21 - 177) |
version |
Version number (1 - 40) |
level |
Error correction level (L / M / Q / H) |
mask |
Mask pattern index (0 - 7) |
get(x, y) |
Module value at coordinate, 0 (light) or 1 (dark) |
toDataURL(...) |
Render to a GIF data URL, see parameters below |
toDataURL uses the built-in GIF encoder, so it works everywhere, no canvas or DOM
required. Output size is size * moduleSize + margin * 2 pixels (the margin is in
pixels, not modules):
| Parameter | Type | Default | Description |
|---|---|---|---|
moduleSize |
number |
2 |
Pixels per module, minimum 1 |
margin |
number |
moduleSize * 4 |
Quiet zone in pixels |
foreground |
[R, G, B] |
[0, 0, 0] |
Dark module color, 0 - 255 |
background |
[R, G, B] |
[255, 255, 255] |
Light module color, 0 - 255 |
qrcode.toDataURL(4, { margin: 16, foreground: [0, 102, 204], background: [255, 255, 255] });For full control (SVG, canvas, styled modules), iterate the matrix yourself with
size and get(x, y).
Decoding is a four step pipeline. The first two steps prepare the image, the third finds candidate QR code regions, and the fourth extracts content. Failed candidates loop back to the detector until it is exhausted:
flowchart LR
subgraph Preprocess ["Step 1 - 2: Preprocess"]
direction LR
A([Image]) -->|"grayscale()"| B["Luminance<br/>Uint8Array"]
B -->|"binarize()"| C["BitMatrix<br/>black / white"]
end
subgraph Scan ["Step 3 - 4: Scan and Decode"]
direction LR
D["Detected<br/>candidate region"]
E{"decoded?"}
F["Decoded<br/>content"]
end
C ==>|"Detector#detect"| D
D -->|"Decoder#decode"| E
E -->|"yes"| F
E -->|"no, next(false)"| D
classDef data fill:#e8f1fb,stroke:#4a7fc1,color:#1a1a1a
classDef step fill:#eef7ec,stroke:#6aa96d,color:#1a1a1a
classDef gate fill:#fdf3e3,stroke:#c9963f,color:#1a1a1a
class A,B,C,F data
class D step
class E gate
Steps 1 and 2 are pure functions exported from the package. They accept any
ImageData-like object (in Node.js, any object with data, width and height).
Detector#detect is a generator that yields every QR code region it finds as a
Detected object. A region is only a candidate and may fail to decode, so the
generator asks you to report success back through next(succeed):
import { binarize, Decoder, Detector, grayscale } from '@nuintun/qrcode';
const image = new Image();
image.crossOrigin = 'anonymous';
image.addEventListener('load', () => {
const { width, height } = image;
const canvas = new OffscreenCanvas(width, height);
const context = canvas.getContext('2d')!;
context.drawImage(image, 0, 0);
const luminances = grayscale(context.getImageData(0, 0, width, height));
const binarized = binarize(luminances, width, height);
const detector = new Detector();
const detected = detector.detect(binarized);
const decoder = new Decoder();
let current = detected.next();
while (!current.done) {
let succeed = false;
try {
const decoded = decoder.decode(current.value.matrix);
console.log(decoded.content);
succeed = true;
} catch {
// Decode failed, skipping...
}
// Notice: pass succeed to next() is very important,
// this can significantly reduce the number of detections.
current = detected.next(succeed);
}
});
image.src = 'https://nuintun.github.io/qrcode/public/images/qrcode.jpg';A Detected object describes where the QR code sits in the original image:
| Property / Method | Description |
|---|---|
matrix |
The perspective corrected symbol matrix, pass it to the Decoder |
size |
Symbol size in modules |
moduleSize |
Estimated module size in image pixels |
finder |
Three finder patterns: topLeft, topRight, bottomLeft |
alignment |
Alignment pattern (undefined for versions without it) |
mapping(x, y) |
Maps symbol coordinates to image coordinates |
Use mapping to overlay highlights on the image, for example the four outer corners
and the timing patterns:
const detect = current.value;
const { size, finder, alignment } = detect;
const { topLeft, topRight, bottomLeft } = finder;
// Corners
const topLeftCorner = detect.mapping(0, 0);
const topRightCorner = detect.mapping(size, 0);
const bottomRightCorner = detect.mapping(size, size);
const bottomLeftCorner = detect.mapping(0, size);
// Timing
const topLeftTiming = detect.mapping(6.5, 6.5);
const topRightTiming = detect.mapping(size - 6.5, 6.5);
const bottomLeftTiming = detect.mapping(6.5, size - 6.5);The Detector accepts one option, strict, which tightens finder pattern ratio
checking. Leave it off for tolerant scanning; enable it to reject marginal candidates:
const detector = new Detector({ strict: true });Decoder#decode takes the sampled matrix of a Detected object (or any BitMatrix
you built yourself) and returns a Decoded object. When parsing fails, it retries
automatically in mirror mode and reports that through mirror:
| Property | Description |
|---|---|
content |
Decoded text content |
version |
Version number (1 - 40) |
level |
Error correction level (L / M / Q / H) |
mask |
Mask pattern index (0 - 7) |
mirror |
Whether the symbol was decoded mirrored |
corrected |
Number of error corrected codewords, a hint of image quality |
symbology |
AIM symbology identifier, ]Q + modifier |
fnc1 |
['GS1'] or ['AIM', indicator] if present, otherwise false |
structured |
{ index, count, parity } for structured append, otherwise false |
codewords |
Raw data codewords, for advanced processing |
Structured append spreads one logical message across several symbols. Collect
structured results across scans and join them by index and count:
if (decoded.structured) {
const { index, count, parity } = decoded.structured;
console.log(`part ${index + 1} of ${count}`);
}declare type Level = 'L' | 'M' | 'Q' | 'H';
export class Charset {
public static readonly CP437: Charset;
public static readonly ISO_8859_1: Charset;
public static readonly ISO_8859_2: Charset;
public static readonly ISO_8859_3: Charset;
public static readonly ISO_8859_4: Charset;
public static readonly ISO_8859_5: Charset;
public static readonly ISO_8859_6: Charset;
public static readonly ISO_8859_7: Charset;
public static readonly ISO_8859_8: Charset;
public static readonly ISO_8859_9: Charset;
public static readonly ISO_8859_10: Charset;
public static readonly ISO_8859_11: Charset;
public static readonly ISO_8859_13: Charset;
public static readonly ISO_8859_14: Charset;
public static readonly ISO_8859_15: Charset;
public static readonly ISO_8859_16: Charset;
public static readonly SHIFT_JIS: Charset;
public static readonly CP1250: Charset;
public static readonly CP1251: Charset;
public static readonly CP1252: Charset;
public static readonly CP1256: Charset;
public static readonly UTF_16BE: Charset;
public static readonly UTF_8: Charset;
public static readonly ASCII: Charset;
public static readonly BIG5: Charset;
public static readonly GB2312: Charset;
public static readonly EUC_KR: Charset;
public static readonly GBK: Charset;
public static readonly GB18030: Charset;
public static readonly UTF_16LE: Charset;
public static readonly UTF_32BE: Charset;
public static readonly UTF_32LE: Charset;
public static readonly ISO_646_INV: Charset;
public static readonly BINARY: Charset;
public constructor(label: string, ...values: number[]);
public get label(): string;
public get values(): readonly number[];
}
declare type FNC1 = readonly [mode: 'GS1'] | readonly [mode: 'AIM', indicator: number];export class Numeric {
public constructor(content: string);
}
export class Alphanumeric {
public constructor(content: string);
}
export class Byte {
public constructor(content: string, charset?: Charset);
}
export class Hanzi {
public constructor(content: string);
}
export class Kanji {
public constructor(content: string);
}
export interface EncoderOptions {
level?: Level;
hints?: { fnc1?: FNC1 };
version?: 'Auto' | number;
encode?: (content: string, charset: Charset) => Uint8Array;
}
declare interface DataURLOptions {
margin?: number;
foreground?: [R: number, G: number, B: number];
background?: [R: number, G: number, B: number];
}
export class Encoded {
public get size(): number;
public get mask(): number;
public get level(): Level;
public get version(): number;
public get(x: number, y: number): 0 | 1;
public toDataURL(moduleSize?: number, options?: DataURLOptions): string;
}
export class Encoder {
public constructor(options?: EncoderOptions);
public encode(...segments: (Alphanumeric | Byte | Hanzi | Kanji | Numeric)[]): Encoded;
}export class BitMatrix {
public constructor(width: number, height: number, bits?: Int32Array);
public get width(): number;
public get height(): number;
public set(x: number, y: number): void;
public get(x: number, y: number): 0 | 1;
public flip(): void;
public flip(x: number, y: number): void;
public clone(): BitMatrix;
public setRegion(left: number, top: number, width: number, height: number): void;
}
export class Point {
public get x(): number;
public get y(): number;
}
export class Pattern extends Point {
public get moduleSize(): number;
}
declare class FinderPatternGroup {
public get topLeft(): Pattern;
public get topRight(): Pattern;
public get bottomLeft(): Pattern;
}
export class Detected {
public get matrix(): BitMatrix;
public get finder(): FinderPatternGroup;
public get alignment(): Pattern | undefined;
public get size(): number;
public get moduleSize(): number;
public mapping(x: number, y: number): Point;
}
declare interface Structured {
readonly index: number;
readonly count: number;
readonly parity: number;
}
export class Decoded {
public get mask(): number;
public get level(): Level;
public get version(): number;
public get mirror(): boolean;
public get content(): string;
public get corrected(): number;
public get symbology(): string;
public get fnc1(): FNC1 | false;
public get codewords(): Uint8Array;
public get structured(): Structured | false;
}
export function grayscale(imageData: ImageData): Uint8Array;
export function binarize(luminances: Uint8Array, width: number, height: number): BitMatrix;
export interface DetectorOptions {
strict?: boolean;
}
export class Detector {
public constructor(options?: DetectorOptions);
public detect(binarized: BitMatrix): Generator<Detected, void, boolean>;
}
export interface DecoderOptions {
decode?: (bytes: Uint8Array, charset: Charset) => string;
}
export class Decoder {
public constructor(options?: DecoderOptions);
public decode(matrix: BitMatrix): Decoded;
}