feature/ui - zatim ne uplne uhlazena ale celkem pouzitelna appka #1

Merged
luke-20 merged 17 commits from feature/ui into main 2025-09-28 21:05:52 +02:00
12 changed files with 1124 additions and 797 deletions
Showing only changes of commit 81df2170f3 - Show all commits

0
cmd.go Normal file → Executable file
View File

0
fyne_ui.go Normal file → Executable file
View File

2
go.mod Normal file → Executable file
View File

@ -5,6 +5,7 @@ go 1.24.0
require ( require (
fyne.io/fyne/v2 v2.6.3 fyne.io/fyne/v2 v2.6.3
github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea
github.com/makiuchi-d/gozxing v0.1.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/spf13/cobra v1.10.1 github.com/spf13/cobra v1.10.1
golang.org/x/crypto v0.42.0 golang.org/x/crypto v0.42.0
@ -44,5 +45,6 @@ require (
golang.org/x/net v0.43.0 // indirect golang.org/x/net v0.43.0 // indirect
golang.org/x/sys v0.36.0 // indirect golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect golang.org/x/text v0.29.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

4
go.sum Normal file → Executable file
View File

@ -51,6 +51,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea h1:uyJ13zfy6l79CM3HnVhDalIyZ4RJAyVfDrbnfFeJoC4= github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea h1:uyJ13zfy6l79CM3HnVhDalIyZ4RJAyVfDrbnfFeJoC4=
github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea/go.mod h1:w4pGU9PkiX2hAWyF0yuHEHmYTQFAd6WHzp6+IY7JVjE= github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea/go.mod h1:w4pGU9PkiX2hAWyF0yuHEHmYTQFAd6WHzp6+IY7JVjE=
github.com/makiuchi-d/gozxing v0.1.0 h1:bLJdKoi5G2wGQnFirTQI9aOSCwNm5N2e0P8ov04Hltk=
github.com/makiuchi-d/gozxing v0.1.0/go.mod h1:eRIHbOjX7QWxLIDJoQuMLhuXg9LAuw6znsUtRkNw9DU=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
@ -90,6 +92,8 @@ golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

0
lib/crypto.go Normal file → Executable file
View File

0
lib/crypto_storage.go Normal file → Executable file
View File

0
main.go Normal file → Executable file
View File

31
qr_manual_test.go Normal file
View File

@ -0,0 +1,31 @@
package main
import (
"image/png"
"os"
"testing"
)
// go test -run TestDecodeQR -v
func TestDecodeQR(t *testing.T) {
path := os.Getenv("QR_TEST_FILE")
if path == "" {
t.Skip("set QR_TEST_FILE to a PNG path to run")
return
}
f, err := os.Open(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()
img, err := png.Decode(f)
if err != nil {
t.Fatal(err)
}
txt, err := DecodeQR(img)
if err != nil {
t.Fatalf("decode error: %v", err)
}
t.Logf("decoded: %d bytes", len(txt))
}

484
qr_support.go Normal file → Executable file
View File

@ -4,10 +4,17 @@ import (
"bytes" "bytes"
"errors" "errors"
"image" "image"
"image/color"
"image/draw"
"image/png" "image/png"
"log"
"math"
"os/exec"
"github.com/liyue201/goqr" "github.com/liyue201/goqr"
"github.com/skip2/go-qrcode" "github.com/makiuchi-d/gozxing"
qrx "github.com/makiuchi-d/gozxing/qrcode"
qrgen "github.com/skip2/go-qrcode"
) )
// GenerateQRPNG returns PNG bytes for the given text. // GenerateQRPNG returns PNG bytes for the given text.
@ -15,7 +22,8 @@ func GenerateQRPNG(text string, size int) ([]byte, error) {
if size <= 0 { if size <= 0 {
size = 256 size = 256
} }
png, err := qrcode.Encode(text, qrcode.Medium, size) // Use Low EC for larger modules; improves decode reliability for long payloads
png, err := qrgen.Encode(text, qrgen.Low, size)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -24,15 +32,483 @@ func GenerateQRPNG(text string, size int) ([]byte, error) {
// DecodeQR decodes first QR code text from an image. // DecodeQR decodes first QR code text from an image.
func DecodeQR(img image.Image) (string, error) { func DecodeQR(img image.Image) (string, error) {
log.Printf("[qr] start decode: %dx%d %T", img.Bounds().Dx(), img.Bounds().Dy(), img)
// Try basic decode first
codes, err := goqr.Recognize(img) codes, err := goqr.Recognize(img)
if err == nil && len(codes) > 0 {
log.Printf("[qr] basic success length=%d", len(codes))
return string(codes[0].Payload), nil
}
if err != nil { if err != nil {
log.Printf("[qr] basic error: %v", err)
} else {
log.Printf("[qr] basic no codes")
}
// Convert palette/indexed images to RGBA first to avoid issues with goqr
if _, ok := img.ColorModel().(color.Palette); ok {
bounds := img.Bounds()
rgba := image.NewRGBA(bounds)
draw.Draw(rgba, bounds, img, bounds.Min, draw.Src)
img = rgba
// Try again after conversion
codes, err := goqr.Recognize(img)
if err == nil && len(codes) > 0 {
log.Printf("[qr] palette-convert success")
return string(codes[0].Payload), nil
}
if err != nil {
log.Printf("[qr] palette-convert err: %v", err)
} else {
log.Printf("[qr] palette-convert no codes")
}
}
// Try multiple preprocessing strategies to improve recognition from clipboard screenshots
attempts := []image.Image{}
add := func(im image.Image) {
if im != nil {
attempts = append(attempts, im)
}
}
add(img)
bounds := img.Bounds()
w, h := bounds.Dx(), bounds.Dy()
// Helper: nearest-neighbour scale
scale := func(src image.Image, fw, fh int) image.Image {
if fw <= 0 || fh <= 0 {
return src
}
dst := image.NewNRGBA(image.Rect(0, 0, fw, fh))
sb := src.Bounds()
sw, sh := float64(sb.Dx()), float64(sb.Dy())
for y := 0; y < fh; y++ {
sy := int(float64(y)/float64(fh)*sh + 0.5)
if sy >= sb.Dy() {
sy = sb.Dy() - 1
}
for x := 0; x < fw; x++ {
sx := int(float64(x)/float64(fw)*sw + 0.5)
if sx >= sb.Dx() {
sx = sb.Dx() - 1
}
dst.Set(x, y, src.At(sb.Min.X+sx, sb.Min.Y+sy))
}
}
return dst
}
// Helper: binarize with adaptive threshold (simple mean + variance fudge)
binarize := func(src image.Image) image.Image {
b := src.Bounds()
gray := image.NewGray(b)
var sum, sum2 float64
total := float64(b.Dx() * b.Dy())
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
r, g, bl, _ := src.At(x, y).RGBA()
lum := float64((r*299 + g*587 + bl*114) / 1000 >> 8) // 0-255 approx
sum += lum
sum2 += lum * lum
}
}
mean := sum / total
variance := (sum2 / total) - mean*mean
stddev := math.Sqrt(math.Max(variance, 0))
// threshold tweaks: lower slightly for dark backgrounds
thr := mean - stddev*0.15
if thr < 50 {
thr = 50
}
if thr > 200 {
thr = 200
}
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
r, g, bl, _ := src.At(x, y).RGBA()
lum := float64((r*299 + g*587 + bl*114) / 1000 >> 8)
var c color.Gray
if lum < thr {
c = color.Gray{Y: 0}
} else {
c = color.Gray{Y: 255}
}
gray.Set(x, y, c)
}
}
return gray
}
// Scale strategy: even if image is not tiny, scaled variants sometimes help goqr lock onto module grid
// 1) If very small, aggressive scale
if w < 280 || h < 280 {
factor := 2
if w < 160 {
factor = 3
}
if w < 100 {
factor = 4
}
add(scale(img, w*factor, h*factor))
}
// 2) Always try moderate upscale variants for mid-sized screenshots (helps when original was downscaled with interpolation)
if w >= 180 && w <= 900 { // avoid exploding huge images
add(scale(img, w*2, h*2))
if w*3 < 2000 && h*3 < 2000 { // guard upper bound
add(scale(img, w*3, h*3))
}
}
// Add binarized versions (original + scaled)
add(binarize(img))
if w < 320 || h < 320 {
factor := 2
add(binarize(scale(img, w*factor, h*factor)))
}
// Also binarize any newly added large upscales (skip if too big)
for _, cand := range attempts { // safe: attempts already contains originals
cw := cand.Bounds().Dx()
ch := cand.Bounds().Dy()
if (cw <= 1400 && ch <= 1400) && (cw > w || ch > h) { // only for upscaled variants within limit
add(binarize(cand))
}
}
// Add contrast-enhanced alpha-flattened variant
flatten := func(src image.Image) image.Image {
b := src.Bounds()
dst := image.NewRGBA(b)
draw.Draw(dst, b, image.Black, image.Point{}, draw.Src)
draw.Draw(dst, b, src, b.Min, draw.Over)
return dst
}
add(flatten(img))
// Rotations (90,180,270) in case of orientation issues
rotate := func(src image.Image) []image.Image {
b := src.Bounds()
w := b.Dx()
h := b.Dy()
r90 := image.NewRGBA(image.Rect(0, 0, h, w))
r180 := image.NewRGBA(image.Rect(0, 0, w, h))
r270 := image.NewRGBA(image.Rect(0, 0, h, w))
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
c := src.At(b.Min.X+x, b.Min.Y+y)
// 90
r90.Set(h-1-y, x, c)
// 180
r180.Set(w-1-x, h-1-y, c)
// 270
r270.Set(y, w-1-x, c)
}
}
return []image.Image{r90, r180, r270}
}
for _, r := range rotate(img) {
add(r)
}
var firstErr error
for idx, attempt := range attempts {
codes, err := goqr.Recognize(attempt)
if err != nil {
if firstErr == nil {
firstErr = err
}
log.Printf("[qr] attempt %d error: %v", idx, err)
continue
}
if len(codes) > 0 {
log.Printf("[qr] attempt %d success (%d codes)", idx, len(codes))
return string(codes[0].Payload), nil
}
log.Printf("[qr] attempt %d no codes", idx)
}
// ZXing helper used across multiple branches
decodeZX := func(src image.Image, hints map[gozxing.DecodeHintType]interface{}) (string, error) {
binSrc := gozxing.NewLuminanceSourceFromImage(src)
// Try hybrid first (usually better for photos)
bmpH, _ := gozxing.NewBinaryBitmap(gozxing.NewHybridBinarizer(binSrc))
reader := qrx.NewQRCodeReader()
if res, err := reader.Decode(bmpH, hints); err == nil {
return res.GetText(), nil
}
// Fallback to global histogram
bmpG, _ := gozxing.NewBinaryBitmap(gozxing.NewGlobalHistgramBinarizer(binSrc))
if res, err := reader.Decode(bmpG, hints); err == nil {
return res.GetText(), nil
} else {
return "", err return "", err
} }
if len(codes) == 0 {
return "", errors.New("no qr code found")
} }
// Also try ZXing on all preprocessed attempts
for idx, attempt := range attempts {
if txt, err := decodeZX(attempt, map[gozxing.DecodeHintType]interface{}{
gozxing.DecodeHintType_TRY_HARDER: true,
gozxing.DecodeHintType_PURE_BARCODE: true,
}); err == nil && txt != "" {
log.Printf("[qr] zxing attempt %d success (pure)", idx)
return txt, nil
}
if txt, err := decodeZX(attempt, map[gozxing.DecodeHintType]interface{}{
gozxing.DecodeHintType_TRY_HARDER: true,
}); err == nil && txt != "" {
log.Printf("[qr] zxing attempt %d success (try-harder)", idx)
return txt, nil
}
}
if firstErr != nil {
log.Printf("[qr] preprocess firstErr: %v", firstErr)
}
// --- Heuristic fallback: auto-invert + quiet-zone pad + gozxing ---
addQuietZone := func(src image.Image) image.Image {
b := src.Bounds()
pad := int(math.Max(float64(b.Dx()/32), 8))
out := image.NewRGBA(image.Rect(0, 0, b.Dx()+pad*2, b.Dy()+pad*2))
// fill white
for y := 0; y < out.Bounds().Dy(); y++ {
for x := 0; x < out.Bounds().Dx(); x++ {
out.Set(x, y, color.White)
}
}
draw.Draw(out, b.Add(image.Point{X: pad, Y: pad}), src, b.Min, draw.Src)
return out
}
invert := func(src image.Image) image.Image {
b := src.Bounds()
out := image.NewRGBA(b)
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
r, g, bb, a := src.At(x, y).RGBA()
out.Set(x, y, color.RGBA{uint8(255 - r/257), uint8(255 - g/257), uint8(255 - bb/257), uint8(a / 257)})
}
}
return out
}
// estimate dark pixel ratio
b := img.Bounds()
var dark, total int
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
r, g, bb, _ := img.At(x, y).RGBA()
lum := (r*299 + g*587 + bb*114) / 1000
if lum < 128<<8 {
dark++
}
total++
}
}
ratio := float64(dark) / float64(total)
needInvert := ratio > 0.85 // almost full dark background (white modules?)
var modified []image.Image
base := img
if needInvert {
base = invert(base)
}
modified = append(modified, addQuietZone(base))
modified = append(modified, addQuietZone(invert(base)))
for _, m := range modified {
if codes, err := goqr.Recognize(m); err == nil && len(codes) > 0 {
log.Printf("[qr] quiet-zone/invert success")
return string(codes[0].Payload), nil return string(codes[0].Payload), nil
} }
// Also try ZXing on these variants before moving on
if txt, err := decodeZX(m, map[gozxing.DecodeHintType]interface{}{
gozxing.DecodeHintType_TRY_HARDER: true,
gozxing.DecodeHintType_PURE_BARCODE: true,
}); err == nil && txt != "" {
log.Printf("[qr] gozxing success (quiet-zone+pure)")
return txt, nil
}
if txt, err := decodeZX(m, map[gozxing.DecodeHintType]interface{}{
gozxing.DecodeHintType_TRY_HARDER: true,
}); err == nil && txt != "" {
log.Printf("[qr] gozxing success (quiet-zone+try-harder)")
return txt, nil
}
}
// ZXing with TRY_HARDER and PURE_BARCODE hints which often fix dense PNGs
hints := map[gozxing.DecodeHintType]interface{}{
gozxing.DecodeHintType_TRY_HARDER: true,
// Many of our images are perfect render PNGs from go-qrcode -> enable PURE_BARCODE
gozxing.DecodeHintType_PURE_BARCODE: true,
}
if txt, err := decodeZX(base, hints); err == nil && txt != "" {
log.Printf("[qr] gozxing success (hints)")
return txt, nil
} else if err != nil {
log.Printf("[qr] gozxing(hints) err: %v", err)
}
// Try again without PURE_BARCODE in case quiet zone is cropped
hints2 := map[gozxing.DecodeHintType]interface{}{gozxing.DecodeHintType_TRY_HARDER: true}
if txt, err := decodeZX(base, hints2); err == nil && txt != "" {
log.Printf("[qr] gozxing success (try-harder)")
return txt, nil
} else if err != nil {
log.Printf("[qr] gozxing(try-harder) err: %v", err)
}
// Try ZXing on rotations as well
for _, r := range rotate(base) {
if txt, err := decodeZX(r, hints2); err == nil && txt != "" {
log.Printf("[qr] gozxing success (rotated)")
return txt, nil
}
if txt, err := decodeZX(r, hints); err == nil && txt != "" {
log.Printf("[qr] gozxing success (rotated pure)")
return txt, nil
}
}
// Extra pure-QR recovery: try small insets and simple upscales with PURE_BARCODE.
inset := func(src image.Image, px int) image.Image {
if px <= 0 {
return src
}
b := src.Bounds()
r := image.Rect(b.Min.X+px, b.Min.Y+px, b.Max.X-px, b.Max.Y-px)
if r.Dx() <= 10 || r.Dy() <= 10 {
return src
}
out := image.NewRGBA(image.Rect(0, 0, r.Dx(), r.Dy()))
draw.Draw(out, out.Bounds(), src, r.Min, draw.Src)
return out
}
cropSides := func(src image.Image, l, t, r, b int) image.Image {
bb := src.Bounds()
rr := image.Rect(bb.Min.X+l, bb.Min.Y+t, bb.Max.X-r, bb.Max.Y-b)
if rr.Dx() <= 10 || rr.Dy() <= 10 || rr.Min.X >= rr.Max.X || rr.Min.Y >= rr.Max.Y {
return src
}
out := image.NewRGBA(image.Rect(0, 0, rr.Dx(), rr.Dy()))
draw.Draw(out, out.Bounds(), src, rr.Min, draw.Src)
return out
}
tryPure := func(src image.Image) (string, bool) {
for _, px := range []int{1, 2, 3} {
if txt, err := decodeZX(inset(src, px), hints); err == nil && txt != "" {
log.Printf("[qr] gozxing pure+inset %d success", px)
return txt, true
}
}
return "", false
}
// Try on base, binarized base and scaled variants
if txt, ok := tryPure(base); ok {
return txt, nil
}
if txt, ok := tryPure(binarize(base)); ok {
return txt, nil
}
if w < 800 && h < 800 { // upscale to mitigate rounding of module size
if txt, ok := tryPure(scale(base, w*2, h*2)); ok {
return txt, nil
}
if txt, ok := tryPure(scale(base, w*3, h*3)); ok {
return txt, nil
}
}
// Brute-force small asymmetric crops to satisfy ZXing's 1 (mod 4) dimension constraint.
for _, src := range []image.Image{base, binarize(base)} {
for l := 0; l <= 2; l++ {
for t := 0; t <= 2; t++ {
for r := 0; r <= 2; r++ {
for btm := 0; btm <= 2; btm++ {
cand := cropSides(src, l, t, r, btm)
if cand == src {
continue
}
if txt, err := decodeZX(cand, hints); err == nil && txt != "" {
log.Printf("[qr] gozxing pure+asym inset %d,%d,%d,%d success", l, t, r, btm)
return txt, nil
}
if txt, err := decodeZX(cand, hints2); err == nil && txt != "" {
log.Printf("[qr] gozxing asym inset %d,%d,%d,%d success", l, t, r, btm)
return txt, nil
}
}
}
}
}
}
// Bounding box crop of dark pixels, then upscale and retry
boundsAll := base.Bounds()
minX, minY := boundsAll.Max.X, boundsAll.Max.Y
maxX, maxY := boundsAll.Min.X, boundsAll.Min.Y
for y := boundsAll.Min.Y; y < boundsAll.Max.Y; y++ {
for x := boundsAll.Min.X; x < boundsAll.Max.X; x++ {
r, g, bb, _ := base.At(x, y).RGBA()
lum := (r*299 + g*587 + bb*114) / 1000
if lum < 180<<8 { // treat as dark
if x < minX {
minX = x
}
if y < minY {
minY = y
}
if x > maxX {
maxX = x
}
if y > maxY {
maxY = y
}
}
}
}
if minX < maxX && minY < maxY {
crop := image.NewRGBA(image.Rect(0, 0, maxX-minX+1, maxY-minY+1))
draw.Draw(crop, crop.Bounds(), base, image.Point{X: minX, Y: minY}, draw.Src)
log.Printf("[qr] crop bbox size=%dx%d", crop.Bounds().Dx(), crop.Bounds().Dy())
for _, up := range []int{2, 3, 4} {
cw := crop.Bounds().Dx() * up
ch := crop.Bounds().Dy() * up
if cw > 2400 || ch > 2400 {
continue
}
scaled := image.NewRGBA(image.Rect(0, 0, cw, ch))
for y := 0; y < ch; y++ {
sy := y / up
for x := 0; x < cw; x++ {
sx := x / up
scaled.Set(x, y, crop.At(sx, sy))
}
}
if txt, err := decodeZX(scaled, hints2); err == nil && txt != "" {
log.Printf("[qr] gozxing crop+scale success up=%d", up)
return txt, nil
} else if err != nil {
log.Printf("[qr] crop up=%d fail: %v", up, err)
}
}
}
if firstErr != nil {
return "", firstErr
}
// Final external fallback: try zbarimg if available on the system
if cmd, lookErr := exec.LookPath("zbarimg"); lookErr == nil {
buf := &bytes.Buffer{}
if err := png.Encode(buf, img); err == nil {
c := exec.Command(cmd, "-q", "--raw", "-")
c.Stdin = bytes.NewReader(buf.Bytes())
out, err := c.Output()
if err == nil && len(out) > 0 {
log.Printf("[qr] zbarimg success")
return string(out), nil
}
if err != nil {
log.Printf("[qr] zbarimg error: %v", err)
}
}
}
return "", errors.New("no qr code found")
}
// LoadPNG decodes raw PNG bytes to image.Image. // LoadPNG decodes raw PNG bytes to image.Image.
func LoadPNG(b []byte) (image.Image, error) { func LoadPNG(b []byte) (image.Image, error) {

0
server.go Normal file → Executable file
View File

1252
ui.go Normal file → Executable file

File diff suppressed because it is too large Load Diff

0
vault_service.go Normal file → Executable file
View File