mirror of
https://github.com/lukaszraczylo/mcp-captcharesolver.git
synced 2026-10-12 00:45:28 +00:00
119 lines
3.2 KiB
Go
119 lines
3.2 KiB
Go
// Package imageutil decodes image/audio inputs (base64, data-URI, file path,
|
|
// http(s) URL) and computes grid geometry for tile-based captcha solving.
|
|
package imageutil
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var dataURIRe = regexp.MustCompile(`^data:([^;,]+)(;base64)?,(.*)$`)
|
|
|
|
// Point is a pixel coordinate.
|
|
type Point struct {
|
|
X int
|
|
Y int
|
|
}
|
|
|
|
// Decode resolves input (data-URI | bare base64 | file path | http(s) URL) to
|
|
// raw bytes plus a best-effort media type sniffed from the content.
|
|
func Decode(input string) ([]byte, string, error) {
|
|
input = strings.TrimSpace(input)
|
|
switch {
|
|
case strings.HasPrefix(input, "data:"):
|
|
m := dataURIRe.FindStringSubmatch(input)
|
|
if m == nil {
|
|
return nil, "", fmt.Errorf("malformed data URI")
|
|
}
|
|
var data []byte
|
|
var err error
|
|
if m[2] == ";base64" {
|
|
data, err = base64.StdEncoding.DecodeString(m[3])
|
|
} else {
|
|
data = []byte(m[3])
|
|
}
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return data, m[1], nil
|
|
case strings.HasPrefix(input, "http://"), strings.HasPrefix(input, "https://"):
|
|
return fetchURL(input)
|
|
default:
|
|
// File path if it exists; otherwise treat as bare base64.
|
|
if data, err := os.ReadFile(input); err == nil { //nolint:gosec // path is caller-provided automation input
|
|
return data, sniff(data), nil
|
|
}
|
|
data, err := base64.StdEncoding.DecodeString(input)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("input is not a data-URI, URL, readable file, or base64: %w", err)
|
|
}
|
|
return data, sniff(data), nil
|
|
}
|
|
}
|
|
|
|
func fetchURL(url string) ([]byte, string, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
if resp.StatusCode >= 300 {
|
|
return nil, "", fmt.Errorf("fetch %s: HTTP %d", url, resp.StatusCode)
|
|
}
|
|
data, err := io.ReadAll(io.LimitReader(resp.Body, 32<<20))
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
mt := resp.Header.Get("Content-Type")
|
|
if mt == "" {
|
|
mt = sniff(data)
|
|
}
|
|
return data, mt, nil
|
|
}
|
|
|
|
// sniff returns a media type from magic bytes, defaulting to image/png.
|
|
func sniff(b []byte) string {
|
|
switch {
|
|
case len(b) >= 4 && b[0] == 0x89 && b[1] == 0x50 && b[2] == 0x4e && b[3] == 0x47:
|
|
return "image/png"
|
|
case len(b) >= 3 && b[0] == 0xff && b[1] == 0xd8 && b[2] == 0xff:
|
|
return "image/jpeg"
|
|
case len(b) >= 4 && b[0] == 'R' && b[1] == 'I' && b[2] == 'F' && b[3] == 'F':
|
|
return "audio/wav"
|
|
case len(b) >= 6 && string(b[:3]) == "ID3":
|
|
return "audio/mpeg"
|
|
default:
|
|
return "image/png"
|
|
}
|
|
}
|
|
|
|
// GridCentroids returns the pixel centre of each tile for a rows x cols grid
|
|
// laid over a width x height image, in row-major order (index 0 = top-left).
|
|
func GridCentroids(width, height, rows, cols int) []Point {
|
|
pts := make([]Point, 0, rows*cols)
|
|
cw := float64(width) / float64(cols)
|
|
ch := float64(height) / float64(rows)
|
|
for r := 0; r < rows; r++ {
|
|
for c := 0; c < cols; c++ {
|
|
pts = append(pts, Point{
|
|
X: int(cw*float64(c) + cw/2),
|
|
Y: int(ch*float64(r) + ch/2),
|
|
})
|
|
}
|
|
}
|
|
return pts
|
|
}
|