// Vendored & modernized from https://github.com/kazuhikoarase/qrcode-generator
// Original: QR Code Generator for JavaScript, Copyright (c) 2009 Kazuhiko Arase, MIT license.
// The word 'QR Code' is a registered trademark of DENSO WAVE INCORPORATED.
//
// Reason for vendoring: the upstream package is unmaintained, ships as legacy closure-based
// JS, and carries a lot of dead weight (GIF/LZW encoder, base64 streams, HTML
renderer,
// Kanji/SJIS conversion tables). This is a compact, typed, tree-shakeable rewrite that keeps the
// proven core (Galois-field math, Reed-Solomon ECC, masking, data encoding) and drops everything
// that modern browsers/Node make trivial:
// - Byte mode now encodes UTF-8 via TextEncoder (the original mangled non-ASCII via `c & 0xff`).
// - Output is SVG (string or data URL), ASCII (terminal), or a 2d canvas context - no GIF/LZW.
//
// The generated module matrix is byte-for-byte identical to the upstream library (verified by
// differential tests against qrcode-generator@2.0.4), so any scanner-validated output stays valid.
// oxlint-disable no-bitwise, prefer-math-trunc -- QR encoding (Galois field, BCH, bit packing) is
// inherently bitwise; `(1 << 0)` terms mirror the spec's generator polynomials
///
/**
* Error correction level, ordered by recovery capacity (and overhead):
* L ~7%, M ~15%, Q ~25%, H ~30%.
*/
export type QrErrorCorrectionLevel = 'L' | 'M' | 'Q' | 'H'
/**
* Encoding mode for the payload.
* - `numeric`: digits only (most compact)
* - `alphanumeric`: 0-9 A-Z and ` $%*+-./:` (uppercase only)
* - `byte`: any string, encoded as UTF-8
*
* When omitted, the smallest applicable mode is auto-detected.
*/
export type QrMode = 'numeric' | 'alphanumeric' | 'byte'
export interface QrCodeOptions {
/**
* Error correction level. Default: `M`.
*/
ecl?: QrErrorCorrectionLevel
/**
* QR version 1..40 (matrix grows by 4 modules per step: v1=21x21, v40=177x177).
* Default: `0` = auto-select the smallest version that fits the payload.
*/
typeNumber?: number
/**
* Force an encoding mode. Default: auto-detect (`numeric` < `alphanumeric` < `byte`).
* Ignored when the content is a `Uint8Array` (always `byte`).
*/
mode?: QrMode
}
export interface QrSvgOptions {
/** Pixels per module (the rendered `width`/`height` = `(size + border*2) * scale`). Default `4`. */
scale?: number
/** Quiet-zone width in modules. The spec recommends `4`. Default `4`. */
border?: number
/** Color of dark modules. Default `#000000`. */
dark?: string
/** Color of light modules / background. Default `#ffffff`. */
light?: string
}
export interface QrAsciiOptions {
/** Quiet-zone width in modules. Default `2`. */
border?: number
/** Swap dark/light glyphs (useful on dark terminal backgrounds). Default `false`. */
invert?: boolean
}
export interface QrCanvasOptions {
/** Pixels per module. Default `4`. */
scale?: number
/** Quiet-zone width in modules. Default `4`. */
border?: number
/** Color of dark modules. Default `#000000`. */
dark?: string
/** Color of light modules. Default `#ffffff`. */
light?: string
}
/**
* Create a QR code from a string or raw bytes.
*
* @example
* createQrCode('https://example.com').toDataUrl()
* createQrCode('HELLO', { ecl: 'H' }).toSvg({ scale: 8 })
*/
export function createQrCode(content: string | Uint8Array, opt: QrCodeOptions = {}): QrCode {
const ecl = opt.ecl ?? 'M'
const segment = makeSegment(content, opt.mode)
const { size, modules } = generate(segment, opt.typeNumber ?? 0, ecl)
return new QrCode(size, modules, ecl)
}
/**
* An immutable QR code: a square matrix of dark/light modules, plus renderers.
*/
export class QrCode {
constructor(
/** Width/height of the matrix in modules (`typeNumber * 4 + 17`). */
readonly size: number,
/** `modules[row][col]` - `true` = dark. */
readonly modules: readonly boolean[][],
/** Error correction level used. */
readonly ecl: QrErrorCorrectionLevel,
) {}
/** Whether the module at `[row, col]` is dark. */
isDark(row: number, col: number): boolean {
return this.modules[row]![col] === true
}
/**
* Render as a standalone SVG document string.
*/
toSvg(opt: QrSvgOptions = {}): string {
const { scale = 4, border = 4, dark = '#000000', light = '#ffffff' } = opt
const dim = this.size + border * 2
const px = dim * scale
let path = ''
for (let row = 0; row < this.size; row++) {
for (let col = 0; col < this.size; col++) {
if (this.modules[row]![col]) {
// a 1x1 module rect, in module-unit coordinates (the viewBox scales it up)
path += `M${col + border},${row + border}h1v1h-1z`
}
}
}
return (
``
)
}
/**
* Render as an `data:image/svg+xml` URL, ready for `` or CSS `background`.
*/
toDataUrl(opt: QrSvgOptions = {}): string {
return `data:image/svg+xml,${encodeURIComponent(this.toSvg(opt))}`
}
/**
* Render as ASCII art using block characters - handy for terminals and logs.
* Each module is 2 chars wide so the output keeps a square aspect ratio.
*/
toAscii(opt: QrAsciiOptions = {}): string {
const { border = 2, invert = false } = opt
const darkCell = invert ? ' ' : '██'
const lightCell = invert ? '██' : ' '
const lines: string[] = []
for (let row = -border; row < this.size + border; row++) {
let line = ''
for (let col = -border; col < this.size + border; col++) {
const dark =
row >= 0 && row < this.size && col >= 0 && col < this.size && this.modules[row]![col]
line += dark ? darkCell : lightCell
}
lines.push(line)
}
return lines.join('\n')
}
/** Same as {@link toAscii} with defaults. */
toString(): string {
return this.toAscii()
}
/**
* Paint the QR code onto a 2d canvas context (browser).
*/
renderToCanvas(ctx: CanvasRenderingContext2D, opt: QrCanvasOptions = {}): void {
const { scale = 4, border = 4, dark = '#000000', light = '#ffffff' } = opt
const px = (this.size + border * 2) * scale
ctx.fillStyle = light
ctx.fillRect(0, 0, px, px)
ctx.fillStyle = dark
for (let row = 0; row < this.size; row++) {
for (let col = 0; col < this.size; col++) {
if (this.modules[row]![col]) {
ctx.fillRect((col + border) * scale, (row + border) * scale, scale, scale)
}
}
}
}
}
// --- internals -------------------------------------------------------------
const MODE_NUMERIC = 1 << 0
const MODE_ALPHANUMERIC = 1 << 1
const MODE_BYTE = 1 << 2
const PAD0 = 0xec
const PAD1 = 0x11
/** ECC level as encoded in the format-info bits. */
const ECL_BITS: Record = { L: 1, M: 0, Q: 3, H: 2 }
/** ECC level as a row offset into the RS block table (ordered L, M, Q, H per version). */
const ECL_OFFSET: Record = { L: 0, M: 1, Q: 2, H: 3 }
const NUMERIC_RE = /^\d+$/
const ALPHANUMERIC_RE = /^[\dA-Z $%*+./:-]+$/
const ALPHANUMERIC_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'
const textEncoder = /* @__PURE__ */ new TextEncoder()
/**
* A single encoded data segment (one mode + payload). The current API always produces exactly
* one segment, but the matrix builder is written against a list to mirror the spec.
*/
interface QrSegment {
mode: number
/** Character count as written into the length field (digits/chars for text, bytes for byte mode). */
length: number
write: (bb: BitBuffer) => void
}
function makeSegment(content: string | Uint8Array, mode: QrMode | undefined): QrSegment {
if (typeof content !== 'string') return byteSegment(content)
const resolved = mode ?? detectMode(content)
if (resolved === 'numeric') return numericSegment(content)
if (resolved === 'alphanumeric') return alphanumericSegment(content)
return byteSegment(textEncoder.encode(content))
}
function detectMode(s: string): QrMode {
if (NUMERIC_RE.test(s)) return 'numeric'
if (ALPHANUMERIC_RE.test(s)) return 'alphanumeric'
return 'byte'
}
function numericSegment(data: string): QrSegment {
return {
mode: MODE_NUMERIC,
length: data.length,
write(bb) {
let i = 0
// 3 digits -> 10 bits
for (; i + 2 < data.length; i += 3) {
bb.put(Number(data.slice(i, i + 3)), 10)
}
// tail: 2 digits -> 7 bits, 1 digit -> 4 bits
if (data.length - i === 2) {
bb.put(Number(data.slice(i, i + 2)), 7)
} else if (data.length - i === 1) {
bb.put(Number(data.slice(i, i + 1)), 4)
}
},
}
}
function alphanumericSegment(data: string): QrSegment {
return {
mode: MODE_ALPHANUMERIC,
length: data.length,
write(bb) {
let i = 0
// 2 chars -> 11 bits (c0 * 45 + c1)
for (; i + 1 < data.length; i += 2) {
bb.put(alphanumericCode(data[i]!) * 45 + alphanumericCode(data[i + 1]!), 11)
}
// tail: 1 char -> 6 bits
if (i < data.length) {
bb.put(alphanumericCode(data[i]!), 6)
}
},
}
}
function alphanumericCode(c: string): number {
const code = ALPHANUMERIC_CHARS.indexOf(c)
if (code === -1) throw new Error(`qr: illegal alphanumeric char: ${c}`)
return code
}
function byteSegment(bytes: Uint8Array): QrSegment {
return {
mode: MODE_BYTE,
length: bytes.length,
write(bb) {
for (const b of bytes) bb.put(b, 8)
},
}
}
/**
* Build the final module matrix: resolve version, encode data, then pick the mask pattern with the
* lowest penalty score (matching the reference scoring, incl. tie-break to the lowest mask index).
*/
function generate(
segment: QrSegment,
typeNumber: number,
ecl: QrErrorCorrectionLevel,
): { size: number; modules: boolean[][] } {
const resolvedType = typeNumber >= 1 ? typeNumber : bestTypeNumber(segment, ecl)
const data = createData(resolvedType, ecl, segment)
let bestMask = 0
let minLostPoint = Number.POSITIVE_INFINITY
for (let mask = 0; mask < 8; mask++) {
// scoring uses "test" modules (format/version bits blanked), as the original does
const testModules = renderModules(resolvedType, ecl, data, mask, true)
const lostPoint = getLostPoint(testModules)
if (lostPoint < minLostPoint) {
minLostPoint = lostPoint
bestMask = mask
}
}
const modules = renderModules(resolvedType, ecl, data, bestMask, false)
return { size: modules.length, modules }
}
/** Smallest version (1..40) whose data capacity fits the segment at the given ECC level. */
function bestTypeNumber(segment: QrSegment, ecl: QrErrorCorrectionLevel): number {
for (let type = 1; type <= 40; type++) {
const bb = new BitBuffer()
writeSegment(bb, segment, type)
let totalDataCount = 0
for (const block of getRsBlocks(type, ecl)) totalDataCount += block.dataCount
if (bb.lengthInBits <= totalDataCount * 8) return type
}
throw new Error('qr: data too long for a single QR code')
}
function renderModules(
typeNumber: number,
ecl: QrErrorCorrectionLevel,
data: number[],
maskPattern: number,
test: boolean,
): boolean[][] {
const size = typeNumber * 4 + 17
// `undefined` = "not yet filled" (function patterns must not be overwritten by data/timing)
const modules: (boolean | undefined)[][] = Array.from({ length: size }, () =>
new Array(size).fill(undefined),
)
setupPositionProbePattern(modules, 0, 0)
setupPositionProbePattern(modules, size - 7, 0)
setupPositionProbePattern(modules, 0, size - 7)
setupPositionAdjustPattern(modules, typeNumber)
setupTimingPattern(modules)
setupTypeInfo(modules, test, maskPattern, ecl)
if (typeNumber >= 7) {
setupTypeNumber(modules, test, typeNumber)
}
mapData(modules, data, maskPattern)
// every cell is filled by now; coerce the sentinel away
return modules as boolean[][]
}
function setupPositionProbePattern(
modules: (boolean | undefined)[][],
row: number,
col: number,
): void {
const size = modules.length
for (let r = -1; r <= 7; r++) {
if (row + r <= -1 || size <= row + r) continue
for (let c = -1; c <= 7; c++) {
if (col + c <= -1 || size <= col + c) continue
modules[row + r]![col + c] =
(r >= 0 && r <= 6 && (c === 0 || c === 6)) ||
(c >= 0 && c <= 6 && (r === 0 || r === 6)) ||
(r >= 2 && r <= 4 && c >= 2 && c <= 4)
}
}
}
function setupPositionAdjustPattern(modules: (boolean | undefined)[][], typeNumber: number): void {
const pos = PATTERN_POSITION_TABLE[typeNumber - 1]!
for (const row of pos) {
for (const col of pos) {
if (modules[row]![col] !== undefined) continue
for (let r = -2; r <= 2; r++) {
for (let c = -2; c <= 2; c++) {
modules[row + r]![col + c] =
r === -2 || r === 2 || c === -2 || c === 2 || (r === 0 && c === 0)
}
}
}
}
}
function setupTimingPattern(modules: (boolean | undefined)[][]): void {
const size = modules.length
for (let r = 8; r < size - 8; r++) {
if (modules[r]![6] === undefined) modules[r]![6] = r % 2 === 0
}
for (let c = 8; c < size - 8; c++) {
if (modules[6]![c] === undefined) modules[6]![c] = c % 2 === 0
}
}
function setupTypeInfo(
modules: (boolean | undefined)[][],
test: boolean,
maskPattern: number,
ecl: QrErrorCorrectionLevel,
): void {
const size = modules.length
const data = (ECL_BITS[ecl] << 3) | maskPattern
const bits = getBchTypeInfo(data)
for (let i = 0; i < 15; i++) {
const mod = !test && ((bits >> i) & 1) === 1
// vertical
if (i < 6) {
modules[i]![8] = mod
} else if (i < 8) {
modules[i + 1]![8] = mod
} else {
modules[size - 15 + i]![8] = mod
}
// horizontal
if (i < 8) {
modules[8]![size - i - 1] = mod
} else if (i < 9) {
modules[8]![15 - i - 1 + 1] = mod
} else {
modules[8]![15 - i - 1] = mod
}
}
// fixed dark module
modules[size - 8]![8] = !test
}
function setupTypeNumber(
modules: (boolean | undefined)[][],
test: boolean,
typeNumber: number,
): void {
const size = modules.length
const bits = getBchTypeNumber(typeNumber)
for (let i = 0; i < 18; i++) {
const mod = !test && ((bits >> i) & 1) === 1
modules[Math.floor(i / 3)]![(i % 3) + size - 8 - 3] = mod
modules[(i % 3) + size - 8 - 3]![Math.floor(i / 3)] = mod
}
}
function mapData(modules: (boolean | undefined)[][], data: number[], maskPattern: number): void {
const size = modules.length
let inc = -1
let row = size - 1
let bitIndex = 7
let byteIndex = 0
const maskFunc = MASK_FUNCTIONS[maskPattern]!
for (let col = size - 1; col > 0; col -= 2) {
if (col === 6) col -= 1
for (;;) {
for (let c = 0; c < 2; c++) {
if (modules[row]![col - c] === undefined) {
let dark = false
if (byteIndex < data.length) {
dark = ((data[byteIndex]! >>> bitIndex) & 1) === 1
}
if (maskFunc(row, col - c)) dark = !dark
modules[row]![col - c] = dark
bitIndex--
if (bitIndex === -1) {
byteIndex++
bitIndex = 7
}
}
}
row += inc
if (row < 0 || size <= row) {
row -= inc
inc = -inc
break
}
}
}
}
/** Mask penalty score - lower is better. Sum of the 4 penalty rules from the QR spec. */
function getLostPoint(modules: boolean[][]): number {
return (
lostPointAdjacent(modules) +
lostPointBlocks(modules) +
lostPointFinderLike(modules) +
lostPointDarkRatio(modules)
)
}
/** LEVEL 1: runs of same-colored modules in a 3x3 neighbourhood. */
function lostPointAdjacent(modules: boolean[][]): number {
const size = modules.length
let lostPoint = 0
for (let row = 0; row < size; row++) {
for (let col = 0; col < size; col++) {
let sameCount = 0
const dark = modules[row]![col]
for (let r = -1; r <= 1; r++) {
if (row + r < 0 || size <= row + r) continue
for (let c = -1; c <= 1; c++) {
if (col + c < 0 || size <= col + c) continue
if (r === 0 && c === 0) continue
if (dark === modules[row + r]![col + c]) sameCount++
}
}
if (sameCount > 5) lostPoint += 3 + sameCount - 5
}
}
return lostPoint
}
/** LEVEL 2: 2x2 blocks of one color. */
function lostPointBlocks(modules: boolean[][]): number {
const size = modules.length
let lostPoint = 0
for (let row = 0; row < size - 1; row++) {
for (let col = 0; col < size - 1; col++) {
let count = 0
if (modules[row]![col]) count++
if (modules[row + 1]![col]) count++
if (modules[row]![col + 1]) count++
if (modules[row + 1]![col + 1]) count++
if (count === 0 || count === 4) lostPoint += 3
}
}
return lostPoint
}
/** LEVEL 3: finder-like 1:1:3:1:1 patterns, horizontal and vertical. */
function lostPointFinderLike(modules: boolean[][]): number {
const size = modules.length
let lostPoint = 0
const isDark = (r: number, c: number): boolean => modules[r]![c] === true
for (let row = 0; row < size; row++) {
for (let col = 0; col < size - 6; col++) {
if (
isDark(row, col) &&
!isDark(row, col + 1) &&
isDark(row, col + 2) &&
isDark(row, col + 3) &&
isDark(row, col + 4) &&
!isDark(row, col + 5) &&
isDark(row, col + 6)
) {
lostPoint += 40
}
}
}
for (let col = 0; col < size; col++) {
for (let row = 0; row < size - 6; row++) {
if (
isDark(row, col) &&
!isDark(row + 1, col) &&
isDark(row + 2, col) &&
isDark(row + 3, col) &&
isDark(row + 4, col) &&
!isDark(row + 5, col) &&
isDark(row + 6, col)
) {
lostPoint += 40
}
}
}
return lostPoint
}
/** LEVEL 4: deviation of the dark-module ratio from 50%. */
function lostPointDarkRatio(modules: boolean[][]): number {
const size = modules.length
let darkCount = 0
for (let col = 0; col < size; col++) {
for (let row = 0; row < size; row++) {
if (modules[row]![col]) darkCount++
}
}
const ratio = Math.abs((100 * darkCount) / size / size - 50) / 5
return ratio * 10
}
// --- data encoding (ECC) ---------------------------------------------------
function createData(typeNumber: number, ecl: QrErrorCorrectionLevel, segment: QrSegment): number[] {
const rsBlocks = getRsBlocks(typeNumber, ecl)
const bb = new BitBuffer()
writeSegment(bb, segment, typeNumber)
let totalDataCount = 0
for (const block of rsBlocks) totalDataCount += block.dataCount
if (bb.lengthInBits > totalDataCount * 8) {
throw new Error(`qr: code length overflow (${bb.lengthInBits} > ${totalDataCount * 8})`)
}
// terminator
if (bb.lengthInBits + 4 <= totalDataCount * 8) bb.put(0, 4)
// pad to byte boundary
while (bb.lengthInBits % 8 !== 0) bb.putBit(false)
// pad with alternating PAD0/PAD1 to capacity
for (;;) {
if (bb.lengthInBits >= totalDataCount * 8) break
bb.put(PAD0, 8)
if (bb.lengthInBits >= totalDataCount * 8) break
bb.put(PAD1, 8)
}
return createBytes(bb, rsBlocks)
}
function writeSegment(bb: BitBuffer, segment: QrSegment, typeNumber: number): void {
bb.put(segment.mode, 4)
bb.put(segment.length, getLengthInBits(segment.mode, typeNumber))
segment.write(bb)
}
/** Interleave data & error-correction codewords across the RS blocks. */
function createBytes(buffer: BitBuffer, rsBlocks: RsBlock[]): number[] {
let offset = 0
let maxDcCount = 0
let maxEcCount = 0
const dcdata: number[][] = new Array(rsBlocks.length)
const ecdata: number[][] = new Array(rsBlocks.length)
const src = buffer.buffer
for (let r = 0; r < rsBlocks.length; r++) {
const dcCount = rsBlocks[r]!.dataCount
const ecCount = rsBlocks[r]!.totalCount - dcCount
maxDcCount = Math.max(maxDcCount, dcCount)
maxEcCount = Math.max(maxEcCount, ecCount)
const dc = new Array(dcCount)
for (let i = 0; i < dcCount; i++) dc[i] = 0xff & src[i + offset]!
dcdata[r] = dc
offset += dcCount
const rsPoly = getErrorCorrectPolynomial(ecCount)
const modPoly = polyMod(newPolynomial(dc, rsPoly.length - 1), rsPoly)
const ec = new Array(rsPoly.length - 1)
for (let i = 0; i < ec.length; i++) {
const modIndex = i + modPoly.length - ec.length
ec[i] = modIndex >= 0 ? modPoly[modIndex]! : 0
}
ecdata[r] = ec
}
let totalCodeCount = 0
for (const block of rsBlocks) totalCodeCount += block.totalCount
const data = new Array(totalCodeCount)
let index = 0
for (let i = 0; i < maxDcCount; i++) {
for (let r = 0; r < rsBlocks.length; r++) {
if (i < dcdata[r]!.length) data[index++] = dcdata[r]![i]!
}
}
for (let i = 0; i < maxEcCount; i++) {
for (let r = 0; r < rsBlocks.length; r++) {
if (i < ecdata[r]!.length) data[index++] = ecdata[r]![i]!
}
}
return data
}
/** Bits in the character-count field, by mode, for version ranges [1-9], [10-26], [27-40]. */
const LENGTH_BITS: Record = {
[MODE_NUMERIC]: [10, 12, 14],
[MODE_ALPHANUMERIC]: [9, 11, 13],
[MODE_BYTE]: [8, 16, 16],
}
/** Number of bits in the character-count field, per mode & version range. */
function getLengthInBits(mode: number, type: number): number {
if (type < 1 || type > 40) throw new Error(`qr: bad type number: ${type}`)
const bits = LENGTH_BITS[mode]
if (!bits) throw new Error(`qr: bad mode: ${mode}`)
const range = type < 10 ? 0 : type < 27 ? 1 : 2
return bits[range]
}
// --- Reed-Solomon polynomials over GF(256) ---------------------------------
/** Drop leading zeros, then right-pad with `shift` zeros. */
function newPolynomial(num: number[], shift: number): number[] {
let offset = 0
while (offset < num.length && num[offset] === 0) offset++
const poly = new Array(num.length - offset + shift).fill(0)
for (let i = 0; i < num.length - offset; i++) poly[i] = num[i + offset]!
return poly
}
function polyMultiply(a: number[], b: number[]): number[] {
const num = new Array(a.length + b.length - 1).fill(0)
for (let i = 0; i < a.length; i++) {
for (let j = 0; j < b.length; j++) {
num[i + j]! ^= gexp(glog(a[i]!) + glog(b[j]!))
}
}
return newPolynomial(num, 0)
}
function polyMod(a: number[], b: number[]): number[] {
if (a.length - b.length < 0) return a
const ratio = glog(a[0]!) - glog(b[0]!)
const num = a.slice()
for (let i = 0; i < b.length; i++) {
num[i]! ^= gexp(glog(b[i]!) + ratio)
}
// recurse until degree drops below the divisor
return polyMod(newPolynomial(num, 0), b)
}
function getErrorCorrectPolynomial(ecLength: number): number[] {
let poly = [1]
for (let i = 0; i < ecLength; i++) {
poly = polyMultiply(poly, [1, gexp(i)])
}
return poly
}
// --- BCH codes (format & version info) -------------------------------------
const G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0)
const G18 = (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0)
const G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1)
function getBchDigit(data: number): number {
let digit = 0
while (data !== 0) {
digit++
data >>>= 1
}
return digit
}
function getBchTypeInfo(data: number): number {
let d = data << 10
while (getBchDigit(d) - getBchDigit(G15) >= 0) {
d ^= G15 << (getBchDigit(d) - getBchDigit(G15))
}
return ((data << 10) | d) ^ G15_MASK
}
function getBchTypeNumber(data: number): number {
let d = data << 12
while (getBchDigit(d) - getBchDigit(G18) >= 0) {
d ^= G18 << (getBchDigit(d) - getBchDigit(G18))
}
return (data << 12) | d
}
// --- Galois field GF(256) math ---------------------------------------------
const EXP_TABLE = new Uint8Array(256)
const LOG_TABLE = new Uint8Array(256)
for (let i = 0; i < 8; i++) EXP_TABLE[i] = 1 << i
for (let i = 8; i < 256; i++) {
EXP_TABLE[i] = EXP_TABLE[i - 4]! ^ EXP_TABLE[i - 5]! ^ EXP_TABLE[i - 6]! ^ EXP_TABLE[i - 8]!
}
for (let i = 0; i < 255; i++) LOG_TABLE[EXP_TABLE[i]!] = i
function glog(n: number): number {
if (n < 1) throw new Error(`qr: glog(${n})`)
return LOG_TABLE[n]!
}
function gexp(n: number): number {
while (n < 0) n += 255
while (n >= 256) n -= 255
return EXP_TABLE[n]!
}
// --- bit buffer ------------------------------------------------------------
class BitBuffer {
readonly buffer: number[] = []
lengthInBits = 0
put(num: number, length: number): void {
for (let i = 0; i < length; i++) {
this.putBit(((num >>> (length - i - 1)) & 1) === 1)
}
}
putBit(bit: boolean): void {
const bufIndex = Math.floor(this.lengthInBits / 8)
if (this.buffer.length <= bufIndex) this.buffer.push(0)
if (bit) this.buffer[bufIndex]! |= 0x80 >>> (this.lengthInBits % 8)
this.lengthInBits++
}
}
// --- RS block & alignment-pattern tables -----------------------------------
interface RsBlock {
totalCount: number
dataCount: number
}
function getRsBlocks(typeNumber: number, ecl: QrErrorCorrectionLevel): RsBlock[] {
const rsBlock = RS_BLOCK_TABLE[(typeNumber - 1) * 4 + ECL_OFFSET[ecl]]
if (!rsBlock) {
throw new Error(`qr: bad rs block @ typeNumber:${typeNumber}/ecl:${ecl}`)
}
const blocks: RsBlock[] = []
// each row is a flat list of [count, totalCount, dataCount] triples
for (let i = 0; i < rsBlock.length; i += 3) {
const count = rsBlock[i]!
const totalCount = rsBlock[i + 1]!
const dataCount = rsBlock[i + 2]!
for (let j = 0; j < count; j++) blocks.push({ totalCount, dataCount })
}
return blocks
}
/** Alignment-pattern centre coordinates, indexed by `typeNumber - 1`. */
const PATTERN_POSITION_TABLE: number[][] = [
[],
[6, 18],
[6, 22],
[6, 26],
[6, 30],
[6, 34],
[6, 22, 38],
[6, 24, 42],
[6, 26, 46],
[6, 28, 50],
[6, 30, 54],
[6, 32, 58],
[6, 34, 62],
[6, 26, 46, 66],
[6, 26, 48, 70],
[6, 26, 50, 74],
[6, 30, 54, 78],
[6, 30, 56, 82],
[6, 30, 58, 86],
[6, 34, 62, 90],
[6, 28, 50, 72, 94],
[6, 26, 50, 74, 98],
[6, 30, 54, 78, 102],
[6, 28, 54, 80, 106],
[6, 32, 58, 84, 110],
[6, 30, 58, 86, 114],
[6, 34, 62, 90, 118],
[6, 26, 50, 74, 98, 122],
[6, 30, 54, 78, 102, 126],
[6, 26, 52, 78, 104, 130],
[6, 30, 56, 82, 108, 134],
[6, 34, 60, 86, 112, 138],
[6, 30, 58, 86, 114, 142],
[6, 34, 62, 90, 118, 146],
[6, 30, 54, 78, 102, 126, 150],
[6, 24, 50, 76, 102, 128, 154],
[6, 28, 54, 80, 106, 132, 158],
[6, 32, 58, 84, 110, 136, 162],
[6, 26, 54, 82, 110, 138, 166],
[6, 30, 58, 86, 114, 142, 170],
]
/** The 8 data-masking functions, indexed by mask pattern (0..7). */
const MASK_FUNCTIONS: ((i: number, j: number) => boolean)[] = [
(i, j) => (i + j) % 2 === 0,
(i, _j) => i % 2 === 0,
(_i, j) => j % 3 === 0,
(i, j) => (i + j) % 3 === 0,
(i, j) => (Math.floor(i / 2) + Math.floor(j / 3)) % 2 === 0,
(i, j) => ((i * j) % 2) + ((i * j) % 3) === 0,
(i, j) => (((i * j) % 2) + ((i * j) % 3)) % 2 === 0,
(i, j) => (((i * j) % 3) + ((i + j) % 2)) % 2 === 0,
]
/**
* RS block layout, 4 rows per version (L, M, Q, H), each a flat list of
* `[count, totalCount, dataCount]` triples. Verbatim from the QR spec.
*/
const RS_BLOCK_TABLE: number[][] = [
// 1
[1, 26, 19],
[1, 26, 16],
[1, 26, 13],
[1, 26, 9],
// 2
[1, 44, 34],
[1, 44, 28],
[1, 44, 22],
[1, 44, 16],
// 3
[1, 70, 55],
[1, 70, 44],
[2, 35, 17],
[2, 35, 13],
// 4
[1, 100, 80],
[2, 50, 32],
[2, 50, 24],
[4, 25, 9],
// 5
[1, 134, 108],
[2, 67, 43],
[2, 33, 15, 2, 34, 16],
[2, 33, 11, 2, 34, 12],
// 6
[2, 86, 68],
[4, 43, 27],
[4, 43, 19],
[4, 43, 15],
// 7
[2, 98, 78],
[4, 49, 31],
[2, 32, 14, 4, 33, 15],
[4, 39, 13, 1, 40, 14],
// 8
[2, 121, 97],
[2, 60, 38, 2, 61, 39],
[4, 40, 18, 2, 41, 19],
[4, 40, 14, 2, 41, 15],
// 9
[2, 146, 116],
[3, 58, 36, 2, 59, 37],
[4, 36, 16, 4, 37, 17],
[4, 36, 12, 4, 37, 13],
// 10
[2, 86, 68, 2, 87, 69],
[4, 69, 43, 1, 70, 44],
[6, 43, 19, 2, 44, 20],
[6, 43, 15, 2, 44, 16],
// 11
[4, 101, 81],
[1, 80, 50, 4, 81, 51],
[4, 50, 22, 4, 51, 23],
[3, 36, 12, 8, 37, 13],
// 12
[2, 116, 92, 2, 117, 93],
[6, 58, 36, 2, 59, 37],
[4, 46, 20, 6, 47, 21],
[7, 42, 14, 4, 43, 15],
// 13
[4, 133, 107],
[8, 59, 37, 1, 60, 38],
[8, 44, 20, 4, 45, 21],
[12, 33, 11, 4, 34, 12],
// 14
[3, 145, 115, 1, 146, 116],
[4, 64, 40, 5, 65, 41],
[11, 36, 16, 5, 37, 17],
[11, 36, 12, 5, 37, 13],
// 15
[5, 109, 87, 1, 110, 88],
[5, 65, 41, 5, 66, 42],
[5, 54, 24, 7, 55, 25],
[11, 36, 12, 7, 37, 13],
// 16
[5, 122, 98, 1, 123, 99],
[7, 73, 45, 3, 74, 46],
[15, 43, 19, 2, 44, 20],
[3, 45, 15, 13, 46, 16],
// 17
[1, 135, 107, 5, 136, 108],
[10, 74, 46, 1, 75, 47],
[1, 50, 22, 15, 51, 23],
[2, 42, 14, 17, 43, 15],
// 18
[5, 150, 120, 1, 151, 121],
[9, 69, 43, 4, 70, 44],
[17, 50, 22, 1, 51, 23],
[2, 42, 14, 19, 43, 15],
// 19
[3, 141, 113, 4, 142, 114],
[3, 70, 44, 11, 71, 45],
[17, 47, 21, 4, 48, 22],
[9, 39, 13, 16, 40, 14],
// 20
[3, 135, 107, 5, 136, 108],
[3, 67, 41, 13, 68, 42],
[15, 54, 24, 5, 55, 25],
[15, 43, 15, 10, 44, 16],
// 21
[4, 144, 116, 4, 145, 117],
[17, 68, 42],
[17, 50, 22, 6, 51, 23],
[19, 46, 16, 6, 47, 17],
// 22
[2, 139, 111, 7, 140, 112],
[17, 74, 46],
[7, 54, 24, 16, 55, 25],
[34, 37, 13],
// 23
[4, 151, 121, 5, 152, 122],
[4, 75, 47, 14, 76, 48],
[11, 54, 24, 14, 55, 25],
[16, 45, 15, 14, 46, 16],
// 24
[6, 147, 117, 4, 148, 118],
[6, 73, 45, 14, 74, 46],
[11, 54, 24, 16, 55, 25],
[30, 46, 16, 2, 47, 17],
// 25
[8, 132, 106, 4, 133, 107],
[8, 75, 47, 13, 76, 48],
[7, 54, 24, 22, 55, 25],
[22, 45, 15, 13, 46, 16],
// 26
[10, 142, 114, 2, 143, 115],
[19, 74, 46, 4, 75, 47],
[28, 50, 22, 6, 51, 23],
[33, 46, 16, 4, 47, 17],
// 27
[8, 152, 122, 4, 153, 123],
[22, 73, 45, 3, 74, 46],
[8, 53, 23, 26, 54, 24],
[12, 45, 15, 28, 46, 16],
// 28
[3, 147, 117, 10, 148, 118],
[3, 73, 45, 23, 74, 46],
[4, 54, 24, 31, 55, 25],
[11, 45, 15, 31, 46, 16],
// 29
[7, 146, 116, 7, 147, 117],
[21, 73, 45, 7, 74, 46],
[1, 53, 23, 37, 54, 24],
[19, 45, 15, 26, 46, 16],
// 30
[5, 145, 115, 10, 146, 116],
[19, 75, 47, 10, 76, 48],
[15, 54, 24, 25, 55, 25],
[23, 45, 15, 25, 46, 16],
// 31
[13, 145, 115, 3, 146, 116],
[2, 74, 46, 29, 75, 47],
[42, 54, 24, 1, 55, 25],
[23, 45, 15, 28, 46, 16],
// 32
[17, 145, 115],
[10, 74, 46, 23, 75, 47],
[10, 54, 24, 35, 55, 25],
[19, 45, 15, 35, 46, 16],
// 33
[17, 145, 115, 1, 146, 116],
[14, 74, 46, 21, 75, 47],
[29, 54, 24, 19, 55, 25],
[11, 45, 15, 46, 46, 16],
// 34
[13, 145, 115, 6, 146, 116],
[14, 74, 46, 23, 75, 47],
[44, 54, 24, 7, 55, 25],
[59, 46, 16, 1, 47, 17],
// 35
[12, 151, 121, 7, 152, 122],
[12, 75, 47, 26, 76, 48],
[39, 54, 24, 14, 55, 25],
[22, 45, 15, 41, 46, 16],
// 36
[6, 151, 121, 14, 152, 122],
[6, 75, 47, 34, 76, 48],
[46, 54, 24, 10, 55, 25],
[2, 45, 15, 64, 46, 16],
// 37
[17, 152, 122, 4, 153, 123],
[29, 74, 46, 14, 75, 47],
[49, 54, 24, 10, 55, 25],
[24, 45, 15, 46, 46, 16],
// 38
[4, 152, 122, 18, 153, 123],
[13, 74, 46, 32, 75, 47],
[48, 54, 24, 14, 55, 25],
[42, 45, 15, 32, 46, 16],
// 39
[20, 147, 117, 4, 148, 118],
[40, 75, 47, 7, 76, 48],
[43, 54, 24, 22, 55, 25],
[10, 45, 15, 67, 46, 16],
// 40
[19, 148, 118, 6, 149, 119],
[18, 75, 47, 31, 76, 48],
[34, 54, 24, 34, 55, 25],
[20, 45, 15, 61, 46, 16],
]