feature/ui - zatim ne uplne uhlazena ale celkem pouzitelna appka #1
0
fyne_ui.go
Normal file → Executable file
0
fyne_ui.go
Normal file → Executable file
2
go.mod
Normal file → Executable file
2
go.mod
Normal file → Executable file
@ -5,6 +5,7 @@ go 1.24.0
|
||||
require (
|
||||
fyne.io/fyne/v2 v2.6.3
|
||||
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/spf13/cobra v1.10.1
|
||||
golang.org/x/crypto v0.42.0
|
||||
@ -44,5 +45,6 @@ require (
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sys v0.36.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
|
||||
)
|
||||
|
||||
4
go.sum
Normal file → Executable file
4
go.sum
Normal file → Executable 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/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/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/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
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/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
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 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
0
lib/crypto.go
Normal file → Executable file
0
lib/crypto.go
Normal file → Executable file
0
lib/crypto_storage.go
Normal file → Executable file
0
lib/crypto_storage.go
Normal file → Executable file
31
qr_manual_test.go
Normal file
31
qr_manual_test.go
Normal 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))
|
||||
}
|
||||
|
||||
498
qr_support.go
Normal file → Executable file
498
qr_support.go
Normal file → Executable file
@ -1,13 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"image"
|
||||
"image/png"
|
||||
"bytes"
|
||||
"errors"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"log"
|
||||
"math"
|
||||
"os/exec"
|
||||
|
||||
"github.com/liyue201/goqr"
|
||||
"github.com/skip2/go-qrcode"
|
||||
"github.com/liyue201/goqr"
|
||||
"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.
|
||||
@ -15,7 +22,8 @@ func GenerateQRPNG(text string, size int) ([]byte, error) {
|
||||
if size <= 0 {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@ -24,14 +32,482 @@ func GenerateQRPNG(text string, size int) ([]byte, error) {
|
||||
|
||||
// DecodeQR decodes first QR code text from an image.
|
||||
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)
|
||||
if err == nil && len(codes) > 0 {
|
||||
log.Printf("[qr] basic success length=%d", len(codes))
|
||||
return string(codes[0].Payload), nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
log.Printf("[qr] basic error: %v", err)
|
||||
} else {
|
||||
log.Printf("[qr] basic no codes")
|
||||
}
|
||||
if len(codes) == 0 {
|
||||
return "", errors.New("no qr code found")
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
return string(codes[0].Payload), nil
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
// 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
|
||||
}
|
||||
// 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.
|
||||
|
||||
0
vault_service.go
Normal file → Executable file
0
vault_service.go
Normal file → Executable file
Loading…
Reference in New Issue
Block a user