QR encoder

@opentui/qrcode includes a standalone QR Code Model 2 encoder in addition to QRCodeRenderable. Use the encoder when you need a module matrix, terminal string, SVG, raw bytes, explicit segments, GS1/FNC1, ECI, or structured append.

bun add @opentui/qrcode

Encode text

import { ErrorCorrectionLevel, QRCode } from "@opentui/qrcode"

const qr = QRCode.encodeText("https://opentui.com", ErrorCorrectionLevel.M)

console.log(`version=${qr.version} size=${qr.size} mask=${qr.mask}`)
console.log(qr.toTerminalString({ ansi: true }))

encodeText() chooses the first version in the configured range that fits, optimizes mixed text into numeric, alphanumeric, byte, and optionally Kanji segments, and automatically chooses the lowest-penalty mask unless one is forced.

The requested error-correction level defaults to M. Because boostEcl defaults to true, the resulting qr.errorCorrectionLevel may be upgraded when the same selected version has room for a stronger level.

Encoding options

interface EncodeOptions {
  minVersion?: number
  maxVersion?: number
  mask?: number
  boostEcl?: boolean
  optimize?: boolean
  kanji?: boolean
  byteEncoding?: ByteEncoding
  eciForUtf8?: boolean
  eciAssignment?: number | null
  fnc1First?: boolean
  fnc1Second?: { applicationIndicator: string | number }
  structuredAppend?: { position: number; total: number; parity: number }
}
OptionDefaultDescription
minVersion1Minimum QR Code Model 2 version, inclusive
maxVersion40Maximum version, inclusive
maskautomaticForce mask 0 through 7; otherwise choose by penalty score
boostEcltrueUpgrade ECC without increasing the selected version when data still fits
optimizetrueUse mixed-mode dynamic-programming optimization for text
kanjifalsePermit Kanji mode for supported Shift JIS characters
byteEncoding"utf-8"Encoding used by text that falls back to byte segments
eciForUtf8true for UTF-8 byte segmentsPrefix UTF-8 byte-mode data with ECI assignment 26
eciAssignmentinferredOverride the ECI before byte segments; null suppresses it
fnc1FirstfalseInsert FNC1 in first position
fnc1Second-Insert FNC1 in second position with an application indicator
structuredAppend-Insert a structured-append header

ByteEncoding is "utf-8" | "iso-8859-1" | "shift-jis". The exported EciAssignment constants are ISO_8859_1: 3, SHIFT_JIS: 20, and UTF_8: 26.

UTF-8 and Shift JIS text byte segments receive their known ECI by default. ISO-8859-1 receives no automatic ECI. Numeric, alphanumeric, and Kanji segments do not need a byte-encoding ECI. Use encodeTextBytes(text, encoding) when the encoded bytes themselves are needed.

Other encoders

All encoder methods default to error-correction level M and EncodeOptions = {} unless shown otherwise.

MethodInput and behavior
QRCode.encodeText(text, ecl?, options?)Optimized text encoding
QRCode.encodeBytes(bytes, ecl?, options?)One raw byte segment; does not add an ECI automatically
QRCode.encodeEciText(text, ecl?, options?)Byte text with six-digit escaped ECI switches
QRCode.encodeGs1Text(data, ecl?, options?)GS1 payload with FNC1 first position and no text ECI
QRCode.encodeSegments(segments, ecl?, options?)Explicit segment sequence
QRCode.encodeStructuredAppend(parts, ecl?, options?)Encode 2–16 arrays of segments with shared parity

Raw bytes and explicit ECI

For raw bytes that require an ECI designator, provide the ECI segment explicitly:

import { EciAssignment, ErrorCorrectionLevel, QRCode, QrSegment } from "@opentui/qrcode"

const qr = QRCode.encodeSegments(
  [QrSegment.makeEci(EciAssignment.ISO_8859_1), QrSegment.makeBytes([0x48, 0xe9])],
  ErrorCorrectionLevel.M,
)

QRCode.encodeEciText() and QrSegment.makeEciSegmentsFromEscapedText() recognize a backslash followed by exactly six decimal digits as an ECI switch. A doubled backslash encodes one literal backslash. The segment helper defaults to ISO-8859-1 until a known ECI changes the encoding.

const qr = QRCode.encodeEciText("\\000026Hello, 世界")

The encoder can signal any ECI assignment from 0 through 999999. It only performs text conversion for its known byte encodings; callers must provide already converted bytes for application-defined ECI transformations.

GS1/FNC1

Pass a prebuilt GS1 payload string, or let Gs1Element[] join application identifiers and data. Parentheses are not encoded. Set separatorAfter where a field separator is required before another element:

const qr = QRCode.encodeGs1Text([
  { ai: "10", data: "LOT42", separatorAfter: true },
  { ai: "17", data: "260731" },
])

For FNC1 second position, applicationIndicator accepts an integer from 0 to 99, exactly two decimal digits, or one Latin letter. fnc1First and fnc1Second cannot be used together.

Structured append

const parts = ["PART ONE", "PART TWO"].map((text) => QrSegment.makeSegments(text))
const symbols = QRCode.encodeStructuredAppend(parts)

encodeStructuredAppend() requires 2–16 parts, computes XOR parity from the source segment bytes, and adds the 1-based position and total to each symbol. For manual headers, use QrSegment.makeStructuredAppendHeader(position, total, parity) or QRCode.computeStructuredAppendParity(segments).

Segment API

QrSegment has a private constructor; create segments with its static methods:

MethodPurpose
makeNumeric(digits)Numeric segment; digits only
makeAlphanumeric(text)0-9, A-Z, space, and $%*+-./:
makeBytes(bytes)Raw byte segment
makeBytesFromText(text, encoding?)Encoded text byte segment; UTF-8 by default
makeKanji(text)QR Kanji segment from supported characters
makeKanjiFromShiftJis(bytes)QR Kanji segment from valid Shift JIS pairs
makeEci(assignment)ECI designator from 0 through 999999
makeFnc1FirstPosition()FNC1 first-position segment
makeFnc1SecondPosition(indicator)FNC1 second-position segment
makeStructuredAppendHeader(position, total, parity)Structured-append header
makeSegments(text, options?)One best whole-input mode, or byte fallback
makeOptimizedSegments(text, version, options?)Mixed-mode optimization for a specific version
makeEciSegmentsFromEscapedText(text, options?)Byte segments separated by escaped ECI switches

Mode predicates isNumeric(), isAlphanumeric(), and isKanji() are also public. segment.getTotalBits(version) returns the encoded segment size for that version, or Infinity when its character count does not fit.

Matrix and metadata

Encoded QRCode instances expose:

MemberDescription
versionModel 2 version, 1–40
sizeMatrix side length, version * 4 + 17
errorCorrectionLevelActual ECC level after optional boosting
maskSelected mask, 0–7
containsEciWhether the final segments contain ECI
fnc1"none", "first", or "second"
symbologyIdentifierAIM QR symbology identifier derived from ECI/FNC1 state
getModule(x, y)Read one module; throws outside the matrix
toMatrix()Deep-copy the matrix as boolean[][]; true is dark

QRCode.validateVersionPublic(version) validates the 1–40 range. QRCode.maskCondition(mask, x, y) exposes the mask formulas for callers that inspect encoded matrices.

Terminal output

console.log(
  qr.toTerminalString({
    border: 4,
    ansi: true,
    invert: false,
  }),
)
OptionDefaultDescription
border4Quiet zone in modules; must be an integer of at least 4
ansifalsePaint explicit black/white ANSI backgrounds
invertfalseSwap light and dark output

Each QR module occupies two terminal columns. The returned rows are joined with "\n" and have no trailing newline. A number passed directly to toTerminalString(number) is treated as the border with ANSI and inversion disabled.

SVG output

const svg = qr.toSvgString({
  border: 4,
  moduleSize: 8,
  lightColor: "#ffffff",
  darkColor: "#111827",
})

toSvgString() defaults to border 4, module size 1, light #FFFFFF, and dark #000000. The border must be an integer of at least 4. Pass a finite positive moduleSize: values less than or equal to zero throw, while the current implementation does not reject NaN or Infinity before producing unusable dimensions. Color strings are XML-escaped before insertion.

For a text-to-SVG shortcut:

import { createQrSvg, ErrorCorrectionLevel } from "@opentui/qrcode"

const svg = createQrSvg("https://opentui.com", {
  ecl: ErrorCorrectionLevel.H,
  border: 4,
  moduleSize: 8,
})

createQrSvg() defaults to ECC M, border 4, and module size 8; other encoding options are forwarded to encodeText().

Root exports and limits

The encoder exports from @opentui/qrcode are QRCode, QrSegment, ErrorCorrectionLevel, EciAssignment, encodeTextBytes, createQrSvg, and the types ByteEncoding, EncodeOptions, StructuredAppendInfo, Fnc1SecondPositionInfo, Gs1Element, and TerminalRenderOptions. The same root also exports QRCodeRenderable, QRCodeOptions, and QRCodeFitMode for OpenTUI rendering.

The implementation generates QR Code Model 2 versions 1–40. It does not generate legacy Model 1 or rMQR symbols and does not decode QR codes. Encoding throws when data cannot fit the requested version range and error-correction level. The SVG and terminal helpers enforce a minimum four-module quiet zone, but successful scanning still depends on output scale, contrast, terminal cell geometry, font, display, and camera conditions.