|
|
|
@ -0,0 +1,504 @@
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* QR code encoder — own, zero-dependency.
|
|
|
|
|
|
|
|
*
|
|
|
|
|
|
|
|
* Implements the open QR standard (ISO/IEC 18004): segment encoding →
|
|
|
|
|
|
|
|
* Reed–Solomon error correction over GF(256) → matrix layout → mask
|
|
|
|
|
|
|
|
* selection. No npm dependency — `$libs` is zero-dep. The algorithm is the
|
|
|
|
|
|
|
|
* public standard; Nayuki's reference implementation was used to cross-check
|
|
|
|
|
|
|
|
* correctness, never imported.
|
|
|
|
|
|
|
|
*
|
|
|
|
|
|
|
|
* Supports versions 1–40, all four error-correction levels, and numeric /
|
|
|
|
|
|
|
|
* alphanumeric / byte (UTF-8) modes with single-best-mode selection.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export type ErrorCorrection = 'L' | 'M' | 'Q' | 'H'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export interface QrResult {
|
|
|
|
|
|
|
|
/** Square module grid, row-major. `true` = dark module. */
|
|
|
|
|
|
|
|
matrix: boolean[][]
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Function-pattern mask, row-major. `true` = the module is a finder /
|
|
|
|
|
|
|
|
* timing / alignment / format / version module (not styleable data).
|
|
|
|
|
|
|
|
* Renderers keep these square so scanners lock on regardless of cell shape.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
reserved: boolean[][]
|
|
|
|
|
|
|
|
/** Side length in modules (`21 + 4 * (version - 1)`). */
|
|
|
|
|
|
|
|
size: number
|
|
|
|
|
|
|
|
/** Chosen QR version (1–40). */
|
|
|
|
|
|
|
|
version: number
|
|
|
|
|
|
|
|
/** Effective error-correction level used. */
|
|
|
|
|
|
|
|
errorCorrection: ErrorCorrection
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export interface EncodeOptions {
|
|
|
|
|
|
|
|
errorCorrection?: ErrorCorrection
|
|
|
|
|
|
|
|
/** Smallest version to try (1–40). @default 1 */
|
|
|
|
|
|
|
|
minVersion?: number
|
|
|
|
|
|
|
|
/** Largest version to try (1–40). @default 40 */
|
|
|
|
|
|
|
|
maxVersion?: number
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ── Error-correction level ordering + format bits ───────────────────────────
|
|
|
|
|
|
|
|
// Format-info 2-bit value per level (NOT the same as the L<M<Q<H ordering).
|
|
|
|
|
|
|
|
const ECC_ORDER: ErrorCorrection[] = ['L', 'M', 'Q', 'H']
|
|
|
|
|
|
|
|
const ECC_FORMAT_BITS: Record<ErrorCorrection, number> = { L: 1, M: 0, Q: 3, H: 2 }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const MIN_VERSION = 1
|
|
|
|
|
|
|
|
const MAX_VERSION = 40
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ── Per-(level, version) tables, indexed [eccOrdinal][version]. Index 0 unused.
|
|
|
|
|
|
|
|
// ECC codewords per block.
|
|
|
|
|
|
|
|
const ECC_CODEWORDS_PER_BLOCK: number[][] = [
|
|
|
|
|
|
|
|
// L
|
|
|
|
|
|
|
|
[-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
|
|
|
|
|
|
// M
|
|
|
|
|
|
|
|
[-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28],
|
|
|
|
|
|
|
|
// Q
|
|
|
|
|
|
|
|
[-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
|
|
|
|
|
|
// H
|
|
|
|
|
|
|
|
[-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Number of error-correction blocks.
|
|
|
|
|
|
|
|
const NUM_ERROR_CORRECTION_BLOCKS: number[][] = [
|
|
|
|
|
|
|
|
// L
|
|
|
|
|
|
|
|
[-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25],
|
|
|
|
|
|
|
|
// M
|
|
|
|
|
|
|
|
[-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49],
|
|
|
|
|
|
|
|
// Q
|
|
|
|
|
|
|
|
[-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68],
|
|
|
|
|
|
|
|
// H
|
|
|
|
|
|
|
|
[-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81]
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Total number of data + ECC codewords (8-module words) for a version. */
|
|
|
|
|
|
|
|
function getNumRawDataModules(version: number): number {
|
|
|
|
|
|
|
|
let result = (16 * version + 128) * version + 64
|
|
|
|
|
|
|
|
if (version >= 2) {
|
|
|
|
|
|
|
|
const numAlign = Math.floor(version / 7) + 2
|
|
|
|
|
|
|
|
result -= (25 * numAlign - 10) * numAlign - 55
|
|
|
|
|
|
|
|
if (version >= 7) result -= 36
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Number of 8-bit data codewords (excludes ECC) for a version + level. */
|
|
|
|
|
|
|
|
function getNumDataCodewords(version: number, ecc: ErrorCorrection): number {
|
|
|
|
|
|
|
|
const o = ECC_ORDER.indexOf(ecc)
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
|
|
Math.floor(getNumRawDataModules(version) / 8) -
|
|
|
|
|
|
|
|
ECC_CODEWORDS_PER_BLOCK[o][version] * NUM_ERROR_CORRECTION_BLOCKS[o][version]
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ── Bit buffer ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
class BitBuffer {
|
|
|
|
|
|
|
|
bits: number[] = []
|
|
|
|
|
|
|
|
append(value: number, length: number): void {
|
|
|
|
|
|
|
|
for (let i = length - 1; i >= 0; i--) this.bits.push((value >>> i) & 1)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ── Segment modes ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
const ALPHANUMERIC = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function isNumeric(text: string): boolean {
|
|
|
|
|
|
|
|
return /^[0-9]*$/.test(text)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function isAlphanumeric(text: string): boolean {
|
|
|
|
|
|
|
|
for (const ch of text) if (!ALPHANUMERIC.includes(ch)) return false
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type Mode = { indicator: number; charCountBits: [number, number, number] }
|
|
|
|
|
|
|
|
const MODE_NUMERIC: Mode = { indicator: 0x1, charCountBits: [10, 12, 14] }
|
|
|
|
|
|
|
|
const MODE_ALPHANUMERIC: Mode = { indicator: 0x2, charCountBits: [9, 11, 13] }
|
|
|
|
|
|
|
|
const MODE_BYTE: Mode = { indicator: 0x4, charCountBits: [8, 16, 16] }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function charCountBits(mode: Mode, version: number): number {
|
|
|
|
|
|
|
|
const i = version <= 9 ? 0 : version <= 26 ? 1 : 2
|
|
|
|
|
|
|
|
return mode.charCountBits[i]
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
interface Segment {
|
|
|
|
|
|
|
|
mode: Mode
|
|
|
|
|
|
|
|
numChars: number
|
|
|
|
|
|
|
|
data: BitBuffer
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function utf8Bytes(text: string): number[] {
|
|
|
|
|
|
|
|
return [...new TextEncoder().encode(text)]
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Pick the single best mode for the whole string + build its segment. */
|
|
|
|
|
|
|
|
function makeSegment(text: string): Segment {
|
|
|
|
|
|
|
|
const buf = new BitBuffer()
|
|
|
|
|
|
|
|
if (isNumeric(text)) {
|
|
|
|
|
|
|
|
for (let i = 0; i < text.length; ) {
|
|
|
|
|
|
|
|
const n = Math.min(3, text.length - i)
|
|
|
|
|
|
|
|
buf.append(parseInt(text.substring(i, i + n), 10), n * 3 + 1)
|
|
|
|
|
|
|
|
i += n
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return { mode: MODE_NUMERIC, numChars: text.length, data: buf }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isAlphanumeric(text)) {
|
|
|
|
|
|
|
|
let i = 0
|
|
|
|
|
|
|
|
for (; i + 2 <= text.length; i += 2) {
|
|
|
|
|
|
|
|
buf.append(ALPHANUMERIC.indexOf(text[i]) * 45 + ALPHANUMERIC.indexOf(text[i + 1]), 11)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (i < text.length) buf.append(ALPHANUMERIC.indexOf(text[i]), 6)
|
|
|
|
|
|
|
|
return { mode: MODE_ALPHANUMERIC, numChars: text.length, data: buf }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const bytes = utf8Bytes(text)
|
|
|
|
|
|
|
|
for (const b of bytes) buf.append(b, 8)
|
|
|
|
|
|
|
|
return { mode: MODE_BYTE, numChars: bytes.length, data: buf }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ── Reed–Solomon over GF(256), primitive 0x11D ──────────────────────────────
|
|
|
|
|
|
|
|
const GF_EXP = new Uint8Array(512)
|
|
|
|
|
|
|
|
const GF_LOG = new Uint8Array(256)
|
|
|
|
|
|
|
|
;(() => {
|
|
|
|
|
|
|
|
let x = 1
|
|
|
|
|
|
|
|
for (let i = 0; i < 255; i++) {
|
|
|
|
|
|
|
|
GF_EXP[i] = x
|
|
|
|
|
|
|
|
GF_LOG[x] = i
|
|
|
|
|
|
|
|
x <<= 1
|
|
|
|
|
|
|
|
if (x & 0x100) x ^= 0x11d
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 255; i < 512; i++) GF_EXP[i] = GF_EXP[i - 255]
|
|
|
|
|
|
|
|
})()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function gfMul(a: number, b: number): number {
|
|
|
|
|
|
|
|
if (a === 0 || b === 0) return 0
|
|
|
|
|
|
|
|
return GF_EXP[GF_LOG[a] + GF_LOG[b]]
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Generator polynomial coefficients for `degree` ECC codewords. */
|
|
|
|
|
|
|
|
function rsGenerator(degree: number): number[] {
|
|
|
|
|
|
|
|
let poly = [1]
|
|
|
|
|
|
|
|
for (let i = 0; i < degree; i++) {
|
|
|
|
|
|
|
|
const next = new Array(poly.length + 1).fill(0)
|
|
|
|
|
|
|
|
for (let j = 0; j < poly.length; j++) {
|
|
|
|
|
|
|
|
next[j] ^= gfMul(poly[j], 1)
|
|
|
|
|
|
|
|
next[j + 1] ^= gfMul(poly[j], GF_EXP[i])
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
poly = next
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return poly
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function rsRemainder(data: number[], generator: number[]): number[] {
|
|
|
|
|
|
|
|
const result = new Array(generator.length - 1).fill(0)
|
|
|
|
|
|
|
|
for (const byte of data) {
|
|
|
|
|
|
|
|
const factor = byte ^ result[0]
|
|
|
|
|
|
|
|
result.shift()
|
|
|
|
|
|
|
|
result.push(0)
|
|
|
|
|
|
|
|
for (let j = 0; j < result.length; j++) result[j] ^= gfMul(generator[j + 1], factor)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ── Alignment-pattern centre positions per version ──────────────────────────
|
|
|
|
|
|
|
|
function alignmentPositions(version: number): number[] {
|
|
|
|
|
|
|
|
if (version === 1) return []
|
|
|
|
|
|
|
|
const num = Math.floor(version / 7) + 2
|
|
|
|
|
|
|
|
const size = version * 4 + 17
|
|
|
|
|
|
|
|
const step = version === 32 ? 26 : Math.ceil((size - 13) / (num * 2 - 2)) * 2
|
|
|
|
|
|
|
|
const result = [6]
|
|
|
|
|
|
|
|
for (let pos = size - 7; result.length < num; pos -= step) result.unshift(pos)
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ── BCH error-correction for format / version info ──────────────────────────
|
|
|
|
|
|
|
|
function bch(data: number, poly: number, bits: number): number {
|
|
|
|
|
|
|
|
let rem = data
|
|
|
|
|
|
|
|
for (let i = 0; i < bits; i++) rem = (rem << 1) ^ ((rem >>> (bits - 1)) * poly)
|
|
|
|
|
|
|
|
return rem & ((1 << bits) - 1)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ── Matrix construction ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
class Matrix {
|
|
|
|
|
|
|
|
size: number
|
|
|
|
|
|
|
|
modules: boolean[][]
|
|
|
|
|
|
|
|
reserved: boolean[][]
|
|
|
|
|
|
|
|
constructor(version: number) {
|
|
|
|
|
|
|
|
this.size = version * 4 + 17
|
|
|
|
|
|
|
|
this.modules = Array.from({ length: this.size }, () => new Array(this.size).fill(false))
|
|
|
|
|
|
|
|
this.reserved = Array.from({ length: this.size }, () => new Array(this.size).fill(false))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
set(x: number, y: number, dark: boolean, reserve = true): void {
|
|
|
|
|
|
|
|
this.modules[y][x] = dark
|
|
|
|
|
|
|
|
if (reserve) this.reserved[y][x] = true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
drawFunctionPatterns(version: number): void {
|
|
|
|
|
|
|
|
// Timing patterns
|
|
|
|
|
|
|
|
for (let i = 0; i < this.size; i++) {
|
|
|
|
|
|
|
|
this.set(6, i, i % 2 === 0)
|
|
|
|
|
|
|
|
this.set(i, 6, i % 2 === 0)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Finder patterns + separators (3 corners)
|
|
|
|
|
|
|
|
this.drawFinder(3, 3)
|
|
|
|
|
|
|
|
this.drawFinder(this.size - 4, 3)
|
|
|
|
|
|
|
|
this.drawFinder(3, this.size - 4)
|
|
|
|
|
|
|
|
// Alignment patterns
|
|
|
|
|
|
|
|
const align = alignmentPositions(version)
|
|
|
|
|
|
|
|
for (const ay of align) {
|
|
|
|
|
|
|
|
for (const ax of align) {
|
|
|
|
|
|
|
|
if ((ax === 6 && ay === 6) || (ax === 6 && ay === this.size - 7) || (ax === this.size - 7 && ay === 6)) continue
|
|
|
|
|
|
|
|
this.drawAlignment(ax, ay)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Dark module
|
|
|
|
|
|
|
|
this.set(8, this.size - 8, true)
|
|
|
|
|
|
|
|
// Reserve format-info areas (filled later)
|
|
|
|
|
|
|
|
this.reserveFormat()
|
|
|
|
|
|
|
|
// Version info (v7+)
|
|
|
|
|
|
|
|
if (version >= 7) this.drawVersionInfo(version)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
private drawFinder(cx: number, cy: number): void {
|
|
|
|
|
|
|
|
for (let dy = -4; dy <= 4; dy++) {
|
|
|
|
|
|
|
|
for (let dx = -4; dx <= 4; dx++) {
|
|
|
|
|
|
|
|
const x = cx + dx
|
|
|
|
|
|
|
|
const y = cy + dy
|
|
|
|
|
|
|
|
if (x < 0 || x >= this.size || y < 0 || y >= this.size) continue
|
|
|
|
|
|
|
|
const dist = Math.max(Math.abs(dx), Math.abs(dy))
|
|
|
|
|
|
|
|
this.set(x, y, dist !== 2 && dist <= 3)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
private drawAlignment(cx: number, cy: number): void {
|
|
|
|
|
|
|
|
for (let dy = -2; dy <= 2; dy++) {
|
|
|
|
|
|
|
|
for (let dx = -2; dx <= 2; dx++) {
|
|
|
|
|
|
|
|
this.set(cx + dx, cy + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
private reserveFormat(): void {
|
|
|
|
|
|
|
|
for (let i = 0; i < 9; i++) {
|
|
|
|
|
|
|
|
this.reserved[i][8] = true
|
|
|
|
|
|
|
|
this.reserved[8][i] = true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < 8; i++) {
|
|
|
|
|
|
|
|
this.reserved[this.size - 1 - i][8] = true
|
|
|
|
|
|
|
|
this.reserved[8][this.size - 1 - i] = true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
private drawVersionInfo(version: number): void {
|
|
|
|
|
|
|
|
// Version info = 6 data bits + 12 BCH bits (generator 0x1F25, degree 12).
|
|
|
|
|
|
|
|
const bits = (version << 12) | bch(version, 0x1f25, 12)
|
|
|
|
|
|
|
|
for (let i = 0; i < 18; i++) {
|
|
|
|
|
|
|
|
const dark = ((bits >>> i) & 1) === 1
|
|
|
|
|
|
|
|
const a = Math.floor(i / 3)
|
|
|
|
|
|
|
|
const b = (i % 3) + this.size - 11
|
|
|
|
|
|
|
|
this.set(a, b, dark)
|
|
|
|
|
|
|
|
this.set(b, a, dark)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
placeData(codewords: number[]): void {
|
|
|
|
|
|
|
|
let i = 0
|
|
|
|
|
|
|
|
for (let right = this.size - 1; right >= 1; right -= 2) {
|
|
|
|
|
|
|
|
if (right === 6) right = 5
|
|
|
|
|
|
|
|
for (let vert = 0; vert < this.size; vert++) {
|
|
|
|
|
|
|
|
for (let j = 0; j < 2; j++) {
|
|
|
|
|
|
|
|
const x = right - j
|
|
|
|
|
|
|
|
const upward = ((right + 1) & 2) === 0
|
|
|
|
|
|
|
|
const y = upward ? this.size - 1 - vert : vert
|
|
|
|
|
|
|
|
if (this.reserved[y][x]) continue
|
|
|
|
|
|
|
|
let dark = false
|
|
|
|
|
|
|
|
if (i < codewords.length * 8) dark = ((codewords[i >>> 3] >>> (7 - (i & 7))) & 1) === 1
|
|
|
|
|
|
|
|
this.modules[y][x] = dark
|
|
|
|
|
|
|
|
i++
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
applyMask(mask: number): void {
|
|
|
|
|
|
|
|
for (let y = 0; y < this.size; y++) {
|
|
|
|
|
|
|
|
for (let x = 0; x < this.size; x++) {
|
|
|
|
|
|
|
|
if (this.reserved[y][x]) continue
|
|
|
|
|
|
|
|
let invert = false
|
|
|
|
|
|
|
|
switch (mask) {
|
|
|
|
|
|
|
|
case 0: invert = (x + y) % 2 === 0; break
|
|
|
|
|
|
|
|
case 1: invert = y % 2 === 0; break
|
|
|
|
|
|
|
|
case 2: invert = x % 3 === 0; break
|
|
|
|
|
|
|
|
case 3: invert = (x + y) % 3 === 0; break
|
|
|
|
|
|
|
|
case 4: invert = (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0; break
|
|
|
|
|
|
|
|
case 5: invert = ((x * y) % 2) + ((x * y) % 3) === 0; break
|
|
|
|
|
|
|
|
case 6: invert = (((x * y) % 2) + ((x * y) % 3)) % 2 === 0; break
|
|
|
|
|
|
|
|
case 7: invert = (((x + y) % 2) + ((x * y) % 3)) % 2 === 0; break
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (invert) this.modules[y][x] = !this.modules[y][x]
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
drawFormatInfo(ecc: ErrorCorrection, mask: number): void {
|
|
|
|
|
|
|
|
const data = (ECC_FORMAT_BITS[ecc] << 3) | mask
|
|
|
|
|
|
|
|
const bits = ((data << 10) | bch(data, 0x537, 10)) ^ 0x5412
|
|
|
|
|
|
|
|
for (let i = 0; i < 15; i++) {
|
|
|
|
|
|
|
|
const dark = ((bits >>> i) & 1) === 1
|
|
|
|
|
|
|
|
// Top-left
|
|
|
|
|
|
|
|
if (i < 6) this.modules[i][8] = dark
|
|
|
|
|
|
|
|
else if (i < 8) this.modules[i + 1][8] = dark
|
|
|
|
|
|
|
|
else if (i === 8) this.modules[8][7] = dark
|
|
|
|
|
|
|
|
else this.modules[8][14 - i] = dark
|
|
|
|
|
|
|
|
// Top-right + bottom-left
|
|
|
|
|
|
|
|
if (i < 8) this.modules[8][this.size - 1 - i] = dark
|
|
|
|
|
|
|
|
else this.modules[this.size - 15 + i][8] = dark
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
this.modules[this.size - 8][8] = true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
penalty(): number {
|
|
|
|
|
|
|
|
const n = this.size
|
|
|
|
|
|
|
|
let p = 0
|
|
|
|
|
|
|
|
const m = this.modules
|
|
|
|
|
|
|
|
// Rule 1: runs of ≥5 same-colour modules (rows + cols)
|
|
|
|
|
|
|
|
for (let y = 0; y < n; y++) {
|
|
|
|
|
|
|
|
let runColor = m[y][0]
|
|
|
|
|
|
|
|
let runLen = 1
|
|
|
|
|
|
|
|
for (let x = 1; x < n; x++) {
|
|
|
|
|
|
|
|
if (m[y][x] === runColor) { runLen++; if (runLen === 5) p += 3; else if (runLen > 5) p++ }
|
|
|
|
|
|
|
|
else { runColor = m[y][x]; runLen = 1 }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let x = 0; x < n; x++) {
|
|
|
|
|
|
|
|
let runColor = m[0][x]
|
|
|
|
|
|
|
|
let runLen = 1
|
|
|
|
|
|
|
|
for (let y = 1; y < n; y++) {
|
|
|
|
|
|
|
|
if (m[y][x] === runColor) { runLen++; if (runLen === 5) p += 3; else if (runLen > 5) p++ }
|
|
|
|
|
|
|
|
else { runColor = m[y][x]; runLen = 1 }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Rule 2: 2×2 blocks of same colour
|
|
|
|
|
|
|
|
for (let y = 0; y < n - 1; y++) {
|
|
|
|
|
|
|
|
for (let x = 0; x < n - 1; x++) {
|
|
|
|
|
|
|
|
const c = m[y][x]
|
|
|
|
|
|
|
|
if (c === m[y][x + 1] && c === m[y + 1][x] && c === m[y + 1][x + 1]) p += 3
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Rule 3: finder-like patterns (1:1:3:1:1 with 4-module light run)
|
|
|
|
|
|
|
|
const pat1 = [true, false, true, true, true, false, true, false, false, false, false]
|
|
|
|
|
|
|
|
const pat2 = [false, false, false, false, true, false, true, true, true, false, true]
|
|
|
|
|
|
|
|
const lineHas = (line: boolean[]) => {
|
|
|
|
|
|
|
|
let count = 0
|
|
|
|
|
|
|
|
for (let i = 0; i + 11 <= n; i++) {
|
|
|
|
|
|
|
|
let ok1 = true, ok2 = true
|
|
|
|
|
|
|
|
for (let k = 0; k < 11; k++) { if (line[i + k] !== pat1[k]) ok1 = false; if (line[i + k] !== pat2[k]) ok2 = false }
|
|
|
|
|
|
|
|
if (ok1 || ok2) count++
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return count
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let y = 0; y < n; y++) p += lineHas(m[y]) * 40
|
|
|
|
|
|
|
|
for (let x = 0; x < n; x++) { const col = m.map((row) => row[x]); p += lineHas(col) * 40 }
|
|
|
|
|
|
|
|
// Rule 4: dark-module proportion
|
|
|
|
|
|
|
|
let dark = 0
|
|
|
|
|
|
|
|
for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) if (m[y][x]) dark++
|
|
|
|
|
|
|
|
const pct = (dark * 100) / (n * n)
|
|
|
|
|
|
|
|
p += Math.floor(Math.abs(pct - 50) / 5) * 10
|
|
|
|
|
|
|
|
return p
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ── Assemble interleaved data + ECC codewords ───────────────────────────────
|
|
|
|
|
|
|
|
function buildCodewords(dataCodewords: number[], version: number, ecc: ErrorCorrection): number[] {
|
|
|
|
|
|
|
|
const o = ECC_ORDER.indexOf(ecc)
|
|
|
|
|
|
|
|
const numBlocks = NUM_ERROR_CORRECTION_BLOCKS[o][version]
|
|
|
|
|
|
|
|
const eccLen = ECC_CODEWORDS_PER_BLOCK[o][version]
|
|
|
|
|
|
|
|
const rawCodewords = Math.floor(getNumRawDataModules(version) / 8)
|
|
|
|
|
|
|
|
const numShort = numBlocks - (rawCodewords % numBlocks)
|
|
|
|
|
|
|
|
const shortLen = Math.floor(rawCodewords / numBlocks) - eccLen
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const generator = rsGenerator(eccLen)
|
|
|
|
|
|
|
|
const blocks: { data: number[]; ecc: number[] }[] = []
|
|
|
|
|
|
|
|
let k = 0
|
|
|
|
|
|
|
|
for (let b = 0; b < numBlocks; b++) {
|
|
|
|
|
|
|
|
const len = shortLen + (b < numShort ? 0 : 1)
|
|
|
|
|
|
|
|
const data = dataCodewords.slice(k, k + len)
|
|
|
|
|
|
|
|
k += len
|
|
|
|
|
|
|
|
blocks.push({ data, ecc: rsRemainder(data, generator) })
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const result: number[] = []
|
|
|
|
|
|
|
|
// Interleave data codewords (short blocks pad with a hole at shortLen)
|
|
|
|
|
|
|
|
for (let i = 0; i <= shortLen; i++) {
|
|
|
|
|
|
|
|
for (let b = 0; b < numBlocks; b++) {
|
|
|
|
|
|
|
|
if (i < shortLen || b >= numShort) result.push(blocks[b].data[i])
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Interleave ECC codewords
|
|
|
|
|
|
|
|
for (let i = 0; i < eccLen; i++) {
|
|
|
|
|
|
|
|
for (let b = 0; b < numBlocks; b++) result.push(blocks[b].ecc[i])
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
/** Encode `value` to a QR matrix. Throws if it doesn't fit in maxVersion. */
|
|
|
|
|
|
|
|
export function encode(value: string, options: EncodeOptions = {}): QrResult {
|
|
|
|
|
|
|
|
const ecc = options.errorCorrection ?? 'M'
|
|
|
|
|
|
|
|
const minV = Math.max(MIN_VERSION, options.minVersion ?? MIN_VERSION)
|
|
|
|
|
|
|
|
const maxV = Math.min(MAX_VERSION, options.maxVersion ?? MAX_VERSION)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const seg = makeSegment(value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Smallest version that fits.
|
|
|
|
|
|
|
|
let version = -1
|
|
|
|
|
|
|
|
let dataCapacityBits = 0
|
|
|
|
|
|
|
|
for (let v = minV; v <= maxV; v++) {
|
|
|
|
|
|
|
|
dataCapacityBits = getNumDataCodewords(v, ecc) * 8
|
|
|
|
|
|
|
|
const used = 4 + charCountBits(seg.mode, v) + seg.data.bits.length
|
|
|
|
|
|
|
|
if (used <= dataCapacityBits) {
|
|
|
|
|
|
|
|
version = v
|
|
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (version === -1) throw new Error('qr: data too long for the requested version range')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Assemble the bit stream.
|
|
|
|
|
|
|
|
const bb = new BitBuffer()
|
|
|
|
|
|
|
|
bb.append(seg.mode.indicator, 4)
|
|
|
|
|
|
|
|
bb.append(seg.numChars, charCountBits(seg.mode, version))
|
|
|
|
|
|
|
|
bb.bits.push(...seg.data.bits)
|
|
|
|
|
|
|
|
// Terminator + bit padding to a byte boundary.
|
|
|
|
|
|
|
|
const capacity = getNumDataCodewords(version, ecc) * 8
|
|
|
|
|
|
|
|
bb.append(0, Math.min(4, capacity - bb.bits.length))
|
|
|
|
|
|
|
|
while (bb.bits.length % 8 !== 0) bb.bits.push(0)
|
|
|
|
|
|
|
|
// Byte padding.
|
|
|
|
|
|
|
|
for (let pad = 0xec; bb.bits.length < capacity; pad ^= 0xec ^ 0x11) bb.append(pad, 8)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const dataCodewords: number[] = []
|
|
|
|
|
|
|
|
for (let i = 0; i < bb.bits.length; i += 8) {
|
|
|
|
|
|
|
|
let byte = 0
|
|
|
|
|
|
|
|
for (let j = 0; j < 8; j++) byte = (byte << 1) | bb.bits[i + j]
|
|
|
|
|
|
|
|
dataCodewords.push(byte)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const allCodewords = buildCodewords(dataCodewords, version, ecc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Build matrix, try all 8 masks, keep the lowest-penalty one.
|
|
|
|
|
|
|
|
let best: Matrix | null = null
|
|
|
|
|
|
|
|
let bestPenalty = Infinity
|
|
|
|
|
|
|
|
let bestMask = 0
|
|
|
|
|
|
|
|
for (let mask = 0; mask < 8; mask++) {
|
|
|
|
|
|
|
|
const matrix = new Matrix(version)
|
|
|
|
|
|
|
|
matrix.drawFunctionPatterns(version)
|
|
|
|
|
|
|
|
matrix.placeData(allCodewords)
|
|
|
|
|
|
|
|
matrix.applyMask(mask)
|
|
|
|
|
|
|
|
matrix.drawFormatInfo(ecc, mask)
|
|
|
|
|
|
|
|
const pen = matrix.penalty()
|
|
|
|
|
|
|
|
if (pen < bestPenalty) {
|
|
|
|
|
|
|
|
bestPenalty = pen
|
|
|
|
|
|
|
|
best = matrix
|
|
|
|
|
|
|
|
bestMask = mask
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const chosen = best!
|
|
|
|
|
|
|
|
void bestMask
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
matrix: chosen.modules,
|
|
|
|
|
|
|
|
reserved: chosen.reserved,
|
|
|
|
|
|
|
|
size: chosen.size,
|
|
|
|
|
|
|
|
version,
|
|
|
|
|
|
|
|
errorCorrection: ecc
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|