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
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:
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user