// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 // Package qrcode writes QR symbols, byte mode on error level M, by // hand: no dependency, no encoder table beyond the block structure the // ISO/IEC 18004 annex defines. The caller gets a complete symbol as an // inline SVG, which is what the admin interface needs to hand an // otpauth URI to a phone camera. // // The supported range is versions 1 to 16, which carries up to 560 // bytes, twice the longest otpauth URI a volumen account can produce. // Longer input is refused rather than silently downgraded: a truncated // URI scans as a symbol that opens nothing. package qrcode import ( "errors" "fmt" "strings" ) // block is one Reed-Solomon block: the codeword total and the data // codewords inside it. type block struct { total, data int } // versionBlocks is one version's structure: the block shapes it mixes // and how many of each. type versionBlocks struct { shapes []block counts []int } // blocks lists the level M block structure per version, index version-1. // Versions from 8 mix two block sizes. var blocks = []versionBlocks{ {blockList(block{26, 16}), []int{1}}, {blockList(block{44, 28}), []int{1}}, {blockList(block{70, 44}), []int{1}}, {blockList(block{50, 32}), []int{2}}, {blockList(block{67, 43}), []int{2}}, {blockList(block{43, 27}), []int{4}}, {blockList(block{49, 31}), []int{4}}, {blockList(block{60, 38}, block{61, 39}), []int{2, 2}}, {blockList(block{58, 36}, block{59, 37}), []int{3, 2}}, {blockList(block{69, 43}, block{70, 44}), []int{4, 1}}, {blockList(block{80, 50}, block{81, 51}), []int{1, 4}}, {blockList(block{58, 36}, block{59, 37}), []int{6, 2}}, {blockList(block{59, 37}, block{60, 38}), []int{8, 1}}, {blockList(block{64, 40}, block{65, 41}), []int{4, 5}}, {blockList(block{65, 41}, block{66, 42}), []int{5, 5}}, {blockList(block{73, 45}, block{74, 46}), []int{7, 3}}, } // blockList exists only because Go cannot spell a slice literal with a // fixed array type on one line readably. func blockList(bs ...block) []block { return bs } // alignment lists the alignment pattern centre coordinates per version; // version 1 carries none. var alignment = [][]int{ {}, {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}, } // capacity returns the byte-mode capacity of a version. func capacity(version int) int { shapes, counts := blocks[version-1].shapes, blocks[version-1].counts dataCodewords := 0 for i, shape := range shapes { dataCodewords += shape.data * counts[i] } headerBits := 12 // mode + 8-bit count if version >= 10 { headerBits = 20 // the count grows to 16 bits } return (dataCodewords*8 - headerBits) / 8 } // ErrTooLong names the refusal of input beyond the supported range. var ErrTooLong = errors.New("qrcode: input exceeds version 16 capacity") // SVG renders text as a complete QR symbol in inline SVG: a white card, // black modules, and the four-module quiet zone the spec demands. The // colours are fixed on purpose: a symbol that follows the page's scheme // can end up light-on-dark, which cameras refuse. func SVG(text string) (string, error) { m, err := encode([]byte(text)) if err != nil { return "", err } n := len(m) var b strings.Builder fmt.Fprintf(&b, ``) return b.String(), nil } // encode builds the final module matrix: codewords, mask, format and // version information all placed. func encode(text []byte) ([][]bool, error) { return encodeMasked(text, -1) } // encodeMasked builds the symbol with a forced mask, or the best one // when mask is negative. The test suite uses the forced form to prove // each mask's placement against a reference implementation. func encodeMasked(text []byte, mask int) ([][]bool, error) { version := 0 for v := 1; v <= len(blocks); v++ { if len(text) <= capacity(v) { version = v break } } if version == 0 { return nil, fmt.Errorf("%w (%d bytes)", ErrTooLong, len(text)) } codewords := codewords(text, version) n := 4*version + 17 m := newMatrix(n) reserveFunction(m, version) place(m, codewords) best, bestScore := 0, -1 if mask >= 0 { best = mask } else { for candidate := range 8 { trial := cloneMatrix(m.data) applyMask(trial, m.function, candidate) if score := penalty(trial); bestScore < 0 || score < bestScore { best, bestScore = candidate, score } } } applyMask(m.data, m.function, best) placeFormat(m, best) if version >= 7 { placeVersion(m, version) } return m.data, nil } // matrix couples the module grid with the map of cells reserved for // function patterns and information fields. type matrix struct { data [][]bool function [][]bool } func newMatrix(n int) *matrix { return &matrix{data: blank(n), function: blank(n)} } func blank(n int) [][]bool { m := make([][]bool, n) for i := range m { m[i] = make([]bool, n) } return m } func cloneMatrix(src [][]bool) [][]bool { dst := make([][]bool, len(src)) for i, row := range src { dst[i] = append([]bool(nil), row...) } return dst } // reserveFunction draws every fixed pattern: finders with separators, // timing lines, alignment patterns, the dark module, and blanks the // format and version areas so data placement skips them. func reserveFunction(m *matrix, version int) { n := len(m.data) set := func(y, x int, dark bool) { m.function[y][x] = true m.data[y][x] = dark } finder := func(top, left int) { for dy := -1; dy <= 7; dy++ { for dx := -1; dx <= 7; dx++ { y, x := top+dy, left+dx if y < 0 || x < 0 || y >= n || x >= n { continue } ring := max(abs(dy-3), abs(dx-3)) set(y, x, ring != 2 && ring <= 3) } } } finder(0, 0) finder(0, n-7) finder(n-7, 0) for i := 8; i < n-8; i++ { set(6, i, i%2 == 0) set(i, 6, i%2 == 0) } // Only the three centres that coincide with a finder's corner are // skipped: an alignment pattern may cross the timing line, and one // centred on it must still be drawn. finderCorner := func(cy, cx int) bool { return (cy == 6 && cx == 6) || (cy == 6 && cx == n-7) || (cy == n-7 && cx == 6) } for _, cy := range alignment[version-1] { for _, cx := range alignment[version-1] { if finderCorner(cy, cx) { continue } for dy := -2; dy <= 2; dy++ { for dx := -2; dx <= 2; dx++ { ring := max(abs(dy), abs(dx)) set(cy+dy, cx+dx, ring != 1) } } } } set(n-8, 8, true) // the dark module // Reserve both format areas and, from version 7, the version cells. for i := range 9 { m.function[8][i] = true m.function[i][8] = true } for i := range 8 { m.function[8][n-1-i] = true m.function[n-1-i][8] = true } if version >= 7 { for i := range 6 { for j := range 3 { m.function[n-11+j][i] = true m.function[i][n-11+j] = true } } } } // place lays the codeword bits into the data region, two columns at a // time from the bottom right, skipping the timing column. func place(m *matrix, codewords []byte) { n := len(m.data) bit := 0 total := len(codewords) * 8 upward := true for col := n - 1; col > 0 && bit < total; col -= 2 { if col == 6 { col-- } for i := 0; i < n && bit < total; i++ { row := n - 1 - i if !upward { row = i } for _, x := range []int{col, col - 1} { if bit < total && !m.function[row][x] { m.data[row][x] = codewords[bit/8]>>(7-uint(bit%8))&1 == 1 bit++ } } } upward = !upward } } // applyMask XORs the mask pattern over the data region. func applyMask(data, function [][]bool, mask int) { n := len(data) for y := range n { for x := range n { if !function[y][x] && maskBit(mask, y, x) { data[y][x] = !data[y][x] } } } } func maskBit(mask, y, x int) bool { switch mask { case 0: return (y+x)%2 == 0 case 1: return y%2 == 0 case 2: return x%3 == 0 case 3: return (y+x)%3 == 0 case 4: return (y/2+x/3)%2 == 0 case 5: return y*x%2+y*x%3 == 0 case 6: return (y*x%2+y*x%3)%2 == 0 default: return ((y+x)%2+y*x%3)%2 == 0 } } // placeFormat writes the BCH-protected format information, level M and // the chosen mask, into its two copies. bit(k) below is the k-th bit // from the most significant one. The first copy runs b14 to b9 along // row 8, b8 to b6 around the finder corner, and b5 to b0 up column 8; // the second carries b7 to b0 rightward along row 8 and b14 to b8 // upward along column 8, with the dark module between the segments. func placeFormat(m *matrix, mask int) { const levelM = 0 bits := formatBits(levelM, mask) n := len(m.data) bit := func(k int) bool { return bits>>(14-uint(k))&1 == 1 } // The copy around the top-left finder, skipping the timing line. for i := range 6 { m.data[8][i] = bit(i) } m.data[8][7] = bit(6) m.data[8][8] = bit(7) m.data[7][8] = bit(8) for i := range 6 { m.data[5-i][8] = bit(9 + i) } // The split copy. for i := range 8 { m.data[8][n-8+i] = bit(7 + i) } for i := range 7 { m.data[n-1-i][8] = bit(i) } } // formatBits builds the 15-bit format word: five data bits, ten BCH // check bits from the 0x537 generator, masked with the XOR pattern that // keeps an all-zero word impossible. func formatBits(level, mask int) uint { data := uint(level<<3 | mask) rem := data << 10 for i := 14; i >= 10; i-- { if rem&(1<>uint(i)&1 == 1 a, b := i/3, i%3 m.data[n-11+b][a] = dark m.data[a][n-11+b] = dark } } func versionBits(version int) uint { rem := uint(version) << 12 for i := 17; i >= 12; i-- { if rem&(1<= 5 { total += run - 2 } run, prev = 1, v } } if run >= 5 { total += run - 2 } } return total } score += runScore(func(y, x int) bool { return data[y][x] }) score += runScore(func(x, y int) bool { return data[y][x] }) for y := 0; y < n-1; y++ { for x := 0; x < n-1; x++ { if data[y][x] == data[y][x+1] && data[y][x] == data[y+1][x] && data[y][x] == data[y+1][x+1] { score += 3 } } } lookalike := func(get func(a, b int) bool) int { total := 0 pattern := []bool{true, false, true, true, true, false, true, false, false, false, false} for a := range n { for b := 0; b+11 <= n; b++ { match, matchRev := true, true for k := range 11 { v := get(a, b+k) if v != pattern[k] { match = false } if v != pattern[10-k] { matchRev = false } } if match { total += 40 } if matchRev { total += 40 } } } return total } score += lookalike(func(y, x int) bool { return data[y][x] }) score += lookalike(func(x, y int) bool { return data[y][x] }) for _, row := range data { for _, v := range row { if v { dark++ } } } total := n * n percent := dark * 100 / total score += 10 * (abs(percent-50) / 5) return score } func abs(v int) int { if v < 0 { return -v } return v }