QR encoder

The QR code component displays a QR code in an OpenTUI render tree. The @opentui/qrcode package root owns the separate QR code Model 2 encoder.

Use the encoder when you need a module matrix, terminal string, SVG, raw-byte input, explicit segments, GS1/FNC1, extended channel interpretation (ECI), or structured append. See Package entry points for the framework registration paths.

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() selects the first version in the configured range that fits. It can optimize mixed text into numeric, alphanumeric, byte, and Kanji segments. It selects the lowest-penalty mask unless you set a mask.

ErrorCorrectionLevel exports L, M, Q, and H. The requested error-correction level defaults to M. Because boostEcl defaults to true, qr.errorCorrectionLevel can increase when the selected version has room.

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
  }
}
Option Default Description
minVersion 1 Minimum QR code Model 2 version, inclusive
maxVersion 40 Maximum version, inclusive
mask automatic Force mask 0 through 7. Otherwise, select by penalty score
boostEcl true Increase error correction without increasing the selected version when the data fits
optimize true Use mixed-mode dynamic programming for text
kanji false Permit Kanji mode for supported Shift JIS characters
byteEncoding "utf-8" Encode text that falls back to byte segments
eciForUtf8 true for UTF-8 byte segments Prefix UTF-8 byte data with ECI assignment 26
eciAssignment inferred Override the ECI before byte segments. null suppresses it
fnc1First false Insert FNC1 in first position
fnc1Second none Insert FNC1 in second position with an application indicator
structuredAppend none 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.

encodeText(), makeSegments(), and makeOptimizedSegments() add a known ECI when they create UTF-8 or Shift JIS byte segments. They add no automatic ECI for ISO-8859-1. Numeric, alphanumeric, and Kanji segments do not need a byte-encoding ECI.

encodeTextBytes(text, encoding) returns the encoded Uint8Array. It throws when ISO-8859-1 or Shift JIS cannot represent a character.

Other encoders#

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

Method Input and behavior
QRCode.encodeText(text, ecl?, options?) Optimized text encoding
QRCode.encodeBytes(bytes, ecl?, options?) One raw byte segment. It 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 segment arrays with shared parity

encodeBytes() accepts a Uint8Array or number array. Each number must be an integer from 0 through 255.

Raw bytes and explicit ECI#

Add the ECI segment explicitly when raw bytes need an ECI designator:

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() treat one backslash plus exactly six decimal digits as an ECI switch. Two backslashes encode one literal backslash. The segment helper uses ISO-8859-1 until a known ECI changes the encoding.

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

The encoder can signal an ECI assignment from 0 through 999999. It converts text only for its known byte encodings. Supply converted bytes for an application-defined ECI transformation.

GS1/FNC1#

encodeGs1Text() accepts a payload string or Gs1Element[]. For elements, the helper concatenates each application identifier and its data without parentheses. It doubles data percent signs and inserts a percent sign when separatorAfter is true before another element.

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

An element application identifier can contain ASCII letters and digits. For FNC1 second position, applicationIndicator accepts an integer from 0 through 99, exactly two decimal digits, or one Latin letter. fnc1First and fnc1Second cannot be active together.

Structured append#

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

encodeStructuredAppend() requires 2-16 parts. It computes XOR parity from the source segment bytes. It adds the 1-based position and total to each symbol.

For manual headers, use QrSegment.makeStructuredAppendHeader(position, total, parity). Use QRCode.computeStructuredAppendParity(segments) to calculate parity.

Segment API#

QrSegment has a private constructor. Create segments with its static methods.

Method Purpose
makeNumeric(digits) Numeric segment with digits only
makeAlphanumeric(text) 0-9, A-Z, space, and $%*+-./:
makeBytes(bytes) Raw byte segment
makeBytesFromText(text, encoding?) Encoded text byte segment. UTF-8 is the 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 one version
makeEciSegmentsFromEscapedText(text, options?) Byte segments separated by escaped ECI switches

The public mode predicates are isNumeric(), isAlphanumeric(), and isKanji(). segment.getTotalBits(version) returns the encoded segment size for that version. It returns Infinity when the character count does not fit.

makeSegments() and makeOptimizedSegments() accept eciForUtf8, kanji, byteEncoding, and eciAssignment. makeEciSegmentsFromEscapedText() accepts defaultEncoding and defaultEci. Its default encoding is ISO-8859-1. The current implementation accepts defaultEci but does not read it before an explicit ECI switch. It does not emit an initial ECI or select a known encoding. Set defaultEncoding for the initial conversion, and include an escaped ECI switch when the symbol must declare that assignment.

Matrix and metadata#

An encoded QRCode instance exposes these members:

Member Description
version Model 2 version from 1 through 40
size Matrix side length, version * 4 + 17
errorCorrectionLevel Actual error-correction level after optional boosting
mask Selected mask from 0 through 7
containsEci Whether the final segments contain ECI
fnc1 "none", "first", or "second"
symbologyIdentifier AIM QR symbology identifier from ECI and FNC1 state
getModule(x, y) Read one module. It throws outside the matrix
toMatrix() Deep-copy the matrix as boolean[][]. true is dark

The symbology identifier has this mapping:

fnc1 containsEci Identifier
"none" false ]Q1
"none" true ]Q2
"first" false ]Q3
"first" true ]Q4
"second" false ]Q5
"second" true ]Q6

QRCode.validateVersionPublic(version) validates the range from 1 through 40. QRCode.maskCondition(mask, x, y) exposes the mask formulas for matrix inspection.

Terminal output#

console.log(
  qr.toTerminalString({
    border: 4,
    ansi: true,
    invert: false,
  }),
)
Option Default Description
border 4 Quiet zone in modules. It must be an integer of at least 4
ansi false Paint explicit black and white ANSI backgrounds
invert false Swap light and dark output

Each QR module occupies two terminal columns and one output row. The returned rows use "\n" separators and have no trailing newline. A number passed directly to toTerminalString(number) sets the border and disables ANSI and inversion.

The terminal helper cannot inspect terminal cell geometry. The apparent module shape depends on the terminal’s cell width and height.

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. Color strings are XML-escaped before insertion.

Use a finite positive moduleSize for useful output. The current validation rejects only values less than or equal to zero. It does not reject NaN or Infinity, which produce non-finite SVG dimensions and coordinates. This validation gap is a known limitation.

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 error-correction level M, border 4, and module size 8. It forwards the other encoding options to encodeText().

Root exports and limits#

The @opentui/qrcode root exports these encoder values: QRCode, QrSegment, ErrorCorrectionLevel, EciAssignment, encodeTextBytes, and createQrSvg.

It exports these encoder types: ByteEncoding, EncodeOptions, StructuredAppendInfo, Fnc1SecondPositionInfo, Gs1Element, and TerminalRenderOptions.

The same root exports QRCodeRenderable, QRCodeOptions, and QRCodeFitMode for OpenTUI display. The React and Solid registration entry points re-export the package root and add registerQRCode(). Read the QR code component for display and registration. Read Package entry points for the full entry point list.

The encoder generates QR code Model 2 versions 1 through 40. It does not generate legacy Model 1 or rMQR symbols. It does not decode QR codes. Encoding throws when data cannot fit the requested version range and error-correction level.

The SVG and terminal helpers require a quiet zone of at least four modules. They do not inspect output scale, contrast, terminal cell geometry, fonts, displays, or cameras.