From f7096b13f9855431c4d0aae8f9e5b6942f78c20e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 5 Oct 2026 14:06:14 -0400 Subject: [PATCH] WIP essentials: QR --- messages_essentials.go | 8 + qr_code.go | 79 ++++++ qr_code_encoder.go | 262 ++++++++++++++++++++ qr_code_encoder_test.go | 81 ++++++ qr_code_grid.go | 242 ++++++++++++++++++ qr_code_test.go | 74 ++++++ testdata/qr/hello-l.txt | 22 ++ testdata/qr/long-h.txt | 90 +++++++ testdata/qr/text-q.txt | 50 ++++ testdata/qr/url-m.txt | 26 ++ testdata/qr/v8-l.txt | 50 ++++ testdata/qr/zh-m.txt | 26 ++ testdata/snapshots/darwin/qr_code-dark.png | Bin 0 -> 6280 bytes testdata/snapshots/darwin/qr_code-light.png | Bin 0 -> 6170 bytes 14 files changed, 1010 insertions(+) create mode 100644 messages_essentials.go create mode 100644 qr_code.go create mode 100644 qr_code_encoder.go create mode 100644 qr_code_encoder_test.go create mode 100644 qr_code_grid.go create mode 100644 qr_code_test.go create mode 100644 testdata/qr/hello-l.txt create mode 100644 testdata/qr/long-h.txt create mode 100644 testdata/qr/text-q.txt create mode 100644 testdata/qr/url-m.txt create mode 100644 testdata/qr/v8-l.txt create mode 100644 testdata/qr/zh-m.txt create mode 100644 testdata/snapshots/darwin/qr_code-dark.png create mode 100644 testdata/snapshots/darwin/qr_code-light.png 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 0000000000000000000000000000000000000000..a6481aa1142dbeccce5b1e9ee78cb591e5ecc44d GIT binary patch literal 6280 zcmcgwcQo8vyB0kddT&w3C`ok2 zh%!V*xtyHuob}y5@4eq$?^=89-(Kt8@AEwGZofCiKu?35gnWQs^#^>J;A}btFNW1Y!sM-TF?hEIx`V@r4BEFG1VWv(7*o&Yo)3_ zl#oha5L|YqYeDSCd{Oev4sJPNfwAW2LhEW-C@_IOO$+)jJ1dng#iTDbF2gU8rj?#3 zx9G^K$i&uA+OQcbCF11a$fvrgkv;&XAw`KX($vl_YZs0Kwp6 zR7I_$BWB(O1HrJmh|z~Op7Z&AkxyJk>u|p>Xc1tBe6V_^UpCG1xctOTzTJBvFl;5H zycXhaXFlSnmA=QenAUOee5K}aTPvMdUx%=&B#DSPJ^n*%4< zPkT8zc^e9Q!JN^g_C!}E{dyq5(GeOg4piVM1}=vd{-OT>d~IDxC^iAcR`yW8kNL*% zAX@d@$a3hqH?{|pMY?cyF-Lc`$)6DoQlv5S0S?X^ZcYnY-BtF%%P|A`6wkP` z-2~>$s^HfH$Jz0_O1XO44iNGZXA6_bcc=${h4T23#@rQ(yrG|BJ>W6g8dd03wCa2EOxDZ5r6UfsQcge;bDglHvCCAnSy?ni} z1WB8V&g?Dy;$tGwy5x9}S%Tl98-TAdkQZ0u{i)TTNby|lZq(y^F+Yaq9%ZBtCb7MZ zKt-T_#i2^*=rJ_iYlz{37xQu1oQbA@Yn}gQ`7hz!l#pg|poe9np|<3q zdv0R?u>CXNr1t?eXe3#Kla%jVgdCA{yD+wHiV5c_Z^YQjNZ|Gs<1?;ZN){?vHUJqy z*(`KCC};2h)s(lSW^Fr1m(3+6E@1~+Ei?r}sq-OJ!K((3?eO+*#wCYbl+iine9DBi z&Z@7HecMwUn02=Wf@zaQDD9Y;m7c&|5Cw^RYr3iw{S^J9CIb{t@8>jz?E=GCfKJQ& zwiskJF8Y))vg_+pFoyq3yM+FO%R22~BU-!a;h5 z_4FAN_9-kkSn?Rx)qgfOy_K3`9nx&Kx~ELc9O~{4tCk&~d?W+(VpkGY6f{OoOU^uO zTv^Lt+qcgwlhrquw5dHr@+k83n$aO1j^Iu|sS>4bv|{o)^lqDC@O{41vm2KMJ^sE8 zqg<8l$(3^qBR8V1+nW94PwBNsXV5SafGp`Hk=#+|If8a8>TQ&$w zjyotXuU8jpH8eALA{?jRQU!20@F@DD!zFWiQN|hFFK&VjdYouRxq(KkkA2bXunSejx3()!ace zm9Xdg9=+iKy}`B^s;o=5&k~;bXWL~@_g8mgZoWk`cyQ*~sVP=ebl+tI{5CR#E^SO+ z-q-YIOyXjGl3wM8k4zc%B*A)Y;VyO4$wDUf<7KW9>Yi9N3$UaXzLX)FIb4}eqpLs_ zr%LYPpRFEA1$j5E{e}b{R}xGxU7AIHsa#V0xMO2q704s>*5dtU&=jw6|MROY{7M=z zan2DM!>&U4N_Le-q03;H%6@-55mHyyTDo89*e)LOK(lgo=Ln*hNXLBpOYy|V42JX& zz{ztHS2LAAy1r3`QYx?8yG;Ll_`XFt*;}R9FjMDP!T?xo*hKtN9sqDRrHk(J_Gfrg zY(gEE_|9Z{VFE$Fz5B?(bZE^B*wz+v$CVSX@J4l{rs-a(}?+;G9V;B?#& z9**CtQCho#r*d&oRY?UJ7Idfvgf531>|wqgGNE>7nE)Xni?8a$yC}oMOJORLA0|g2 zy$$B2BHO}tC40;HA})UKqJMhCA2|de)(J=}wMqE1v!v8jO$OF#v61Z-ZwqXi_p318Y*O1BF z>W$~Af2?e7tm$Tc*T_FIvdM*5`j#}2X=HXx_9SZ?9vgpC+g5IK_lS#7Lm*O<^6vHxOUCHwky;S0-&Arl?)6)%}9LE*3cY?Xu6L zbUDGnEGGvzEHVb%(!W1%P7NelnTZNw3`4%tDA8uW+@qWwT^i*21xfsIKkVU+u}T>x zea??rsLi*xnWtRi%so9=h@Mn7ecmONPSsVYAbdJRd~~GqDYu13Hb&Eo@M=>C-y}ig zXQddMqquw)8R~cyJw|r&S*HEK^WIISu_TIP!(KGR>8(`Nhie~bGKYV1RIyWBt3arM zxbZ6?(%ThRp*ze2E&KwSUvBCQI{A<)RySCHnD4>o`sB6Z7;PIdVsN|^$r!6H8)b_ zSvXo6=^RywUdBds$~#Y^#i4637{&2%xH*%;G!hC^NzDr1(1b7monA+-Byfp#*a~Ns zHGSOwmOra-kg#@H6MzwQ&v85(WqnS9S1)QU#VMF}kSj@E^z1c|y{h7&^t=PG(7aBo z)|jC*Tg-Wq8-`6XDLuDalz`1Q>uATO!z^dihW=2U6=2<^p-y>;LrSW})=TK7NodB7 z@0QPFw-i8^zjrV{7m~5OH^3S&hHV{t44vEy8@qM*bCbGZlT!8pn60Hw4{Vff2;;j` zW=qe%k&I4(nAJ~mIll&0`Dw8`5TF`(E|cGswWIyQ{J!OM>U=c>p*{6Ne4M+rkG+Go z=&^ZAmiqoV*N8CBY*7sCQ~#`2jOb(j`D&uvBx8E3dtBr&U%a%uD& zDz9iU3=OtpdK@Xjz%GG42XC5RNoQFHww-^b%txL(q0t<(FnOz+{a`irbdc4@oJRYJsb)Qmi$!`9{;obBo{s)$)W>{9VpYERM(?dOqYenp6A4)NOowt0PHJP_gmw|3I>VnykDV<`f8 z4o1!_jIb!r1(Yb13@_M<`6pY~-t~J;Dc9>8@0=}>72$Q52ms50SJ$#Jka+XZY4~dE zwikZC(CX`Fa?N~t{S>r)w6u>v);IlHBq+&?K~o%-DZNny&Yk@S-MA?6=duaGVB~S% zALEU!Ns}!;7?+rV*^pvTta<&ZcK0A$N27rbr0|My%o0a-5r3eTkOZc?I)c%WvUi?{LF`HfjOkDh(l?t?;ko(Oep49zg& zj=Hre+w_VJ#P1=#1%rR)EvldA<@#~|P}!mnX3Vy?%cwmby54QC6 zUF=eEmb2`qAL866%rMdcy}G-W9OsB&^vLr%jPtDyc_j%bl`-dT8~B0*+HKOrX~gz} z)%KUvrAjlNaf`&a?ttTX{0NnM4rwDNwz8bdVV+?h3)dE{bw);0daw^*hFnf{0qYwv zU7k11VQj?83ZJg<$ue6}WXarPbR9m)iVn7|bgd&JAwHTaQ=$s~qv zq^tVs@4edMUW=20kWWxI<)%+XPf9dbyG$ZecaT{HDNY2FL2uz}HTpO)+GhfTW@+$; ze0Cg@MTYyJhDk00eY4^!{_WWp)scT25@#^sli+(NA7QKgx^B^LzaCfRw6Ot_nEQn& z1}N$abj~P;!A^XA_^R%^OzyvO$5wsOvpZy z>iNJOZXemT?`y}^-9x(s={L7V-aV;T(hwZ}`98(XYNt?zZ{zdcO0A<~`7l zfu`Yq))Zw(xu`~2B>GRl>s~n&waG8HDJXbc(;b)i`1_iz+1a1ODeUQ_5FI8%g=%X< z!z#-UNP?Wm6bu@hB?x~@`a728OEFpnz!7TvciMh#Bktc8VArh>xhYLKWCgblv4mR- zIaso7vzoEnn1M(-NN5p4SDWKs){?>hxA(tyEZ}*TwZAwfwqFjcCF6l!FZi#W{X296 zFnbzvKgIP@cyGUP3<=8PaeUzPLw@fVGC==(JlcNa4nE9WcQj&rylmP^nay#h%CYau zMy$V{w>(H+S16B5zt%P)@WrI0-50JzU43dIDqM%Qj;$JM{BU}W` zx2*()rK%skyWdt<)_hQKmXJqk|BN{uUWKv6p0Ea05y#G{PG$ z*}SX1PpyTGZXoMmwgGD^nfFUw-e^bCF4>Zf?P4GVSNOJ=;TIRf&X@7FIQhBBY6+Pl zw#;nN514EZ(`|i-al8rc;eE@)AR!Kot$0LO}D(N&Ja}#Jb{$L=7vPeNQFL%^~3Bax^$8(CXPNM1Q8kV80O#S_t%K>g& zT|pr!6@oAW!bV#_h%bR);{Vzd%14wV4yv{@$W5tMO`Drv#afKn~2#XcoBc?!GMfb>i#%TgBDl?FVV8@1yzWj6~(&Nj2RPrjjUD-k?MGU4Q~emK{p^mK0ED)wl2 zF%ViZAM`!T9>Le-cRW0u{A!~{8wi7;ht-BE3Y<>XrjYu*^N+DuOn8x>yFir9m$JHU zXx_`|b_}w-wsEYaxCHyYt9n{bX`&C$X;-vZG`1sYmZGx}*pm$F&P zV%|htWTFtRWVV77;#!ixe+`DiNnnOP*?!^f1AYRS7ut`664EjU;+7fXJBK!%Ik7Fs2yu;)#lUw={;LVZ#Iaw+gQuj()=0NhEgO!nKHrnpB7R4gBF`* z55!V+Doprzf&nw5zcd%{53PTRRvE-G-E%5e=`$ucla+(={Pc?*^Th!&>aUAy4rRUK zmp;Xa`FM8%trXc{g5l7VwnRuTa%B_Z6S`nz9BilFTT!3YWGd|(D(=|kr0%owGb0@v zYG>a+bud2L{3+3=G<0jZs4_kG$jkiffi`!@%O4Im`t@I=_%42t;m`DuLC7G6pCAC* z?0Dp1fD=~XG7`S`&eSza*z1~HyDUK^vOJ@vRLE()%5Q)J77g!-eEM+utZu++M1+du z2dKT+Jr;?~Y{qK7rkTUx#wgyNOVn9^h~MjDr2w(`=6lVFcp4iJj|qx&wo`E0Mo+Yb zg56>31hL55Z+nOVHEj|=BYtwMdwx1q#IDcS_lDfQS`J)kqC^*(^{8Q&U`l)Y$@=GY z_3JE8hIEFiL#UbQs`^cS2GI!PzZdPPP|s%toUEO3 zW@_q=IN9jnf#EcAk?MppvAgl6?N^UIyrNi(cp9OIW3T)O>lm;$M6mTD!&TYNI{`Xr zAG;!i=(CLfC_Wp0?x`&qf9?JvOUz7*2mM-0xgE8y4D(tG#h$1{tq&i}d_A|CP7Zq5 zS`9vz*cC1P{Xjvr8<%uFu8eaj-1qcVjyU&E%c>F>+S&RbXQ(V4z(eP$5JVN4sO!Sx z+g+*t(08&+;HM2Ik?Amjy0y_nA92^zxXIUe?oyyyezS<(P|^uO`2>MR_pOOT zU1k(miN4*MN*CPF)mfY^&$wxIA!g7BM*@>qVt*Us$Wn#O5|rj*r=c zZu^EcbnOwY*DnIiO9xnl!-(D;B{U)4?lO4EBfFd4{>^Jja?=L$# zx{^vF{_bA+8<%~O|R6F|O zK%lhe$qx#Wv}i0hm?3Dntp>Muoo82ozK&NyWO2S*UfNO+w)|9+C9WdP;GFkuCz&VV zClKJw^(c68?Xlh?Me*a~#Z_1?e@Vn>+2XLfbS|&sXDvjUhvo9BrSxHMszuk*_?4SH zC6hFs-dcHH^U8PBpLE^Gcf4549~P5=dm4Mxr^A{tqa2TX5EFvgUS55 z{qahLWl8cSKdH96J!xCLzMXib_u*@)o&uFhMKM%_=}LKrAMcU_2R(^^WrWx}M46ix(7QYNoDP zA)vPz$Eji*J`kcU9^X@P>y9~gNzW_D0I42I2sW7kXQmTCIUyUaKB3BjAPF$51)1_f z@$|T*#!7f_ZBD#o*_mN`WdgMieSKdIb!-J*O6E zmtVlykSupg`ax^U!)t<2qXPTK$7T&AHwKUBz!sDocXrT*Od)w9e{JCXFxX8A=41w8 zq->I-zkZ-aZ6#yMAf-6f7)2t8{pSq{w)y#k`Yb0>ZVRX0jh=$Ms{`r47;u8phc2t} zIL1X=GmPL#7UQ+*?}CmldEf4^0PygQ(96ju+q0b6)T=?~`2AOg!#hCT`4Nkb@f`qG z3-d$l#Q~veAnVrvTe8}7%#97@O#*JgGuaj%d5k7J!3kle8(KmD;GbJ5X*1Q0h6_ZO zLO8yfQEF>Acig!`qXpKgf6%nSGkOJilg-mfZhonlEKds1}ml+`v5=_S?fv3|4$#DV?O8j7u%fRbl$_UlK>OzGot z@9)k*(V{XBq+Wj8%%dQ+q|T5_%l%ob7g`x!9Qt~kBBoqop6Gj zr8N{pJ}1Kg0IHWPXUOwe!EZnZEM_ktSHv#qT9 zRie8HmXlxZ{tm~xy?Bnz$Ow#_`FIZ1pe!YNkzlQiIsy(@waqZq-avvNN1ZN&$A%7z z#{u7%_fM>3UwvzuI{0P_q^@H1IMiCYI%P zY51bVt40#rzVgXcpotm`Zfd+od8#jAnMhM+vtFFBFU2Ka7V8ZyORya-y`S195Pbhp zk^kM158*N4OPlBEqC`m+_d*47G?{L5^eKl#nMk_v6E_`pYLkY}_R%44RjEqNxLmvV zQis;uO&mwJ%z}AWx)p(4N`fNcp3N3i z0K8tyi15C}V!FE(QSSB7GKuJdHQPIx(|aN6(wi~QZF)!p z)Tc|*W6qb5LlcCS9vvF@w5ngRSPdS2m+J5LmR`y&o;U**x30HXU*G|-K(SuN`%M6 zil{?xn;%V&vHC%Yn{Yfn!o%lAgVfdVqKVzC9cIbBN%j3EUyGSffxfTeY5I2##7KD* zrN$}+T8tMvrvdQ6L4Z~hjo%NDrc>cO? zE}!$N7nZ3@^yNhr9-BMzF<}VVa-{3FDI!+t#s2 z;G0M77PZ$WyPHB-re=d5*g`UGLsG$B2)dE-z{$RJVmfgxxXkjRnO}~ESbk?AO<}%5 z+~O>b^udm^ZZFpG=Xde^?w>AFL$A(~`fE3^vOD-8u%#eTm(!u(cgs}P9#2cm_&oyL00;~ zR=@%-+NSem(bK#Et6J@a1hK5dY?ey+jlh4+fR$qk|AROlmqAi6Ik}2kfY5FU)_hgq zM;X^on525tG&EDP`yOafd(S*mBE-CDeXN=UBo)X%;zbk>>^Ixb5f7PQ-qsKRm%LHp zmWM|(Eu`VXA6x)s^M~`inCX;*+PPnImNnk7+XZhYB-2U1|2MgR0@!wr{tC*^hpRA@ z!!5EQ2^s|Ry3qj9OMfT)Bk_y9k6`bIwhh^>eyJXtLhAL#l%gvDpIcH!@y5+TC}If zUf9XpO4x~u(4Bxc(_iG|bd||l$m6H^xnO@s-DloE|M)k;-;+8=$vU}RMe-qv*sfc^ zRK}W>&ch5+Is9w5aKZk3M^MEMwak$is}HGU!jJE2QqP zkxJP1&S?4k@+&$R>V7H;b(f(x7NsF1^Pt5JeZi?(g-VP7aA}s7GOsYgGkpY;0)B-D zwk=+_ovFUe6wN#jXXsfW?i5~#0~hK*5<@9&6opa6g;{Z^QG1sD?AyzjM_p!DuJoC- zLuZL=zmZ|!W$v)zxUv0SJ;EKHf?tN84kBs+BxmU&i57j)YECqwTvtCn39{l~2@l3I z2{?=!U186(l>&P#l>~GocNj=qm8TFG`Z|Djd!y%AVbr}s>`EI;_0w<s%=v2dT!>m!nkH|9Z|M!QwDx%uzsptTv7yRp$`92iE)SE|0 zNEAUKFarXXIXx(8KV(E)>Cr-j8%Ep`St{Ra;8^54|Mh?Qn)^SvKd!r2w+