Initial commit
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s

Assisted-by: GLM 5.3
This commit is contained in:
2026-09-29 10:03:32 +02:00
commit f8ed33df83
206 changed files with 44165 additions and 0 deletions
+161
View File
@@ -0,0 +1,161 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
package qrcode
// Galois field arithmetic over GF(256) with the QR field polynomial
// 0x11D: log and antilog tables built once at init, and the
// Reed-Solomon remainder the annex specifies for error level M.
var (
gfLog [256]byte
gfAnt [256]byte
rsPoly [][]byte // generator of degree i, index ecc length
)
func init() {
x := 1
for i := range 255 {
gfAnt[i] = byte(x)
gfLog[x] = byte(i)
x <<= 1
if x&0x100 != 0 {
x ^= 0x11D
}
}
// Degrees the supported versions need: level M ecc sizes run from
// 10 to 30 codewords per block.
rsPoly = make([][]byte, 31)
rsPoly[0] = []byte{1}
for d := 1; d < len(rsPoly); d++ {
rsPoly[d] = polyMul(rsPoly[d-1], []byte{1, gfAnt[d-1]})
}
}
func gfMul(a, b byte) byte {
if a == 0 || b == 0 {
return 0
}
return gfAnt[(int(gfLog[a])+int(gfLog[b]))%255]
}
func polyMul(a, b []byte) []byte {
out := make([]byte, len(a)+len(b)-1)
for i, av := range a {
for j, bv := range b {
out[i+j] ^= gfMul(av, bv)
}
}
return out
}
// rsRemainder divides data by the generator of the given degree and
// returns the remainder, the error correction codewords.
func rsRemainder(data []byte, degree int) []byte {
gen := rsPoly[degree]
rem := make([]byte, degree)
for _, b := range data {
factor := b ^ rem[0]
copy(rem, rem[1:])
rem[degree-1] = 0
if factor != 0 {
for i, g := range gen[1:] {
rem[i] ^= gfMul(g, factor)
}
}
}
return rem
}
// codewords packs the payload and returns the interleaved stream of
// data and error correction codewords the symbol carries.
func codewords(text []byte, version int) []byte {
shapes, counts := blocks[version-1].shapes, blocks[version-1].counts
dataCodewords := 0
blockCount := 0
for i, shape := range shapes {
dataCodewords += shape.data * counts[i]
blockCount += counts[i]
}
// The bit stream: mode, count, bytes, terminator, byte alignment
// and the alternating pad bytes.
var bit buf
bit.push(4, 4) // byte mode
if version >= 10 {
bit.push(uint(len(text)), 16)
} else {
bit.push(uint(len(text)), 8)
}
for _, b := range text {
bit.push(uint(b), 8)
}
bit.push(0, min(4, dataCodewords*8-bit.len()))
bit.align()
stream := bit.bytes()
for len(stream) < dataCodewords {
stream = append(stream, 0xEC, 0x11)
}
stream = stream[:dataCodewords]
// Split into blocks, correct each, then interleave data and error
// codewords the way the symbol reads them.
type rsBlock struct {
data []byte
ecc []byte
}
var list []rsBlock
offset := 0
for i, shape := range shapes {
for c := 0; c < counts[i]; c++ {
data := append([]byte(nil), stream[offset:offset+shape.data]...)
offset += shape.data
list = append(list, rsBlock{data: data, ecc: rsRemainder(data, shape.total-shape.data)})
}
}
out := make([]byte, 0, dataCodewords+blockCount*(shapes[0].total-shapes[0].data))
maxData := 0
for _, shape := range shapes {
maxData = max(maxData, shape.data)
}
for i := 0; i < maxData; i++ {
for _, b := range list {
if i < len(b.data) {
out = append(out, b.data[i])
}
}
}
maxEcc := 0
for _, b := range list {
maxEcc = max(maxEcc, len(b.ecc))
}
for i := 0; i < maxEcc; i++ {
for _, b := range list {
if i < len(b.ecc) {
out = append(out, b.ecc[i])
}
}
}
return out
}
// buf is the bit-level head of the codeword stream.
type buf struct {
b []byte
nbits int
}
func (b *buf) push(v uint, n int) {
for i := n - 1; i >= 0; i-- {
if b.nbits%8 == 0 {
b.b = append(b.b, 0)
}
if v&(1<<uint(i)) != 0 {
b.b[len(b.b)-1] |= 1 << uint(7-b.nbits%8)
}
b.nbits++
}
}
func (b *buf) len() int { return b.nbits }
func (b *buf) align() {} // push already writes byte-aligned bytes
func (b *buf) bytes() []byte { return b.b }
+464
View File
@@ -0,0 +1,464 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" role="img" aria-hidden="true" shape-rendering="crispEdges">`, n+8, n+8)
b.WriteString(`<rect width="100%" height="100%" fill="#fff"/>`)
b.WriteString(`<path fill="#000" d="`)
for y, row := range m {
for x, dark := range row {
if dark {
fmt.Fprintf(&b, "M%d %dh1v1h-1z", x+4, y+4)
}
}
}
b.WriteString(`"/></svg>`)
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)) != 0 {
rem ^= 0x537 << uint(i-10)
}
}
return (data<<10 | rem) ^ 0x5412
}
// placeVersion writes the 18-bit version word of versions 7 and up
// into its two strips, least significant bit first, each strip read
// top to bottom in columns left to right.
func placeVersion(m *matrix, version int) {
bits := versionBits(version)
n := len(m.data)
for i := range 18 {
dark := bits>>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<<uint(i)) != 0 {
rem ^= 0x1F25 << uint(i-12)
}
}
return uint(version)<<12 | rem
}
// penalty scores a masked matrix by the four rules of the spec: long
// runs, large blocks, the finder lookalike pattern, and a colour
// balance far from half.
func penalty(data [][]bool) int {
n := len(data)
score := 0
dark := 0
runScore := func(get func(a, b int) bool) int {
total := 0
for a := range n {
run, prev := 0, get(a, 0)
for b := range n {
v := get(a, b)
if v == prev {
run++
} else {
if run >= 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
}
+144
View File
@@ -0,0 +1,144 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
package qrcode
import (
"os/exec"
"strings"
"testing"
)
// reference renders the same symbol with the system qrencode and
// returns its module matrix, or skips the test when the tool is absent.
func reference(t *testing.T, text string) [][]bool {
t.Helper()
binary, err := exec.LookPath("qrencode")
if err != nil {
t.Skip("qrencode is not installed; the differential test cannot run")
}
out, err := exec.Command(binary, "-l", "M", "-m", "0", "--type=ASCII", text).Output()
if err != nil {
t.Fatalf("qrencode: %v", err)
}
var rows [][]bool
for line := range strings.SplitSeq(string(out), "\n") {
line = strings.TrimSuffix(line, "\r")
if line == "" {
continue
}
// A row may be entirely light modules, which trims to spaces:
// only a truly empty line separates rows, so nothing is dropped.
if len(line)%2 != 0 {
t.Fatalf("odd ASCII line %q", line)
}
row := make([]bool, len(line)/2)
for i := range row {
row[i] = line[i*2] == '#'
}
rows = append(rows, row)
}
return rows
}
// referenceMask decodes the mask the reference chose from its own
// format information, using the layout this package writes.
func referenceMask(t *testing.T, ref [][]bool) int {
t.Helper()
var word uint
cells := func(y, x int) uint {
if ref[y][x] {
return 1
}
return 0
}
for i := range 6 {
word = word<<1 | cells(8, i)
}
word = word<<1 | cells(8, 7)
word = word<<1 | cells(8, 8)
word = word<<1 | cells(7, 8)
for i := range 6 {
word = word<<1 | cells(5-i, 8)
}
// The stored word carries the XOR mask; the data sits on top once
// it is undone.
return int((word^0x5412)>>10) & 7
}
func diffMatrices(t *testing.T, text string) {
t.Helper()
// The reference's own mask, forced: every other bit of the symbol
// must then match, which proves the encoding, the error correction,
// the placement and the format information. The mask each encoder
// picks for itself is a choice among equally valid symbols.
theirs := reference(t, text)
mask := referenceMask(t, theirs)
ours, err := encodeMasked([]byte(text), mask)
if err != nil {
t.Fatalf("encode %q: %v", text, err)
}
if len(theirs) != len(ours) {
t.Fatalf("%q: size %d, reference %d", text, len(ours), len(theirs))
}
for y := range ours {
for x := range ours[y] {
if ours[y][x] != theirs[y][x] {
t.Fatalf("%q: module differs at (%d,%d)", text, x, y)
}
}
}
}
// TestAgainstQrencode is the proof of the encoder: for the full range
// of payloads, every module must match the reference implementation
// once its own mask is forced. The payloads stay clear of the
// reference's segmentation optimiser: letters and underscores carry no
// alphanumeric runs worth switching for, so it speaks plain byte mode
// the way this package always does.
func TestAgainstQrencode(t *testing.T) {
payloads := []string{
"hello world",
"otpauth_totp_volumen_petr_secret_and_issuer_example_uri",
"https_petrbalvin_org_neco_delsi_cesta_query_hodnota_druha_ano",
strings.Repeat("a", 14), // v1 edge
strings.Repeat("a", 15), // v2
strings.Repeat("b", 84), // v5 edge
strings.Repeat("b", 85), // v6
strings.Repeat("c", 180), // v9 edge
strings.Repeat("c", 181), // v10 (16-bit count)
strings.Repeat("d", 450), // v16 edge
}
for i := range 10 {
payloads = append(payloads, strings.Repeat("x", 1+i*45))
}
for _, p := range payloads {
diffMatrices(t, p)
}
}
func TestSVGShape(t *testing.T) {
svg, err := SVG("https://example.com/")
if err != nil {
t.Fatalf("SVG: %v", err)
}
for _, want := range []string{
`viewBox="0 0 33 33"`, // v1 + quiet zone
`<rect width="100%" height="100%" fill="#fff"/>`,
`fill="#000"`,
`</svg>`,
} {
if !strings.Contains(svg, want) {
t.Fatalf("missing %q in %s", want, svg[:min(len(svg), 200)])
}
}
}
func TestTooLongIsRefused(t *testing.T) {
if _, err := SVG(strings.Repeat("z", 450)); err != nil {
t.Fatalf("version 16 capacity refused: %v", err)
}
if _, err := SVG(strings.Repeat("z", 451)); err == nil {
t.Fatal("input beyond version 16 accepted")
}
}