diff --git a/messages_essentials.go b/messages_essentials.go new file mode 100644 index 0000000..88cfc55 --- /dev/null +++ b/messages_essentials.go @@ -0,0 +1,8 @@ +package mujica + +func init() { + addMessages(map[string][2]string{ + "qr_code.label": {"QR code", "二维码"}, + "qr_code.too_long": {"Text too long for a QR code", "文字过长,无法生成二维码"}, + }) +} diff --git a/qr_code.go b/qr_code.go new file mode 100644 index 0000000..09a90b6 --- /dev/null +++ b/qr_code.go @@ -0,0 +1,79 @@ +package mujica + +import ( + "cmp" + "log/slog" + "math" + + "github.com/egoist/mygo/ui" + + "mujicaui/theme" +) + +// QRCodeOptions configure a QRCode. +type QRCodeOptions struct { + Level QRLevel + // Size is the side in DIPs with the quiet zone; zero is 160. + Size float32 + // Label names the code for assistive technology; empty uses "QR code". + Label string +} + +// qrCodeCache keeps the last encoding so it runs only when the text or level changes. +type qrCodeCache struct { + text string + level QRLevel + m QRMatrix + err error + done bool +} + +// QRCode draws text as a QR code, encoded offline; too long a text shows an error. +func QRCode(c *ui.Context, text string, opts QRCodeOptions) *ui.Element { + _ = opts.Level.String() + if !(opts.Size >= 0) || math.IsInf(float64(opts.Size), 1) { + panic("mujica: QRCode Size must be finite and >= 0") + } + size := opts.Size + if size == 0 { + size = 160 + } + k := Tokens(c) + e := ui.Box(c).Size(size, size).Shrink(0).Role(ui.RoleImage).Label(cmp.Or(opts.Label, msg(c, "qr_code.label"))) + cache := ui.Local(e, "qr_code", func() qrCodeCache { return qrCodeCache{} }) + if !cache.done || cache.text != text || cache.level != opts.Level { + m, err := EncodeQR([]byte(text), opts.Level) + *cache = qrCodeCache{text: text, level: opts.Level, m: m, err: err, done: true} + slog.Debug("mujica: qr_code encoded", "version", m.Version, "level", opts.Level, "err", err) + } + if cache.err != nil { + e.Center().Padding(Density(c).Unit()*2).Background(k.Surface).Border(theme.BorderWidth, k.Danger).Radius(theme.CardRadius). + Description(cache.err.Error()) + e.Children(func() { + ui.Text(c, msg(c, "qr_code.too_long")).TextColor(k.Danger).FontSize(FontSize(c, theme.CaptionSize)).TextAlign(ui.Center) + }) + return e + } + m := cache.m + // Scanners need dark modules on a light field, in either theme. + light, dark := theme.Light().Surface, theme.Light().Text + e.Draw(func(p *ui.Painter, r ui.Rect) { + n := m.Size() + 8 + s := p.Scale() + mod := float32(math.Floor(float64(r.W*s)/float64(n))) / s + if mod <= 0 { + return + } + ox := float32(math.Round(float64((r.X+(r.W-mod*float32(n))/2)*s))) / s + oy := float32(math.Round(float64((r.Y+(r.H-mod*float32(n))/2)*s))) / s + p.Fill(r, light, theme.ControlRadius) + for y, row := range m.Modules { + for x, on := range row { + if on { + p.Fill(ui.Rect{X: ox + float32(x+4)*mod, Y: oy + float32(y+4)*mod, W: mod, H: mod}, dark, 0) + } + } + } + }) + return e +} diff --git a/qr_code_encoder.go b/qr_code_encoder.go new file mode 100644 index 0000000..4408273 --- /dev/null +++ b/qr_code_encoder.go @@ -0,0 +1,262 @@ +package mujica + +import ( + "fmt" +) + +// QRLevel is a QR code's error correction level. The zero value is QRMedium. +type QRLevel int + +const ( + QRMedium QRLevel = iota // M: about 15% recoverable + QRLow // L: about 7% + QRQuartile // Q: about 25% + QRHigh // H: about 30% +) + +func (l QRLevel) String() string { + switch l { + case QRLow: + return "L" + case QRMedium: + return "M" + case QRQuartile: + return "Q" + case QRHigh: + return "H" + } + panic(fmt.Sprintf("mujica: unknown QRLevel %d", int(l))) +} + +// index is the level's column in qrCodeTable. +func (l QRLevel) index() int { return [4]int{1, 0, 2, 3}[l] } + +// formatBits are the level's two format information bits. +func (l QRLevel) formatBits() int { return [4]int{0, 1, 3, 2}[l] } + +// qrCodeBlocks is one version and level: EC codewords per block, then two block groups. +type qrCodeBlocks struct{ ec, n1, d1, n2, d2 int } + +func (b qrCodeBlocks) data() int { return b.n1*b.d1 + b.n2*b.d2 } + +// qrCodeTable is ISO/IEC 18004 Table 9, indexed [version-1][L, M, Q, H]. +var qrCodeTable = [40][4]qrCodeBlocks{ + {{7, 1, 19, 0, 0}, {10, 1, 16, 0, 0}, {13, 1, 13, 0, 0}, {17, 1, 9, 0, 0}}, + {{10, 1, 34, 0, 0}, {16, 1, 28, 0, 0}, {22, 1, 22, 0, 0}, {28, 1, 16, 0, 0}}, + {{15, 1, 55, 0, 0}, {26, 1, 44, 0, 0}, {18, 2, 17, 0, 0}, {22, 2, 13, 0, 0}}, + {{20, 1, 80, 0, 0}, {18, 2, 32, 0, 0}, {26, 2, 24, 0, 0}, {16, 4, 9, 0, 0}}, + {{26, 1, 108, 0, 0}, {24, 2, 43, 0, 0}, {18, 2, 15, 2, 16}, {22, 2, 11, 2, 12}}, + {{18, 2, 68, 0, 0}, {16, 4, 27, 0, 0}, {24, 4, 19, 0, 0}, {28, 4, 15, 0, 0}}, + {{20, 2, 78, 0, 0}, {18, 4, 31, 0, 0}, {18, 2, 14, 4, 15}, {26, 4, 13, 1, 14}}, + {{24, 2, 97, 0, 0}, {22, 2, 38, 2, 39}, {22, 4, 18, 2, 19}, {26, 4, 14, 2, 15}}, + {{30, 2, 116, 0, 0}, {22, 3, 36, 2, 37}, {20, 4, 16, 4, 17}, {24, 4, 12, 4, 13}}, + {{18, 2, 68, 2, 69}, {26, 4, 43, 1, 44}, {24, 6, 19, 2, 20}, {28, 6, 15, 2, 16}}, + {{20, 4, 81, 0, 0}, {30, 1, 50, 4, 51}, {28, 4, 22, 4, 23}, {24, 3, 12, 8, 13}}, + {{24, 2, 92, 2, 93}, {22, 6, 36, 2, 37}, {26, 4, 20, 6, 21}, {28, 7, 14, 4, 15}}, + {{26, 4, 107, 0, 0}, {22, 8, 37, 1, 38}, {24, 8, 20, 4, 21}, {22, 12, 11, 4, 12}}, + {{30, 3, 115, 1, 116}, {24, 4, 40, 5, 41}, {20, 11, 16, 5, 17}, {24, 11, 12, 5, 13}}, + {{22, 5, 87, 1, 88}, {24, 5, 41, 5, 42}, {30, 5, 24, 7, 25}, {24, 11, 12, 7, 13}}, + {{24, 5, 98, 1, 99}, {28, 7, 45, 3, 46}, {24, 15, 19, 2, 20}, {30, 3, 15, 13, 16}}, + {{28, 1, 107, 5, 108}, {28, 10, 46, 1, 47}, {28, 1, 22, 15, 23}, {28, 2, 14, 17, 15}}, + {{30, 5, 120, 1, 121}, {26, 9, 43, 4, 44}, {28, 17, 22, 1, 23}, {28, 2, 14, 19, 15}}, + {{28, 3, 113, 4, 114}, {26, 3, 44, 11, 45}, {26, 17, 21, 4, 22}, {26, 9, 13, 16, 14}}, + {{28, 3, 107, 5, 108}, {26, 3, 41, 13, 42}, {30, 15, 24, 5, 25}, {28, 15, 15, 10, 16}}, + {{28, 4, 116, 4, 117}, {26, 17, 42, 0, 0}, {28, 17, 22, 6, 23}, {30, 19, 16, 6, 17}}, + {{28, 2, 111, 7, 112}, {28, 17, 46, 0, 0}, {30, 7, 24, 16, 25}, {24, 34, 13, 0, 0}}, + {{30, 4, 121, 5, 122}, {28, 4, 47, 14, 48}, {30, 11, 24, 14, 25}, {30, 16, 15, 14, 16}}, + {{30, 6, 117, 4, 118}, {28, 6, 45, 14, 46}, {30, 11, 24, 16, 25}, {30, 30, 16, 2, 17}}, + {{26, 8, 106, 4, 107}, {28, 8, 47, 13, 48}, {30, 7, 24, 22, 25}, {30, 22, 15, 13, 16}}, + {{28, 10, 114, 2, 115}, {28, 19, 46, 4, 47}, {28, 28, 22, 6, 23}, {30, 33, 16, 4, 17}}, + {{30, 8, 122, 4, 123}, {28, 22, 45, 3, 46}, {30, 8, 23, 26, 24}, {30, 12, 15, 28, 16}}, + {{30, 3, 117, 10, 118}, {28, 3, 45, 23, 46}, {30, 4, 24, 31, 25}, {30, 11, 15, 31, 16}}, + {{30, 7, 116, 7, 117}, {28, 21, 45, 7, 46}, {30, 1, 23, 37, 24}, {30, 19, 15, 26, 16}}, + {{30, 5, 115, 10, 116}, {28, 19, 47, 10, 48}, {30, 15, 24, 25, 25}, {30, 23, 15, 25, 16}}, + {{30, 13, 115, 3, 116}, {28, 2, 46, 29, 47}, {30, 42, 24, 1, 25}, {30, 23, 15, 28, 16}}, + {{30, 17, 115, 0, 0}, {28, 10, 46, 23, 47}, {30, 10, 24, 35, 25}, {30, 19, 15, 35, 16}}, + {{30, 17, 115, 1, 116}, {28, 14, 46, 21, 47}, {30, 29, 24, 19, 25}, {30, 11, 15, 46, 16}}, + {{30, 13, 115, 6, 116}, {28, 14, 46, 23, 47}, {30, 44, 24, 7, 25}, {30, 59, 16, 1, 17}}, + {{30, 12, 121, 7, 122}, {28, 12, 47, 26, 48}, {30, 39, 24, 14, 25}, {30, 22, 15, 41, 16}}, + {{30, 6, 121, 14, 122}, {28, 6, 47, 34, 48}, {30, 46, 24, 10, 25}, {30, 2, 15, 64, 16}}, + {{30, 17, 122, 4, 123}, {28, 29, 46, 14, 47}, {30, 49, 24, 10, 25}, {30, 24, 15, 46, 16}}, + {{30, 4, 122, 18, 123}, {28, 13, 46, 32, 47}, {30, 48, 24, 14, 25}, {30, 42, 15, 32, 16}}, + {{30, 20, 117, 4, 118}, {28, 40, 47, 7, 48}, {30, 43, 24, 22, 25}, {30, 10, 15, 67, 16}}, + {{30, 19, 118, 6, 119}, {28, 18, 47, 31, 48}, {30, 34, 24, 34, 25}, {30, 20, 15, 61, 16}}, +} + +// QRMatrix is an encoded QR code: Modules[y][x] is true for dark. +type QRMatrix struct { + Version int + Level QRLevel + Mask int + Modules [][]bool +} + +// Size is the side of the symbol in modules, without the quiet zone. +func (m QRMatrix) Size() int { return len(m.Modules) } + +// EncodeQR encodes data in byte mode at the smallest version that fits, choosing the mask by penalty. +func EncodeQR(data []byte, level QRLevel) (QRMatrix, error) { + _ = level.String() + for v := 1; v <= 40; v++ { + cc := 8 + if v >= 10 { + cc = 16 + } + if len(data) >= 1<= 10 { + w.put(len(data), 16) + } else { + w.put(len(data), 8) + } + for _, d := range data { + w.put(int(d), 8) + } + capBits := 8 * b.data() + w.put(0, min(4, capBits-w.n)) + w.put(0, (8-w.n%8)%8) + for pad := 0xEC; w.n < capBits; pad ^= 0xEC ^ 0x11 { + w.put(pad, 8) + } + words := qrCodeInterleave(w.bytes, b) + g := newQRCodeGrid(v) + g.place(words) + best, bestPenalty := 0, -1 + for m := range 8 { + t := g.clone() + t.apply(m) + if p := t.penalty(); bestPenalty < 0 || p < bestPenalty { + best, bestPenalty = m, p + } + } + g.apply(best) + g.format(level.formatBits(), best) + g.version(true) + return QRMatrix{Version: v, Level: level, Mask: best, Modules: g.dark} +} + +// qrCodeBits appends bits most significant first. +type qrCodeBits struct { + bytes []byte + n int +} + +func (w *qrCodeBits) put(v, count int) { + for i := count - 1; i >= 0; i-- { + if w.n%8 == 0 { + w.bytes = append(w.bytes, 0) + } + if v>>i&1 == 1 { + w.bytes[w.n/8] |= 0x80 >> (w.n % 8) + } + w.n++ + } +} + +// qrCodeInterleave splits data into blocks, adds Reed-Solomon codewords and interleaves both. +func qrCodeInterleave(data []byte, b qrCodeBlocks) []byte { + var blocks, ecs [][]byte + gen := qrCodeGenerator(b.ec) + at := 0 + for i := range b.n1 + b.n2 { + n := b.d1 + if i >= b.n1 { + n = b.d2 + } + blk := data[at : at+n] + at += n + blocks = append(blocks, blk) + ecs = append(ecs, qrCodeRemainder(blk, gen)) + } + var out []byte + for i := range max(b.d1, b.d2) { + for _, blk := range blocks { + if i < len(blk) { + out = append(out, blk[i]) + } + } + } + for i := range b.ec { + for _, e := range ecs { + out = append(out, e[i]) + } + } + return out +} + +// qrCodeMul multiplies in GF(256) modulo x^8+x^4+x^3+x^2+1. +func qrCodeMul(x, y byte) byte { + var z byte + for i := 7; i >= 0; i-- { + hi := z & 0x80 + z <<= 1 + if hi != 0 { + z ^= 0x1D + } + if y>>i&1 == 1 { + z ^= x + } + } + return z +} + +// qrCodeGenerator returns the degree-n generator's coefficients, highest first, leading 1 dropped. +func qrCodeGenerator(n int) []byte { + g := make([]byte, n) + g[n-1] = 1 + root := byte(1) + for range n { + for j := range n { + g[j] = qrCodeMul(g[j], root) + if j+1 < n { + g[j] ^= g[j+1] + } + } + root = qrCodeMul(root, 2) + } + return g +} + +func qrCodeRemainder(data, gen []byte) []byte { + r := make([]byte, len(gen)) + for _, d := range data { + f := d ^ r[0] + copy(r, r[1:]) + r[len(r)-1] = 0 + for i := range r { + r[i] ^= qrCodeMul(gen[i], f) + } + } + return r +} + +// qrCodeAlignment returns the alignment pattern centers of version v. +func qrCodeAlignment(v int) []int { + if v == 1 { + return nil + } + n := v/7 + 2 + step := 26 + if v != 32 { + step = (v*4 + n*2 + 1) / (n*2 - 2) * 2 + } + pos := make([]int, n) + pos[0] = 6 + for i, p := n-1, v*4+10; i >= 1; i, p = i-1, p-step { + pos[i] = p + } + return pos +} diff --git a/qr_code_encoder_test.go b/qr_code_encoder_test.go new file mode 100644 index 0000000..49f0a26 --- /dev/null +++ b/qr_code_encoder_test.go @@ -0,0 +1,81 @@ +package mujica + +import ( + "encoding/hex" + "os" + "path/filepath" + "strconv" + "strings" + "testing" +) + +// testdata/qr: segno 1.6.6 byte-mode symbols, padding fixed to ISO 7.4.10. +func TestEncodeQRMatchesReference(t *testing.T) { + files, err := filepath.Glob("testdata/qr/*.txt") + if err != nil || len(files) < 6 { + t.Fatalf("reference files: %v %v", files, err) + } + levels := map[string]QRLevel{"L": QRLow, "M": QRMedium, "Q": QRQuartile, "H": QRHigh} + for _, f := range files { + b, err := os.ReadFile(f) + if err != nil { + t.Fatal(err) + } + lines := strings.Split(strings.TrimSpace(string(b)), "\n") + head := strings.Fields(lines[0]) + version, _ := strconv.Atoi(head[0]) + mask, _ := strconv.Atoi(head[2]) + data, err := hex.DecodeString(head[3]) + if err != nil { + t.Fatal(err) + } + m, err := EncodeQR(data, levels[head[1]]) + if err != nil { + t.Fatalf("%s: %v", f, err) + } + if m.Version != version || m.Mask != mask { + t.Errorf("%s: version %d mask %d, want %d %d", f, m.Version, m.Mask, version, mask) + continue + } + for y, row := range lines[1:] { + for x, ch := range row { + if m.Modules[y][x] != (ch == '1') { + t.Fatalf("%s: module (%d,%d) differs", f, x, y) + } + } + } + } +} + +func TestQRCodeAlignmentPositions(t *testing.T) { + for v, want := range map[int]string{1: "[]", 2: "[6 18]", 7: "[6 22 38]", 32: "[6 34 60 86 112 138]", 40: "[6 30 58 86 114 142 170]"} { + if got := fmtInts(qrCodeAlignment(v)); got != want { + t.Errorf("version %d: %s, want %s", v, got, want) + } + } +} + +func fmtInts(v []int) string { + s := make([]string, len(v)) + for i, n := range v { + s[i] = strconv.Itoa(n) + } + return "[" + strings.Join(s, " ") + "]" +} + +func TestEncodeQRLimits(t *testing.T) { + m, err := EncodeQR(make([]byte, 2953), QRLow) + if err != nil || m.Version != 40 || m.Size() != 177 { + t.Fatalf("largest L symbol: %v %v", m.Version, err) + } + if _, err := EncodeQR(make([]byte, 2954), QRLow); err == nil { + t.Fatal("2954 bytes fit at L") + } + if _, err := EncodeQR(make([]byte, 1274), QRHigh); err == nil { + t.Fatal("1274 bytes fit at H") + } + m, err = EncodeQR(nil, QRHigh) + if err != nil || m.Version != 1 { + t.Fatalf("empty data: %v %v", m.Version, err) + } +} diff --git a/qr_code_grid.go b/qr_code_grid.go new file mode 100644 index 0000000..39dbade --- /dev/null +++ b/qr_code_grid.go @@ -0,0 +1,242 @@ +package mujica + +// qrCodeGrid is a symbol under construction; fixed marks function modules. +type qrCodeGrid struct { + v int + dark, fixed [][]bool +} + +func newQRCodeGrid(v int) *qrCodeGrid { + n := 4*v + 17 + g := &qrCodeGrid{v: v, dark: make([][]bool, n), fixed: make([][]bool, n)} + for i := range n { + g.dark[i], g.fixed[i] = make([]bool, n), make([]bool, n) + } + for i := range n { + g.set(6, i, i%2 == 0) + g.set(i, 6, i%2 == 0) + } + g.finder(3, 3) + g.finder(n-4, 3) + g.finder(3, n-4) + al := qrCodeAlignment(v) + for i, x := range al { + for j, y := range al { + if i == 0 && j == 0 || i == 0 && j == len(al)-1 || i == len(al)-1 && j == 0 { + continue + } + for dy := -2; dy <= 2; dy++ { + for dx := -2; dx <= 2; dx++ { + g.set(x+dx, y+dy, max(qrCodeAbs(dx), qrCodeAbs(dy)) != 1) + } + } + } + } + // Reserve format and version areas; apply fills them. + g.format(-1, 0) + g.version(false) + return g +} + +func qrCodeAbs(v int) int { return max(v, -v) } + +func (g *qrCodeGrid) set(x, y int, dark bool) { + g.dark[y][x], g.fixed[y][x] = dark, true +} + +// finder draws a finder pattern and its separator centered at (cx, cy). +func (g *qrCodeGrid) finder(cx, cy int) { + n := len(g.dark) + for dy := -4; dy <= 4; dy++ { + for dx := -4; dx <= 4; dx++ { + x, y := cx+dx, cy+dy + if x < 0 || y < 0 || x >= n || y >= n { + continue + } + d := max(qrCodeAbs(dx), qrCodeAbs(dy)) + g.set(x, y, d != 2 && d != 4) + } + } +} + +// format writes the 15 format bits and the dark module; levelBits -1 leaves them light. +func (g *qrCodeGrid) format(levelBits, mask int) { + data := levelBits<<3 | mask + rem := data + for range 10 { + rem = rem<<1 ^ (rem>>9)*0x537 + } + b := (data<<10 | rem) ^ 0x5412 + bit := func(i int) bool { return levelBits >= 0 && b>>i&1 == 1 } + n := len(g.dark) + for i := 0; i <= 5; i++ { + g.set(8, i, bit(i)) + } + g.set(8, 7, bit(6)) + g.set(8, 8, bit(7)) + g.set(7, 8, bit(8)) + for i := 9; i < 15; i++ { + g.set(14-i, 8, bit(i)) + } + for i := range 8 { + g.set(n-1-i, 8, bit(i)) + } + for i := 8; i < 15; i++ { + g.set(8, n-15+i, bit(i)) + } + g.set(8, n-8, levelBits >= 0) +} + +// version writes the 18 version bits from version 7 on, or light ones. +func (g *qrCodeGrid) version(on bool) { + if g.v < 7 { + return + } + rem := g.v + for range 12 { + rem = rem<<1 ^ (rem>>11)*0x1F25 + } + b := g.v<<12 | rem + n := len(g.dark) + for i := range 18 { + dark := on && b>>i&1 == 1 + a, c := n-11+i%3, i/3 + g.set(a, c, dark) + g.set(c, a, dark) + } +} + +// place lays the codewords in the zigzag from the bottom right; remainder bits stay light. +func (g *qrCodeGrid) place(words []byte) { + n := len(g.dark) + i := 0 + for right := n - 1; right >= 1; right -= 2 { + if right == 6 { + right = 5 + } + for vert := range n { + for j := range 2 { + x := right - j + up := (right+1)&2 == 0 + y := vert + if up { + y = n - 1 - vert + } + if g.fixed[y][x] || i >= len(words)*8 { + continue + } + g.dark[y][x] = words[i/8]>>(7-i%8)&1 == 1 + i++ + } + } + } +} + +func (g *qrCodeGrid) clone() *qrCodeGrid { + t := &qrCodeGrid{v: g.v, dark: make([][]bool, len(g.dark)), fixed: g.fixed} + for i, row := range g.dark { + t.dark[i] = append([]bool(nil), row...) + } + return t +} + +// apply XORs mask over the data modules; the information areas stay light, as 7.8.3 evaluates them. +func (g *qrCodeGrid) apply(mask int) { + for y, row := range g.dark { + for x := range row { + if g.fixed[y][x] { + continue + } + var flip bool + switch mask { + case 0: + flip = (x+y)%2 == 0 + case 1: + flip = y%2 == 0 + case 2: + flip = x%3 == 0 + case 3: + flip = (x+y)%3 == 0 + case 4: + flip = (x/3+y/2)%2 == 0 + case 5: + flip = x*y%2+x*y%3 == 0 + case 6: + flip = (x*y%2+x*y%3)%2 == 0 + case 7: + flip = ((x+y)%2+x*y%3)%2 == 0 + } + row[x] = row[x] != flip + } + } +} + +// penalty scores the symbol by the four rules of ISO/IEC 18004 7.8.3. +func (g *qrCodeGrid) penalty() int { + n := len(g.dark) + p, dark := 0, 0 + row, col := make([]bool, n), make([]bool, n) + for i := range n { + for j := range n { + row[j], col[j] = g.dark[i][j], g.dark[j][i] + if row[j] { + dark++ + } + if i > 0 && j > 0 && row[j] == row[j-1] && row[j] == g.dark[i-1][j] && row[j] == g.dark[i-1][j-1] { + p += 3 + } + } + p += qrCodeRuns(row) + qrCodeFinders(row) + qrCodeRuns(col) + qrCodeFinders(col) + } + return p + 10*(qrCodeAbs(dark*100-50*n*n)/(5*n*n)) +} + +// qrCodeRuns scores runs of five or more equal modules. +func qrCodeRuns(seq []bool) int { + p, run := 0, 1 + for j := 1; j <= len(seq); j++ { + if j < len(seq) && seq[j] == seq[j-1] { + run++ + continue + } + if run >= 5 { + p += run - 2 + } + run = 1 + } + return p +} + +// qrCodeFinders scores 1:1:3:1:1 patterns with four light modules on a side. +func qrCodeFinders(seq []bool) int { + core := [7]bool{true, false, true, true, true, false, true} + n, p := len(seq), 0 + light := func(a, b int) bool { + for i := max(a, 0); i < min(b, n); i++ { + if seq[i] { + return false + } + } + return true + } + for idx := 0; idx+7 <= n; { + match := true + for i, d := range core { + if seq[idx+i] != d { + match = false + break + } + } + if !match { + idx++ + continue + } + if idx == 0 || idx == n-7 || light(idx-4, idx) || light(idx+7, idx+11) { + p += 40 + idx += 7 + } else { + idx += 4 + } + } + return p +} diff --git a/qr_code_test.go b/qr_code_test.go new file mode 100644 index 0000000..19bf83c --- /dev/null +++ b/qr_code_test.go @@ -0,0 +1,74 @@ +package mujica + +import ( + "strings" + "testing" + + "github.com/egoist/mygo/ui" + + "mujicaui/theme" +) + +func TestQRCodeDrawsModulesAndCaches(t *testing.T) { + text := "HELLO" + var box ui.Rect + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{Mode: Dark}) + ui.Row(c).Children(func() { box = QRCode(c, text, QRCodeOptions{Level: QRLow, Size: 116}).Bounds() }) + }, 200, 200) + tt.Frame() + if box.W != 116 { + t.Fatalf("size %v", box) + } + m, _ := EncodeQR([]byte(text), QRLow) + // 21 modules plus 8 of quiet zone in 116 DIPs: 4 DIPs each. + px := func(x, y int) ui.Color { + r, g, b, _ := tt.Image().At(int(box.X)+(x+4)*4+2, int(box.Y)+(y+4)*4+2).RGBA() + return ui.RGB(uint8(r>>8), uint8(g>>8), uint8(b>>8)) + } + for _, at := range [][2]int{{0, 0}, {3, 3}, {7, 7}, {10, 6}, {12, 12}} { + want := theme.Light().Surface + if m.Modules[at[1]][at[0]] { + want = theme.Light().Text + } + if got := px(at[0], at[1]); got != want { + t.Errorf("module %v is %v, want %v", at, got, want) + } + } + if _, ok := tt.Find("QR code"); !ok { + t.Fatal("no accessible name") + } +} + +func TestQRCodeTooLong(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + QRCode(c, strings.Repeat("x", 1300), QRCodeOptions{Level: QRHigh}) + }, 300, 300) + if !tt.HasText("Text too long for a QR code") { + t.Fatalf("texts %q", tt.Texts()) + } +} + +func TestQRCodeRejectsBadOptions(t *testing.T) { + for name, o := range map[string]QRCodeOptions{"level": {Level: 9}, "size": {Size: -1}} { + func() { + defer func() { + if recover() == nil { + t.Errorf("%s: no panic", name) + } + }() + ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); QRCode(c, "x", o) }, 200, 200) + }() + } +} + +func TestQRCodeSnapshot(t *testing.T) { + snapshot(t, "qr_code", 420, 200, func(c *ui.Context) { + ui.Row(c).Padding(16).Gap(16).Children(func() { + QRCode(c, "https://example.com/mujica", QRCodeOptions{}) + QRCode(c, "MujicaUI", QRCodeOptions{Level: QRHigh, Size: 100}) + QRCode(c, strings.Repeat("x", 1300), QRCodeOptions{Level: QRHigh, Size: 100}) + }) + }) +} diff --git a/testdata/qr/hello-l.txt b/testdata/qr/hello-l.txt new file mode 100644 index 0000000..2e671dd --- /dev/null +++ b/testdata/qr/hello-l.txt @@ -0,0 +1,22 @@ +1 L 4 48454c4c4f +111111101111101111111 +100000101010101000001 +101110101000001011101 +101110101111001011101 +101110100000101011101 +100000101101001000001 +111111101010101111111 +000000000111100000000 +110011100101000101111 +101001000101100101111 +000101101111001110010 +100000011000011010000 +111011110100111000110 +000000001000111001011 +111111100100110001010 +100000101101100100010 +101110101011001110101 +101110100111100001011 +101110100011001111000 +100000101110011000000 +111111101110111110101 diff --git a/testdata/qr/long-h.txt b/testdata/qr/long-h.txt new file mode 100644 index 0000000..3b7ee86 --- /dev/null +++ b/testdata/qr/long-h.txt @@ -0,0 +1,90 @@ +18 H 7 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff4d756a69636155494d756a69636155494d756a69636155494d756a69636155494d756a69636155494d756a6963615549 +11111110100011001001111011010000010111101000100000010111000011001100110010001011101111111 +10000010101011101101011100110101101000101010010110111010110010101001011001101101001000001 +10111010001001001111010000100101011110100001011011110100001001100001010111110100001011101 +10111010100100010101001110100110011011010101000010101110111111000100010001010110101011101 +10111010101010111100111110011111101001100110111101011011111000110110110110110101001011101 +10000010100000010011011110101000101011101010001101001110001110001010000111101101001000001 +11111110101010101010101010101010101010101010101010101010101010101010101010101010101111111 +00000000000100111011011011011000110111111010011111101010001010001100011010010101100000000 +00010010010011001101111101111111111101110010000000011011111001110001110101100000100111011 +10101101110010101001001010110100000011101100111000001100111100100010001000100111101011101 +11001010010011001011011100010001110110101010100100000101101110010000110111110101111010110 +11110000111100111011111111011011111000000010010001001001010111000011111111011010100110100 +00011111010010111001101001111011001001101010011011011010101110011101110011001100010010110 +01010100101111110011101011110110000011001001011100111101010000110000101111010000010100010 +01100111001111011000101010110001101010011010000001010011101000110001100001010111010001011 +00110100101010000011010010100001100101100111010011111011111111010100110111000100100111000 +11011011110100011110000001001001101110100100010111110100001101111001001001001000111110010 +00001100000011111111011101111010100110111111011001010010111101011111001010011111000000001 +10100111011010111101110011011000101010111111110010101010001110011101100111111011110000110 +10100100110000110010110111110010110101000001001111000001111110111111110110101111010110010 +10111010001101001111110100110101001000001010110010111101110011011010000110101101010100000 +11110101111110111001001000100111101000100001111110010100100111101110100010001001010101111 +10111011110011100011110101100100000100100101001011011011100100010100111111010111011111011 +00011101001011101011111010111010110101001001011011100101101101110100010011100000001011101 +01001111001100011011100000101110000000111101111011010101100100110010011001100110100101100 +11111100110100101110101001000110011010110001000101011011001110100111100011100010101100101 +10111010100111110001100011100100001100111000010000110111010010010001110000110010111111010 +10111000100010011100000000100111110110011010000110101101111000101001101001110110111111010 +10101111110101100000000101001111111100101100111100010111111110100111110010101111111111011 +10101000111001101001101111101000110111110111001000100010001111110001110000110101100011010 +11111010101011010010001001011010101111101110010101010110101110101100110011001000101010110 +11001000110100110111101011001000110000111000001000111010001001000100111000011101100010001 +00111111101010100101011100011111111110110101110010110011111000000110100100000111111111110 +01110100010100001010101100111001010010001000010110100110000011010111011100100010111100001 +11110110001111001101001101001010010111001000110111010011010101101011011000000001101110001 +00000001010110101011111100100111010111011000110011011110100001011110100000001011100110111 +10001111111111010000000001001000010010010011000100101001010111001010100000000011100110110 +01110101010101111111001101111110011111001011110010010100010100001101010000101000001001000 +01110110111011101011001011010001010010000111111010100011101111001110010111100000111010100 +01111000101111011100100000111101100111010010100001111100011101011100001100111000000110001 +11010110010110100001001100010011001010110011111110101101111101001101000001101101001110111 +01000100000000100000011110001100101110100110000000011000001001000000111011010111010010111 +01110010001101101110101110100100100011000011100001110101011001100010001010001100001111100 +00100001110101101010001100100110001001111010010000111100100010101110011110010011011001011 +01101011010011011011010111100011000001100010011100011001010110101100110100100001001010101 +11111001101110000110011010000001000010100111010011110011111001011101110111111001101010000 +00010011101111111111011101111010101100100011100000001110100011000111100011000101001111101 +11101000011100000110100100101011111100001100000111000110111111100001110011111010110000100 +11111010100111100010000011110011101010001010011001000000110001100110000000100011001111001 +00100000100100111111110000110100011101001000111111100010010000110111111100100110000100011 +01001011001011100101100110001011100000010100000000110111011100100010001000110101101101100 +01111001101101011101001100001111010101100101010000110010011111110111111011101110110011111 +00010011000010000100010100100110011001000101011101001010101010101110110001010110110011100 +00101100110100111101000011010110110011100000110100110010001011101000010011110010011101101 +01111111110111000100011111101111100010101010010000110111111011001001111110101100111110011 +11111000101000010111101000001000110010001111100011000010001110011101111010000001100010100 +01111010110010101100110011001010100110001001001010001110101100000110101101010111101011001 +10111000111100000111101000001000110101000111101110000110001111110111111111010110100011011 +00001111101000100111001010111111101101111100011011010111111000010001110001101101111110110 +10111100001110011110110000000110001111001011001000011011001101011010011111011000001011010 +10001010001110011001101000000010001011111111011100000011001010111010101110111111101000011 +00001100001000010000010101111001111110111011001101111100111011000101011000001101110100001 +01001010111011000100111111011101011101101011010010101100011110011001000111001111100100011 +01100101010011110111101001010100001111110111011001100000100000001000100010011100111111100 +11001010010110110101001000111110001111000011101001100010010000001101011100011010000110010 +11111101110001011011011010100011010100111111100001101110000110010011111101000001001000010 +01101110111110000010110111001000101010000010011111101000111010111101000001100011100101011 +10101101100011110111111001100111111111011101100110101000011111100011100111101010001111110 +00101110100111110111010110110100110111011001000101101011011011100001010011111000011110011 +11110101001100110111010001110100000111001001000001000111111010000101110111011101110100101 +10111111000011010010111111011010101101110111010010011111010101101101101010100010001111000 +00100100001100000000110101000001000101110100100010100001000010011110001010000111110110001 +10000011001111010110111100101101011110001000001101110001110000000001000100010000011010111 +01111000111000001101011100100000001001001010100100101010111101111010110110110111001101110 +01110110010010000011101111101110110110011000110001011011011111000110110100100011011111001 +11111000001000010011001111011111010010110010101101101111111110001001100000010000001100110 +11101110100111111001110001010110111000011000100001100110001001010000101111010001110100101 +01100100001010000001110010110010001101100111000000011010100010101100010111101000111100000 +10010010110001100010101010000100111011100101110010110111001011101010110010001111011110101 +11001101101000010100111000110111011001110010100001011110011111100010100001111011010011101 +10010010110011101100111101111111100101001010101100000111111101010011111001010010111110100 +00000000100111100111001010011000111111010100011101010010001100101010101011101110100011110 +11111110010000000110100100001010110101000111000011110010101101001111000000001001101010000 +10000010001100100100001000101000101111110111101100011110001110111100100000101101100010001 +10111010010110011100100111011111111101010101001011011111111111111111111111101111111111000 +10111010100111001001001111001011011111111000010101000010010101011000001100011100001110111 +10111010011110001100111110000001010110010101100011110110001010111011001011111101010010101 +10000010010100100000000110110110011001110010010011111000011010111010001100101011000000010 +11111110010101101000100101001010110001111100100010001101100101111010000101101011010111010 diff --git a/testdata/qr/text-q.txt b/testdata/qr/text-q.txt new file mode 100644 index 0000000..ac74755 --- /dev/null +++ b/testdata/qr/text-q.txt @@ -0,0 +1,50 @@ +8 Q 7 436f75727420676f7468696320636f6d706f6e656e747320666f72204d79476f2e20436f75727420676f7468696320636f6d706f6e656e747320666f72204d79476f2e20436f75727420676f7468696320636f6d706f6e656e747320666f72204d79476f2e20 +1111111010000000011000011010001101101100101111111 +1000001001110010010000011101111111110111101000001 +1011101011011000111001101101000010110001101011101 +1011101010000101101110110110100000100001001011101 +1011101001110100001101111110111000001000001011101 +1000001010110100011000100010111111101010001000001 +1111111010101010101010101010101010101010101111111 +0000000010011100110000100010110101011110000000000 +0101011111100110100111111110111001111110011101101 +1110000001011111100110101101000000001000010011011 +0010101010110111110001110101001110010000000110111 +1111100010101110100010010101011101001101010100010 +0100011010111010111010100000010111011010010011011 +0011110101011111010100001100000000110100100001101 +1001101100011110010011010110110000011010110101001 +0011110000100101001011000100101110101011001011001 +1000111010110011111011101000100110111010010010110 +1100100110001000010010011000111101010010010101001 +1010101010000100010001101010101100010100110101011 +1001000100000000010001111110101011011101110110101 +1001101000011010111000111011100101101110010100000 +1000110000100011001110111101000010000000000000110 +1000111111110000110000111110000000100100111110111 +0001100010111001101001100010011100101011100010001 +0100101011100111010110101010100111101001101010011 +1100100010011010100001100010110110111101100010101 +1011111111111000100110111110110101011010111111101 +0000000000101101101110111010110111011000101000010 +0110001000011100110101101111001010011110011101111 +1011110100110010010001101101110110000010100010111 +1100101000111000111101011101000010011000111000011 +1111010100011101111100110011110000111000001100100 +0001001010010000010001100101000111001010011011001 +1100010101101101011000100111100110001100110000011 +0100011111111010101101011101100000111100011110011 +1000100110011000000010111001111000101100001111001 +0001111011111011100011110001100111001110001100010 +0110100001010111011011010110100100101100110101111 +0100011010111000011100101111110110011010101000101 +0111000110000100010100010100110111010110011000001 +1110001100001010111101111111011110011010111110101 +0000000011011100100111100010100001000011100010101 +1111111011100101010110101011011100101010101011011 +1000001011111010001011100011001011111110100010110 +1011101000010000100010111111101101001110111111011 +1011101011100000000001100000100011010001101010011 +1011101000001111011101101011110110100000010011011 +1000001010111010110010001001000111111001110100000 +1111111001000101010110100001101111111100110010011 diff --git a/testdata/qr/url-m.txt b/testdata/qr/url-m.txt new file mode 100644 index 0000000..02002b2 --- /dev/null +++ b/testdata/qr/url-m.txt @@ -0,0 +1,26 @@ +2 M 1 68747470733a2f2f6578616d706c652e636f6d2f6d756a696361 +1111111011100010001111111 +1000001000110110101000001 +1011101010101100101011101 +1011101001100110001011101 +1011101000011010001011101 +1000001011100010001000001 +1111111010101010101111111 +0000000001110011100000000 +1010001100110010100100101 +1110110100110101011101011 +0100101101010001010011101 +1110100110110010100101000 +0001101111101001101100001 +0011110111000101101100011 +1110101011010101111001101 +0011010110000010110111000 +1100001100111000111110010 +0000000010111010100010001 +1111111011010100101010001 +1000001000110011100010001 +1011101000111001111110010 +1011101001011100010010110 +1011101011010011110111011 +1000001001100001111110000 +1111111011101000101001001 diff --git a/testdata/qr/v8-l.txt b/testdata/qr/v8-l.txt new file mode 100644 index 0000000..52d312b --- /dev/null +++ b/testdata/qr/v8-l.txt @@ -0,0 +1,50 @@ +8 L 2 78787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878 +1111111001110110010100001101011000001000101111111 +1000001010011111010000100111110010100011101000001 +1011101000010011100011001010100111110001101011101 +1011101011110110010100010000001101011101001011101 +1011101000011001001001111111011000001100001011101 +1000001011100010000101100011110010100110001000001 +1111111010101010101010101010101010101010101111111 +0000000000010001001001100010100111110011100000000 +1111101110010100111110111110001101011000110101010 +1110100111010110010101100101011000001100011001110 +1001111000111001001001010111110010100111010011011 +0001010000110101111010010010100111110011100110001 +0010111111110001001000111000001101011000101100100 +0101100101101100111110101101011000001100011001110 +1111101000100011100011100111110010100111010011011 +0010010010001000101110101010100111110011100110001 +1010101000110100111111001000001101011000101100100 +1101000111111110010101101101011000001100011001110 +0011001000000111010001000111110010100111010011011 +0101110000001011100010010010100111110011100110001 +0011011000100110010100111000001101011000101100100 +1100010101001001001001001101011000001100011001110 +1111111110111010000101111111110010100111111111011 +0001100011111001001000100010100111110011100010001 +0101101010100100111111101010001101011001101010100 +1011100010100110010101100011011000001100100011110 +0001111111100001001001111111110010100110111111011 +0100000011111101111010100110100111110010110010001 +1011001101110001001001010010001101011001000110100 +1000010010110100111111011001011000001101001101110 +1111111010101011100010101101110010100110111001011 +1000110011000000101110100110100111110010110010001 +1010101100010100111110110010001101011001000110100 +0111100001011110010100011001011000001101001101110 +1111101111101111010001101101110010100110111001011 +1000000111101011100010000110100111110010110010001 +1100001010111110010101010010001101011001000110100 +1110100110001001001001011001011000001101001101110 +0100011101111010000100101101110010100110111001011 +0111000000101001001001100110100111110010110010001 +1110001011110100111111111110001101011001111110101 +0000000010011110010101100011011000001101100011110 +1111111011101001001000101011110010100111101011011 +1000001000100101111011100010100111110011100010001 +1011101011101001001001111110001101011000111110100 +1011101010110100111110010011011000001100100111100 +1011101011100011100011111001110010100110001101000 +1000001011110000101111001100100111110011011000001 +1111111010110100111110000110001101011001110010111 diff --git a/testdata/qr/zh-m.txt b/testdata/qr/zh-m.txt new file mode 100644 index 0000000..563da6d --- /dev/null +++ b/testdata/qr/zh-m.txt @@ -0,0 +1,26 @@ +2 M 2 e5aeabe5bbb7e593a5e789b9e7bb84e4bbb6e5ba93 +1111111001000010001111111 +1000001000001110101000001 +1011101010111100001011101 +1011101010010110101011101 +1011101011011000001011101 +1000001010100010001000001 +1111111010101010101111111 +0000000011111111100000000 +1011111000001101001111100 +0010110001110110100111100 +0001011110011100011100100 +0101010111001101110100100 +0000101110001001000011110 +1000010101000011001010000 +1011011110111100100110000 +1001110100110010011001111 +1010001111010000111110111 +0000000011001100100011100 +1111111000100110101010110 +1000001011101101100011111 +1011101011001010111110111 +1011101010000000100100011 +1011101010111000010010101 +1000001001110011100100101 +1111111011010000111101111 diff --git a/testdata/snapshots/darwin/qr_code-dark.png b/testdata/snapshots/darwin/qr_code-dark.png new file mode 100644 index 0000000..a6481aa Binary files /dev/null and b/testdata/snapshots/darwin/qr_code-dark.png differ diff --git a/testdata/snapshots/darwin/qr_code-light.png b/testdata/snapshots/darwin/qr_code-light.png new file mode 100644 index 0000000..0c46034 Binary files /dev/null and b/testdata/snapshots/darwin/qr_code-light.png differ