Skip to content

QR Code Generator — Go source

Turn any text or URL into a downloadable QR code. Full ISO encoder with all error-correction levels, versions 1-40, SVG and PNG export, colors and quiet-zone control — runs entirely in your browser.

This is the Go implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Package qrcodegen is the Go twin of CosmoDev's src/lib/qr.ts + src/lib/qr-tables.ts
// (dual source: the web lib is TypeScript, the CLI lib is Go — kept in lock-step).
// Pure QR encoder — ISO/IEC 18004, versions 1-40, EC levels L/M/Q/H, numeric +
// alphanumeric + byte modes, all 8 masks with penalty auto-select. Deterministic;
// invalid input returns nil (null in the TS lib).
//
// The table-driven tests in qr-code-generator_test.go share vectors with
// src/lib/qr.test.ts — including the Thonky 1-Q "HELLO WORLD" golden matrix —
// so the two implementations are held to one contract.
//
// Pipeline mirrors the TS lib exactly: mode auto-selection → smallest fitting
// version → bit buffer (mode + count + payload) → terminator/alignment/padding →
// block split → Reed-Solomon EC over GF(256) → interleave → matrix (finders,
// timing, alignment, dark module, format/version reservations) → zigzag data
// placement → mask (forced or penalty-selected from a pristine base) → format
// and version information.
package qrcodegen

import (
	"fmt"
	"math"
	"strconv"
	"strings"
)

// EcLevel is the error-correction level of the symbol.
type EcLevel int

const (
	// EcM is the zero value so Options{} defaults to M, matching the TS
	// default (ecLevel: 'M').
	EcM EcLevel = iota
	EcL
	EcQ
	EcH
)

// String returns the one-letter spec name ("L", "M", "Q", "H").
func (l EcLevel) String() string {
	switch l {
	case EcL:
		return "L"
	case EcM:
		return "M"
	case EcQ:
		return "Q"
	case EcH:
		return "H"
	}
	return "?"
}

// Data mode names, identical to the TS union 'numeric' | 'alphanumeric' | 'byte'.
const (
	modeNumeric     = "numeric"
	modeAlphanumeric = "alphanumeric"
	modeByte        = "byte"
)

// Options configures Encode. The zero value (Options{}) matches the TS default
// (encode(text)): level M, versions 1..40, automatic mask selection.
//
// Mask is a *int so the zero value means "automatic penalty selection" while a
// non-nil 0..7 forces that mask — exactly like the TS lib's distinction between
// an omitted and an explicit mask.
type Options struct {
	EcLevel    EcLevel // default EcM (zero value)
	MinVersion int     // 0 → 1
	MaxVersion int     // 0 → 40; values above 40 clamp to 40
	Mask       *int    // nil → automatic; 0-7 forced, anything else → nil result
}

// Result is the encoded symbol. Modules is row-major; Modules[y][x]; true = dark.
type Result struct {
	Version int
	EcLevel EcLevel
	Size    int
	Mask    int
	Mode    string
	Modules [][]bool
}

// SvgOptions configures ToSVG. The zero value matches the TS default
// (scale 4, margin 4, black on white).
type SvgOptions struct {
	Scale  int    // 0 → 4
	Margin int    // 0 → 4
	Fg     string // "" → "#000000"
	Bg     string // "" → "#ffffff"
}

// ---------------------------------------------------------------------------
// Modes and character sets
// ---------------------------------------------------------------------------

// alnumCharset mirrors ALNUM_CHARSET in src/lib/qr.ts (ISO 18004 Table 5 JIS8
// subset + the four symbols), value = index.
const alnumCharset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"

func pickMode(text string) string {
	if len(text) > 0 && allIn(text, '0', '9') {
		return modeNumeric
	}
	if len(text) > 0 && isAlphanumeric(text) {
		return modeAlphanumeric
	}
	return modeByte
}

func allIn(s string, lo, hi rune) bool {
	for _, r := range s {
		if r < lo || r > hi {
			return false
		}
	}
	return true
}

func isAlphanumeric(s string) bool {
	for _, r := range s {
		if !strings.ContainsRune(alnumCharset, r) {
			return false
		}
	}
	return true
}

func countBitsFor(mode string, version int) int {
	if mode == modeByte {
		if version <= 9 {
			return 8
		}
		return 16
	}
	if version <= 9 {
		return 9
	} else if version <= 26 {
		return 11
	}
	return 13
}

func modeIndicator(mode string) int {
	switch mode {
	case modeNumeric:
		return 0b0001
	case modeAlphanumeric:
		return 0b0010
	default:
		return 0b0100
	}
}

func alnumIndex(c byte) int { return strings.IndexByte(alnumCharset, c) }

// ---------------------------------------------------------------------------
// BitBuffer — MSB-first packing
// ---------------------------------------------------------------------------

type bitBuffer struct{ bits []bool }

func (b *bitBuffer) length() int { return len(b.bits) }

func (b *bitBuffer) push(value, count int) {
	for i := count - 1; i >= 0; i-- {
		b.bits = append(b.bits, (value>>uint(i))&1 == 1)
	}
}

func (b *bitBuffer) toBytes() []int {
	// The caller pads to a byte boundary before calling, so every group is 8.
	out := make([]int, 0, len(b.bits)/8)
	for i := 0; i < len(b.bits); i += 8 {
		v := 0
		for j := 0; j < 8; j++ {
			v = v<<1 | b2i(b.bits[i+j])
		}
		out = append(out, v)
	}
	return out
}

func b2i(b bool) int {
	if b {
		return 1
	}
	return 0
}

// ---------------------------------------------------------------------------
// Reed-Solomon over GF(256), primitive polynomial 0x11D
// ---------------------------------------------------------------------------

var gfExp = func() [256]int {
	var exp [256]int
	x := 1
	for i := 0; i < 255; i++ {
		exp[i] = x
		x <<= 1
		if x&0x100 != 0 {
			x ^= 0x11d
		}
	}
	return exp
}()

var gfLog = func() [256]int {
	var log [256]int
	x := 1
	for i := 0; i < 255; i++ {
		log[x] = i
		x <<= 1
		if x&0x100 != 0 {
			x ^= 0x11d
		}
	}
	return log
}()

// GFMul multiplies in GF(256) with primitive 0x11D. Exported to mirror the
// TS lib's gfMul.
func GFMul(a, b int) int {
	if a == 0 || b == 0 {
		return 0
	}
	return gfExp[(gfLog[a]+gfLog[b])%255]
}

// rsGenerator returns the generator polynomial of the given degree,
// coefficients highest-first, monic.
func rsGenerator(degree int) []int {
	poly := []int{1}
	for i := 0; i < degree; i++ {
		next := make([]int, len(poly)+1)
		for j := 0; j < len(poly); j++ {
			next[j] ^= poly[j]
			next[j+1] ^= GFMul(poly[j], gfExp[i])
		}
		poly = next
	}
	return poly
}

// rsEncode returns the error-correction codewords for data (synthetic
// division by the generator).
func rsEncode(data []int, ecLen int) []int {
	gen := rsGenerator(ecLen)
	rem := make([]int, ecLen)
	for _, b := range data {
		factor := b ^ rem[0]
		copy(rem, rem[1:])
		rem[ecLen-1] = 0
		if factor != 0 {
			for i := 0; i < ecLen; i++ {
				rem[i] ^= GFMul(gen[i+1], factor)
			}
		}
	}
	return rem
}

// ---------------------------------------------------------------------------
// BCH format / version information (computed, not transcribed)
// ---------------------------------------------------------------------------

func ecFormatBits(level EcLevel) (int, bool) {
	switch level {
	case EcL:
		return 0b01, true
	case EcM:
		return 0b00, true
	case EcQ:
		return 0b11, true
	case EcH:
		return 0b10, true
	}
	return 0, false
}

// FormatInfoString returns the 15-bit format info string (MSB first) for
// (level, mask), including the 0x5412 XOR mask.
func FormatInfoString(level EcLevel, mask int) string {
	bits, ok := ecFormatBits(level)
	if !ok {
		return ""
	}
	data := bits<<3 | mask
	v := data << 10
	const G = 0x537
	for i := 14; i >= 10; i-- {
		if v&(1<<uint(i)) != 0 {
			v ^= G << uint(i-10)
		}
	}
	rem := v & 0x3ff
	full := ((data << 10) | rem) ^ 0x5412
	return fmt.Sprintf("%015b", full)
}

// VersionInfoString returns the 18-bit version info string (MSB first) for
// versions >= 7.
func VersionInfoString(version int) string {
	v := version << 12
	const G = 0x1f25
	for i := 17; i >= 12; i-- {
		if v&(1<<uint(i)) != 0 {
			v ^= G << uint(i-12)
		}
	}
	full := (version << 12) | (v & 0xfff)
	return fmt.Sprintf("%018b", full)
}

// ---------------------------------------------------------------------------
// Matrix construction
// ---------------------------------------------------------------------------

type matrix struct {
	size     int
	modules  [][]bool
	reserved [][]bool
}

func newMatrix(size int) *matrix {
	m := &matrix{size: size}
	m.modules = make([][]bool, size)
	m.reserved = make([][]bool, size)
	for i := range m.modules {
		m.modules[i] = make([]bool, size)
		m.reserved[i] = make([]bool, size)
	}
	return m
}

func (m *matrix) set(x, y int, dark bool) {
	m.modules[y][x] = dark
	m.reserved[y][x] = true
}

// write writes a DATA module — marks nothing reserved, so masking still applies.
func (m *matrix) write(x, y int, dark bool) {
	m.modules[y][x] = dark
}

// reserve marks a format/version cell; written after masking.
func (m *matrix) reserve(x, y int) {
	m.reserved[y][x] = true
}

func (m *matrix) drawFinder(x, y int) {
	for dy := -1; dy <= 7; dy++ {
		for dx := -1; dx <= 7; dx++ {
			X, Y := x+dx, y+dy
			if X < 0 || X >= m.size || Y < 0 || Y >= m.size {
				continue
			}
			dark := (dx >= 0 && dx <= 6 && (dy == 0 || dy == 6)) ||
				(dy >= 0 && dy <= 6 && (dx == 0 || dx == 6)) ||
				(dx >= 2 && dx <= 4 && dy >= 2 && dy <= 4)
			m.set(X, Y, dark)
		}
	}
}

func (m *matrix) drawAlignment(cx, cy int) {
	for dy := -2; dy <= 2; dy++ {
		for dx := -2; dx <= 2; dx++ {
			dark := max(abs(dx), abs(dy)) != 1
			m.set(cx+dx, cy+dy, dark)
		}
	}
}

func abs(v int) int {
	if v < 0 {
		return -v
	}
	return v
}

func (m *matrix) drawFunctionPatterns(version int) {
	size := m.size
	m.drawFinder(0, 0)
	m.drawFinder(size-7, 0)
	m.drawFinder(0, size-7)

	for i := 8; i < size-8; i++ {
		dark := i%2 == 0
		m.set(i, 6, dark)
		m.set(6, i, dark)
	}

	m.set(8, size-8, true) // dark module: (col 8, row 4*version+9) = size-8

	// Alignment patterns — skip the three that would overlap finders.
	// (Version 1 has no centers, so `last` is only read when non-empty.)
	if centers := alignmentCenters[version-1]; len(centers) > 0 {
		last := centers[len(centers)-1]
		for _, cy := range centers {
			for _, cx := range centers {
				if (cx == 6 && cy == 6) || (cx == 6 && cy == last) || (cx == last && cy == 6) {
					continue
				}
				m.drawAlignment(cx, cy)
			}
		}
	}

	m.reserveFormatAreas(version)
}

// reserveFormatAreas reserves both format copies + the version info areas (v >= 7).
func (m *matrix) reserveFormatAreas(version int) {
	size := m.size
	for i := 0; i <= 8; i++ {
		m.reserve(i, 8) // row 8, cols 0-8 (skip col 6 — already timing)
		if i != 6 {
			m.reserve(8, i) // col 8, rows 0-8
		}
	}
	for i := 0; i < 8; i++ {
		m.reserve(size-1-i, 8) // col 8, bottom 8 rows
	}
	for i := 0; i < 8; i++ {
		m.reserve(8, size-1-i) // row 8, right 8 cols
	}

	if version >= 7 {
		// 3x6 + 6x3 blocks at bottom-right of the top-right finder and
		// right of the bottom-left finder. Both copies mirror each other.
		for i := 0; i < 18; i++ {
			a := size - 11 + (i % 3)
			b := i / 3
			m.reserve(a, b)
			m.reserve(b, a)
		}
	}
}

func (m *matrix) drawFormatAndVersion(ecLevel EcLevel, mask, version int) {
	size := m.size
	format := FormatInfoString(ecLevel, mask)
	bit := func(i int) bool { return format[i] == '1' }

	// Copy 1 (top-left): row 8 cols 0-5,7,8 then col 8 rows 7,5..0 — MSB first.
	copy1 := [15][2]int{
		{0, 8}, {1, 8}, {2, 8}, {3, 8}, {4, 8}, {5, 8}, {7, 8}, {8, 8},
		{8, 7}, {8, 5}, {8, 4}, {8, 3}, {8, 2}, {8, 1}, {8, 0},
	}
	for i, p := range copy1 {
		m.set(p[0], p[1], bit(i))
	}

	// Copy 2: col 8 bottom 7 rows, then row 8 right 8 cols — MSB first.
	for i := 0; i < 7; i++ {
		m.set(8, size-1-i, bit(i))
	}
	for i := 7; i < 15; i++ {
		m.set(size-15+i, 8, bit(i))
	}

	if version >= 7 {
		vinfo := VersionInfoString(version)
		for i := 0; i < 18; i++ {
			dark := vinfo[i] == '1'
			a := size - 11 + (i % 3)
			b := i / 3
			m.set(a, b, dark)
			m.set(b, a, dark)
		}
	}
}

// ---------------------------------------------------------------------------
// Masking and penalty
// ---------------------------------------------------------------------------

func maskBit(mask, x, y int) bool {
	switch mask {
	case 0:
		return (x+y)%2 == 0
	case 1:
		return y%2 == 0
	case 2:
		return x%3 == 0
	case 3:
		return (x+y)%3 == 0
	case 4:
		return (y/2 + x/3) % 2 == 0
	case 5:
		return (x*y)%2+(x*y)%3 == 0
	case 6:
		return ((x*y)%2+(x*y)%3)%2 == 0
	default:
		return ((x+y)%2+(x*y)%3)%2 == 0
	}
}

func penalty(m *matrix) int {
	size := m.size
	mod := m.modules
	score := 0

	// N1: runs of 5+ same color, rows and columns.
	runScore := func(get func(i, j int) bool) int {
		s := 0
		for i := 0; i < size; i++ {
			run := 1
			color := get(i, 0)
			for j := 1; j < size; j++ {
				c := get(i, j)
				if c == color {
					run++
					if run == 5 {
						s += 3
					} else if run > 5 {
						s++
					}
				} else {
					run = 1
					color = c
				}
			}
		}
		return s
	}
	score += runScore(func(y, x int) bool { return mod[y][x] })
	score += runScore(func(x, y int) bool { return mod[y][x] })

	// N2: 2x2 blocks of one color.
	for y := 0; y < size-1; y++ {
		for x := 0; x < size-1; x++ {
			c := mod[y][x]
			if c == mod[y][x+1] && c == mod[y+1][x] && c == mod[y+1][x+1] {
				score += 3
			}
		}
	}

	// N3: finder-like 1011101 with 4 light modules on one side.
	finderPenalty := func(get func(i, j int) bool) int {
		s := 0
		for i := 0; i < size; i++ {
			for j := 0; j <= size-11; j++ {
				fwd, rev := true, true
				for k := 0; k < 11; k++ {
					dark := get(i, j+k)
					if dark != seqPattern[k] {
						fwd = false
					}
					if dark != seqPatternRev[k] {
						rev = false
					}
				}
				if fwd || rev {
					s += 40
				}
			}
		}
		return s
	}
	score += finderPenalty(func(y, x int) bool { return mod[y][x] })
	score += finderPenalty(func(x, y int) bool { return mod[y][x] })

	// N4: dark-module ratio deviation from 50%.
	dark := 0
	for _, row := range mod {
		for _, cell := range row {
			if cell {
				dark++
			}
		}
	}
	total := size * size
	k := int(math.Floor(math.Abs(float64(dark)*100/float64(total)-50) / 5))
	score += k * 10

	return score
}

var seqPattern = [11]bool{true, false, true, true, true, false, true, false, false, false, false}
var seqPatternRev = [11]bool{false, false, false, false, true, false, true, true, true, false, true}

// ---------------------------------------------------------------------------
// Encoder pipeline
// ---------------------------------------------------------------------------

func dataCapacityCodewords(version int, ecLevel EcLevel) int {
	spec := ecTable(ecLevel)[version-1]
	sum := 0
	for _, b := range spec.blocks {
		sum += b.count * b.dataCodewords
	}
	return sum
}

func payloadBits(mode string, text string, bytes []int, version int, buf *bitBuffer) {
	count := len(text)
	if mode == modeByte {
		count = len(bytes)
	}
	buf.push(modeIndicator(mode), 4)
	buf.push(count, countBitsFor(mode, version))

	if mode == modeNumeric {
		for i := 0; i < len(text); i += 3 {
			end := i + 3
			if end > len(text) {
				end = len(text)
			}
			chunk := text[i:end]
			value, _ := strconv.Atoi(chunk)
			switch len(chunk) {
			case 3:
				buf.push(value, 10)
			case 2:
				buf.push(value, 7)
			default:
				buf.push(value, 4)
			}
		}
	} else if mode == modeAlphanumeric {
		for i := 0; i < len(text); i += 2 {
			if i+1 < len(text) {
				buf.push(alnumIndex(text[i])*45+alnumIndex(text[i+1]), 11)
			} else {
				buf.push(alnumIndex(text[i]), 6)
			}
		}
	} else {
		for _, b := range bytes {
			buf.push(b, 8)
		}
	}
}

// estimatePayloadSize returns the payload bit count for the mode, independent
// of the count-indicator width.
func estimatePayloadSize(mode string, text string, bytes []int) int {
	switch mode {
	case modeNumeric:
		pairs := len(text) / 3
		rest := len(text) - pairs*3
		bits := pairs * 10
		if rest == 2 {
			bits += 7
		} else if rest == 1 {
			bits += 4
		}
		return bits
	case modeAlphanumeric:
		pairs := len(text) / 2
		rest := len(text) - pairs*2
		if rest == 1 {
			return pairs*11 + 6
		}
		return pairs * 11
	default:
		return len(bytes) * 8
	}
}

// Encode produces the QR symbol for text. It returns nil when the options are
// inconsistent (bad mask range, min > max, invalid level) or no version in
// [MinVersion..MaxVersion] can hold the text — nil is the TS null.
func Encode(text string, opts Options) *Result {
	ecLevel := opts.EcLevel
	if ecTable(ecLevel) == nil {
		return nil
	}
	minVersion := opts.MinVersion
	if minVersion == 0 {
		minVersion = 1
	}
	maxVersion := opts.MaxVersion
	if maxVersion == 0 {
		maxVersion = 40
	}
	if maxVersion > 40 {
		maxVersion = 40
	}
	if minVersion < 1 || minVersion > maxVersion {
		return nil
	}

	mode := pickMode(text)
	var bytes []int
	if mode == modeByte {
		for _, b := range []byte(text) {
			bytes = append(bytes, int(b))
		}
	}

	// Smallest version whose data capacity fits the payload.
	version := 0
	for v := minVersion; v <= maxVersion; v++ {
		needed := 4 + countBitsFor(mode, v) + estimatePayloadSize(mode, text, bytes)
		if needed <= dataCapacityCodewords(v, ecLevel)*8 {
			version = v
			break
		}
	}
	if version == 0 {
		return nil
	}

	capacityBits := dataCapacityCodewords(version, ecLevel) * 8
	buf := &bitBuffer{}
	payloadBits(mode, text, bytes, version, buf)

	// Terminator (up to 4 bits), byte alignment, pad codewords.
	maxTerminator := 4
	if capacityBits-buf.length() < maxTerminator {
		maxTerminator = capacityBits - buf.length()
	}
	if maxTerminator > 0 {
		buf.push(0, maxTerminator)
	}
	for buf.length()%8 != 0 {
		buf.push(0, 1)
	}
	padBytes := [2]int{0xec, 0x11}
	padIndex := 0
	for buf.length() < capacityBits {
		buf.push(padBytes[padIndex%2], 8)
		padIndex++
	}
	dataCodewords := buf.toBytes()

	// Split into blocks, compute EC, interleave.
	spec := ecTable(ecLevel)[version-1]
	var dataBlocks [][]int
	for _, group := range spec.blocks {
		for b := 0; b < group.count; b++ {
			offset := 0
			for _, blk := range dataBlocks {
				offset += len(blk)
			}
			dataBlocks = append(dataBlocks, dataCodewords[offset:offset+group.dataCodewords])
		}
	}
	ecBlocks := make([][]int, len(dataBlocks))
	for i, blk := range dataBlocks {
		ecBlocks[i] = rsEncode(blk, spec.ecPerBlock)
	}
	var finalCodewords []int
	maxData := 0
	for _, blk := range dataBlocks {
		if len(blk) > maxData {
			maxData = len(blk)
		}
	}
	for i := 0; i < maxData; i++ {
		for _, blk := range dataBlocks {
			if i < len(blk) {
				finalCodewords = append(finalCodewords, blk[i])
			}
		}
	}
	for i := 0; i < spec.ecPerBlock; i++ {
		for _, blk := range ecBlocks {
			finalCodewords = append(finalCodewords, blk[i])
		}
	}

	// Matrix: function patterns, zigzag data, mask, format/version info.
	size := version*4 + 17
	m := newMatrix(size)
	m.drawFunctionPatterns(version)

	var bitStream []bool
	for _, cw := range finalCodewords {
		for i := 7; i >= 0; i-- {
			bitStream = append(bitStream, (cw>>uint(i))&1 == 1)
		}
	}

	bitIndex := 0
	upward := true
	for right := size - 1; right >= 1; right -= 2 {
		if right == 6 {
			right--
		}
		for vert := 0; vert < size; vert++ {
			y := vert
			if upward {
				y = size - 1 - vert
			}
			for _, x := range [2]int{right, right - 1} {
				if m.reserved[y][x] {
					continue
				}
				dark := false
				if bitIndex < len(bitStream) {
					dark = bitStream[bitIndex]
				}
				m.write(x, y, dark)
				bitIndex++
			}
		}
		upward = !upward
	}

	// Mask from a pristine post-placement base — candidates never accumulate.
	base := make([][]bool, size)
	for y := range base {
		base[y] = append([]bool(nil), m.modules[y]...)
	}
	applyToModules := func(mask int) {
		for y := 0; y < size; y++ {
			for x := 0; x < size; x++ {
				m.modules[y][x] = base[y][x]
				if !m.reserved[y][x] && maskBit(mask, x, y) {
					m.modules[y][x] = !m.modules[y][x]
				}
			}
		}
	}

	chosenMask := 0
	if opts.Mask != nil {
		if *opts.Mask < 0 || *opts.Mask > 7 {
			return nil
		}
		chosenMask = *opts.Mask
		applyToModules(chosenMask)
	} else {
		best := math.MaxInt
		for mask := 0; mask < 8; mask++ {
			applyToModules(mask)
			if p := penalty(m); p < best {
				best = p
				chosenMask = mask
			}
		}
		applyToModules(chosenMask)
	}

	m.drawFormatAndVersion(ecLevel, chosenMask, version)

	return &Result{Version: version, EcLevel: ecLevel, Size: size, Mask: chosenMask, Mode: mode, Modules: m.modules}
}

// ---------------------------------------------------------------------------
// SVG rendering
// ---------------------------------------------------------------------------

// ToSVG renders the symbol as a standalone SVG string (mirrors toSvg in the
// TS lib): one path subcommand per dark module, crispEdges, aria-label.
func ToSVG(res *Result, o SvgOptions) string {
	scale := o.Scale
	if scale == 0 {
		scale = 4
	}
	margin := o.Margin
	if margin == 0 {
		margin = 4
	}
	fg := o.Fg
	if fg == "" {
		fg = "#000000"
	}
	bg := o.Bg
	if bg == "" {
		bg = "#ffffff"
	}
	dim := (res.Size + margin*2) * scale

	var path strings.Builder
	for y := 0; y < res.Size; y++ {
		for x := 0; x < res.Size; x++ {
			if !res.Modules[y][x] {
				continue
			}
			px := (x + margin) * scale
			py := (y + margin) * scale
			fmt.Fprintf(&path, "M%d %dh%dv%dh-%dz", px, py, scale, scale, scale)
		}
	}

	return fmt.Sprintf(
		`<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d" `+
			`shape-rendering="crispEdges" role="img" aria-label="QR code">`+
			`<rect width="%d" height="%d" fill="%s"/>`+
			`<path d="%s" fill="%s"/>`+
			`</svg>`,
		dim, dim, dim, dim, dim, dim, bg, path.String(), fg)
}

// ---------------------------------------------------------------------------
// Spec tables — transcribed verbatim from src/lib/qr-tables.ts
//
// qr-tables.ts documents its three independent sources (nayuki QR-Code-generator,
// thonky.com, davidshimjs/qrcodejs) and the codeword invariant every entry
// satisfies; qr-code-generator_test.go re-proves that invariant here.
// ---------------------------------------------------------------------------

// ecBlockGroup is one block group: count blocks, each carrying dataCodewords
// data codewords.
type ecBlockGroup struct {
	count         int
	dataCodewords int
}

// ecSpec is the error-correction layout of one (version, level) pair.
type ecSpec struct {
	totalCodewords int
	ecPerBlock     int
	blocks         []ecBlockGroup
}

// The four level tables, indexed [version-1]. L/M/Q/H order as in the TS file.
var (
	ecBlocksL = [40]ecSpec{
		/*  1-L */ {26, 7, []ecBlockGroup{{1, 19}}},
		/*  2-L */ {44, 10, []ecBlockGroup{{1, 34}}},
		/*  3-L */ {70, 15, []ecBlockGroup{{1, 55}}},
		/*  4-L */ {100, 20, []ecBlockGroup{{1, 80}}},
		/*  5-L */ {134, 26, []ecBlockGroup{{1, 108}}},
		/*  6-L */ {172, 18, []ecBlockGroup{{2, 68}}},
		/*  7-L */ {196, 20, []ecBlockGroup{{2, 78}}},
		/*  8-L */ {242, 24, []ecBlockGroup{{2, 97}}},
		/*  9-L */ {292, 30, []ecBlockGroup{{2, 116}}},
		/* 10-L */ {346, 18, []ecBlockGroup{{2, 68}, {2, 69}}},
		/* 11-L */ {404, 20, []ecBlockGroup{{4, 81}}},
		/* 12-L */ {466, 24, []ecBlockGroup{{2, 92}, {2, 93}}},
		/* 13-L */ {532, 26, []ecBlockGroup{{4, 107}}},
		/* 14-L */ {581, 30, []ecBlockGroup{{3, 115}, {1, 116}}},
		/* 15-L */ {655, 22, []ecBlockGroup{{5, 87}, {1, 88}}},
		/* 16-L */ {733, 24, []ecBlockGroup{{5, 98}, {1, 99}}},
		/* 17-L */ {815, 28, []ecBlockGroup{{1, 107}, {5, 108}}},
		/* 18-L */ {901, 30, []ecBlockGroup{{5, 120}, {1, 121}}},
		/* 19-L */ {991, 28, []ecBlockGroup{{3, 113}, {4, 114}}},
		/* 20-L */ {1085, 28, []ecBlockGroup{{3, 107}, {5, 108}}},
		/* 21-L */ {1156, 28, []ecBlockGroup{{4, 116}, {4, 117}}},
		/* 22-L */ {1258, 28, []ecBlockGroup{{2, 111}, {7, 112}}},
		/* 23-L */ {1364, 30, []ecBlockGroup{{4, 121}, {5, 122}}},
		/* 24-L */ {1474, 30, []ecBlockGroup{{6, 117}, {4, 118}}},
		/* 25-L */ {1588, 26, []ecBlockGroup{{8, 106}, {4, 107}}},
		/* 26-L */ {1706, 28, []ecBlockGroup{{10, 114}, {2, 115}}},
		/* 27-L */ {1828, 30, []ecBlockGroup{{8, 122}, {4, 123}}},
		/* 28-L */ {1921, 30, []ecBlockGroup{{3, 117}, {10, 118}}},
		/* 29-L */ {2051, 30, []ecBlockGroup{{7, 116}, {7, 117}}},
		/* 30-L */ {2185, 30, []ecBlockGroup{{5, 115}, {10, 116}}},
		/* 31-L */ {2323, 30, []ecBlockGroup{{13, 115}, {3, 116}}},
		/* 32-L */ {2465, 30, []ecBlockGroup{{17, 115}}},
		/* 33-L */ {2611, 30, []ecBlockGroup{{17, 115}, {1, 116}}},
		/* 34-L */ {2761, 30, []ecBlockGroup{{13, 115}, {6, 116}}},
		/* 35-L */ {2876, 30, []ecBlockGroup{{12, 121}, {7, 122}}},
		/* 36-L */ {3034, 30, []ecBlockGroup{{6, 121}, {14, 122}}},
		/* 37-L */ {3196, 30, []ecBlockGroup{{17, 122}, {4, 123}}},
		/* 38-L */ {3362, 30, []ecBlockGroup{{4, 122}, {18, 123}}},
		/* 39-L */ {3532, 30, []ecBlockGroup{{20, 117}, {4, 118}}},
		/* 40-L */ {3706, 30, []ecBlockGroup{{19, 118}, {6, 119}}},
	}

	ecBlocksM = [40]ecSpec{
		/*  1-M */ {26, 10, []ecBlockGroup{{1, 16}}},
		/*  2-M */ {44, 16, []ecBlockGroup{{1, 28}}},
		/*  3-M */ {70, 26, []ecBlockGroup{{1, 44}}},
		/*  4-M */ {100, 18, []ecBlockGroup{{2, 32}}},
		/*  5-M */ {134, 24, []ecBlockGroup{{2, 43}}},
		/*  6-M */ {172, 16, []ecBlockGroup{{4, 27}}},
		/*  7-M */ {196, 18, []ecBlockGroup{{4, 31}}},
		/*  8-M */ {242, 22, []ecBlockGroup{{2, 38}, {2, 39}}},
		/*  9-M */ {292, 22, []ecBlockGroup{{3, 36}, {2, 37}}},
		/* 10-M */ {346, 26, []ecBlockGroup{{4, 43}, {1, 44}}},
		/* 11-M */ {404, 30, []ecBlockGroup{{1, 50}, {4, 51}}},
		/* 12-M */ {466, 22, []ecBlockGroup{{6, 36}, {2, 37}}},
		/* 13-M */ {532, 22, []ecBlockGroup{{8, 37}, {1, 38}}},
		/* 14-M */ {581, 24, []ecBlockGroup{{4, 40}, {5, 41}}},
		/* 15-M */ {655, 24, []ecBlockGroup{{5, 41}, {5, 42}}},
		/* 16-M */ {733, 28, []ecBlockGroup{{7, 45}, {3, 46}}},
		/* 17-M */ {815, 28, []ecBlockGroup{{10, 46}, {1, 47}}},
		/* 18-M */ {901, 26, []ecBlockGroup{{9, 43}, {4, 44}}},
		/* 19-M */ {991, 26, []ecBlockGroup{{3, 44}, {11, 45}}},
		/* 20-M */ {1085, 26, []ecBlockGroup{{3, 41}, {13, 42}}},
		/* 21-M */ {1156, 26, []ecBlockGroup{{17, 42}}},
		/* 22-M */ {1258, 28, []ecBlockGroup{{17, 46}}},
		/* 23-M */ {1364, 28, []ecBlockGroup{{4, 47}, {14, 48}}},
		/* 24-M */ {1474, 28, []ecBlockGroup{{6, 45}, {14, 46}}},
		/* 25-M */ {1588, 28, []ecBlockGroup{{8, 47}, {13, 48}}},
		/* 26-M */ {1706, 28, []ecBlockGroup{{19, 46}, {4, 47}}},
		/* 27-M */ {1828, 28, []ecBlockGroup{{22, 45}, {3, 46}}},
		/* 28-M */ {1921, 28, []ecBlockGroup{{3, 45}, {23, 46}}},
		/* 29-M */ {2051, 28, []ecBlockGroup{{21, 45}, {7, 46}}},
		/* 30-M */ {2185, 28, []ecBlockGroup{{19, 47}, {10, 48}}},
		/* 31-M */ {2323, 28, []ecBlockGroup{{2, 46}, {29, 47}}},
		/* 32-M */ {2465, 28, []ecBlockGroup{{10, 46}, {23, 47}}},
		/* 33-M */ {2611, 28, []ecBlockGroup{{14, 46}, {21, 47}}},
		/* 34-M */ {2761, 28, []ecBlockGroup{{14, 46}, {23, 47}}},
		/* 35-M */ {2876, 28, []ecBlockGroup{{12, 47}, {26, 48}}},
		/* 36-M */ {3034, 28, []ecBlockGroup{{6, 47}, {34, 48}}},
		/* 37-M */ {3196, 28, []ecBlockGroup{{29, 46}, {14, 47}}},
		/* 38-M */ {3362, 28, []ecBlockGroup{{13, 46}, {32, 47}}},
		/* 39-M */ {3532, 28, []ecBlockGroup{{40, 47}, {7, 48}}},
		/* 40-M */ {3706, 28, []ecBlockGroup{{18, 47}, {31, 48}}},
	}

	ecBlocksQ = [40]ecSpec{
		/*  1-Q */ {26, 13, []ecBlockGroup{{1, 13}}},
		/*  2-Q */ {44, 22, []ecBlockGroup{{1, 22}}},
		/*  3-Q */ {70, 18, []ecBlockGroup{{2, 17}}},
		/*  4-Q */ {100, 26, []ecBlockGroup{{2, 24}}},
		/*  5-Q */ {134, 18, []ecBlockGroup{{2, 15}, {2, 16}}},
		/*  6-Q */ {172, 24, []ecBlockGroup{{4, 19}}},
		/*  7-Q */ {196, 18, []ecBlockGroup{{2, 14}, {4, 15}}},
		/*  8-Q */ {242, 22, []ecBlockGroup{{4, 18}, {2, 19}}},
		/*  9-Q */ {292, 20, []ecBlockGroup{{4, 16}, {4, 17}}},
		/* 10-Q */ {346, 24, []ecBlockGroup{{6, 19}, {2, 20}}},
		/* 11-Q */ {404, 28, []ecBlockGroup{{4, 22}, {4, 23}}},
		/* 12-Q */ {466, 26, []ecBlockGroup{{4, 20}, {6, 21}}},
		/* 13-Q */ {532, 24, []ecBlockGroup{{8, 20}, {4, 21}}},
		/* 14-Q */ {581, 20, []ecBlockGroup{{11, 16}, {5, 17}}},
		/* 15-Q */ {655, 30, []ecBlockGroup{{5, 24}, {7, 25}}},
		/* 16-Q */ {733, 24, []ecBlockGroup{{15, 19}, {2, 20}}},
		/* 17-Q */ {815, 28, []ecBlockGroup{{1, 22}, {15, 23}}},
		/* 18-Q */ {901, 28, []ecBlockGroup{{17, 22}, {1, 23}}},
		/* 19-Q */ {991, 26, []ecBlockGroup{{17, 21}, {4, 22}}},
		/* 20-Q */ {1085, 30, []ecBlockGroup{{15, 24}, {5, 25}}},
		/* 21-Q */ {1156, 28, []ecBlockGroup{{17, 22}, {6, 23}}},
		/* 22-Q */ {1258, 30, []ecBlockGroup{{7, 24}, {16, 25}}},
		/* 23-Q */ {1364, 30, []ecBlockGroup{{11, 24}, {14, 25}}},
		/* 24-Q */ {1474, 30, []ecBlockGroup{{11, 24}, {16, 25}}},
		/* 25-Q */ {1588, 30, []ecBlockGroup{{7, 24}, {22, 25}}},
		/* 26-Q */ {1706, 28, []ecBlockGroup{{28, 22}, {6, 23}}},
		/* 27-Q */ {1828, 30, []ecBlockGroup{{8, 23}, {26, 24}}},
		/* 28-Q */ {1921, 30, []ecBlockGroup{{4, 24}, {31, 25}}},
		/* 29-Q */ {2051, 30, []ecBlockGroup{{1, 23}, {37, 24}}},
		/* 30-Q */ {2185, 30, []ecBlockGroup{{15, 24}, {25, 25}}},
		/* 31-Q */ {2323, 30, []ecBlockGroup{{42, 24}, {1, 25}}},
		/* 32-Q */ {2465, 30, []ecBlockGroup{{10, 24}, {35, 25}}},
		/* 33-Q */ {2611, 30, []ecBlockGroup{{29, 24}, {19, 25}}},
		/* 34-Q */ {2761, 30, []ecBlockGroup{{44, 24}, {7, 25}}},
		/* 35-Q */ {2876, 30, []ecBlockGroup{{39, 24}, {14, 25}}},
		/* 36-Q */ {3034, 30, []ecBlockGroup{{46, 24}, {10, 25}}},
		/* 37-Q */ {3196, 30, []ecBlockGroup{{49, 24}, {10, 25}}},
		/* 38-Q */ {3362, 30, []ecBlockGroup{{48, 24}, {14, 25}}},
		/* 39-Q */ {3532, 30, []ecBlockGroup{{43, 24}, {22, 25}}},
		/* 40-Q */ {3706, 30, []ecBlockGroup{{34, 24}, {34, 25}}},
	}

	ecBlocksH = [40]ecSpec{
		/*  1-H */ {26, 17, []ecBlockGroup{{1, 9}}},
		/*  2-H */ {44, 28, []ecBlockGroup{{1, 16}}},
		/*  3-H */ {70, 22, []ecBlockGroup{{2, 13}}},
		/*  4-H */ {100, 16, []ecBlockGroup{{4, 9}}},
		/*  5-H */ {134, 22, []ecBlockGroup{{2, 11}, {2, 12}}},
		/*  6-H */ {172, 28, []ecBlockGroup{{4, 15}}},
		/*  7-H */ {196, 26, []ecBlockGroup{{4, 13}, {1, 14}}},
		/*  8-H */ {242, 26, []ecBlockGroup{{4, 14}, {2, 15}}},
		/*  9-H */ {292, 24, []ecBlockGroup{{4, 12}, {4, 13}}},
		/* 10-H */ {346, 28, []ecBlockGroup{{6, 15}, {2, 16}}},
		/* 11-H */ {404, 24, []ecBlockGroup{{3, 12}, {8, 13}}},
		/* 12-H */ {466, 28, []ecBlockGroup{{7, 14}, {4, 15}}},
		/* 13-H */ {532, 22, []ecBlockGroup{{12, 11}, {4, 12}}},
		/* 14-H */ {581, 24, []ecBlockGroup{{11, 12}, {5, 13}}},
		/* 15-H */ {655, 24, []ecBlockGroup{{11, 12}, {7, 13}}},
		/* 16-H */ {733, 30, []ecBlockGroup{{3, 15}, {13, 16}}},
		/* 17-H */ {815, 28, []ecBlockGroup{{2, 14}, {17, 15}}},
		/* 18-H */ {901, 28, []ecBlockGroup{{2, 14}, {19, 15}}},
		/* 19-H */ {991, 26, []ecBlockGroup{{9, 13}, {16, 14}}},
		/* 20-H */ {1085, 28, []ecBlockGroup{{15, 15}, {10, 16}}},
		/* 21-H */ {1156, 30, []ecBlockGroup{{19, 16}, {6, 17}}},
		/* 22-H */ {1258, 24, []ecBlockGroup{{34, 13}}},
		/* 23-H */ {1364, 30, []ecBlockGroup{{16, 15}, {14, 16}}},
		/* 24-H */ {1474, 30, []ecBlockGroup{{30, 16}, {2, 17}}},
		/* 25-H */ {1588, 30, []ecBlockGroup{{22, 15}, {13, 16}}},
		/* 26-H */ {1706, 30, []ecBlockGroup{{33, 16}, {4, 17}}},
		/* 27-H */ {1828, 30, []ecBlockGroup{{12, 15}, {28, 16}}},
		/* 28-H */ {1921, 30, []ecBlockGroup{{11, 15}, {31, 16}}},
		/* 29-H */ {2051, 30, []ecBlockGroup{{19, 15}, {26, 16}}},
		/* 30-H */ {2185, 30, []ecBlockGroup{{23, 15}, {25, 16}}},
		/* 31-H */ {2323, 30, []ecBlockGroup{{23, 15}, {28, 16}}},
		/* 32-H */ {2465, 30, []ecBlockGroup{{19, 15}, {35, 16}}},
		/* 33-H */ {2611, 30, []ecBlockGroup{{11, 15}, {46, 16}}},
		/* 34-H */ {2761, 30, []ecBlockGroup{{59, 16}, {1, 17}}},
		/* 35-H */ {2876, 30, []ecBlockGroup{{22, 15}, {41, 16}}},
		/* 36-H */ {3034, 30, []ecBlockGroup{{2, 15}, {64, 16}}},
		/* 37-H */ {3196, 30, []ecBlockGroup{{24, 15}, {46, 16}}},
		/* 38-H */ {3362, 30, []ecBlockGroup{{42, 15}, {32, 16}}},
		/* 39-H */ {3532, 30, []ecBlockGroup{{10, 15}, {67, 16}}},
		/* 40-H */ {3706, 30, []ecBlockGroup{{20, 15}, {61, 16}}},
	}
)

// ecTable returns the level's block table, or nil for an unknown level.
func ecTable(level EcLevel) *[40]ecSpec {
	switch level {
	case EcL:
		return &ecBlocksL
	case EcM:
		return &ecBlocksM
	case EcQ:
		return &ecBlocksQ
	case EcH:
		return &ecBlocksH
	}
	return nil
}

// alignmentCenters holds the alignment-pattern center coordinates (row =
// column), by version. Indexed [version-1]; version 1 has none; the last
// center of every version is (17 + 4*version) - 7.
var alignmentCenters = [40][]int{
	/*  1 */ {},
	/*  2 */ {6, 18},
	/*  3 */ {6, 22},
	/*  4 */ {6, 26},
	/*  5 */ {6, 30},
	/*  6 */ {6, 34},
	/*  7 */ {6, 22, 38},
	/*  8 */ {6, 24, 42},
	/*  9 */ {6, 26, 46},
	/* 10 */ {6, 28, 50},
	/* 11 */ {6, 30, 54},
	/* 12 */ {6, 32, 58},
	/* 13 */ {6, 34, 62},
	/* 14 */ {6, 26, 46, 66},
	/* 15 */ {6, 26, 48, 70},
	/* 16 */ {6, 26, 50, 74},
	/* 17 */ {6, 30, 54, 78},
	/* 18 */ {6, 30, 56, 82},
	/* 19 */ {6, 30, 58, 86},
	/* 20 */ {6, 34, 62, 90},
	/* 21 */ {6, 28, 50, 72, 94},
	/* 22 */ {6, 26, 50, 74, 98},
	/* 23 */ {6, 30, 54, 78, 102},
	/* 24 */ {6, 28, 54, 80, 106},
	/* 25 */ {6, 32, 58, 84, 110},
	/* 26 */ {6, 30, 58, 86, 114},
	/* 27 */ {6, 34, 62, 90, 118},
	/* 28 */ {6, 26, 50, 74, 98, 122},
	/* 29 */ {6, 30, 54, 78, 102, 126},
	/* 30 */ {6, 26, 52, 78, 104, 130},
	/* 31 */ {6, 30, 56, 82, 108, 134},
	/* 32 */ {6, 34, 60, 86, 112, 138},
	/* 33 */ {6, 30, 58, 86, 114, 142},
	/* 34 */ {6, 34, 62, 90, 118, 146},
	/* 35 */ {6, 30, 54, 78, 102, 126, 150},
	/* 36 */ {6, 24, 50, 76, 102, 128, 154},
	/* 37 */ {6, 28, 54, 80, 106, 132, 158},
	/* 38 */ {6, 32, 58, 84, 110, 136, 162},
	/* 39 */ {6, 26, 54, 82, 110, 138, 166},
	/* 40 */ {6, 30, 58, 86, 114, 142, 170},
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →